Delphi API HOOK完全说明(存在错误的原文,含修正)
发布时间:2021-07-06 05:28:31 所属栏目:大数据 来源: https://blog.csdn.net/wts
导读:@H_403_0@ ?从网上看到《Delphi API HOOK完全说明》这篇文章,基本上都是大家转来转去,原文出处我已经找不到了。 @H_404_3@ @H_403_0@ 这篇文章写的很不错,但最后部分“PermuteFunction 的终极版本”描述的不太清楚,完全按照该文章代码执行,是不行的。 @H
@H_403_0@?从网上看到《Delphi API HOOK完全说明》这篇文章,基本上都是大家转来转去,原文出处我已经找不到了。@H_404_3@
@H_403_0@这篇文章写的很不错,但最后部分“PermuteFunction 的终极版本”描述的不太清楚,完全按照该文章代码执行,是不行的。@H_404_3@
@H_403_0@可能是作者故意这样做的?本文最后提供修正后的下载地址。@H_404_3@
@H_403_0@原文如下:@H_404_3@
@H_403_0@一、关于API Hook@H_404_3@ @H_403_0@1.什么是API Hook不知道大家是否还记得,在DOS 系统中编程,经常会采取截取中断向量的技术:我们可以设置新的中断服务程序,当系统其他的程序调用这个中断时,就让它先调用我们自己设置的新的中断服务程 序,然后再调用原来的中断服务程序,这样就能够获得非凡的控制权。许多优秀的软件和大多数DOS 病毒程序都采用了这个方法。@H_404_3@ @H_403_0@在Windows 中,我们也可以采取类似技术。当系统调用某个API 函数时,就会先进入我们自己的函数,然后再调用原来的API 函数,这样,我们的程序就可以取得更多的控制权,我们就可对Windows 系统中的任意一个函数调用进行动态拦截、跟踪、修改和恢复,就可让Windows 系统中的任意一个函数按我们的设想工作。这种技术有许多名称,比如“陷阱技术”、“重入技术”等,不过我认为还是API Hook 最贴切。原因嘛,等一下你看编程就明白了。@H_404_3@ @H_403_0@这样重要的技术,大家已经都知道了吧?哈哈,知道的都不说,不知道的呢,你就自己慢慢去摸索吧。偶尔有一两篇文章见于报端,不是藏头露尾,就是已经 过时了。还有的即使把原理都告诉你了,但就是不说它调用了哪些函数。要源代码?行,拿钱来。有人说了,这种技术用来编病毒最合适,所以..(因为菜刀可以 杀人,所以菜刀已被禁止使用了)。而实际呢,你看看使用了这种技术的国产软件就知道了:金山词霸、东方快车、RichWin、东方词圣..在这里,我感觉 有必要简单说说金山词霸的工作原理。@H_404_3@ @H_403_0@2.金山词霸的工作原理大家都用过金山词霸吧?当你把光标指向一个单词,词霸就会自动弹出一个窗口并把单词的意思翻译出来。这究竟是怎么做出来的呢?我在这里简单说明一下。@H_404_3@ @H_403_0@( 1 ) 安装鼠标钩子。 ( 2 )一旦光标在屏幕上移动,系统就会调用鼠标钩子,词霸通过鼠标钩子能够获得光标的坐标( x,y ) ,并安装TextOut()、ExtTextOut()等API 函数钩子。 (3)词霸向光标下的窗口发重画消息,负责绘制该点的应用程序在收到WM_PAINT 消息后,就可能调用TextOut()、ExtTextOut()等函数重绘字体。 (4)调用的函数将被钩子函数拦截,词霸就能够截获该次调用,从应用程序的数据段中将“文字”指针的内容取出,并作翻译处理。 (5)退出跟踪程序,返回到鼠标钩子,解除对TextOut()、ExtTextOut()等API 函数的跟踪。 (6)完成了一次“屏幕抓字”。 这里的关键有两点:安装鼠标钩子和API 钩子。安装鼠标钩子非常简单,而API 钩子正是取词的核心代码。@H_404_3@ @H_403_0@3.关于Delphi 事实上,随着互联网的普及,许多秘密都已不再是秘密,API Hook 也一样。在网上,你已经可以找到这样的免费源代码,但是大部分可能已经过时,而且这些源代码大都是基于VC++的。如果你想找到用Delphi 编写的源代码,那么,你还是读一读我的文章吧。 Delphi 是编程工具史上的一个里程碑式的作品。如果你在使用它,我向你表示祝贺。如果你没有使用它,你也没有什么损失。网上关于几种语言谁好谁坏都吵得天翻地覆的了,我不想增加新仇也不想算算旧恨。每种语言都有它的优缺点,每个人都有自己选择的权利嘛! 不过,用Delphi 编写API Hook 有几处“陷阱”。我想,除了介绍API Hook 以外,这也是为什么我要写这篇文章的一个原因吧!@H_404_3@ @H_403_0@4.哪些人可以读这篇文章 当然,读这篇文章并没有什么限制。但是你最好已经懂得鼠标钩子的制作过程,手边有MSDN 那就再好不过了。我认为,只要你是Windows 的程序员,就一定要有MSDN。原因?有一套就明白啦。如果你懂得PE文件结构,那就更好了。在这篇文章里,我给出了所有的源代码(还不到2 0 0行)。如果你想修改程序,最好用SoftIce。@H_404_3@ @H_403_0@5.关于我的程序 本文中的程序在Windows Me 的操作环境下,使用Delphi5.0 编程调试通过。无论是商用还是个人使用,你都可以随意使用和修改本文中的程序,并且不需要在程序中加注我的个人信息。@H_404_3@ @H_403_0@二、用Delphi 编写API Hook@H_404_3@ @H_403_0@1.改写API 函数 为了使我们改写的代码正确运行,我们的函数必须和要改写的API 函数具有同样形式的形参。在我的程序中,我拦截了MessageBoxA 和MessageBoxW 两个函数。所以我这样定义自己的函数:function MyBoxA(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal):integer;stdcall;function MyBoxW(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal):integer;stdcall; 注意到我使用了stdcall 关键字,这是为了我的函数的形参的进出栈顺序与我们要拦截的函数一致。我们知道,为了系统的安全,Win32 并不允许直接改写内存中的代码段。所以,有人想了好多种方法绕过系统的保护。实际上,Win32 为了我们能安全地改写内存中的代码,提供了一个函数:WriteProcessMemory。有许多人曾经告诉我,WriteProcessMemory 也不能用来改写,不过我一直使用得很好。也许用它产生了一些BUG,只是我并不知道罢了,所以还请这方面的专家指正。在PE文件中,当你呼叫另一模块中的 函数(例如USER32.DLL中的GetMessage),编译器编译出来的CALL指令并不会把控制权直接传给DLL 中的函数,而是传给一个JMP DWORDPTR [XXXXXXXX]指令,[XXXXXXXX]内含该函数的真正地址(函数进入点)。为了得到A P I 函数的地址,我们可以这样:Address:=@MessageBoxA。如上文所说的,我们得到的仅仅是一个跳转指令,后面紧接着的才是 MessageBoxA真正的开始代码的地址(具体可以查阅PE 文件的资料)。在下面的程序中我自定义了一个结构(叫记录?我习惯了,改不过口来),注意,这里使用了packed 关键字: TImportCode = packed record JumpInstruction : Word; // 应该是$25FF,JUMP 指令 AddressOfPointerToFunction: PPointer;// 真正的开始地址 end; PImportCode = ^TImportCode; 其中,PPointer = ^Pointer ; 用以下函数返回函数的真正地址: function TrueFunctionAddress(func: Pointer): Pointer; var Code: PImportCode; begin Result:= func; if func = nil then exit; try Code := func; if (Code.JumpInstruction = $25FF) then begin Result := Code.AddressOfPointerToFunction^; end; except Result := nil; end; end; 这样,只要用我们的函数的地址替代它就可以了。替换函数: Procedure PermuteFunction(OldFunc:PPOinter; NewFunc:Pointer); var written: DWORD; begin WriteProcessMemory(GetCurrentProcess,OldFunc,@NewFunc,4,written); end; 你新建一个Unit APIHook,把上面的函数和结构写进去并保存下来。@H_404_3@ @H_403_0@2.第一个程序 新建一个Application TRY1,主Form 的单元名称不妨叫TRYUnit1。把上面的Unit APIHook 加进来。再新建一个UnitMESS,添加如下代码: unit mess; interface uses Windows,Messages,SysUtils,Classes,APIHook; procedure API_Hookup; procedure Un_API_Hook; var FuncMessageBoxA,FuncMessageBoxW: PImportCode; implementation type TMessageA = function(hwn: hwnd; lptext: pchar; lpcapion: pchar; utype: cardinal): integer; stdcall; TMessageW = function(hwn: hwnd; lptext: pwidechar; lpcapion: pwidechar; utype: cardinal): integer; stdcall; var OldMessageBoxA: TMessageA; OldMessageBoxW: TMessageW; function MyBoxA(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal): integer; stdcall; begin result := OldMessageBoxA(hwn,'Succes Hook A !',lpcapion,utype); end; function MyBoxw(hwn:hwnd;lptext:pwidechar;lpcapion:pwidechar;utype:cardinal): integer; stdcall; begin result := OldMessageBoxW(hwn,'成功挂上W!',utype); end; procedure API_Hookup; begin if @OldMessageBoxA = nil then @OldMessageBoxA := TrueFunctionAddress(@messageBoxA); if @OldMessageBoxW = nil then @OldMessageBoxW := TrueFunctionAddress(@messageBoxW); PermuteFunction(FuncMessageBoxA.AddressOfPointerToFunction,@MyBoxA); PermuteFunction(FuncMessageBoxW.AddressOfPointerToFunction,@MyBoxW); end; procedure Un_API_hook; begin If @OldMessageBoxA <> nil then begin PermuteFunction(FuncMessageBoxA.AddressOfPointerToFunction, @OldMessageBoxA); PermuteFunction(FuncMessageBoxW.AddressOfPointerToFunction, @OldMessageBoxW); end; end; initialization FuncMessageBoxA := @MessageBoxA; FuncMessageBoxW := @MessageBoxW; end. 在主窗体上添加三个按钮,添加Onclick 代码,如下: procedure TForm1.Button1Click( Sender : TObject); begin API_HookUp; end; procedure TForm1.Button3Click( Sender : TObject); begin Un_API_Hook; end; procedure TForm1.Button2Click( Sender : TObject); begin MessageBoxA(Form1.Handle,'NO HOOK UP A','MessageBoxA',MB_OK); MessageBoxW(Form1.Handle,'NO HOOK UP W','MessageBoxW',MB_OK); end; 记得要保存,在后面我们还要使用它们。编译一下, 运行..啊哈,成功了!且慢,别高兴得太早。如果现在新建 一个Application TestTry,在Form 上添加一个按钮,Onclick事 件如下: procedure TForm1.Button1Click(Sender: TObject); begin MessageBoxA(Form1.Handle,MB_OK); MessageBox (Form1.Handle,'NO HOOK UP Box','MessageBox',MB_OK); end; 先运行T R Y 1 ,再运行T e s t T r y 。结果呢?原来,APIHook 仅仅在TRY1 中挂上了,并没有在所有的系统进程中挂上。 你也许听说过,必须把我们的函数和A P I _ H o o k u p 、Un_API_Hook 放在一个动态链接库里(这是对的)。那么,你就试试吧。如果你这样做了,也并不能在所有的系统进程中挂上。原因很简单,我们仅仅改变了进程指向API 函数的指针,系统中其他的进程还是各管各的。@H_404_3@ @H_403_0@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吗?@H_404_3@ @H_403_0@4.第二个程序 说干就干。新建一个动态链接库library TryDLL,把原来的Unit APIHook 和 Unit MESS 加进来。library TryDLL 的代码如下: uses Windows, SysUtils, Classes, APIHook in 'APIHook.pas', mess in 'mess.pas'; {$R *.RES} function GetMsgProc(code: integer; removal: integer; msg: Pointer): Integer; stdcall; begin Result := 0; end; Var HookHandle: THandle; procedure StartHook; stdcall; begin HookHandle := SetWindowsHookEx(WH_GETMESSAGE,@GetMsgProc,HInstance, 0); end; procedure StopHook; stdcall; begin UnhookWindowsHookEx( HookHandle ); end; exports StartHook,StopHook; begin API_Hookup; //加载时挂上 end. 为了卸载时能解除钩子,在Unit MESS 单元最后加上一句: finalization Un_API_hook; 当然,别忘了对MESS 做相应修改。编译好后别忘了存盘。新建Application TRY2 程序,主Form 的单元名称不妨叫TRYUnit2,在Form1 上添加三个Button,并声明: procedure StartHook; stdcall; external 'TRYDLL.DLL'; procedure StopHook; stdcall; external 'TRYDLL.DLL'; 三个Button 的OnClick 代码如下: procedure TForm1.Button1Click(Sender: TObject); begin StartHook; end; procedure TForm1.Button2Click(Sender: TObject); begin MessageBoxA(Form1.Handle,MB_OK); end; procedure TForm1.Button3Click(Sender: TObject); begin StopHook; end; 编译好后运行吧。这次倒好,无论你怎样测试,都得不到钩上的信息。整理一下我们的工作吧:TRY2 运行 --> TryDLL 加载 --> 运行API_Hookup,为TryDLL 挂上MyBox。即使我们按下了Button1,其他的进程加载了TryDLL,也仅仅是运行API_Hookup,为TryDLL 挂上MyBox 而已,而其他的进程(包括TRY2 本身)并没有挂上MyBox。不信,你可以在TryDLL 中加上一个启动MyBox 的函数,测试一下。例如,你可以在T r y D L L 的S t o p H o o k 函数中的UnhookWindowsHookEx 语句前,加上一句:MessageBoxW(0,'这是测试DLL是否加载了MyBox',MB_OK);你可以看到弹出窗 口中的信息是“成功挂上W!”而不是“MessageBoxW”。并且由于我们的TryDLL 加载时就启动了API_Hookup,卸载时才运行Un_API_Hook,所以不论你是否按下Button1 和Button3,并且不论你按下了几次,每次你按下Button3 都会得到“成功挂上W!”的信息。看来,真正的麻烦才刚刚开始。实际上,我们刚才的工作都是有用的。我们剩下的工作就是改进Unit APIHook 中的TrueFunctionAddress 函数而已。想不到吧?为了能让其他的进程挂上MyBox,我们必须了解一下PE文件的格式。@H_404_3@ @H_403_0@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结构,该结构是由一个双字的标志项和两个子结构构成: Signature : DWORD; FileHeader : TImageFileHeader; OptionalHeader : TImageOptionalHeader; 标志项是为了说明该可执行文件是“PE/O/O”、“NE”还是“LE”。TImageFileHeader包含了编译器产生的COFF OBJ信息。TImageOptionalHeader包含有堆栈初始大小、块表大小、数据目录(Data Directory)及其他一些重要信息。你并不需要知道TImageOptionalHeader的所有字段。最重要的两个字段是I m a g e B a s e 和S u b s y s t e m 。其中还有一个重要的字段——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的格式描述如下: DWORD Characteristics/OriginalFirstThunk:这是一个偏移值(一个RVA),对应于一个输入函数。DWORD TimeDateStamp:这是文件的产生时刻。通常此栏为0 。然而微软的B I N D 程序可以将此IMAGE_IMPORT_DESCRIPTOR所对应的DLL的产生时刻写到这里来。 DWORD ForwarderChain:这个字段关系到所谓的forwarding(转交),意味着一个DLL 函数在参考(呼叫、利用)另一个DLL。这个字段内含一个索引,指向FirstThunk数组。被这个索引所指定的函数就是一个转交函数。DWORD Name:这是一个RVA,指向一个以NULL为结束字符的ASCII字符串,内含imported DLL的名称。PIMAGE_THUNK_DATA FirstThunk:这是一个R V A , 指向一个D W O R D s(IMAGE_THUNK_DATA)数组。大部分情况下,该D W O R D 被解释为一个指向IMAGE_IMPORT_BY_NAME 结构的指针。然而,以函数序号(而非函数名称)输入,也是有可能的。IMAGE_IMPORT_DESCRIPTOR结构中,最重要的部分是i m p o r t e d D L L 的名称以及两个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结构,也许是我 没有找到。在我的程序里,自定义了这个结构,为了找到这个结构都快把我累疯了。你也找找吧,如果找到了可要告诉我哦。 type PIMAGE_IMPORT_DESCRIPTOR = ^ IMAGE_IMPORT_DESCRIPTOR; IMAGE_IMPORT_DESCRIPTOR = packed record OriginalFirstThunk : DWORD; TimeDateStamp : DWORD; ForwarderChain : DWORD; Name : DWORD; FirstThunk : DWORD; end; 注意,这里也使用了packed 关键字。packed record相当于C 语言中的structure(知道为什么我把packedrecord 称作结构了吧)。 还记得我们前面提到的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,哈哈,一切都有了(你有我有全都有)! 为了保证正确地拦截,我们必须穷举P E 文件中的IMAGE_IMPORT_DESCRIPTOR 数组,看是否有我们的M o d u l e (例如USER32.DLL)。如是,则穷举了IMAGE_THUNK_DATA,看是否引入了我们需拦截的函数。在我的程序里, 我穷举了P E 文件中的IMAGE_IMPORT_DESCRIPTOR 数组,并穷举IMAGE_THUNK_DATA,和我们拦截的函数的真正地址比较,如是,则替换它。这样做的好处是我们不必知道我们拦截的函数是从 USER32.DLL、GDI32.DLL还是从KERNEL32.DLL中引入的。 唉,说多了让人心烦,也许你早就全知道啦,实在不行,聪明的你读读代码也就全明白了。还是看看关键的截获代码吧,这是PermuteFunction 的终极版本,只需用它代替原版本,程序就全部完成了。关键的代码只有10行哦。@H_404_3@ @H_403_0@4.关键的代码@H_404_3@ @H_403_0@ ?@H_404_3@
(编辑:北几岛) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |