API Hook基本原理和实现

时间:2022-03-04 08:18:05

windows系统下的编程,消息message的传递是贯穿其始终的。这个消息我们可以简单理解为一个有特定意义的整数,正如我们看过的老故事片中的“ 长江长江,我是黄河”一个含义。windows中定义的消息给初学者的印象似乎是“不计其数”的,常见的一部分消息在winuser.h头文件中定义。 hook与消息有着非常密切的联系,它的中文含义是“钩子”,这样理解起来我们不难得出“hook是消息处理中的一个环节,用于监控消息在系统中的传递,并在这些消息到达最终的消息处理过程前,处 理某些特定的消息”。这也是hook分为不同种类的原因。 
hook的这个本领,使它能够将自身的代码“融入”被hook住的程序的进程中,成为目标进程的一个部分。我们也知道,在windows2000以后的系 统中,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 hook的出现给我们开拓了解决此类问题的道路。 

api hook是什么? 
在windows系统下编程,应该会接触到api函数的使用,常用的api函数大概有2000个左右。今天随着控件,stl等高效编程技术的出现,api 的使用概率在普通的用户程序上就变得越来越小了。当诸如控件这些现成的手段不能实现的功能时,我们还需要借助api。最初有些人对某些api函数的功能不 太满意,,就产生了如何修改这些api,使之更好的服务于程序的想法,这样api hook就自然而然的出现了。我们可以通过api hook,改变一个系统api的原有功能。基本的方法就是通过hook“接触”到需要修改的api函数入口点,改变它的地址指向新的自定义的函数。api hook并不属于msdn上介绍的13类hook中的任何一种。所以说,api hook并不是什么特别不同的hook,它也需要通过基本的hook提高自己的权限,跨越不同进程间访问的限制,达到修改api函数地址的目的。对于自身进程空间下使用到的api函数地址的修改,是不需要用到api hook技术就可以实现的。 

api hook和pe格式的关系 
api hook技术的难点,并不在于hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握hook的基本使用技术。但是如何修改api函数的入口地 址?这就需要学习pe可执行文件(.exe,.dll等)如何被系统映射到进程空间中,这就需要学习pe格式的基本知识。windows已经提供了很多数 据结构struct帮助我们访问pe格式,借助它们,我们就不要自己计算格式的具体字节位置这些繁琐的细节。但是从api hook的实现来看,pe格式的访问部分仍然是整个编程实现中最复杂的一部分,对于经常crack的朋友不在此列。 
假设我们已经了解了pe格式,那么我们在哪里修改api的函数入口点比较合适呢?这个就是输入符号表imported symbols table(间接)指向的输入符号地址。 
下面对于pe格式的介绍这一部分,对于没有接触过pe格式学习的朋友应该是看不太明白的,但我已经把精华部分提取出来了,学习了pe格式后再看这些就很容易了。 

pe格式的基本组成 
+-------------------+ 
| DOS-stub | --DOS-头 
+-------------------+ 
| file-header | --文件头 
+-------------------+ 
| optional header | --可选头 
|- - - - - - - - - -| 
| | 
| data directories | --(可选头尾的)数据目录 
| | 
+-------------------+ 
| | 
| section headers | --节头 
| | 
+-------------------+ 
| | 
| section 1 | --节1 
| | 
+-------------------+ 
| | 
| section 2 | --节2 
| | 
+-------------------+ 
| | 
| ... | 
| | 
+-------------------+ 
| | 
| section n | --节n 
| | 
+-------------------+ 
在上图中,我们需要从“可选头”尾的“数据目录”数组中的第二个元素——输入符号表的位置,它是一个IMAGE_DATA_DIRECTORY结构,从它中的VirtualAddress地址,“顺藤摸瓜”找到api函数的入口地点。 
下图的简单说明如下: 
OriginalFirstThunk 指向IMAGE_THUNK_DATA结构数组,为方便只画了数组的一个元素,AddressOfData 指向IMAGE_IMPORT_BY_NAME结构。 
IMAGE_IMPORT_DESCRIPTOR数组:每个引入的dll文件都对应数组中的一个元素,以全0的元素(20个bytes的0)表示数组的结束 
IMAGE_THUNK_DATA32数组:同一组的以全0的元素(4个bytes的0)表示数组的结束,每个元素对应一个IMAGE_IMPORT_BY_NAME结构 
IMAGE_IMPORT_BY_NAME:如[email protected]@initialization$qqrv. 表示 
Unmangled Borland C++ Function: qualified function __fastcall Consts::initialization() 
 
为了减少这个图的大小,不得已将汇编和c++的结构都用上了。这个图是输入符号表初始化的情形,此时两个IMAGE_THUNK_DATA结构数组的对应元素都指向同一个IMAGE_IMPORT_BY_NAME结构。 
程序加载到进程空间后,两个IMAGE_THUNK_DATA结构数组指向有所不同了。看下图: 

 
始化的,“两个结构都指向同一个IMAGE_IMPORT_BY_NAME”,此时还没有api函数地址 

 
当PE文件准备执行时,前图已转换成上图。一个结构指向不变,另一个出现api函数地址 

