与模块有关的API函数
相关函数列表:
l GetModuleUsage
l GetProcAddress
l GetModuleHandle
l GetModuleFileName
l LoadLibrary
l FreeLibrary
GetModuleUsage
这是一个老函数用于16位Windows,现在已经不用了。
GetProcAddress和IGetProcAddress
GetProcAddress是Win32程序设计中的一个关键函数,因为它是动态载入DLL的方法(相反的方法是所谓隐式载入,implicitly link)。只要指定模块(HMODULE)和函数(名称或序号均可),GetProcAddress就会返回函数入口地址。为了完成任务,GetProcAddress首先必须找出特定模块的module database,然后再访问其输出函数表(exported function table),以获取函数入口地址。
GetProcAddress实际上仅进行参数验证而已。它先确认lpszProc参数是字符串还是序号。如果该参数高位(high WORD)是0,低位(low WORD)就是序号。如果高位不是0,lpszProc就被认为是一个PSTR,于是GetProcAddress扫描这个字符串,寻找NULL结束符。如果PSTR不符合要求,扫描过程就会发生异常情况,被结构化异常处理函数捕捉,传回0(表示失败)。如果一切顺利,控制权将被传递给IGetProcAddress,那里才真正取得函数入口地址。
IGetProcAddress把寻找函数入口地址的动作管理在一个高层面上,留下琐碎的工作给两个更低一级的函数。IGetProcAddress首先做一些线程同步控制动作,确保当前线程不会被不适当的中断掉。接下来它调用MRFromHLib取得一个指针,指向MODREF。(MRFromHLib是Kernel32的内部函数)然后开始遍历当前进程的MODREF链表,查找符合要求的HMODULE。找到之后IGetProcAddress即利用MODREF结构中模块的索引值,寻找对应的IMTE。
IGetProcAddress的第二个动作就是查找函数地址。由于IGetProcAddress可能以函数名称或序号为参数,所以它必须先决定是哪一种参数,才能调用下层函数。如果传来的是序号,就调用x_FindAddressFormatExportOrdinal;如果传入的是字符串,就调用x_FindAddressFormatExportName。如果这两种情况都未能找到函数地址,IGetProcAddress就会返回一个错误诊断信息,并传回0。
如果你查找的是Kernel32函数,IGetProcAddress则不允许你以序号来查找函数。这是因为Kernel32中有一堆未公开的函数,而且只以序号导出。由于这些函数名称不在Kernel32.DLL中,所以它们也不存在于Kernel32的import library之中,因此应用程序就不能够调用这些微软保护的保留品。
这是否意味着如果你知道未公开的函数的序号,就可以调用了?不,IGetProcAddress中一段可怕的代码阻止了这样的企图,它不允许GetProcAddress使用Kernel32函数序号。
让我们回到正题。当成功找到函数地址之后,IGetProcAddress并不会就此结束。为了某些诡异的理由,当一个进程被Debugger加载后,它所呼叫的任何System DLLs(位于2GB地址空间)会县通过一段特殊的代码(由加载器动态产生出来)。这些代码的目的是为了阻止Debugger进入ring3 system DLLs之中。对于隐式链接,加载器会处理每一个必要的动作:然而如果使用显示链接(也就是利用GetProcAddress),程序调用由GetProcAddress传回的函数指针,将因此跳过那段特殊的代码。因此,GetProcAddress必须先检查程序是否处于调试状态:如果IGetProcAddress获得的地址位于2GB中,IGetProcAddress会先寻找对应的那段特殊代码,并传回那段代码的起始地址。
如果函数没有找到,IGetProcAddress设定错误代码,让GetLastError返回ERROR_PROC_NOT_FOUND。最后,IGetProcAddress离开Critical Section(那时它在函数一开始所采用的同步控制机制)。
x_FindAddressFromExportOrdinal
x_FindAddressFromExportOrdinal(不是微软的正式名称,作者自己取的)是Kernel32的一个核心函数。他不只被GetProcAddress调用,也被PE加载器调用(在修正对隐式链接的函数的调用时)。
x_FindAddressFromExportOrdinal十分依赖PE文档中的IMAGE_NT_HEADERs和.edata的内容。它们都被映射到内存中,用以构建模组。所以,研究PE文档的格式变得更加重要—即使你不打算直接在PE格式上做点什么动作。
虽然x_FindAddressFromExportOrdinal之中有相当多的代码,但其原理其实相对简单。在模组的export table(也就是.edata)中,你可以获得一个RVA(Relative Virtual Address)数组,用来描述模组中的每个导出函数。这个数组被称作export address table。数组中的第一个元素内含输出序号为1的函数的RVA,第二个元素内含输出序号为2的函数的RVA,以此类推。x_FindAddressFromExportOrdinal唯一要做的就是进入该数组取得对应的RVA,然后把RVA加上模组的基地址,使它成为一个可用的线性地址。不过这其中有两点需要注意。
第一点是,x_FindAddressFromExportOrdinal需要计算序号基址。在PE文档中,最低输出序号就是基址。这可以使放置输出函数地址的表格比较小一些。例如,我们假设一个DLL的输出序号是100至109。直观想法则需要一个110个元素的数组,而只有最后10个被使用。为了节省空间,链接器设定序号基址为100,于是数组只要10个元素就够了。找到一个导出函数后,x_FindAddressFromExportOrdinal要记得把序号基址加到索引值上,才成为真正可用的索引值。
第二点是,x_FindAddressFromExportOrdinal必须处理转交函数(forwarded function)。转交函数后面会有详细解释。目前你只要知道,所谓转交函数就是针对[位于另一个DLL中的导出函数]的一个别名。例如,Windows NT Kernel32.DLL的HeapAlloc函数被转交到NTDLL.DLL的RtlAllocateHeap函数。导出函数地址数组中的转交函数的地址总是放在.edata section中。它并不是导出函数的地址,而是指向一个像是NTDLL.RtlAllocateHeap之类的字符串。如果,x_FindAddressFromExportOrdinal看到这样的事情发生,它就分别取出模组名称和函数名称,然后以此为参数再次调用GetProcAddress。是的,如果用GetProcAddress寻找一个转交函数,有可能会陷入recursive之中。
x_FindAddressFromExportName
这是x_FindAddressFromExportName的同伴。这两个函数之间的差异在于,本函数以名称为查找对象。其第一部分与前一个函数相同。
x_FindAddressFromExportName函数的真实意义在于它从哪里寻找函数名称以吻合lpszProc参数。如果找到一个吻合的字符串,此函数就根据AddressOfNameOrdinals数组,把字符串数组索引转换为输出函数地址表格的索引。然后,x_FindAddressFromExportName就可以找出导出的RVA并传回给其调用者。因此,这个函数其实是把它所发现的序列号交给x_FindAddressFromExportOrdinal,让后者做它该做的事情。
简单再说一次,函数地址可以根据名称或序号来取得。然而在底层动作中,地址总是根据序号来寻找。当你把一个函数名称交给GetProcAddress,或是以名称输入一个函数,Kernel32只不过是注入一个额外步骤,把字符串先转换为序号罢了。
GetModuleFileName和IGetModuleFileName
GetModuleFileName接受一个HMODULE参数,传回对应的EXE或DLL的完整路径。GetModuleFileNameA本身很简单,只不过做参数的确认动作。在确认lpszPath参数(准备用来存放档案名称)为合法值之后,GetModuleFileName跳转到IgetModuleFileName函数中。
注:
函数名称最后带“A”者,表示这是个ANSI字符串。如果是“W”表示这是个Unicode字符串。
除了文件名这一主题,IgetModuleFileName是十分简单的。所有它需要做的事情就是把完整的文件名从正确的IMTE拷贝的输出缓冲区内。然而由于每个进程认为它有自己的模块数组,IgetModuleFileName不能够直接查找pModuleTableArray。IgetModuleFileName使用MRFromHLib找出这个模块的MODREF。有了MODREF,IgetModuleFileName使用mteIndex进入pModuleTableArray并取得其IMTE指针。一旦有了IMTE指针,剩下的事情就是把IMTE的pszFileName内的字符串拷贝到缓冲区中—那是GetModuleFileName的一个参数。
GetModuleHandle和IgetModuleHandle
GetModuleHandle函数执行GetModuleFileName的相反工作。给定一个模块名称,这个函数会返回其对应的HMODULE。不幸的是微软文档中对于模块名称的解释有点模糊。不过,总的来说,模块名称可以是EXE或DLL的一个基本名称,也可以是一个完整路径。如果扩展名是DLL,则可省略。因此,下列四种情况都可以说是C:/WINDOWS/SYSTEM/USER32.DLL的模块名称
n USER32
n USER32.DLL
n C:/WINDOWS/SYSTEM/USER32
n C:/WINDOWS/SYSTEM/USER32.DLL
真正的GetModuleHandle函数代码很短,它只是确认lpszModule参数为一个合法的字符串指针。如果确实如此,GetModuleHandle就跳转到IgetModuleHandle去。就像IgetModuleFileName一样,IgetModuleHandle的核心也有一部分用来执行ANSI和OEM字符串之间的转换(如果需要的话)。这部分代码首先把模块名称全部改为大写,以便稍后的对比可以快速一些。接下来检查是否有一个扩展名在最后面。如果没有扩展名,就自动加上一个.DLL。
接下来的函数核心代码调用两个辅助函数:x_GetMODREFFromFilename和x_GetHModuleFromMODREF。首先,x_GetMODREFFromFilename扫描当前进程的MODREFs链表,直到发现一个吻合的文件名,然后返回该MODREF的指针。接下来,x_GetHModuleFromMODREF获取一个PMODREF参数,返回对应的HMODULE。
x_GetMODREFFromFilename
这个函数扫描当前进程的MODREFs链表,把其中的文件名称拿来和函数参数lpszModName进行比较。如果吻合,就返回PMODREF,否则传回NULL。
有趣的是,x_GetMODREFFromFilename所做的字符串比较动作不只是一次,也不只是两个,而是四个。第一次比较基础名称(例如,KERNEL32.DLL),如果失败,再比较完整路径。如果又失败,再比较基础名称的第二份副本,以及完整路径的第二次副本。只要有一次比较成功,就返回相应MODREF指针。
为了加快比较,x_GetMODREFFromFilename首先计算字符串参数的长度。由于存储在MODREF结构中的字符串,其长度已记录与结构中,所以,x_GetMODREFFromFilename先比较长度是否吻合。如果长度不吻合,也不必比了。
x_GetHModuleFromMODREF
这个函数需要一个PMODREF作为参数,返回对应的HMODULE(也就是基位地址)。函数只要从MODREF中取出指向module database(一个IMAGE_NT_HEADERS结构)的指针,然后从该结构中取出模块基位地址,那就是一个HMODULE。
KERNEL32对象
K32对象是关键性的系统资料结构,存放在KERNEL32 Heap中。有各式各样的K32对象,统统都是以相同的表头开始。决定它是否为一个K32对象的方法就是问一个问题:应用程序中是否用Handle代表此对象?例如,应用程序可以拥有file handles或event handles,所以file和event都是K32对象。
补充:
这里提到的K32(KERNEL32)对象实际上就是指内核对象,内核对象只能由内核访问,应用程序可以通过内核对象的句柄来访问这些内核对象,但不能直接更改这些内核对象的内容。而且这些句柄是与进程相关的,将一个进程所拥有的句柄传递给另一个进程是无效的,但可通过一些特殊方式跨越进程边界来共享一个内核对象句柄(参见:《Windows核心编程》第三章)。
内核对象(即K32对象)由系统内核所拥有,并不为进程所拥有。系统使用引用计数器来记录有多少进程在使用某个内核对象。当一个内核对象被创建时,其引用计数为1,当另一个进程使用该内核对象时,其引用计数递增1,当该进程终止时,系统自动递减该进程使用的所有内核对象的计数器(每次减一),当一个内核对象其引用计数为0时,系统自动撤销该内核对象。
在Windows NT/2000及其后续系统中,内核对象能够得到安全描述符的保护。
需要注意的是,窗口、菜单、字体和GDI对象并不是内核对象。
当一个进程在初始化时,系统会为其分配一个句柄表,该句柄表就是本文中提到的模块链表。对于Windows 98/2000/CE它们的实现方式都不相同,而且微软也并没有提供相应的文档资料。
每个K32对象都以一个共同的表头开始。此表头有如下结构成员:
00h DWORD
对象类型,此值决定其后的结构成员如何解释
04h DWORD
这是内核对象的引用计数(reference count),代表对象被使用的次数。
在随后的部分中,我们将主要讨论进程对象和线程对象。一个process database其实就是一个K32_PROCES对象,而一个thread database其实就是一个K32_THREAD对象。一个进程句柄表(process handle table)实际上就是一个指针数组。每个指针指向各式各样的K32对象。