钩子api:如何实现API钩子





序言对大多数Windows开发者来说如何在Win32系统中对API进行拦截直是项极富挑战性课题这将是对你所掌握计算机知识较为全面考验尤其是些在如今使用RAD进行软件Software开发时并不常用知识这包括了操作系统原理、汇编语言甚至是有关机器指令代码(听上去真是有点恐怖不过这是事实)

当前广泛使用Windows操作系统中像Win 9x和Win NT/2K都提供了种比较稳健机制来使得各个进程内存地址空间的间是相互独立也就是说个进程中某个有效内存地址对另个进程来说是无意义这种内存保护措施大大增加了系统稳定性不过这也使得进行系统级API拦截工作难度也大大加大了

当然我这里所指是比较文雅拦截方式通过修改可执行文件在内存中映像中有关代码实现对API动态拦截;而不是采用比较暴力方式直接对可执行文件磁盘存储中机器代码进行改写


2、
API钩子系统般框架通常我们把拦截API这个过程称为是安装个API钩子(API Hook)个API钩子至少有两个模块组成:个是钩子服务器(Hook Server)模块般为EXE形式;个是钩子驱动器(Hook Driver)模块般为DLL形式

服务器主要负责向目标进程注入驱动器使得驱动器工作在目标进程地址空间中这是关键驱动器则负责实际API拦截工作以便在我们所关心API前后能做些我们需要工作

个大家比较常见API钩子例子就是些实时翻译软件Software(像金山词霸)中必备功能:屏幕抓词它主要是对些GDI 进行了拦截获取它们输入参数中然后在自己窗口中显示出来针对上述两个部分有以下两点需要我们重点考虑: 选用何种DLL注入技术 采用何种API拦截机制

3、
注入技术选用由于在Win32系统中各个进程地址是互相独立因此我们无法在个进程中对另个进程代码进行有效修改而你要完成API钩子工作就必须进行这种操作因此我们必须采取某种独特手段使得API钩子(准确说是钩子驱动器)能够成为目标进程中部分才有较大可能来对目标进程数据和代码进行有控制修改

通常有以下几种注入方式:

1.利用注册表如果我们准备拦截进程连接了User32.dll也就是使用了User32中API(般图形界面应用都符合这个条件)那么就可以简单把你钩子驱动器DLL名字作为值添加在下面注册表键下: HKEY_LOCAL_MACHINE\\Software\\Microsoft\\WindowsNT\\CurrentVersion\\Windows\\AppInit_DLLs 值形式可以为单个DLL文件名或者是组DLL文件名相邻名称的间用逗号或空格间隔所有由该值标识DLL将在符合条件应用启动时候装载这是个操作系统内建机制相对其他方式来说危险性较小但它有些比较明显缺点: 该思路方法仅适用于NT/2K操作系统看看键名称你就应该明白 为了激活或停止钩子注入必须重新启动Windows这个就似乎太不方便了 不能用此思路方法向没有使用User32应用注入DLL例如控制台应用 不管需要和否钩子DLL将注入每个GUI应用这将导致整个系统性能下降

2.

建立系统范围Windows钩子要向某个进程注入DLL个十分普遍也是比较简单思路方法就是建立在标准Windows钩子基础上Windows钩子般是在DLL中实现这是个全局性Windows钩子基本要求这也符合我们需要当我们成功地SetWindowsHookEx的后便在系统中安装了某种类型消息钩子这个钩子可以是针对某个进程也可以是针对系统中所有进程旦某个进程中产生了该类型消息操作系统会自动把该钩子所在DLL映像到该进程地址空间中从而使得消息回调(在SetWindowsHookEx参数中指定)能够对此消息进行适当处理在这里我们所感兴趣当然不是对消息进行什么处理因此在消息回调中只需把消息钩子向后传递就可以了但是我们所需DLL已经成功地注入了目标进程地址空间从而可以完成后续工作

我们知道区别进程中使用DLL的间是不能直接共享数据它们活动在区别地址空间中但在Windows钩子DLL中些数据例如Windows钩子句柄HHook这是由SetWindowsHookEx返回值得到并且作为参数将在CallNextHookEx和UnhookWindoesHookEx中使用显然使用SetWindowsHookEx进程和使用CallNextHookEx进程般不会是同个进程因此我们必须能够使句柄在所有地址空间中都是有效有意义也就是说值必须必须在这些钩子DLL所挂钩进程的间是共享为了达到这个目我们就应该把它存储在个共享数据区域中