如果PE文件从kernel32.dll中引入10个函数,那么IMAGE_IMPORT_DESCRIPTOR 结构的 Name1域包含指向字符串"kernel32.dll"的RVA,同时每个IMAGE_THUNK_DATA 数组有10个元素。(RVA是指相对地址,每一个可执行文件在加载到内存空间前,都以一个基址作为起点,其他地址以基址为准,均以相对地址表示。这样系统 加载程序到不同的内存空间时,都可以方便的算出地址) 
上述这些结构可以在winnt.h头文件里查到。 

具体编程实现 
我将手上的vc示例代码进行了适当修正,修改了一些资源泄漏的小问题,移植到c++builder6 & update4上,经过测试已经可以完成基本的api hook功能。有几个知识点说明一下: 
1、 dll*享内存变量的实现 
正常编译下的dll,它的变量使用到的内存是独立的。比如你同时运行两个调用了某个dll的用户程序,试图对某一个在dll中定义的全局变量修改赋值的时候,两个程序里的变量值仍然是不同的。 
共享的方法为:在.cpp文件(.h文件里如此设置会提示编译错误)的头部写上如上两行: 
#pragma option -zRSHSEG // 改变缺省数据段名 
#pragma option -zTSHCLASS // 改变缺省数据类名 

HINSTANCE hdll = NULL; // 用来保存该动态连接库的句柄 
HHOOK hApiHook = NULL; // 钩子句柄 
HHOOK hWndProc = NULL; // 窗口过程钩子用来拦截SendMessage 
int threadId = 0; 

另外建立一个与dll同名,不同后缀的def文件,如HookDll.def文件,写上: 
LIBRARY HookDll.dll 
EXPORTS 
;... 
SEGMENTS 
SHSEG CLASS ‘SHCLASS‘ SHARED 
;end 

这样设置后在.cpp文件中定义的变量,如果进行了初始化,将进入“SHCLASS”共享内存段(如果不初始化,将不改变其默认段属性)。 
 
上述的共享对于本示例代码并不是必须的,只是稍微演示了一下。 

2、 api hook修改api函数入口点地址的时机 
很显然,我们必须通过hook进入目标进程的地址空间后,再在位于该地址空间里的hook消息处理过程里修改输入符号表“指向”的api函数入口点地址, 退出hook前也必须在这个消息处理过程里恢复原来的地址。只要我们牢记修改的过程发生在目标进程的地址空间中,就不会发生访问违例的错误了。 
示例代码使用了WH_GETMESSAGE、WH_CALLWNDPROC两中hook来演示如何hook api,但WH_GETMESSAGE实际上并没有完成具体的功能。 
为了让初学者尽快的掌握重点,我将代码进行了简化,是一个不健壮、不灵活的演示示例。 

3、 函数的内外部表现形式 
例如api函数MessageBox,这个形式是我们通常用到的,但到了dll里,它的名字很可能出现了两个形式,一个是MessageBoxA,另一个 是MessageBoxW,这是因为系统需要适应Ansi和Unicode编码的两种形式,我们不在函数尾端添加“A”或“W”,是不能hook到需要的 函数的。 

4、 辅助pe格式查看工具 
PE Explorer是一个非常好的查看pe资源的工具,通过它可以验证自己手工计算的pe地址,可以更快的掌握pe格式。 
调试器ollydbg也是非常好的辅助工具,例如查看输入符号表中的api函数。 

5、 程序文件列表 
dll基本文件:Hook.h,Hook.cpp,HookDll.def 
client验证方基本文件:HookTest.h,HookTest.cpp,ApiHookTest.cpp 
 
 

6、 实现的功能 
对记事本的MessageBoxW函数进行了hook,先执行自定义的 
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR M1, LPCWSTR M2, UINT M3) 

return oldMessageBoxW(hWnd, M1, L"my api hook", M3); 

从这里可以看到,由于目标进程空间中的执行线程并不知道你已经改变了api函数的实际入口地址,它在调用时仍旧将参数一成不变的压入堆栈(这个说法是汇编代码时看到的等价情形),事实上你已经提前接收到了函数调用的所有参数。这里就是篇首帖子的回复了。 

 
hook之前 

 
hook以后 

示例代码 
1、client验证方的代码非常简单。建立一个Application工程,在窗体上放一个memo(提示信息),两个button(一个SetHook,另一个RemoveHook)。 
void __fastcall TForm1::Button1Click(TObject *Sender) 

DWORD dwProcessId, dwThreadID; 

HWND hWnd = FindWindow("Notepad", NULL); 
if (!hWnd) 

Memo1->Lines->Add("Nodepad is not found"); 

else 

dwThreadID = GetWindowThreadProcessId(hWnd, &dwProcessId); 
Memo1->Lines->Add(dwThreadID); 
SetHook(dwThreadID); 


//--------------------------------------------------------------------------- 
void __fastcall TForm1::Button2Click(TObject *Sender) 

RemoveHook(); 

//--------------------------------------------------------------------------- 

2、api hook dll稍微复杂些,建立一个dll工程之后,修改之。代码中有一些函数并未用上,ReplaceApiAddress是核心函数,完整代码参见附件。
参考文献 
1、《iczelion汇编程序设计教程》pe专题部分 
2、《WINDOWS核心编程》第22章 
3、《PE文件格式 1.9版》汉译版,原著B. Luevelsmeyer 
4、《跨进程API Hook》,出自,作者detrox 
5、《DLL木马注入程序》,出自 
6、另有两vc6下的源代码包,APIHOOK与pw,因时间久远,出处不明。在此对原作者的辛勤工作表示真挚的谢意。