Ring3下Hook API实现分析

     本文主要针对用户级别下HOOK API的方法进行一下总结。对应的,自然也有ring0下的HOOK API方法,但是这个需要一些驱动方面的基础,暂时不在本文讨论范围内。另外ring3下的HOOK API方法也有很多种,我只列举我所能想到的。

    所谓HOOK API,与Windows 下的HOOK其实是完全两个概念,风马牛不相及。当然在之后的讨论中你也会看到有关于windows hook的讨论,但是这是另外一个话题了。

    那么到底什么是HOOK API呢?我们可以暂且简单的认为HOOK API就是一种改变原始API功能的方法。最简单的例子莫过于MessageBox(其实是MessageBoxA或MessageBoxW),我们可以通过HOOK这个API改变其功能,比如换个标题。目的很明确,就是改变原始API的行为,但是方法有多种。再继续讨论之前我们先要搞清楚一个问题,HOOK API是HOOK谁的API调用?当然你可以自己写一个程序,然后在这个程序里HOOK所有本程序MessageBoxA/W的调用,但是更多的用途是HOOK其他进程的。比如你可以HOOK一个进程的winsock函数从而监视其网络行为。既然是跨进程的,势必会用到DLL注入,这篇文章中也会介绍到几种基本的方法。如果你还不知道DLL注入是什么或者有什么用的话也无妨,因为不影响接下来对HOOK API的理解,你可以暂时理解为对本进程的HOOK。

    接下来我们就针对HOOK API这个话题开始介绍,我们还是以MessageBoxA为例,它是定义在user32.dll中的一个函数。现在我们就假设我们要完成一个函数HookMessageBoxA(...), 在这个函数调用之后MessageBoxA的行为就改变了。HOOK API有几种方法意味着这个函数就有几种可能的实现,至于代码我会分段给出(所以可能不能直接拿来编译),只供参看。

 

1. 修改IAT

    我们从最常用的方法开始:修改IAT. 如果你还不知道IAT是什么原理,不妨跟我一起看一下API调用的原理(暂不考虑LoadLibrary/GetProcAddress).

    试想一下,在我们的程序里调用MessageBoxA意味着什么?如果你没兴趣深入研究,不妨让我直接告诉你,每一个API的地址都保存在一个表里,这个表就叫IAT(import address table). 而相应的调用就是对这个表中某一项的引用。那么这个表中的地址是什么时候来的呢?就是在PE加载的时候。如果你对IAT有兴趣,不妨看一下这篇文章: http://blog.csdn.net/panda1987/archive/2010/10/08/5928078.aspx

    现在你应该有思路了,既然我们知道API的地址保存在哪里,那我们只要更改这个地址的值,使它指向我们自定义的API不就可以了?是的,一点没错,而我们的关键是在于如果找到这个地址,这需要一些PE结构的基础。

乍一看似乎比想象的复杂了一点:

1. 因为我把几种HOOK API的方法合在一个项目里了,有些数据结构和函数可能考虑到了共用。

2. 因为我考虑的不单是一个API的HOOK,所以需要一个数组来保存每个HOOK的信息。

 

不过没关系,稍微解释一下就明白了。搞清楚两个问题上面的代码就没有任何难处了:

1. 为了找到IAT中的对应项,我们需要提供什么?

2. 为了能恢复到原始的状态(没有HOOK之前的状态),我们需要保存什么?

 

    我们先来看第一个问题,如果不考虑效率,我们只需要一个函数名或者函数地址。这也是提供了两个UpdateIATAddress的原因。当然如果能提供模块名(比如user32.dll)最好,能够提高效率。那么第二个问题是我们需要保存什么呢?原始API的地址必不可少,要不然怎么恢复?除此之外呢?新的API地址也一起保存下来吧,有时可能会用到,以防万一。另外还需要什么?理论上不需要了,但是从效率的角度考虑,我们也把对应IAT的地址保存下来了,否则下次恢复的时候又要重新找一遍。

    除此之外,我们还需要了解看一下函数WriteAddress(ULONG_PTR* pAddress, ULONG_PTR value). 这个函数就是把value写入地址pAddress中. 但是不能直接写,因为这个地址可能是只读的,我们需要通过VirtualProtect改变其内存属性然后再写入。

  那么我们的HookMessageBoxA呢?就很简单了:

  这里我们使用了MyMessageBoxA_1作为我们新的MessageBox. 所以当HookFunction_1之后的MessageBoxA调用的时候其实是走到了MyMessageBoxA_1中。那么还有一个问题,在我们自定义的MessageBoxA中如何使用原始的(也就是user32.dll中的)MessaegBoxA?在这个例子中比较简单,我们使用GetProcAddress就可以获得并直接使用。但是在后面的例子中会发现没有这么简单。另外在修改IAT这个方法中还有一个需要注意的地方,就是HookFunction_1对应的第三个参数。在这个例子中我们使用了"HookAPI.dll",因为接下来MessageBoxA的调用在DLL中,如果需要Hook EXE中的MessageBoxA,那么我们需要相应的把这个参数改为NULL. 这意味着如果我们需要HOOK一个进程所有的MessageBoxA的调用,我们需要遍历所有的模块逐个HOOK. 这个工作量还是比较大的。

 

  接下来我们总结一下用修改IAT这种方法进行API HOOK有什么问题:

1. 如果需要HOOK一个进程的API, 我们需要HOOK该进程所有的模块。甚至我们还要处理新导入的模块。

2. 如果这个API是用GetProcAddress获得的,这个办法失效。

3. DelayLoad道理同上。

4. 一些可能的特殊情况: http://www.codeproject.com/KB/DLL/Win32APIHooking_Trouble.aspx

 

 

2. Jump

这个方法解决了修改IAT方法的弊端。我们所做的是直接修改MessageBoxA的开始几个字节,使他跳转到我们自己的函数。这个时候问题来了,我们如何在我们新的API里调用原始的?肯定不能直接用了,因为它已经被我们更改过了。我们需要在使用之前恢复到原始状态。那么还有一个问题,保存几个字节为好?我们需要的无非就是一个Jump指令,Jump指令有两种:相对跳转和绝对跳转。相比而言相对跳转更简单,也更节约空间:E9 XXXX(相对地址). 另外使用相对跳转的好处还有一点:x86和x64格式一样,都是5个字节。但是问题来了,相对跳转能跳多远?正负2G.对于x86已经足够了,因为0x80000000(当然你可以设置系统空间为1G,这里不考虑)开始属于系统空间,而我们定义的API跟user32中的API都同时位于用户空间。而对于x64来说,2G太小了。所以我们不得不在必要的时候使用绝对跳转。x86和x64下有不同的指令格式:

FF 25 XXXX

YYYY

这个是x86的. XXXX表示下一行指令的绝对地址,而YYYY表示我们的API的绝对地址。总共10个字节。

 

FF 25 0000

YYYYYYYY

这个是x64的. YYYYYYYY表示我们的API的绝对地址。总共14字节。

 

理解了这些,我们先看两个函数:

代码不难理解。那么在这种方法中我们需要保存什么呢?原始API地址和新的API地址毫无疑问是我们需要的,除此之外我们还需要记录被修改的字数以及字节数,另外由于这种方法的特殊性,我们需要记录一个状态能够知道当前是否处于HOOK状态。这些信息我们存在一个指针中:mSavedData. 第一个字节表示HOOK状态,第二个字节表示修改的字节数,其余的字节表示被修改的字节,用于以后恢复。

原本在几个cpp中的代码我全部放到这里了,UpdateOriginalFunction用于交换原始API的开头的几个字节与保存下来的字节。为什么是交换?将保存下来的字节恢复到原始API不就可以了么?是的,其实这就足够了,但是我们为了方便再次HOOK,我们交换来交换去,这样方便很多。所以在新的API中如果要使用原始的API我们所需要做的就像这样:

另外在HookFunction_2中的if(pHookInfo != NULL)这个条件你不用关心,这个是处理已经HOOK以后再次HOOK,属于特殊情况。我们关心else部分就足够了。

 

确实,这个办法要比第一种好很多。我们不需要针对每个模块逐个HOOK了,也不担心GetProcAddress引起的问题。但是,这个方法确不能很好地在多线程环境中工作,因为如果在恢复原始API之后线程切换并调用了这个API,那么我们是HOOK不到的。这个不难理解。

 

3. 拷贝函数