在VC中我们可以采用预编译指令#pragma data_seg在DLL文件中创建个新并且在DEF文件中把该段属性设置为“shared”这样就建立了个共享数据段对于使用Delphi人来说就没有这么幸运了:没有类似比较简单思路方法(或许是有但我没有找到)不过我们还是可以利用内存映像技术来申请使用块各进程可以共享内存区域主要是利用了CreateFileMapping和MapViewOfFile这两个这倒是个通用思路方法适合所有开发语言只要它能使用WindowsAPI

在BorlandBCB中有个指令#pragma codeseg和VC#pragma data_seg指令有点类似应该也能起到作用但我试了没有没有效果而BCB联机帮助中对此也提到不多不知怎样才能正确使用旦钩子DLL加载进入目标进程地址空间后在我们UnHookWindowsHookEx的前是无法使它停止工作除非目标进程关闭

这种DLL注入方式有两个优点: 这种机制在Win 9x/Me和Win NT/2K中都是得到支持预计在以后版本中也将得到支持 钩子DLL可以在不需要时候可由我们主动UnHookWindowsHookEx来卸载比起使用注册表机制来说方便了许多尽管这是种相当简洁明了思路方法但它也有些显而易见缺点: 首先值得我们注意Windows钩子将会降低整个系统性能它额外增加了系统在消息处理方面时间 其次只有当目标进程准备接受某种消息时钩子所在DLL才会被系统映射到该进程地址空间中钩子才能真正开始发挥作用因此如果我们要对某些进程整个生命周期内API情况进行监控用这种思路方法显然会遗漏某些API



3.
使用 CreateRemoteThread在我看来这是个相当棒思路方法然而不幸CreateRemoteThread这个只能在Win NT/2K系统中才得到支持虽然在Win 9x中这个API也能被安全而不出错但它除了返回个空值的外什么也不做整个DLL注入过程十分简单我们知道任何个进程都可以使用LoadLibrary来动态地加载个DLL但问题是我们如何让目标进程在我们控制下来加载我们钩子DLL(也就是钩子驱动器)呢?这里有个APICreateRemoteThread通过它可在个进程中可建立并运行个远程线程

该API需要指定个线程指针作为参数该线程原型如下: Function ThreadProc(lpParam: Poer): DWORD;我们再来看下LoadLibrary原型: Function LoadLibrary(lpFileName: PChar): HModule;可以看出这两个原型实质上是完全相同(其实返回值是否相同关系不大我们是无法得到远程线程返回值)只是叫法区别而已这种相同使得我们可以把直接把LoadLibrary当做线程来使用从而在目标进程中加载钩子DLL

类似当我们需要卸载钩子DLL时也可以FreeLibrary作为线程来使用在目标进程中移去钩子DLL切看来是十分简洁方便通过GetProcAddress我们可以得到LoadLibrary地址由于LoadLibrary是Kernel32中而这个系统DLL映射地址对每个进程来说都是相同因此LoadLibrary地址也是如此这点将确保我们能把该地址作为个有效参数传递给CreateRemoteThread使用
AddrOfLoadLibrary := GetProcAddress(GetModuleHandle(‘Kernel32.dll’), ‘LoadLibrary’);
HremoteThread := CreateRemoteThread(HTargetProcess, nil, 0, AddrOfLoadLibrary, HookDllName, 0, nil);

要使用CreateRemoteThread我们需要目标进程句柄作为参数当我们用OpenProcess来得到进程句柄时通常是希望对此进程有全权存取操作也就是以PROCESS_ALL_ACCESS为标志打开进程但对于些系统级进程直接这样显然是不行只能返回空句柄(值为零)为此我们必须把自己设置为拥有调试级特权这样将具有最大存取权限从而使得我们能对这些系统级进程也可以进行些必要操作

4.

通过BHO来注入DLL 有时我们想要注入DLL对象仅仅是Internet Explorer幸运Windows操作系统为我们提供了个简单归档思路方法(这保证了它可靠性)―― 利用Browser Helper Objects(BHO)个BHO是个在 DLL中实现COM对象它主要实现了个IObjectWithSite接口而每当IE运行时它会自动加载所有实现了该接口COM对象

4、

拦截机制在钩子应用系统级别方面有两类API拦截机制――内核级拦截和用户级拦截内核级钩子主要是通过个内核模式驱动来实现显然它功能应该最为强大能捕捉到系统活动任何细节但难度也较大不在我们探讨范围的内(尤其对我这个使用Delphi人来说还没涉足这块领域因此也无法探讨);

而用户级钩子则通常是在普通DLL中实现整个API拦截工作这才是我们现在所重点关注拦截API般可有以下几种思路方法:
1. 代理DLL(特洛伊木马)个容易想到可行思路方法是用个同名DLL去替换原先那个输出我们准备拦截API所在DLL当然代理DLL也要和原来输出所有如果想到DLL中可能输出了上百个我们就应该明白这种思路方法效率是不高另外我们还得考虑DLL版本问题

2.改写执行代码有许多拦截思路方法是基于可执行代码改写其中个就是改变在CALL指令中使用地址这种思路方法有些难度也比较容易出错基本思路是检索出在内存中所有你所要拦截APICALL指令然后把原先地址改成为你自己提供地址

另外种代码改写思路方法实现思路方法更为复杂主要实现步骤是先找到原先API地址然后把该开始几个字节用个JMP指令代替(有时还不得不改用个INT指令)使得对该API能够转向我们自己实现这种思路方法要牵涉到系列压栈和出栈这样较底层操作显然对我们汇编语言和操作系统底层方面知识是种考验这个思路方法倒和很多病毒感染机制相类似

3.以调试器身份进行拦截另个可选思路方法是在目标中安置个调试断点使得进程运行到此处就进入调试状态然而这样些问题也随的而来其中较主要是调试异常产生将把进程中所有线程都挂起它也需要个额外调试模块来处理所有异常整个进程将直在调试状态下运行直至它运行结束
4.改写输入地址表这种思路方法主要得益于现如今Windows系统中所使用可执行文件(包括EXE文件和DLL文件)良好结构――PE文件格式(Portable Executable File Format)因此它相当稳健又简单易行要理解这种思路方法是如何运作首先你得对PE文件格式有所理解

个PE文件结构大致如下图所示: 般PE文件开始是段DOS当你在不支持Windows环境中运行时它就会显示“This Program cannot be run in DOS mode”这样警告语句接着这个DOS文件头就开始真正PE文件内容了首先是段称为“IMAGE_NT_HEADER”数据其中是许多有关整个PE文件消息在这段数据尾端是个称为Data Directory数据表通过它能快速定位些PE文件中段(section)地址在这段数据的后则是个“IMAGE_SECTION_HEADER”列表其中项都详细描述了后面个段相关信息接着它就是PE文件中最主要段数据了执行代码、数据和资源等等信息就分别存放在这些段中

在所有这些段里个被称为“.idata”段(输入数据段)值得我们去注意该段中包含着些被称为输入地址表(IATImport Address Table)数据列表每个用隐式方式加载API所在DLL都有个IAT和的对应同时个API地址也和IAT中项相对应个应用加载到内存中后针对每个API相应产生如下汇编指令:


JMP DWORD PTR [XXXXXXXX]

如果在VC中使用了_delcspec(import)那么相应指令就成为
CALL DWORD PTR [XXXXXXXX]

不管怎样上述方括号中总是个地址指向了输入地址表中个项个DWORD而正是这个DWORD才是API在内存中真正地址因此我们要想拦截个API只要简单把那个DWORD改为我们自己地址那么所有有关这个API将转到我们自己中去拦截工作也就宣告顺利成功了这里要注意自定义形式应该是API方式也就是stdcall方式而Delphi中默认是pascal方式也就是register方式它们在参数传递方式等方面存在着较大区别

另外自定义参数形式可以和原先API相同不过这也不是必须而且这样话在有些时候也会出现些问题我在后面将会提到因此要拦截API首先我们就要得到相应IAT地址系统把个进程模块加载到内存中其实就是把PE文件几乎是原封不动映射到进程地址空间中去而模块句柄HModule实际上就是模块映像在内存中地址PE文件中些数据项地址都是相对于这个地址偏移量因此被称为相对虚拟地址(RVARelative Virtual Address)

于是我们就可以从HModule开始经过系列地址偏移而得到IAT地址不过我这里有个简单思路方法它使用了个现有API ImageDirectoryEntryToData它帮助我们在定位IAT时能少走几步省得把偏移地址弄错了走上弯路不过纯粹使用RVA从HModule开始来定位IAT地址其实并不麻烦而且这样还更有助于我们对PE文件结构了解上面提到API是在DbgHelp.dll中输出(这是从Win 2K才开始有在这的前是由ImageHlp.dll提供)有关这个详细介绍可参见MSDN

在找到IAT的后我们只需在其中遍历找到我们需要API地址然后用我们自己地址去覆盖它下面给出段对应源码:
procedure RedirectApiCall; var ImportDesc:PIMAGE_IMPORT_DESCRIPTOR; FirstThunk:PIMAGE_THUNK_DATA32; sz:DWORD;
begin
//得到个输入描述结构列表首地址每个DLL都对应个这样结构 ImportDesc:=ImageDirectoryEntryToData(Poer(HTargetModule), true, IMAGE_DIRECTORY_ENTRY_IMPORT, sz);

