聊聊 API hooking
今天接着说说 API hook。API hook 大致上有两种手段:修改 IAT 和修改 API 指令。那种用一个自己写的同名 DLL 来替换目标 DLL 的手段就不提啦,可操作性太差。今天聊一下直接修改 API 指令这种方法。
直接修改 API 指令,简单的说就是在目标 API 代码的开始处修改一些指令,让执行绪跳转到自己的代码里面。这里需要面对的问题有 3 个:如何找到 API 的代码地址,如何修改代码段,如何构造执行绪转移指令。
前两个问题比较简单,无非是相关 API 的调用,用 GetProcAddress() 可以取到目标 API 的代码所在地址;用 VirtualProtect() 可以修改代码所在地址的访问权限,有了写权限之后就可以对目标 API 的代码做手脚了。
至于如何构造执行绪转移指令,其实说白了就是在目标 API 代码的开始处放置一条无条件跳转指令 jmp。对于 32bits 系统,jmp 的机器码是 0xE9(这里不讨论短跳转 0xEB 的原因是通常我们自己代码跟目标 API 代码之间的偏移都大于 128),加上 32bits 的相对地址,一条无条件跳转指令的长度为 5 个字节。也就是说我们要至少覆盖目标 API 开始处的 5 个字节,但是直接覆盖别人的代码是肯定会出问题地,所以我们在覆盖目标 API 开始处指令之前要把这些指令搬到其它地方去,后面还有用。
对于 jmp 指令后面那个相对地址,不是简单的拿目标地址减去当前 jmp 指令的地址就行,由于 CPU 要执行过 jmp 指令,拿到跳转地址再进行跳转,所以计算相对地址的时候要用“目的跳转地址 – ( jmp 所在指令地址 + 5 )”。目前为止,把对目标 API 的调用劫持到我们自己的函数这个目的已经达到了。但是我们还是得在需要的时候调用原始 API 来完成 caller 想要完成的功能,不过别忘了原始 API 已经被我们破坏掉了,所以调用原始 API 还要靠我们之前保存的 API 代码起始处的那些指令。
假设我们刚好从目标 API 处复制了 5 个字节的完整指令(注意一定要是完整指令)放在内存 A 处,那么在 A + 5 处再构造一条跳转指令跳回到 API + 5 处就等于是把断成两截的 API 代码又连接了起来。这样一来,在需要调用原本的 API 功能时我们要调用的其实是 A。
以下是一些示例代码:
- typedef HRESULT (WINAPI *_DirectDrawCreate)( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );
- _DirectDrawCreate pOldDirectDrawCreate = NULL;
- HRESULT WINAPI
- MyDirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter )
- {
- MessageBox( NULL, "MyDirectDrawCreate", "MyDirectDrawCreate", 0 );
- return( pOldDirectDrawCreate( lpGUID, lplpDD, pUnkOuter) );
- }
- int main( int argc, char *argv[] )
- {
- HMODULE hDll = LoadLibrary( "DDraw.dll" );
- if( hDll )
- {
- pOldDirectDrawCreate = (_DirectDrawCreate)GetProcAddress( hDll, "DirectDrawCreate" );
- HookAPI( (PVOID *)(&pOldDirectDrawCreate), MyDirectDrawCreate );
- }
- LPDIRECTDRAW pDDraw = NULL;
- HRESULT hr = DirectDrawCreate( NULL, &pDDraw, NULL );
- if( DD_OK == hr )
- hr = pDDraw->SetCooperativeLevel( NULL, DDSCL_NORMAL );
- return( 0 );
- }
以上代码成功执行后,当我们调用 DirectDrawCreate() 的时候会被之前设置的 hook 函数截获,显示一个对话框。注意 HookAPI() 的第一个参数是指向指针的指针,所以我们开始时拿到的“原始” API 地址其实被换掉了(见上面描述,原始 API 代码已经被我们断成两截啦)。
下面是代码里用到的两个关键数据结构:
- #pragma pack (1)
- struct HookTarget
- {
- char jmp;
- int address;
- HookTarget()
- {
- jmp = char( 0xE9 );
- }
- };
- struct HookThunk
- {
- char instructions[16];
- HookTarget HookProc;
- HookThunk()
- {
- memset( instructions, 0x90, sizeof( instructions ) );
- }
- };
- #pragma pack ()
HookTarget 是用来写覆盖在目标 API 起始处的无条件跳转指令的,HookThunk 是用来把断成两截的 API 代码连起来的。0×90 是 nop 的机器码,就算我们只从目标 API 那里复制了 5 个字节,后面的 nop 也会安全的让执行绪运行到 jmp 指令处。
下面是 HookAPI() 的代码:
- bool
- HookAPI( PVOID *ppProc, LPCVOID pMyProc )
- {
- do
- {
- DWORD dwOldProtect = 0;
- if( !RemoveReadOnly( *ppProc, 16, &dwOldProtect ) )
- break;
- HookThunk *pThunk = new HookThunk;
- int cbInstructions = CheckInstructions( (LPBYTE)*ppProc );
- memcpy( pThunk, *ppProc, cbInstructions );
- pThunk->HookProc.address = ( (int)*ppProc + cbInstructions )
- - ( (int)pThunk + sizeof( HookThunk ) );
- DWORD dwTemp = 0;
- if( !VirtualProtect( pThunk, sizeof( HookThunk ), dwOldProtect, &dwTemp ) )
- break;
- HookTarget *pOriginalProc = (HookTarget *)*ppProc;
- pOriginalProc->jmp = char( 0xE9 );
- pOriginalProc->address = (int)MyDirectDrawCreate - ( (int)pOriginalProc + 5 );
- if( !VirtualProtect( *ppProc, 16, dwOldProtect, &dwTemp ) )
- break;
- *ppProc = pThunk;
- return( true );
- }
- while( false );
- return( false );
- }
就不详细解释了,看完了上面那一大坨原理分析,HookAPI() 这个函数很容易看懂。上面代码里面的 RemoveReadOnly() 就是对 VirtualProtect() 调用的包装而已,对于我们这个示例的目标 API——DirectDrawCreate(),CheckInstructions() 简单的返回 5 就可以了(如果程序执行异常,最好自己用调试器看一下指令边界在哪里)。
注意以上的示例代码只是用来做测试之用,没有做完备的错误处理。而且在实际环境中目标 API 代码的起始指令边界不一定正好是 5 个字节,如果边界是 4 个字节和 6 个字节处,由于我们至少要覆盖 5 个字节,所以要多复制一些(复制 6 个字节)。另外代码中使用的指令集和地址都只适用于 32bits x86 系统,在其它 CPU 和 OS 上要做相应的修改才能正常运行。最后,在实际的环境中,还要写卸载 hook 的代码;)


十二月 15th, 2009 at 01:48
看这个
http://research.microsoft.com/en-us/projects/detours/
十二月 15th, 2009 at 12:33
Detours 之前下载过的,但是如果通读了他的代码再来实现自己的 API hook,不就学不到东西了吗?:) 接下来有时间倒是想看看 Detours 处理 API 开头指令的方法,这个活儿要建个表格来收集指令长度,基本纯体力。