[转] Delphi API HOOK 完全说明

来源:互联网 发布:sql注入直接写入一句话 编辑:程序博客网 时间:2024/05/21 06:12

// 本文转自网络, 原始出处不明确.
// 转载目的: 学习 + 分享


一、关于 API Hook


1. 什么是 API Hook
不知道大家是否还记得,在 DOS 系统中编程,经常会采取截取中断向量的技术:我们可以设置新的中断服务程序,当系统其他的程序调用这个中断时,就让它先调用我们自己设置的新的中断服务程序,然后再调用原来的中断服务程序,这样就能够获得非凡的控制权。许多优秀的软件和大多数 DOS 病毒程序都采用了这个方法。

在 Windows 中,我们也可以采取类似技术。当系统调用某个 API  函数时,就会先进入我们自己的函数,然后再调用原来的 API 函数,这样,我们的程序就可以取得更多的控制权,我们就可对 Windows 系统中的任意一个函数调用进行动态拦截、跟踪、修改和恢复,就可让 Windows 系统中的任意一个函数按我们的设想工作。这种技术有许多名称,比如“陷阱技术”、“重入技术”等,不过我认为还是 API Hook 最贴切。原因嘛,等一下你看编程就明白了。

这样重要的技术,大家已经都知道了吧?哈哈,知道的都不说,不知道的呢,你就自己慢慢去摸索吧。偶尔有一两篇文章见于报端,不是藏头露尾,就是已经过时了。还有的即使把原理都告诉你了,但就是不说它调用了哪些函数。要源代码?行,拿钱来。有人说了,这种技术用来编病毒最合适,所以..(因为菜刀可以杀人,所以菜刀已被禁止使用了)。而实际呢,你看看使用了这种技术的国产软件就知道了:金山词霸、东方快车、RichWin、东方词圣..在这里,我感觉有必要简单说说金山词霸的工作原理。

2. 金山词霸的工作原理
大家都用过金山词霸吧?当你把光标指向一个单词,词霸就会自动弹出一个窗口并把单词的意思翻译出来。这究竟是怎么做出来的呢?我在这里简单说明一下。

(1) 安装鼠标钩子。
(2) 一旦光标在屏幕上移动,系统就会调用鼠标钩子,词霸通过鼠标钩子能够获得光标的坐标 (x, y) ,并安装 TextOut()、ExtTextOut() 等 API 函数钩子。
(3) 词霸向光标下的窗口发重画消息,负责绘制该点的应用程序在收到 WM_PAINT 消息后,就可能调用 TextOut()、ExtTextOut() 等函数重绘字体。
(4) 调用的函数将被钩子函数拦截,词霸就能够截获该次调用,从应用程序的数据段中将“文字”指针的内容取出,并作翻译处理。
(5) 退出跟踪程序,返回到鼠标钩子,解除对 TextOut()、ExtTextOut() 等 API 函数的跟踪。
(6) 完成了一次“屏幕抓字”。
这里的关键有两点:安装鼠标钩子和 API 钩子。安装鼠标钩子非常简单,而 API 钩子正是取词的核心代码。

3. 关于 Delphi
事实上,随着互联网的普及,许多秘密都已不再是秘密,API Hook 也一样。在网上,你已经可以找到这样的免费源代码,但是大部分可能已经过时,而且这些源代码大都是基于 VC++ 的。如果你想找到用 Delphi 编写的源代码,那么,你还是读一读我的文章吧。
Delphi 是编程工具史上的一个里程碑式的作品。如果你在使用它,我向你表示祝贺。如果你没有使用它,你也没有什么损失。网上关于几种语言谁好谁坏都吵得天翻地覆的了,我不想增加新仇也不想算算旧恨。每种语言都有它的优缺点,每个人都有自己选择的权利嘛!
不过,用 Delphi 编写 API Hook 有几处“陷阱”。我想,除了介绍 API Hook 以外,这也是为什么我要写这篇文章的一个原因吧!

4. 哪些人可以读这篇文章
当然,读这篇文章并没有什么限制。但是你最好已经懂得鼠标钩子的制作过程,手边有 MSDN 那就再好不过了。我认为,只要你是 Windows 的程序员,就一定要有 MSDN。原因?有一套就明白啦。如果你懂得 PE 文件结构,那就更好了。在这篇文章里,我给出了所有的源代码(还不到 200 行)。如果你想修改程序,最好用 SoftIce。

5. 关于我的程序
本文中的程序在 Windows Me 的操作环境下,使用 Delphi5.0 编程调试通过。无论是商用还是个人使用,你都可以随意使用和修改本文中的程序,并且不需要在程序中加注我的个人信息。


二、用 Delphi 编写 API Hook


1. 改写 API 函数

为了使我们改写的代码正确运行,我们的函数必须和要改写的 API 函数具有同样形式的形参。在我的程序中,我拦截了 MessageBoxA 和 MessageBoxW 两个函数。所以我这样定义自己的函数:

注意到我使用了 stdcall 关键字,这是为了我的函数的形参的进出栈顺序与我们要拦截的函数一致。我们知道,为了系统的安全,Win32 并不允许直接改写内存中的代码段。所以,有人想了好多种方法绕过系统的保护。实际上,Win32 为了我们能安全地改写内存中的代码,提供了一个函数:WriteProcessMemory。有许多人曾经告诉我,WriteProcessMemory 也不能用来改写,不过我一直使用得很好。也许用它产生了一些 BUG,只是我并不知道罢了,所以还请这方面的专家指正。在 PE 文件中,当你呼叫另一模块中的函数(例如 USER32.DLL 中的GetMessage),编译器编译出来的 CALL 指令并不会把控制权直接传给 DLL 中的函数,而是传给一个 JMP DWORDPTR [XXXXXXXX] 指令,[XXXXXXXX] 内含该函数的真正地址(函数进入点)。为了得到 API 函数的地址,我们可以这样:Address := @MessageBoxA。如上文所说的,我们得到的仅仅是一个跳转指令,后面紧接着的才是 MessageBoxA 真正的开始代码的地址(具体可以查阅 PE 文件的资料)。在下面的程序中我自定义了一个结构(叫记录?我习惯了,改不过口来),注意,这里使用了 packed 关键字:

其中,PPointer = ^Pointer ;
用以下函数返回函数的真正地址:

这样,只要用我们的函数的地址替代它就可以了。替换函数:

你新建一个 Unit APIHook,把上面的函数和结构写进去并保存下来。

2. 第一个程序
新建一个 Application TRY1,主 Form 的单元名称不妨叫 TRYUnit1。把上面的 Unit APIHook 加进来。再新建一个 UnitMESS,添加如下代码:

在主窗体上添加三个按钮,添加 Onclick 代码,如下:

记得要保存,在后面我们还要使用它们。编译一下,
运行..啊哈,成功了!且慢,别高兴得太早。如果现在新建一个 Application TestTry,在 Form 上添加一个按钮,OnClick 事
件如下:

先运行 TRY1,再运行 TestTry。结果呢?原来,API Hook 仅仅在 TRY1 中挂上了,并没有在所有的系统进程中挂上。
你也许听说过,必须把我们的函数和 API_Hookup、Un_API_Hook 放在一个动态链接库里(这是对的)。那么,你就试试吧。如果你这样做了,也并不能在所有的系统进程中挂上。原因很简单,我们仅仅改变了进程指向 API 函数的指针,系统中其他的进程还是各管各的。

3. Hook Hook Hook Hook Hook Hook..
我们想想做鼠标钩子时的做法:用 SetWindowsHookEx 挂上鼠标钩子,当其他的进程发出鼠标消息时,我们的程序就会拦截到并作出响应。我们还可以用 UnhookWindowsHookEx 解除鼠标钩子。我们也必须为我们的函数挂上钩子。不过,鼠标有各种消息响应其他进程,我们的进程有什么消息呢?如果没有消息又怎样响应其他进程呢?即使我们自定义了消息,其他的进程又怎样“懂得”我们的消息呢?真像走入了绝境。
不用怕,至少我们有两种方法。一是完全模仿 SetWindowsHookEx,编制自己的 MySetWindowsHookEx,我看过大多数的 API Hook 程序里都用了这个方法。其实,我们不必舍近而求远,我们完全可以继续使用 SetWindowsHookEx,因为系统还为我们提供了一个函数:GetMsgProc。在 Delphi 中输入 GetMsgProc,然后光标停在上面,按 F1 键。怎么样,Delphi 帮助里讲得够清楚的吧?好了,我们的消息也有了。
想一想吧:我们在动态链接库中挂上 WH_GETMESSAGE 消息钩子,当其他的进程发出 WH_GETMESSAGE 消息时,就会加载我们的动态链接库,如果在我们的 DLL 加载时自动运行 API_Hook,不就可以让其他的进程挂上我们的 API Hook 吗?

4. 第二个程序
说干就干。新建一个动态链接库 Library TryDLL,把原来的 Unit APIHook 和 Unit MESS 加进来。Library TryDLL 的代码如下:

当然,别忘了对 mess 做相应修改。编译好后别忘了存盘。新建 Application TRY2 程序,主 Form 的单元名称不妨叫 TRYUnit2,在 Form1 上添加三个 Button,并声明:

三个 Button 的 OnClick 代码如下:

编译好后运行吧。这次倒好,无论你怎样测试,都得不到钩上的信息。整理一下我们的工作吧:TRY2 运行 --> TryDLL 加载 --> 运行 API_Hookup,为 TryDLL 挂上 MyBox。即使我们按下了 Button1,其他的进程加载了 TryDLL,也仅仅是运行 API_Hookup,为 TryDLL 挂上 MyBox 而已,而其他的进程(包括 TRY2 本身)并没有挂上 MyBox。不信,你可以在 TryDLL 中加上一个启动 MyBox 的函数,测试一下。例如,你可以在 TryDLL 的 StopHook 函数中的 UnhookWindowsHookEx 语句前,加上一句:MessageBoxW(0, 'MessageBoxW', '这是测试DLL是否加载了MyBox', MB_OK); 你可以看到弹出窗口中的信息是“成功挂上W!”而不是“MessageBoxW”。并且由于我们的 TryDLL 加载时就启动了 API_Hookup,卸载时才运行 Un_API_Hook,所以不论你是否按下 Button1 和 Button3,并且不论你按下了几次,每次你按下 Button3 都会得到“成功挂上W!”的信息。看来,真正的麻烦才刚刚开始。实际上,我们刚才的工作都是有用的。我们剩下的工作就是改进 Unit APIHook 中的 TrueFunctionAddress 函数而已。想不到吧?为了能让其他的进程挂上 MyBox,我们必须了解一下 PE 文件的格式。

3. PE文件格式
分配表、页模式、虚拟内存、内存映射..够写一本书的了吧?我这儿只想简单地说两句。PE 文件格式是 Windows 9X 以上版本和 Windows NT 操作系统中广泛采用的 32 位可执行文件格式。与 16 位的 NE 格式不同的是,如果在内存中建立 Module(Module这一术语通常用来表示已装入内存的可执行文件或 DLL 的代码、数据及资源,除了程序中直接用到的代码和数据外,一个 Module 也指用于判定代码及数据在内存中位置的支撑数据结构),则这个 Module 中的代码、数据、输入表、输出表以及其他有用的数据结构等使用的内存都放在一个连续的内存块中。编程人员只要知道装载程序文件映像到内存后的地址,即可通过映像后面的各种指针找到 Module 中的所有内容。具体来说,PE 格式中的许多项是以 RVA(相对虚拟地址)方式指定的,RVA 就是某个项相对于文件映像地址的偏移。例如,装载程序将一个 PE 文件装入到虚拟地址空间,从 0x10000 开始的内存中,如果 PE 文件中某个表在映像中的起始地址是 0x10800,那么该表的 RVA 就是 0x800。将 RVA 转化为可用的指针,只要将 RVA 的值加上 Module 的基地址即可。基地址是指装入到内存中的 EXE 或 DLL 程序的开始地址,它是 Windows 编程中的一个重要概念。为了方便起见,Win32 将 Module 的基地址作为 Module 的实例句柄(Instance Handle)。在 Win32 中,你可以直接调用 GetModuleHandle 取得指向 DLL 的指针,通过该指针访问该 DLL Module 的内容。
PE 文件格式可执行文件共有五部分组成:MS-DOS 首部、PE 首部、信息块表、信息块、辅助信息。MS-DOS 首部是一个极小的 DOS 程序,一般是为了显示像“Thisprogram cannot be run in MS-DOS mode”这类的信息。在 Delphi 中,其定义在 PImageDosHeader 结构中。该首部还给出了 PE 首部结构的起始地址,_lfanew 字段就是真正 PE 首部的相对偏移。PE 首部在 Delphi 中定义为 PImageNTHeaders 结构,该结构是由一个双字的标志项和两个子结构构成:

标志项是为了说明该可执行文件是“PE/O/O”、“NE”还是“LE”。TImageFileHeader 包含了编译器产生的 COFF OBJ 信息。TImageOptionalHeader 包含有堆栈初始大小、块表大小、数据目录(Data Directory)及其他一些重要信息。你并不需要知道 TImageOptionalHeader 的所有字段。最重要的两个字段是 ImageBase 和 Subsystem。其中还有一个重要的字段——IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]。数组一开始的元素内含可执行文件重要部位的 RVA 及大小。数组的第一个元素代表 exported function table(如果有的话)的地址和大小,第二个元素代表 imported functiontable 的地址和大小,依此类推。在我们的程序里,用它得到 RVA,RVA := NT^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
这样,我们也得到了 exported function table 和 imported function table。这里有必要谈一谈 importedfunction table。
EXE/DLL 在被加载内存之前,存放在 PE 文件的 imported table(或称为 .idata)中的信息是给加载器用来决定函数地址并修补它们,以便完成 image 用的。而在被加载之后,.idata 内含的是指针,指向 EXE/DLL 的输入函数。.idata section(imported table)是以一个 IMAGE_IMPORT_DESCRIPTOR 数组开始。在 PE 文件中联结(implicitly link)的 DLLs 都会在此有一个对应的 IMAGE_IMPORT_DESCRIPTOR 结构。最后一个 IMAGE_IMPORT_DESCRIPTOR 结构的内容全部为 NULL,以此作为结束符号。 IMAGE_IMPORT_DESCRIPTOR 的格式描述如下:

IMAGE_IMPORT_DESCRIPTOR 结构中,最重要的部分是 imported DLL 的名称以及两个 IMAGE_THUNK_DATA DWORDs 数组。每一个 IMAGE_THUNK_DATA DWORDs 对应一个输入函数。在 EXE 文件中,两个数组(分别由 Characteristics 和 FirstThunk 栏位指向)平行存在,并且都以 NULL 为结束符号。第一个数组(由 Characteristics 指向)从不被修改,有时候它又被称为 hint-name table。第二个数组(由 FirstThunk 指向)则被加载器改写。载入器一一检阅每一个 IMAGE_THUNK_DATA,并找出它所记录的函数的地址,然后把位址写入 IMAGE_THUNK_DATA 这个 DWORD 之中。
我们在改写 API Hook 那一节谈过,对 DLL 函数的呼叫会导致一个 JMP DWORD PTR[XXXXXXXX] 指令。[XXXXXXXX] 事实上参考到 FirstThunk 数组中的一个元素。由于这个 IMAGE_THUNK_DATA 数组内容已被加载器改写为输入函数的地址,所以它又被称为 ImportedAddress Table(IAT)。
我希望大家能仔细地研究一下 MSDN 里关于这方面的文章。你天天编程都和 PE 文件打交道,不了解可不行哟。我感觉说得太多了。我之所以说了这么多,特别是对 IMAGE_IMPORT_DESCRIPTOR 结构,因为Delphi好像并没有定义 IMAGE_IMPORT_DESCRIPTOR 结构,也许是我没有找到。在我的程序里,自定义了这个结构,为了找到这个结构都快把我累疯了。你也找找吧,如果找到了可要告诉我哦。

注意,这里也使用了 packed 关键字。packed record 相当于 C 语言中的 structure(知道为什么我把 packed record 称作结构了吧)。
还记得我们前面提到的 GetModuleHandle 吗?Delphi 的帮助里是这么说的:“Parameters lpModuleName Pointsto a null-terminated string that names a Win32 module(either a .DLL or .EXE file)..If this parameter isNULL, GetModuleHandle returns a handle of the fileused to create the calling process.”如果我们把 GetModuleHandle 的参数设为 NULL,哈哈,一切都有了(你有我有全都有)!
为了保证正确地拦截,我们必须穷举 PE 文件中的 IMAGE_IMPORT_DESCRIPTOR 数组,看是否有我们的 Module (例如 USER32.DLL)。如是,则穷举了 IMAGE_THUNK_DATA,看是否引入了我们需拦截的函数。在我的程序里, 我穷举了 PE 文件中的 IMAGE_IMPORT_DESCRIPTOR 数组,并穷举 IMAGE_THUNK_DATA,和我们拦截的函数的真正地址比较,如是,则替换它。这样做的好处是我们不必知道我们拦截的函数是从 USER32.DLL、GDI32.DLL 还是从 KERNEL32.DLL 中引入的。
唉,说多了让人心烦,也许你早就全知道啦,实在不行,聪明的你读读代码也就全明白了。还是看看关键的截获代码吧,这是 PermuteFunction 的终极版本,只需用它代替原版本,程序就全部完成了。关键的代码只有 10 行哦。

4.关键的代码


从网上看到《Delphi API HOOK 完全说明》这篇文章,基本上都是大家转来转去,原文出处我已经找不到了。

这篇文章写的很不错,但最后部分“PermuteFunction 的终极版本”描述的不太清楚,完全按照该文章代码执行,是不行的。需要修改 mess.pas 中代码才行。

其实文中提到的一个结构,代码中并没有使用:

mess.pas 中要修改的部分主要是:

另外在 mess.pas 中声明的“FuncMessageboxA, FuncMessageboxW: PImportCode;”也是没用了。
修改后的代码,大家可以到 http://download.csdn.net/source/1564512 
下载。

代码里,我故意加了一个 MB_YESNO 并且标题为“are you sure”的 MessageBox,执行的话,可以看到这个 MessageBox 并不会弹出,而是直接显示一个 ShowMessage('yes'); 或者 ShowMessage('no')。

这样也就是说我们可以拦截第三方程序的特定 MessageBox,并使其产生我们预期的结果值,从而控制目标程序流程。

原创粉丝点击