while Poer(ImportDesc.Name)<>nil do
begin //判断是否是所需DLL输入描述
StrIComp(PChar(DllName),PChar(HTargetModule+ImportDesc.Name))=0 then begin
//得到IAT首地址
FirstThunk:=PIMAGE_THUNK_DATA32(HTargetModule+ImportDesc.FirstThunk);

while FirstThunk.Func<>nil do
begin
FirstThunk.Func=OldAddressOfAPI then
begin
//找到了匹配API地址 ……
//改写API地址
;
end;
Inc(FirstThunk);
end;
end;
Inc(ImportDesc);
end;
end;

最后有点要指出如果我们手工执行钩子DLL退出目标进程那么在退出前应该把地址改回原先地址也就是API真正地址旦你DLL退出了改写地址将指向个毫无意义内存区域再使用它显然会出现个非法操作

5、

替换编写 前面关键两步做完了个API钩子基本上也就完成了不过还有些相关东西需要我们研究包括怎样做个替换 下面是个做替换步骤: 首先不失般性我们先假设有这样个API原型如下:
function someAPI(param1: Pchar;param2: Integer): DWORD;

接着再建立个和的有相同参数和返回值类型:
type FuncType= function (param1: Pchar;param2: Integer): DWORD;
然后我们把someAPI地址存放在OldAddress指针中接着我们就可以着手写替换代码了:
function DummyFunc(param1: Pchar;param2: Integer): DWORD; begin ……
//做操作
result := FuncType(OldAddress) (param1 , param2);
//原先API ……
//做操作
end;

我们再把这个地址保存到NewAddress中接着用这地址覆盖掉原先API地址这样当目标进程该API时候实际上是了我们自己在其中我们可以做些操作然后在原先API结果就像什么也没发生过当然我们也可以改变输入参数甚至是屏蔽调这个API

尽管上述思路方法是可行但有个明显不足:这种替换制作思路方法不具有通用性只能针对少量如果只有几个API要拦截那么只需照上述说重复几次就行了但如果有各种各样API要处理它们参数个数和类型以及返回值类型是各不相同还是采用这种思路方法就太没效率了

确是上面给出只是个最简单最容易想到思路方法只是个替换基本构架正如我前面所提到替换和原先API参数类型不必相同我们可以设计个没有参数也没有返回值通过窍门技巧使它能适应各种各样API不过这得要求你对汇编语言有了解

下面我就对此详细 首先我们来看下执行到内部前堆栈情况(这里方式为stdcall) 由上面例图可知参数是按照从右到左顺序压入堆栈(堆栈是由高端向低端发展)同时还压入了返回地址在进入的前ESP正指向返回地址因此我们只要从ESP+4开始就可以取得这个参数了每取个参数递增4另外当从中返回时般在EAX中存放返回值



了解了上述知识我们就可以设计如下个比较通用替换它利用了Delphi内嵌式汇编语言特性
Procedure DummyFunc;
asm add esp,4 mov eax,esp//得到第个参数
mov eax,esp+4//得到第 2个参数 ……
//做些处理这里要保证esp在这的后恢复原样
call OldAddress //原先API ……
//做些其它事情
end;

当然这个替换还是比较简单你可以在其中些纯粹用OP语言写或过程去完成些更复杂操作(要是都用汇编来完成那可得把你忙死了)不过应该这些方式统设置为stdcall方式这使它们只利用堆栈来传递参数因此你也只需时刻掌握好堆栈变化情况就行了如果你直接把上述汇编代码所对应机器指令存放在个字节然后把地址当作地址来使用效果是

以上代码在 Win 2K/xp & Delphi 6.0 中实现


6、后记
个API钩子确是件不容易事情尤其对我这个使用Delphi人来说为了解决某个问题经常在OP、C和汇编语言资料中东查西找调试中还不时发生些意想不到事情自己是手忙脚乱不过好歹总算做出了个API钩子雏形还是令自己十分高兴对计算机系统方面知识也掌握了不少受益非浅当初在写这篇文章的前我只是想翻译篇从网上Down下来英文资料(网址为www.codeproject.com 文章名叫“API Hook Revealed”举例源代码是用VC这里不得不佩服老外水平文章写得很有深度而且每个细节都讲十分详细)

不过翻着翻着就觉得自己真是水平有限很多地方虽然自己明白了就是不知怎样用中文来表达意思才明确于是只得把已经翻译出来觉得还可以那部分加上些自己在实际操作中体会和心得体会杂凑出了上面这篇文章尽管可能有些不伦不类但也是化了我很多时间呕心沥血啊(惭愧惭愧!)希望高手不要见笑请多多指教

Tags:  全局钩子 夺钩子 键盘钩子 钩子api

延伸阅读

最新评论

发表评论