这个方法的想法是这样的,我们可以在内存中拷贝一份原始的实现。这样我们如果需要调用原始函数的时候就不需要跟第二种方法一样恢复原始API了,同时也很好的解决了多线程的问题。但是新的问题产生了,哪里去找原始API的实现?即使我们知道MessageBoxA位于user32.dll中,我们也知道这个函数位于user32的具体地址。但是我们如何知道函数到哪里结束?如果这个函数简单的不能再简单,或者我们能推测这个函数的长度,然后拷贝一份到内存。但是通常没有这么简单。有一个相对比较好的变通的方法,我们可以拷贝整个DLL的实现(DLL的长度不难获得),然后根据这个函数在这个DLL中的地址推测在新的API在拷贝内存中的地址: x = baseNewAlloc + (oldAddrOfAPI - oldAddrOfDLL)

这样我们唯一需要做的就是两件事情1. 拷贝DLL. 2. 更新原始DLL中的API使其跳转.

 

这种方法有一个不足的地方就是占用内存。一个DLL可能有好几兆,就为了HOOK一个API花的代价似乎有点大。这种方法我没有尝试,不难实现。

 

4. Bridge

这种方法相比第二种具有多线程性,相比第三种不需要浪费这么多的内存。应该说是一种值得考虑的HOOK API方法。我们回想一下第二种方法,我们在一个API的头部写入了一个jump指令,虽然我们把原来的字节都保存下来了,但是如果不恢复到原始API中的话这些字节其实是没有意义的。因为很有可能我们在写入Jump的时候把一个指令拆分了。试想,如果有一种方法能够取到一个完整的指令,那我们是不是可以保存几条完整的指针到某一个地方,然后再跳转到原始API的某个位置(同时也是一条完整指令的开始)。具体说来大概是这样(以MessageBoxA为例):

 

7657FEAE 8B FF mov edi,edi

7657FEB0 55    push ebp

7657FEB1 8B EC mov ebp,esp

7657FEB3 6A 00 push 0

 

其实这不是一个很好的例子,因为写入一个jump需要5个字节,而5个字节正好是一个指令的结尾。现在想象一下第三条指令需要三个字节,那么5个字节正好位于这条指令的中间。于是,我们可以这样:

1. 在原始API的开头位置写入一个Jump(5个指令). Jump到哪里呢?当然是我们自己的API.

2. 拷贝6个字节的指令到一个内存地址(我们称之bridge). 这样这6个字节就是完整的,可以独立的运行.

3. 在这个bridge的后面,也就是第7个字节再写入一个Jump. 这次Jump到哪里呢?Jump到原始API的第7个字节。这又是一个指令的开始。

 

再回顾一下,我们把原始的API分割成两个完整的部分,然后通过一个Jump(bridge中的Jump)将这两个部分连接起来。那么bridge什么时候会用到?就是在我们自己的API里面!是不是很巧妙?

 

别忘了还有一个问题我们没有解决,如何才能判断一个完整的指令?有一个第三方的库可以帮助我们完成这个事情:distorm. 它不但支持x86还支持x64。通过这个工具,我们可以从一串二进制代码中分析出对应的汇编代码。

 

 

几点说明:

1. gBridgeBuffer是VirtualAlloc出来的一个内存块. 用于存放所有的Bridge.

2. gBufferIndex只是用于HOOK多个API时能够把Bridge连续的存放在gBridgeBuffer中.

3. 每个HOOK API对应的bridge的地址其实存放在mHookBridge中. 在新的API中我们正是通过这个值获得对应的bridge地址.

 

 

到此为止似乎这种办法没有任何弊端。但是再仔细看看CreateBridge中的一段代码:

这个代码是我在HOOK x64下的MessageBoxA/W是加上的。看一下注释不难发现有些指令可能是"相对"的,什么意思呢?就是指令操作数是相对的,我们一旦把这条指令拷贝到bridge中就挂了。当然我们可以手动的修改操作数使他指向原始位置,但是这无疑也是一个有难度的工作,因为我们不知道哪条指令是"相对"的,而且每条相对指令我们的处理方法也不是统一的,因为操作数位于指令的位置和长度可能不一样。我们所能做的就是遇到一个处理一个。

 

5. INT 3

