设为主页 | 加入收藏 | 繁體中文

黑客编程:hook系统函数

  一、媒介
  对大少数的Windows开发者来说,如安在Win32体系中对API函数的挪用进行阻拦一直是项极富挑战性的课题,由于这将是对你所掌握的计算机知识较为片面的磨练,尤其是一些在现在利用RAD进行软件开发时并不常用的知识,这包罗了操作体系原理、汇编言语甚至是机器指令(听上去真是有点恐怖,不外这是究竟)。
  以后广泛利用的Windows操作体系中,像Win 9x和Win NT/2K,都提供了一种比力妥当的机制来使得各个历程的内存地点空间之间是互相独立,也就是说一个历程中的某个有用的内存地点对另一个历程来说是无意义的,这种内存掩护措施大大增加了体系的稳固性。不外,这也使得进行体系级的API阻拦的事变的难度也大大加大了。
  当然,我这里所指的是比力文雅的阻拦方式,经过修改可实行文件在内存中的映像中有关代码,实现对API挪用的动态阻拦;而不是采用比力暴力的方式,直接对可实行文件的磁盘存储中机器代码进行改写。
  二、API钩子体系一般框架
  通常,我们把阻拦API的挪用的这个过程称为是安装一个API钩子(API Hook)。一个API钩子基本是由两个模块构成:一个是钩子办事器(Hook Server)模块,一般为EXE的情势;一个是钩子驱动器(Hook Driver)模块,一般为DLL的情势。
  钩子办事器主要卖力向目标历程注入钩子驱动器,使得钩子驱动器运行在目标历程的地点空间中,这是要害的第一步,而钩子驱动器则卖力现实的API阻拦处置惩罚事变,以便在我们所体贴的API函数挪用的之前或之后能做一些我们所盼望的事变。一个比力常见的API钩子的例子就是一些实时翻译软件(像金山词霸)中必备的的功能:屏幕抓词。它主要是对一些Win32 API中的GDI函数进行了阻拦,获取它们的输入参数中的字符串,然后在自己的窗口中表现出来。
  针对上述关于API钩子的两个部分,有以下两点需要我们重点考虑的: 选用何种DLL注入技能,以及采用何种API阻拦机制。
  三、注入技能的选用
  由于在Win32体系中各个历程的地点是互相独立的,因而我们无法在一个历程中对另一个历程的代码进行有用的修改,但要是你要完成API钩子的事变又必须如此。因而,我们必须接纳某种奇特的手段,使得API钩子(准确的说是钩子驱动器)能够成为目标历程中的一部分,才有较大的大概来对目标历程数据和代码进行有控制的修改。
  通常可采用的几种注入方式:
  1.利用注册表
  要是我们准备阻拦的历程毗连了User32.dll,也就是利用了User32.dll中的API(一般图形界面的应用程序都是符合这个条件),那么就可以简单把你的钩子驱动器DLL的名字作为值添加在下面注册表的键下: HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_DLLs 值的情势可以为单个DLL的文件名,或者是一组DLL的文件名,相邻的称号之间用逗号或空格隔断。所有由该值标识的DLL将在符合条件的应用程序启动的时间装载。这是一个操作体系内建的机制,绝对其他方式来说伤害性较小,但它也有一些比力明显的缺点: 该要领仅适用于NT/2K操作体系,显然看看键的称号就可以明确;要是需要激活或停止钩子的注入,只要重新启动Windows,这个就彷佛太不方便了;最后一点也很显然,不克不及用此要领向没有利用User32.dll的应用程序注入DLL,例如控制台应用程序等。别的,不论是否为你所盼望,钩子DLL将注入每一个GUI应用程序,这将招致整个体系功能的下降!
  2.建立体系范畴的Windows钩子
  要向某个历程注入DLL,一个十分普遍也是比力简单的要领就是建立在尺度的Windows钩子的基础上。Windows钩子一般是在DLL中实现的,这是一个全局性的Windows钩子的基本要求,这也很符合我们的需要。当我们乐成地挪用SetWindowsHookEx函数之后,便在体系中安装了某种范例的音讯钩子,这个钩子可以是针对某个历程,也可以是针对体系中的所有历程。一旦某个历程中孕育发生了该范例的音讯,操作体系会自动把该钩子地点的DLL映像到该历程的地点空间中,从而使得音讯回调函数(在SetWindowsHookEx的参数中指定)能够对此音讯进行适当的处置惩罚,在这里,我们所感兴味确当然不是对音讯进行什么处置惩罚,因而在音讯回调函数中只需把音讯钩子向后传递就可以了,但是我们所需的DLL曾经乐成地注入了目标历程的地点空间,从而可以完成后续事变。
  我们知道,不同的历程之间是不克不及直接共享数据的,由于它们活动在不同的地点空间中。但在Windows钩子DLL中,有一些数据,例如Windows钩子句柄HHook,这是由SetWindowsHookEx函数返回值失掉的,并且作为参数将在CallNextHookEx函数和UnhookWindoesHookEx函数中利用,显然利用SetWindowsHookEx函数的历程和利用CallNextHookEx函数的历程一般不会是统一个历程,因而我们必须能够使句柄在所有的地点空间中都是有用的有意义的,也就是说,它的值必须必须在这些钩子DLL所挂钩的历程之间是共享的。为了到达这个目的,我们就应该把它存储在一个共享的数据区域中。
  在VC++中我们可以采用预编译指令#pragma data_seg在DLL文件中创建一个新的段,并且在DEF文件中把该段的属性设置为"shared",如许就建立了一个共享数据段。对于利用Delphi的人来说就没有这么幸运了:没有类似的比力简单的要领(大概是有的,但我没有找到)。不外我们还是可以利用内存映像技能来请求利用一块各历程可以共享的内存区域,主要是利用了CreateFileMapping和MapViewOfFile这两个函数,这却是一个通用的要领,适合所有的开发言语,只要它能直接或直接的利用Windows的API。
  在Borland的BCB中有一个指令#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也能被宁静的挪用而不出错,但它除了返回一个空值之外什么也不做。该注入过程也十分简单:我们知道,任何一个历程都可以利用LoadLibrary来动态地加载一个DLL。但题目是,我们如何让目标历程(大概正在运行中)在我们的控制上去加载我们的钩子DLL(也就是钩子驱动器)呢?有一个API函数CreateRemoteThread,经过它可在一个历程中可建立并运行一个长途的线程--这个好像和注入没什么关系嘛?往下看!
  挪用该API需要指定一个线程函数指针作为参数,该线程函数的原型如下: Function ThreadProc(lpParam: Pointer): DWORD,我们再来看一下LoadLibrary的函数原型: Function LoadLibrary(lpFileName: PChar): HModule。发现了吧!这两个函数原型险些是一样的(实在返回值是否相同关系不大,由于我们是无法失掉长途线程函数的返回值的),这种类似使得我们可以把直接把LoadLibrary当做线程函数来利用,从而在目标历程中加载钩子DLL。
  与此类似,当我们需要卸载钩子DLL时,也可以FreeLibrary作为线程函数来利用,在目标历程中卸载钩子DLL,统统看来是十分的简便方便。经过挪用GetProcAddress函数,我们可以失掉LoadLibrary函数的地点。由于LoadLibrary是Kernel32中的函数,而这个体系DLL的映射地点对每一个历程来说都是相同的,因而LoadLibrary函数的地点也是如此。这点将确保我们能把该函数的地点作为一个有用的参数传递给CreateRemoteThread利用。 FreeLibrary也是一样的。
  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对象。
  四、阻拦机制
  在钩子应用的体系级别方面,有两类API阻拦的机制――内核级的阻拦和用户级的阻拦。内核级的钩子主要是经过一个内核模式的驱动程序来实现,显然它的功能应该最为壮大,能捕获到体系活动的任何细节,但难度也较大,不在本文的探究范畴之内(尤其对我这个利用Delphi的人来说,还没涉足这块领域,因而也无法探究,呵呵)。
  而用户级的钩子则通常是在普通的DLL中实现整个API的阻拦事变,这才是这次重点存眷的。阻拦API函数的挪用,一般可有以下几种要领:
  1. 代理DLL(特洛伊木马
  一个容易想到的可行的要领是用一个同名的DLL去更换原先那个输出我们准备阻拦的API地点的DLL。当然代理DLL也要和原来的一样,输出所有函数。但要是想到DLL中大概输出了上百个函数,我们就应该明确这种要领的服从是不高的,预计是要累去世人的。别的,我们还不得不考虑DLL的版本题目,非常麻烦。
  2.改写实行代码
  有许多阻拦的要领是基于可实行代码的改写,其中一个就是改变在CALL指令中利用的函数地点,这种要领有些难度,也比力容易出错。它的基本思绪是检索出在内存中所有你所要阻拦的API的CALL指令,然后把原先的地点改成为你自己提供的函数的地点。
  别的一种代码改写的要领的实现要领更为庞大,它的主要的实现步骤是先找到原先的API函数的地点,然后把该函数开始的几个字节用一个JMP指令代替(有时还不得不改用一个INT指令),从而使得对该API函数的挪用能够转向我们自己的函数挪用。实现这种要领要牵涉到一系列压栈和出栈如许的较底层的操作,显然对我们的汇编言语和操作体系底层方面的知识是一种磨练。这个要领倒和很多文件型病毒的感染机制相类似。
  3.以调试器的身份进行阻拦
  另一个可选的要领是在目标函数中安置一个调试断点,使得历程运行到此处就进入调试状态。然而如许一些题目也随之而来,其中较主要的是调试异常的孕育发生将把历程中所有的线程都挂起。它也需要一个额外的调试模块来处置惩罚所有的异常,整个历程将一直在调试状态下运行,直至它运行结束。
  4.改写PE文件的输入地点表
  这种要领主要得益于现现在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"的段(输入数据段)值得我们去细致,该段中包罗着一些被称为输入地点表(IAT,Import 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中默许的挪用商定是register,它们在参数的传递要领等方面存在着较大的区别。
  别的,自定义的函数的参数情势一般来讲和原先的API函数是相同的,不外这也不是必须的,并且如许的话在有些时间也会呈现一些题目,我在后面将会提到。因而要阻拦API的挪用,首先我们就要失掉相应的IAT的地点。体系把一个历程模块加载到内存中,实在就是把PE文件险些是原封不动的映射到历程的地点空间中去,而模块句柄HModule现实上就是模块映像在内存中的地点,PE文件中一些数据项的地点,都是绝对于这个地点的偏移量,因而被称为绝对虚拟地点(RVA,Relative 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(Pointer(HTargetModule), true, IMAGE_DIRECTORY_ENTRY_IMPORT, sz);
  while Pointer(ImportDesc.Name)<>nil do
  begin //果断是否是所需的DLL输入描述
  if StrIComp(PChar(DllName),PChar(HTargetModule+ImportDesc.Name))=0 then begin
  //失掉IAT的首地点
  FirstThunk:=PIMAGE_THUNK_DATA32(HTargetModule+ImportDesc.FirstThunk);
  while FirstThunk.Func<>nil do
  begin
  if FirstThunk.Func=OldAddressOfAPI then
  begin
  //找到了立室的API地点 ......
  //改写API的地点
  break;
  end;
  Inc(FirstThunk);
  end;
  end;
  Inc(ImportDesc);
  end;
  end;
  最后有一点要指出,要是我们手工实行钩子DLL的加入目标历程,那么在加入前应该把函数挪用地点改回原先的地点,也就是API的真正地点,由于一旦你的DLL加入了,改写的新的地点将指向一个毫无意义的内存区域,要是此时目标历程再利用这个函数显然会呈现一个合法操作。
  五、更换函数的编写
  后面要害的两步做完了,一个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);
  //做一些挪用后的操作
  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//失掉第二个参数 ......
  //做一些处置惩罚,这里要包管esp在这之后规复原样
  call OldAddress //挪用原先的API函数 ......
  //做一些别的的事变
  end;
  当然,这个更换函数还是比力简单的,你可以在其中挪用一些纯粹用OP言语写的函数或过程,去完成一些更庞大的操作(要是都用汇编来完成,那可得把你忙去世了),不外应该把这些函数的挪用方式统一设置为stdcall方式,这使它们只利用货仓来传递参数,因而你也只需时刻掌握好货仓的变化环境就行了。要是你直接把上述汇编代码所对应的机器指令存放在一个字节数组中,然后把数组的地点当作函数地点来利用,效果是一样的。
  六、后记
  做一个API钩子的确是件不容易的事变,尤其对我这个利用Delphi的人来说,为相识决某个题目,每每在OP、C++和汇编言语的资料中东查西找,在程序调试中还不时的发生一些意想不到的事变,弄的自己是手忙脚乱。不外,好歹总算做出了一个API钩子的雏形,还是令自己十分的高兴,对计算机体系方面的知识也掌握了不少,受益非浅。现在在写这篇文章之前,我只是想翻译一篇从网上Down上去的英文资料(网址为http://www.codeproject.com/ ,文章名叫"API Hook Revealed",示例源代码是用VC++写的,这里不得不敬佩老外的水平,文章写得很有深度,并且每个细节都讲的十分细致)。
 


    文章作者: 福州军威计算机技术有限公司
    军威网络是福州最专业的电脑维修公司,专业承接福州电脑维修、上门维修、IT外包、企业电脑包年维护、局域网网络布线、网吧承包等相关维修服务。
    版权声明:原创作品,允许转载,转载时请务必以超链接形式标明文章原始出处 、作者信息和声明。否则将追究法律责任。

TAG:
评论加载中...
内容:
评论者: 验证码: