第32章 计算器显示中文数字
本文最后更新于:2022年5月19日 晚上
第32章计算器显示中文数字
API钩取技术中有一种是通过注入DLL文件来钩取某个API的,DLL文件注入目标进程后,修 改IAT来更改进程中调用的特定API的功能。
本章讲解API钩取技术时将以Windows计算器(calc.exe)为示例,向计算器进程插入用户的DLL文件,钩取IAT的user32.SetWindowTextW() API地址。负责向计算器显示文本的SetWindowTextW() API被钩取之后,计算器中显示出的将是中文数字,而不是原来的阿拉伯数字。
32.1 技术图表
图32-1的API钩取技术图表中,带有下划线的部分就是“通过DLL注入实现IAT钩取的技术”。这项技术的优点是工作原理与具体实现都比较简单(只需先将要钩取的API在用户的DLL中重定 义,然后再注入目标进程即可);缺点是,如果想钩取的API不在目标进程的IAT中,那么就无法使用该技术进行钩取操作。换言之,如果要钩取的API是由程序代码动态加载DLL文件而得以使用的,那么我们将无法使用这项技术钩取它。
32.2 选定目标API
确定了任务目标,并且选择了要使用的API钩取技术后,接下来的重要一步是选定目标API,即要钩取的API。初学者往往不知所措,因为他们不知道究竟哪个API提供了要钩取的那个功能。操作系统中,某项功能最终都是由某个或某些API提供的,比如创建文件由kernel32!CreateFile()API负责,创建注册表新键advapi32!RegCreateKeyEx() API负责,网络连接由ws2_32!connect()API等负责。对拥有丰富开发经验或逆向技术知识的人来说,他们能够很容易地想起需要的API。而对于尚未掌握这部分知识的人而言,要知道答案必须先学会检索。如果要钩取尚未公开的API(undocumented API),就必须学会使用检索功能。若搜索不到,可以先根据已有经验(或直觉)推测,然后再验证确认。
选定API前要先明确任务目标。本章示例的目标是“把计算器的文本显示框中显示的阿拉伯数字更改为中文数字”。首先,使用010Editor工具查看计算器(calc.exe)中导入的API,如图32-2所示。
图32-2中有2个API引人注目,分别为SetWindowTextW()、SetDlgItemTextW(),它们负责向计算器的文本显示框中显示文本。由于SetDlgItemTextW()在其内部又调用了SetWindowTextW(),所以我们先假设只要钩取SetWindowTextW()这1个API就可以了。SetWindowTextW() API定义(出处:MSDN)如下:
1 |
|
它拥有2个参数,第一个参数为窗口句柄(hWnd),第二个参数为字符串指针(IpString)。其 中,我们感兴趣的是第二个参数—字符串指针(IpString)。钩取时查看字符串(IpString)中的内容,将其中的阿拉伯数字更改为中文数字就行了。
API名称中最后面的“W”表示该API是宽字符(Wide character)版本。与之对应,若API名称最后面的字符为“A”,则表示该API是ASCII码字符(ASCII character)版本。Windows OS内部使用的宽字符指的就是Unicode码。 如:SetWindowTextA()、SetWindowTextW()
下面使用OllyDbg验证上面的猜测是否正确。
示例中使用的是Windows 7(32位)中的 calc.exe, Windows Vista、 Windows XP SP3 (32位)中的calc.exe工作原理也是一样的。
如图32-3所示,使用鼠标右键菜单的Search for All intermodular calls命令,查找计算器(calc.exe)代码中调用SetWindowTextW() API的部分。然后在所有调用它的地方设置断点,运行计算器(calc.exe),调试器在设置断点的地方暂停,如图32-4所示。
在图32-4中查看栈窗口,可以看到SetWindowTextW() API的IpString参数的值为22C9CF0(OllyDbg中指出它是一个“Text”)。进入22C9CF0地址,可以看到字符串 “0.” 被保存为Unicode码 形式。该字符串就是显示在计算器显示框中的初始值,继续运行。
如图32-5所示,计算器(calc.exe)正常运行后,显示框中显示图32-4中的字符串 “0.”(是计算器自动添加的字符串)。为了继续调试,在计算器中随意输入数字7。由于前面已经设置了断点,所以调试器会在设置的断点处暂停,如图32-4所示。
如图32-6所示,保存在Text参数中的字符串地址为22C9CD8,与图32-4中的22C9CF0不同。进入22C9CD8地址,可以看到输入的字符串 “7.”(末尾的是由计算器自动添加的字符串)。下面尝试把阿拉伯数字 “7” 更改为中文数字“七”,测试一下。请注意:中文数字“七”对应的Unicode码为4e03。
Unicode码中每个汉字占用2个字节。
如图32-7所示,将中文数字“七”的Unicode码(4e03)覆写到22C9CD8地址。
由于X86系列的CPU采用小端序标记法,所以覆写时要逆序(034e)进行。如上所示,修改了SetWindowTextW()API的IpString (或Text)参数内容后,继续运行计算器,可以看到原本显示在计算器中的阿拉伯数字 “7” 变为了中文数字“七”,如图32-8所示。
对SetWindowTextW() API的验证到此结束,验证结果表明我们前面的猜测完全正确。经过上述过程,我们知道了代码中调用SetWindowTextW() API的位置(01003678),并且确定了一个事实:只要修改参数字符串中的内容,就能修改计算器中显示的格式。下面继续学习IAT钩取操作及实现原理,并讲解计算器SetWindowTextW() API的IAT钩取源代码。
32.3 IAT钩取工作原理
进程的IAT中保存着程序中调用的API的地址。
有关IAT的说明请参考第13章。
IAT钩取通过修改IAT中保存的API地址来钩取某个API。请先看图32-9。
图32-9描述的是计算器(calc.exe)进程正常调用user32.SetWindowTextW() API的情形。地址01001110属于IAT区域,程序开始运行时,PE装载器会将user32.SetWindowTextW() API地址(77D0960E)记录到该地址(01001110)。01002628地址处的CALL DWORD PTR [01001110]指令最终会调用保存在01001110地址(77D0960E)处的函数,直接等同于CALL 77D0960E命令。
执行地址01002628处的CALL命令后,运行将转移至user32.SetWindowTextW()函数的起始地址(77D0960E)处(①),函数执行完毕后返回(②)。
下面看看IAT被钩取后计算器进程的运行过程,如图32-10所示
钩取IAT前,首先向计算器进程(calc.exe)注入hookiat.dll文件
关于DLL注入的讲解请参考第23章。
hookiat.dll文件中提供了名为MySetWindowTextW()的钩取函数(10001000)。地址01002628处的CALL命令与图32-9中的CALL命令完全一致。但是跟踪进入01001110地址中可以发现,它的值已经变为10001000,地址10001000是hookiat.MySetWindowTextW()函数的起始地址。也就是说,在保持运行代码不变的前提下,将IAT中保存的API起始地址变为用户函数的起始地址。这就是IAT钩取的基本工作原理。
执行完01002628地址处的CALL命令后,运行转到hookiat.MySetWindowTextW()函数的起始地址(10001000)(①),经过一系列处理后,执行1000107D地址处的CALL命令,转到(原来要调用的)user32.SetWindowTextW()函数的起始地址(②)。
地址1000B6B8位于hookiat.dll的data节区,它是全局变量g_pOrgFunc的地址。注入DLL时,DllMain()会获取并保存user32.SetWindowTextW()函数的起始地址。
user32.SetWindowTextW() API执行完毕后,执行会返回到hookiat.dll的1000107D地址的下一条指令(③),然后返回到01002628地址(calc.exe的代码区域)的下一条指令继续执行(④)。也就是说,程序调用user32.SetWindowTextW() API之前,会先调用hookiat.MySetWindowTextW()函数。像这样,先向目标进程(calc.exe)注入用户DLL (hookiat.dll),然后在calc.exe进程的IAT区域中更改4个字节大小的地址,就可以轻松钩取指定API (这种通过修改IAT来钩取API的技术 也称为IAT钩取技术)。希望各位先理解上面讲解的IAT钩取工作原理,再跟着做后面的练习示例。
32.4 练习示例
本练习示例的目标是在计算器的显示框中用中文数字代替原来的阿拉伯数字。为达成目标,本节中我们将使用前面讲过的“通过修改IAT来实现API钩取的技术”。
hookiat.dll 与 InjectDll.exe 文件均使用 VC++ Express Edition 编写而成,并在 Windows XP SP3、Windows 7 (32位)中通过测试。
首先复制示例文件到工作目录(c:\work),然后运行计算器(calc.exe)程序,再使用Process Explorer查看计算器进程的PID值,如图32-11所示。
在命令行窗口中输入图32-12中的命令,按Enter键执行。
这里在进行实验时发现通过命令行不能注进去,可以通过VS注进去,在C盘根目录下依旧不行,原因未知。
可以在Process Explorer中看到hookiat.dll文件已经成功注入calc.exe,如图32-13所示。
接下来在计算器中任意输入一些数字并计算,如图32-14所示。
从图32-14可以看到,输入的所有数字都被转换成了中文数字形式,并且计算器的计算功能也非常正常(请注意,我们只是钩取了数字的显示,除此之外其他功能均正常运行)。
下面尝试一下“脱钩”操作。“脱钩”就是把IAT恢复原值,弹岀并卸载已插入的DLL(hookiat.dll)。在命
令窗口中输入并执行图32-15中的命令。
执行完上述命令后,再次向计算器中输入数字,如图32-16所示。
可以看到数字正常显示为阿拉伯数字形式,表明“脱钩”成功
32.5 源代码分析
本节将详细分析示例程序(hookiat.dll)的源代码,借此深人了解IAT钩取的工作原理及具体实现方法。
所有源代码均使用VC-H- 2010 Express Edition开发而成,且在Windows XP SP3、Windows 7 (32位)系统环境中顺利通过测试。为便于讲解,后面的源代码中省略了返回值检查与错误处理的语句。
InjectDll.cpp源代码与以前讲解过的内容(注入DLL的代码)基本结构类似(详细说明请参考第23章)。下面将详细讲解hookiat.dll的源代码(hookiat.cpp)。
32.5.1 DIIMain()
1 |
|
DllMain()函数代码一如既往地简单,下面看看其中比较重要的代码。
保存SetWindowTextW()地址
1 |
|
在DLL_PROCESS_ATTACH事件中先获取user32.SetWindowTextW() API的地址,然后将其保存到全局变量(gjOrgFunc),后面“脱钩”时会用到这个地址。
由于计算器已经加载了 user32.dll,所以像上面那样直接调用GetProcAddress()函数不会有什么问题。但实际操作中,必须先确定提供(要钩取的)API的DLL已经正常 加载到相应进程(若相应DLL在钩取前尚未被加载,则应该先调用LoadLirary() API加载它)。
IAT钩取
1 |
|
上面这条语句用来调用hook_iat()函数,钩取IAT (即将user32.SetWindowTextW()的地址更改为hookiat.MySetWindowTextW()的地址)。上面这两个语句是发生DLL加载事件(DLL_PROCESS_ATTACH)时执行的所有操作。
IAT “脱钩”
1 |
|
卸载DLL时会触发DLL_PROCESS_DETACH事件,发生该事件时,我们将进行IAT “脱钩” (hookiat.MySetWindowTextW()的地址更改为user32.SetWindowTextW()的地址)。以上就是对DllMain()函数的讲解。接下来分析MySetWindowTextW()函数,它是user32.SetWindowTextW()的钩取函数(5.3节中将详细说明hook_iat()函数)。
32.5.2 MySetWindowTextW()
下面看看MySetWindowTextW()函数,它是SetWindowTextW()的钩取函数。
1 |
|
计算器进程(calc.exe)的IAT被钩取后,每当代码中调用user32.SetWindowTextW()函数时,都会首先调用hookiat.MySetWindowTextW()函数。
接下来分析MySetWindowTextW()函数中的重要代码。MySetWindowTextW()函数的IpString参数是一块缓冲区,该缓冲区用来存放要输岀显示的字符串。所以,操作IpString参数即可在计算器中显示用户指定的字符串。
1 |
|
上述for循环将存放在IpString的阿拉伯数字字符串转换为中文数字字符串。图35-17描述的是IpString缓冲区更改前后的情形。
从图中可以看到,阿拉伯数字“123” 被更改为了中文数字“一二三”,即阿拉伯数字与中文数字是1 : 1的关系。利用这种特性可以不加任何修改地使用原缓冲区,也就是说,把阿拉伯数字转换为对应的中文数字时,缓冲区尺寸并未改变。从代码32-2中可知,IpString字符串的缓冲区中直接保存的是变换之后的(中文)字符串。
若将阿拉伯数字“123” 更改为英文数字 “ONETWOTHREE”,显然英文数字要长得多,所以不能直接使用原缓冲区(123),而要先开辟一块新缓冲区,再将新缓冲区的地址传递给原始API。
1 |
|
for循环结束后,最后再调用函数指针g_pOrgFunc,它指向user32.SetWindowTextW()API的起始地址(该地址在DllMain()中已经获取并保存下来)。也就是说,调用原来的SetWindowTextW()函数,将(变换后的)中文数字显示在计算器的显示框中。总结一下MySetWindowTextW()函数:首先更改作为参数传递过来的IpString字符串缓冲区中的内容,然后调用SetWindowTextW()函数, 将lpString字符串缓冲区中的(更改后的)内容显示在计算器的显示框中。
下一小节将分析hook_iat()函数,它具体负责钩取IAT。
32.5.3 hook_iat()
1 |
|
该函数是具体执行IAT钩取的函数。函数代码虽然不长,但其中含有较多注释,使函数自身看上去较长。接下来逐一查看:hook_iat()函数的前半部分用来读取PE文件头信息,并查找IAT的位置(要理解这部分代码需要先了解IAT的结构)。
1 |
|
上面这几行代码首先从ImageBase开始,经由PE签名找到IDT。plmportDesc变量中存储着IMAGE_IMPORT_DESCRIPTOR结构体的起始地址,后者是calc.exe进程IDT的第一个结构体。IDT是由IMAGE_IMPORT_DESCR!PTOR结构体组成的数组。若想查找到IAT,先要查找到这个位置。上面的代码中,plmportDesc变量的值为01012B80,使用PEView查看该地址,如图32-18所示。
图32-18是计算器进程的IMAGE_IMPORT_DESCRIPTOR(数组),它在PEView中名为IDT。我们要查找的user32.dll位于图32-18的最下方,接下来使用for循环遍历该IDT。
1 |
|
在上面的for循环中比较pImportDesc->Name与szDllName(“user32.dll”),通过比较查找到user32.dll的IMAGE_IMPORT_DESCRIPTOR结构体地址。最终plmportDesc的值为01012BE4 (参考图32-18)。接下来进入user32的IAT。pImportDesc->FirstThunk成员所指的就是IAT。
1 |
|
以上代码中,pThunk就是user32.dll的IAT (010010A4,参考图32-18)。使用PEView查看该地址,如图32-19所示。
可以看到,user32.dll的IAT中导入了相当多的函数。我们要查找的SetWindowTextW位于010013F8地址处,其值为77D2612B。
1 |
|
在上面的for循环中比较pThunk->ul.Function与pfnOrg (77D2612BSetWindowTextW的起始地址),准确查找到 SetWindowTextW 的 IAT 地址(010013F8)(当前 pThunk=010013F8,pThunk->ul.Function=77D2612B)。
上述代码就是从计算器进程的ImageBase开始查找user32.SetWindowTextW的IAT地址的整个过程。
若不怎么理解上述代码,请参考第13章中有关IAT的部分,使用PEView逐一查找。
查找到IAT地址后,接下来就要修改(hooking)它的值
1 |
|
pThunk->ul.Function中,原来的值为77CF61C9 (SetWindowTextW地址),上面语句将其修改为(10001000 hookiat.MySetWindowTextW的地址)。这样,计算器代码调用user32.SetWindowTextW() API时,实际会先调用hookiat.MySetWindowTextW()函数。
从上述hook_iat()函数代码中可以看到,钩取前先调用了 VirtualProtect()函数,将相应IAT的内存区域更改为“可读写”模式,钩取之后重新返回原模式的代码是存在的。该语句用来改变内存属性,由于计算器进程的IAT内存区域是只读的,所以需要使用该语句将其更改为“可读写”模式。
对hook_iat.cpp代码的分析到此结束。如果理解了 IAT钩取的内部工作原理,再阅读hook_iat.cpp代码时就会感到很容易,理解起来也不会有什么难度。
32.6 调试被注入的DLL文件
本节将使用OllyDbg调试钩取代码,并查看被钩取的IAT内存区域。此外还要学习如何调试注入进程的DLL文件。我们要调试的是hookiat.dll文件,它被注入计算器(calc.exe)进程。首先运行计算器程序,然后用Process Explorer查看计算器进程的PID值,如图32-20所示。
接下来将calc.exe进程附加到OllyDbg,如图32-21所示。
我们使用的是OllyDbg 2.0版本,用OllyDbg 1.10调试被注入的DLL会遇到一些Bug,导致调试进程意外终止。
附加成功后,按F9运行键运行calc.exe进程。然后设置OllyDbg选项,如图32-22所示。这样,注入DLL文件(hookiat.dll)时,控制权就会转给调试器。
如图32-22所示,在OllyDbg的Options窗口中复选Pause on new module (DLL)选项后,每当有DLL加载(含注入)到被调试进程时,控制权就会转移给调试器。设置好选项后,在OllyDbg中按F9运行键正常运行计算器进程。在命令行窗口中输入相应参数,运行InjectDll.exe,将hookiat.dll注入计算器进程(参考图32-23)。
这里使用了VS,命令行会注入失败。
calc.exe进程中发生DLL加载事件时,相关事件就会被通知到OllyDbg,如图32-22所示设置好选项后,调试器就会在hookiat.dll的EP处暂停下来,如图32-24所示。
有DLL被加载时,调试器会自动暂停在被加载的DLL的EP处,这是OllyDbg2.0中提供的功能。若使用的是OllyDbg1.1,调试器会在非EP的其他代码位置处(ntdll.dll 区域)暂停。
接下来,取消图32-22中复选的Pause on new module (DLL)选项,查找DllMain()代码。
在调试器中查找hookiat.dll的DllMain()函数最简单的方法是,检索DllMain()中使用的字符串或API (当然也可以使用Step In(F7)命令逐行跟踪查找)。参考代码32-1可知,DllMain()函数中使用的字符串有“user32.dll”与“SetWindowTextW”。下面通过查找代码中使用的“user32.dll”与 “SetWindowTextW”字符串来找DllMain()函数。在OllyDbg的代码窗口中选择鼠标右键菜单Search for All referenced strings选项,如图32-25所示。
从图32-25可知,“user32.dll”字符串有2处,“SetWindowTextW”字符串有1处。转到引用“SetWindowTextW”字符串的代码地址1ED112E处,如图32-26所示。
图32-26黑色线框中的反汇编代码与代码32-1中的C语言代码是一致的。因此该部分(1ED1120~)就是DllMain()函数(DllMain()函数的起始地址为1ED1120)。这就是调试注入进程的DLL的方法。
32.6.1 DIIMain()
下面从DllMain()函数起始位置开始调试(与代码32-1比较查看,理解起来会更容易)。继续调试DllMain(),出现如图32-27所示的代码。
地址1ED1151处的CALL hook_iat指令就是调用hook_iat()函数的部分。由于函数参数逆序存储在栈中,所以各参数含义与注释的描述一样(请比较代码32-1与图32-27)。需要注意的是,在代码32-1中可以看到hook_iat()函数有3个参数,而图32-27中的hook_iat()的参数只有2个。
从栈窗口中可以清楚地看到这一点,需要注意的是栈中只有一个参数。另一个参数是原始函数的地址,它通过EDX寄存器进行传递,这是通过指令
MOV EDX,EAX
完成的。这就是所谓的寄存器传参。
仔细查看可以发现,hook_iat()的第一个参数“user32.dll”字符串 的地址和原始函数的地址被省略了。这是VC++编辑器进行代码优化的结果,字符串的地址(4字节常数)并未作为函数参数传入,而被硬编码到hook_iat()函数中。大家以后调试自己编写的程序时,会经常遇到上述代码优化现象。
32.6.2 hook_iat()
hook_iat()是具体负责实施IAT钩取的核心函数,下面开始调试它。
查找IMAGE_IMPORT_DESCRIPTION
图32-28灰色部分代码描述了从PE文件头中查找IMAGE_IMPORT_DESCRIPTION Table (下 称“IID Table”)的过程(在PEView中可以看到多个IID—起组成了IDT)。以上代码(1ED1093~1ED10A5)仅用6条汇编指令就找到了IID Table。
如果你尚未掌握PE文件头的结构,或初次接触上面这样的汇编代码,那么很可能不理解代码内容。此时可以同时打开PEView,边参考PE文件结构边调试。其实这不是什么困难的事情,看多了自然就明白了。等以后熟悉了,只要看到[EDI+3C]、[EDI+EAX+80]等代码,也能轻松知道它们是用来跟踪IID Table的代码。
地址1ED10BA处的CALL _stricmp
指令用于调用stricmp()函数。通过遍历IID Table比较IID.Name与“user32.dll”字符串,最终查找到user32.dll对应的IID。 在IAT中查找SetWindowTextW API的位置查找到user32.dll对应的IID后,下面的代码用来在IAT中查找SetWindowTextW API的位置(参考图32-29)。然后修改其中的内容,从而实现对API的钩取。
1ED10D2地址处的CMP DWORD PTR DS:[ESI],EBX
指令中,ESI的值为user32.dll的IAT起始地址,EBP的值为SetWindowTextW的地址(77D2612B)。图32-29的代码运行循环进入IAT,查找位于01001110的SetWindowTextW的地址值(77D2612B)。
IAT钩取
图32-30是实际钩取IAT的代码。
1ED1103地址处的MOV DWORD PTR DS:[ESI],EAX
指令用来将MySetWindowTextW (hooking函数)的地址覆写到前面从IAT中获取的SetWindowTextW的地址(010013F8)。地址010013F8在calc.exe进程中位于user32.dll的IAT区域,该地址原来存储着SetWindowTextW地址(77D2612B)(参考图32-29)。
执行1ED1103地址处的MOV指令后,user32.SetWindowTextW地址(77D2612B)被更改为hookiat.MySetWindowTextW地址(1ED1000)(参考图32-30)。从现在开始,calc.exe进程代码(通过IAT)调用user32.SetWindowTextW() API时,实际调用的是hookiat.MySetWindowTextW()。
32.6.3 MySetWindowTextW()
完成IAT钩取操作后,在OllyDbg中按F9键正常运行计算器(calc.exe)进程。
在calc.exe进程中调用user32.SetWindowTextW() API的代码处设置断点,调试hookiat.MySetWindowTextW()函数被调用的情形。首先在调用user32.SetWindowTextW() API的代码处设置断点。使用OllyDbg的Search for All intermodular calls功能,打开如图32-31所示的对话框。
可以看到,之前所有的SetWindowTextW函数都成功被Hook成了MySetWindowTextW。
地址10013F8中原来保存的是user32.SetWindowTextW()的地址,钩取后,存储的地址变为 hookiat.MySetWindowTextW()的地址(1ED1000),如图32-32所示。进入MySetWindowTextW()函数继续调试,岀现图32-33所示的代码。
MySetWindowTextW()函数主要有2个功能,它首先将阿拉伯数字转换为中文数字(字符串),然后调用原来的 user32.SetWindowTextW() API。1ED1068地址处的 CALL DWORD PTR DS:[g_pOrgFunc]指令就是用来调用user32.SetWindowTextW() API的。1EDEB24地址是hookiat.dll中.data节区的全局变量(g_pOrgFunc), DllMain()函数会事先将SetWindowTextW的地址存入此处(参考代码32-1、图32-7)。至此,对注入calc.exe进程的hookiat.dll的调试就全部结束了。
32.7 小结
IAT钩取是API钩取技术之一,本章详细讲解了该技术的内部工作原理,并通过DLL注入技术将hookiatdll注入目标进程,由此进行API钩取调试练习。理解了这些工作原理与相关概念后,就可以继续下一章的学习。
参考
《逆向工程核心原理》 第32章