还有一个HOOK API的方法就是利用SEH(不知道SEH是什么东西的话不妨自己百度下), 我们所要做的就是在API的开头写入0xCC(一个字节, 当然同时要保持原来的字节). 当这个API被调用到的时候就会产生一个软件异常,并调用异常处理函数,这里的异常处理函数就成了我们的新的API,我们需要提供这个异常处理函数并加入到SEH链表中(网上有很多教程)。在这里我们需要关心的是在这个异常处理函数中我们需要做什么,当然第一件事情是恢复原来的那个字节,然后让程序重新执行这条异常指令(这个是SEH提供的功能之一:再次执行异常指令)。这个时候不会再次产生异常,因为我们已经恢复了原始的API状态。结束了么?没有,我们还要找一个合适的时间把0xCC再次写入这个API的开头,否则下次再调用到这个API就HOOK不到了。这种方法我没有尝试,理论上没有问题。但是用这种方法似乎很难解决同时HOOK多个API的问题,当异常处理函数被调用的时候我如何知道是哪个HOOK函数被调用了呢?no idea about this...

 

远程HOOK API的问题...

关于HOOK API的问题暂且介绍到这里,之前我们提到了HOOK API往往是对其他进程进行HOOK. 现在我们针对这个问题再多说几句。我们先理一下思路,现在假设我们已经选定了一种方法(Jump-修改API的开始几个字节)对MessageBoxA进行HOOK, 我们需要做什么?

1. 我们需要提供一个自定义的MyMessageBoxA. 这个不难,通过VirtualAllocEx和WriteProcessMemory我们可以把一个函数拷贝到另外的进程中。

2. 我们还需要修改远程进程中原始API的开头几个字节。这个也可以实现,通过VritualProtect和WriteProcessMemory可以做到。新API在远程地址的地址是什么?就是第一步VirtualAllocEx返回的地址。

乍一看似乎已经完成了,至少当MessageBoxA被调用的时候能够成功得跳转到我们的MyMessageBoxA中。但是问题来了,在MyMessageBoxA里面我们能做什么?假设我们只想简单地调用原始API,怎么实现?思考一两分钟以后你会发现这是个严峻的问题。当然我们可以通过ReadProcessMemory在写入jump指令前先保存起来,但是保存在哪里?是我们自己的进程中。而MyMessageBoxA的调用是在目标进程中,如果要访问的话还要通过进程间通信。一个相对比较简单的办法是把这些字节也写到目标进程中,但是没这么简单,写到哪里?MyMessageBoxA怎么知道这个地址?其实这个问题我在研究Self Delete的时候就遇到过,一个可行的办法是把这些字节写在MyMessageBoxA的前面,然后通过一些标记字节寻找。不管怎样,这不是一个好的选择,工作量太大。

再来想一个问题,可能你只是想尝试这个办法是否能成功,所以在MyMessageBoxA你只是简单的调用了MessageBoxW. 可行么?答案是否定的...因为IAT变了...不明白的话再回想一下API调用的原理。实际上在MyMessageBoxA中我们不能使用任何API. 可见使用这种方法进行远程API的HOOK是没有什么意义的,虽然存在理论上的可行性。

 

既然这样,我们需要寻找一种更加灵活的办法。DLL无非是最好的选择,我们知道当一个DLL被加载到一个EXE的时候它其实已经成了这个EXE的一部分,它们共享同一个空间,而在这个DLL中做任何处理就像EXE自己的代码一样。万事俱备只欠东风了,我们唯一要做的是把这个DLL加载到目标EXE中,而我们希望做的事情可以放在DllMain中,这样一旦DLL被加载我们的代码就被调用了。接下来的话题就跟HOOK API没有关系了。

 

我们有很多种方法使一个DLL加载到目标进程,网上也可以找到很多这方面的话题。这里我主要介绍两种最常用的: 1. CreateRemoteThread+LoadLibrary. 2. SetWindowsHook.

 

CreateRemoteThread+LoadLibrary

CreateRemoteThread顾名思义是创建一个远程线程,我们先看一下这个函数的原型:

我们最关心的参数有两个lpStartAddress和lpParameter. 很显然,这个是线程函数和参数的地址。但是,别忘了这些都是在目标线程中的地址,我们需要先拷贝到目标进程中。这个线程函数是我们随便定义的,只需要满足一个条件:只有一个指针型参数。既然如此,LoadLibrary是不是也满足?而且LoadLibrary的地址在每个进程中都一样,我们只要拷贝lpParameter到目标进程就可以了。代码如下:

这两个函数分别实现了DLL到目标进程的加载和卸载。代码就不多解释了。

 

SetWindowsHook

另外一种常用的方法是windows hook. 前面已经说过,windows hook跟hook api没有任何关系。windows hook常用于截获windows的消息,而在这里我们甚至没有用过这个功能,我们只是利用了windows hook的一个附带作用:会把DLL挂入目标进程。而接下来的一些基础知识有助于你对之后代码的理解:

1. SetWindowsHook需要提供一个相当于winproc一样的函数,消息来临时会被调用。另外还有一个参数可以指定希望HOOK的消息类型。

2. 并不是SetWindowsHook之后DLL就被加载到目标进程了,而是要等待第一个消息的来临。

3. 全局的SetWindowsHook会在一定程度上影响效率。而在我们的例子中我们只希望用它来挂入DLL,所以最好能在DLL挂入以后卸载掉这个windows hook. 但是在DLL挂入以后如果直接调用UnhookWindowsHook来解决这个问题是不可行的,因为DLL会被卸载(别忘了我们的自定义API就定义在DLL中)。

4. UnhookWindowsHook会导致DLL被卸载是因为DLL的引用计数为0了。LoadLibrary可以增加引用计数。

好了,整理一下思路,再来看下面的代码:

有了之前的基础,代码应该不难理解。

 

一个有意思的问题...

看到这里,其实这个话题已经结束了。但是如果你还是兴致盎然的话,不妨再看一个在我开发中遇到的一个有意思的问题。这其实也是我们"精益求精"的后果。为什么这么说呢?先来看一下我想做什么。

1. 我需要一个EXE简称A. 在这个EXE里面弹出一系列的MessageBoxA.

2. 我还需要一个EXE简称B, 上面有一个按钮。点击一下就HOOK住A中的MessageBoxA. 再点击一下UNHOOK. 如此往复。

是不是很简单?只要对这个按钮循环调用InjectDll/UnmapDll就可以。还记得我们有两套机制都提供了InjectDll/UnmapDll: 1.CreateRemoteThread+LoadLibrary. 2. windows hook. 第一种很显然InjectDll和UnmapDll分别会加载和卸载DLL. 而第二种由于我们考虑到了windows hook可能会影响到效率所以我们没有提供HookAPI/UnHookAPI这样的函数, 而是直接通过DLL的加载和卸载同时实现API的HOOK和UNHOOK. 但是问题来了, 试想下面的情形:

1. 启动A. 一个MessageBoxA弹出。这个时候是没有HOOK的。

2. 启动B. 点击按钮. 这个时候HOOK了(DLL加载了).

3. 点击A中MessageBoxA的OK. 又一个MessageBoxA弹出. 我们发现这个时候是HOOK住了.

4. 再次点击B中按钮. 这个时候我们希望A的下一个MessageBoxA是UNHOOK的. (DLL卸载了)

5. 点击A中MessageBoxA的OK. 程序异常退出。

没有跟我们设想的那样. 问题在哪里?

第三步弹出的MessageBoxA是HOOK住的,也就是在MyMessageBoxA中调用的。而MyMessageBoxA定义在HookAPI.dll中。第四步显然会把HookAPI.dll卸载掉. 而第五步我们点击OK以后其实程序的返回地址是在HookAPI.dll中, 而此时HookAPI.dll已经不在内存中了. 所以非法访问内存地址, 程序异常退出.

解决的办法当然很简单, 我们可以提供一个HookAPI/UnHookAPI这样的函数, 在B的按钮点击之后不卸载DLL, 而是简单的UNHOOK. 但是为什么说这个问题有意思呢, 是因为我联想到了Self Delete时遇到的一种方法。有没有什么办法只修改MyMessageBoxA让程序正常运行下去呢?也就是说,在MyMessageBoxA里面调用MessageBoxA之后要立刻返回到A(EXE)的地址空间(也就是下一条MessageBoxA的地址). 办法跟Self Delete其中一个方法如出一辙, 有兴趣的话可以看一下我之前那篇介绍Self Delete的文章. 这里我只给出代码, 不多解释了:

 

 

Over...Thanks for reading:)

  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值