re-core-principle-3
逆向工程核心原理笔记-3
0x00 API钩取:逆向分析之花
代码逆向分析中,钩取(Hooking)是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的流程如下:(1)使用反汇编器/调试器熟悉程序的结构与工作原理;(2)开发钩子代码;(3)操作可执行文件与进程内存,部署钩子代码。
 其中,钩取Win32 API的技术被称为API钩取。分析程序时若有程序源码,大部分情况都不需要使用钩取技术。
API概念
API(Application Programming Interface,应用程序编程接口)。Windows中,程序要使用系统资源(内存、文件、网络、视频、音频等)时无法直接访问,这些资源都是由Windows管理的,Windows禁止用户程序直接访问它们。程序需要使用这些资源时,必须向系统内核(Kernel)申请,申请的方法就是使用微软提供的API。下图为32位windows进程内存的情况:

 可以看到,由ntdll.dll向内核提出申请。
 所有进程都会默认加载kernel32.dll库(但是有一些例外),kernel32.dll又会加载ntdll.dll库。在GUI程序中,必须加载user32.dll与gdi32.dll库。
API钩取
 正常调用API过程举例:(1)在应用程序代码区域中调用CreateFile API。(2)由于CreateFile API是kerel32.dll的导出函数,所以,kernel32.dll区域中的CreateFile API会被调用执行并正常返回。

 API钩取调用过程举例:用户先使用DLL注入技术将hook.dll注入目标进程的内存空间。(2)用hook!MyCreateFile钩取对kernel32!CreateFile的调用。(3)每当目标进程要调用kernel32!CreateFile时都会先调用hook!MyCreateFile。

API钩取技术图表(Tech Map)
常用的技术在下图中已用下划线标出。

动态与静态钩取的不同如下:

钩取位置:
- IAT。将程序内部的API地址更改为钩取函数地址。该方法的优点是实现起来非常简单,缺点是无法钩取不在IAT而在程序中使用的API,如动态加载并使用DLL时。
 - 代码。系统库(
*.dll)映射到进程内存时,从中查找API的实际地址,并直接修改代码。 
向目标进程内存设置钩取函数的具体技术:
- 调试。这里所说的调试器并不是
OllyDbg等,而是用户直接编写的、用来钩取的程序(在用户编写的程序中使用调试API附加到目标进程)。当然也可以在ollyDbg上使用自动化脚本,自动钩取API。使用这种方法,即便是在钩取API的过程中,用户也可以暂停程序运行,进行添加、修改、删除API钩取等操作。 - 注入。DLL注入与代码注入。
 
0x01 记事本WriteFile的API钩取(调试器)
此章主要讲解以下API钩取技术(红框):

本章的钩取将会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以使用进程内存。
调试器相关知识
1  | 调试器(Debugger) :进行调试的程序  | 
调试器可以逐一执行被调试者的命令,并拥有对寄存器和内存的所有访问权限。
 调试器的工作原理:调试进程经过注册后,每当被调试者发生调试事件(Debug Event)时,OS就会暂停其运行并向调试器报告相应事件。调试器对相应事件做适当处理后,使被调试者继续运行。其中,一般的异常(Exception)也属于调试事件。若相应进程处于非调试,调试事件会在其被调试程序自身的异常处理或OS的异常处理机制中被处理掉。调试器无法处理或不关心的调试事件最终由OS处理。
调试器工作原理如下图所示:

 各种调试事件(Debug Event)如下所示:
1  | EXCEPTION_DEBUG_EVENT  | 
 上述调试事件中,真正与调试相关的事件是:EXCEPTION_DEBUG_EVENT,与其相关的异常有很多,例如EXCEPTION_BREAKPOINT,表示断点。
 断点对应的汇编指令为INT3,IA-32指令为0xCC。代码调试遇到INT3指令即中断运行。调试器实现断点的方法:(1)找到要设置断点的代码在内存中的起始地址,把1个字节修改为0xCC。(2)若想继续调试,将它恢复原值即可。
注:IA-32代表32位版本的x86指令集架构,由Intel设计。
调试技术流程
 下面说明借助调试技术钩取API的方法。基本思路是:将被调试者的API起始部分修改为0xCC,控制权转移到调试器后执行指定操作,最后使被调试者重新进入运行状态。
 具体流程如下:
1  | 1. 对想钩取的进程进行附加操作,使之成为被调试者  | 
练习:记事本WriteFile的API钩取
 此示例的功能:钩取Notepad.exe的WriteFile(),保存文件时操作输入参数,将小写字母全部转换为大写字母。即,在Notepad中保存文件内容时,其中输入的所有小写字母都会先被转换为大写字母,然后再保存。感觉挺有意思,效果如下所示:

工作原理
 假设notepad要保存文件中的某些内容时会调kernel32!WriteFile,那么我们首先要确定一下假设是否正确。
 kernel32!WriteFile的定义如下:

1  | lpBuffer: 数据缓冲区指针  | 
 打开Notepad.exe,在kernel32!WriteFile打断点,然后输入字符后,保存,程序停在了kernel32!WriteFile处。此时堆栈结构如下:

上述红框地址保存了输入到Notepad中的的字符。
 那么我们确定,notepad要保存文件内容时会调kernel32!WriteFile。且我们验证了我们的思路:钩取WriteFile后,用指定字符串覆盖上述红框中的字符串即可完成上述功能。
 下面我们使用调试方法来钩取API。利用给出的hookdbg.exe,在WriteFile起始地址处设置断点(INT3)后,被调试进程(notepad.exe)保存文件时,EXCEPTION_BREAKPOINT事件就会传给调试器(hookdbg.exe)。
 此时被调试者(notepad.exe)的EIP值不是WriteFile的起始地址,而是WriteFile的起始地址+1。为什么呢?原因在于,我们在WriteFile API的起始地址处设置了断点,被调试者(notepad.exe)内部调用WriteFile时,会在起始地址遇到INT3(0xCC)指令。执行该指令后EIP的值会增加1个字节,然后控制权会转移给调试器(hookdbg.exe)。因为在调试器被调试者关系中,被调试者中发生的异常需要由调试器处理。覆写了数据缓冲区的内容(将0xCC改为原来的值)后,EIP值被重新更改为WriteFile的起始地址继续运行。Ollydbg也是这样做的,只不过页面不展示。
 下面分析hookdbg.exe的代码,注释写的很清楚了,不再赘述。
1  | // hookdbg.cpp -> hookdbg.exe  | 
 注:还有一个函数叫做:DebugSetProcessKillOnExit,它可以在不销毁被调试进程的情况下退出调试器,也就是detach。需要注意的是,必须在调试器终止前脱钩(也就是把断点恢复),否则,调用API时就会因为其起始部分仍为0xCC而导致异常,从而终止被调试进程。
 上述代码中Sleep(0)的作用(重点):调用Sleep(0)函数可以释放当前线程的剩余时间片,即放弃当前线程执行的CPU时间片。也就是说,调用Sleep(0)函数后,CPU会立即执行其他线程。
 具体是啥意思呢?就是,如果不加入Sleep(0),CPU会运行完这个时间片,这个时间片是调试进程hookdbg.exe占用的,然后,notepad.exe还没开始运行正常的writefile过程,这个时间片就运行到hookdbg.exe的下一句了,也就是更改首字节为0xCC。
0x02 关于调试器
略。
0x03 计算器显示中文数字
 本节中,通过注入DLL文件来钩取某个API,DLL文件注入目标进程后,修改IAT来更改进程中调用的特定API。以计算器(calc.exe)为示例,向计算器进程插入用户的DLL文件,钩取IAT的user32.SetWindowTextW地址。此函数被钩取之后,计算器中显示出的将是中文数字,而不是阿拉伯数字。本节的技术图表如下:

优点:实现简单,只需先将要钩取的API在用户的DLL中重定义,然后再注入目标进程即可。缺点:如果想钩取的API不在目标进程的IAT中,那么就无法使用该技术进行钩取操作,即如果要钩取的API是由程序代码动态加载DLL文件来使用的,那么将无法使用这项技术。
使用PEview看calc.exe所用的API,如下所示:

 注意两个API:SetWindowTextW、SetDlgItemTextW。其中SetDlgItemTextW调用了SetWindowTextW,所以我们只需要钩取SetWindowTextW即可。SetWindowTextW定义如下:

 所以,钩取时只需要将lpString中的阿拉伯数字转为中文数字即可。注:(1)SetWindowTextW中的W代表宽字符(Unicode),与其对应的SetWindowTextA代表ASCII字符。(2)Unicode码中每个汉字占2个字节。具体细节见P311。
IAT钩取工作原理
 下图表示正常调用SetWindowTextW的程序执行流:

 下图是IAT被钩取后SetWindowTextW的调用流程:

可以保证,在保持运行代码不变的前提下,将IAT中保存的API地址变为用户函数的地址。函数操作见P315。
源代码分析
补充:
- FARPROC关键字。FARPROC是一个函数指针类型,通常用于Windows平台的动态链接库(DLL)中。在Windows平台上,DLL中的函数通常被导出为外部符号(external symbols),并且在运行时动态链接到程序中。FARPROC是一个函数指针类型,可以用来指向这些导出的函数。它的定义如下:
 
1  | typedef int (FAR WINAPI *FARPROC)();  | 
 其中,FAR和WINAPI是Windows API中的宏定义,用于表示函数指针的调用约定和存储方式。FARPROC指向一个无返回值、无参数的函数,可以根据实际情况进行类型转换。在使用DLL中的函数时,可以使用Windows API中的GetProcAddress函数获取导出函数的地址,并将其转换为FARPROC类型的函数指针,然后通过调用该指针来调用DLL中的函数。
 钩取过程如下:使用InjectDll.exe,向计算器进程calc.exe中注入hookiat.dll。
hookiat.dll代码如下:
1  | // hookiat.cpp -> hookiat.dll  | 
InjectDll.exe代码如下:
1  | // InjectDll.cpp -> InjectDll.exe  | 
0x04 隐藏进程
本节介绍:(1)通过修改API代码(Code Patch)实现API钩取的技术;(2)全局钩取(Global hooking),它能钩取所有进程;(3)使用全局钩取隐藏(Stealth)特定进程。
相关技术图表如下:

0x03中,介绍了 IAT 钩取技术,如果要钩取的 API不在进程的 IAT中,那么就无法使用该技术,而 API code patch 没有这一限制。
API code patch 原理:
 IAT钩取通过操作进程的特定IAT值来实现API钩取,而API代码修改技术则将API代码的前5个字节修改为JMP XXXXXXXX指来钩取API。调用执行被钩取的API时,(修改后的)JMP XXXXXXXX指令就会被执行,从而转到hooking函数。
钩取之前正常调用的API:

 钩取之后调用API的流程如下(procexp.exe注入stealth.dll文件后,钩取ntdll.ZwQuerySystemInformation()的整个过程。ntdll.ZwQuerySystemInformation是为了隐藏进程而需要钩取的API):

上图过程的详细解释如下:
1  | 首先,把stealth.dll注入目标进程,钩取ntdll.ZwQuerySystemInformation。ntdll.ZwQuerySystemInformation起始地址(7C93D92E)的5个字节代码被修改为JMP 10001120(仅修改5个字节代码)。10001120是stealth.MyZwOuerySystemInformation函数的地址。此时,在procexp.exe代码中调用ntd11ZwQuerySystemInformation,程序将按如下顺序执行:  | 
API code patch可以钩取任意API,而IAT钩取只能钩取表中有的API。
注:API code patch就是指直接修改映射到目标进程内存空间的系统 DLL的代码。但是,进程的其他线程正在读(read)某个函数时,尝试修改其代码会怎么样呢?这样做会引发非法访问(Access Violation)异常。
进程隐藏原理:
隐形战机是在自身进行喷涂,使得雷达无法检测到。而进程隐藏则是:要潜入其他所有进程内存,钩取相关API。也就是说,把所有人的雷达都干掉。
 一般来说,进程可以通过一些API检测到其他的进程,这些API有:CreateToolhelp32Snapshot,EnumProcess。这两个API在内部都调用了ntdll.ZwQuerySystemInformation。因此,借助此API就可以获得运行中所有进程的信息,形成一个列表,操作该列表(删除某条目)即可隐藏相关进程。注意,我们要钩取的目标进程不是要隐藏的进程,而是其他所有的进程。
 假如我们要隐藏的进程为test.exe,如果钩取运行中的ProExp.exe(进程查看器)(或者任务管理器taskmgr.exe)进程的ZwQuerySystemInformation,那么ProcExp.exe就无法查找到test.exe。这种方法存在两个问题:(1)不是只有ProExp.exe才能查看要运行的进程,我们只钩取了一个。(2)如果新开一个ProExp.exe,新开的ProExp.exe能够查看我们想要隐藏的进程。为了解决上述问题?我们要对运行中的所有进程都要进行钩取,并且对后面要启动的进程也要做钩取操作。
练习
 HideProc.exe负责将stealth.dll文件注入所有运行中的进程。stealth.dll负责钩取进程的ntdll.ZwQuerySystemInformation。需要说明的是,上述文件不能解决全局钩取的问题,因此这是一种不完全隐藏技术。具体过程在P334页,跟着做就好。
HideProc.exe 源代码如下:
1  | 
  | 
 在说明stealth.dll之前,由于钩取了ntdll.ZwQuerySystemlnformation的API,因此,我们要先了解这个API。

 简单讲解:将SystemInformationClass参数设置为SystemProcessInformation后调用ZwQuerySystemInformation,SystemInformation [in/out]参数中存储的是SYSTEM_PROCESS_INFORMATION结构体单向链表(sigle linked list)的起始地址。该结构体链表中存储着运行中的所有进程的信息。所以,隐藏某进程前,先要查找与之对应的链表成员,然后断开其与链表的链接。
 下面是stealth.dll的代码:
1  | // stealth.cpp -> stealth.dll  | 
上述代码有几个坑:
1  | 1. 路径要写全,例如stealth.dll要写成绝对路径。如果不加绝对路径,那只能注入到当前目录下开的进程。  | 
上面的JMP指令每次都要计算相对地址,我们也可以直接用绝对地址跳转,例如:
1  | (1) PUSH+RET  | 
全局API钩取
针对的是:(1)当前所有进程;(2)未来所有进程。上述示例不满足条件(2)。
Kernel32.CreateProcess API
 Kernel32.CreateProcess 用来创建新进程,其他启动运行进程的API(WinExec、ShellExecute、system)在内部也调用的Kernel32.CreateProcess 函数。Kernel32.CreateProcess API定义如下:

 因此,我们不仅要钩取ZwQuerySystemlnformation,还要钩取CreateProcess。由于所有进程都是由父进程(使用CreateProcess)创建的,所以,钩取父进程的CreateProcess就可以将stealth.dll文件注入所有子进程(父进程通常都是explorer.exe)。但是还要充分考虑以下几个方面:
1  | 1. 钩取CreateProcess时,还要分别钩取kernel32.CreateProcessA、kernel32.CreateProcessW这2个API(ASCI版本与Unicode版本)。  | 
 但是,我们很懒,我们总想一步到位,有这样的好事吗?有,就是钩取Ntdll.ZwResumeThread。其定义如下所示:

 ZwResumeThread函数在进程创建后、主线程运行前被调用执行(CreateProcess内部调用执行)。所以只要钩取这个函数,即可在不运行子进程代码的状态下钩取API。注:ZwResumeThread是一个尚未公开的API,将来的某个时候可能会被改变。
 练习具体在P346。注:要想全局钩取,就要把stealth2.dll放到%SYSTEM%(C:\\Windows\\System32)文件夹下。
 HiddenProc2.cpp与之前一样。
 stealth2.cpp代码如下所示:(钩取的CreateProcessA、CreateProcessW以及ZwQuerySystemlnformation。)
1  | // stealth2.cpp -> stealth2.dll  | 
“热补丁”技术钩取API
 上述代码中,NewCreateProcessA函数的结构简单梳理如下:

 每当在程序内部调用CreateProcessA 时,NewCreateProcessA就会被调用执行,不断重复脱钩/挂钩。这种反复进行的脱钩/挂钩操作有两个问题:(1)整体性能低下;(2)在多线程环境下还会产生运行时错误。(例如,线程尝试运行某段代码时,若另一进程正在对该段代码进行写操作,这时就会出现冲突,最终引发运行时错误。)热补丁(Hot Patch)技术比修改5个字节代码的方法更稳定。
热补丁修改7个字节的代码。
通过观察多个Windows API的代码,代码有2个明显的特点:
1  | 1. API代码以 "MOV EDI, EDI"(0x8bff) 指令开始。  | 
这些代码没啥意义。微软布置这些的原因是为了方便打”热补丁”。“热补丁”由API钩取组成,在进程处于运行状态时临时更改进程内存中的库文件(重启系统时,修改的目标库文件会被完全取代)。
热补丁工作原理
二次跳转
 将API起始代码之前的5个字节修改为FAR JMP指令(E9XXXXXXXX),跳转到用户钩取函数处(用户写的API的封装)(10001000)。然后将API起始代码的2个字节修改为SHORT JMP(EB F9)指令。该SHORT JMP指令用来跳转到前面的FAR JMP处。像这样经过二次连续跳转,就可以完成对指定API的钩取操作。
热补丁钩取与5字节代码修改的比较如下:


在热补丁技术钩取API时,不需要在钩取函数内部进行脱钩/挂钩工作,因为在5字节代码修改中,脱钩是为了调用原API,但是使用热技术时,一直可以调用原API。除去了脱钩操作,因此热补丁技术更加稳定。
源代码分析
 HideProc3.cpp与之前一样。
 stealth3.cpp如下所示:
1  | 
  | 
 热补丁要求被钩取的API要有NOP*5 + MOV EDI, EDI,因此,并非所有API都能使用热补丁。例如,ntdll.dll的API代码都很短,且不满足热补丁条件。进行ntdll.dll API代码钩取时,先将原 API备份到用户内存区域,然后使用5字节代码修改技术修改原API的起始部分。在用户钩取函数内部调用原API时,只需调用备份的 API即可,这样实现的 API钩取既简单又稳定。
0x05 高级全局API钩取:IE连接控制
 本节中钩取IE浏览器,当IE连接指定网站时转而连接我的博客。(可以当作拦截恶意网页,当用户访问恶意网页时,转到其他的网页。)常见的与网络连接相关的库有:(1)ws2_32.dll(套接字库);(2)wininet.dll、winhttp.dll(网络访问相关的库);
 通过Process Explorer可以看到,IE加载了ws2_32.dll与wininet.dll。其中wininet.dll中有一个IntermetConnect的API,其用来连接某个网站。此API介绍如下:

其中,34.1的实验并未成功,思考应该是盗版win7的原因。但是换了正版win7还是不行。
 IE浏览器的每一个选项卡tab都是一个进程,当开启一个新的网页时,都会创建一个新的进程。因此,要实现全局钩取,否则新开网页的时候就不会达到我们的目的。之前,我们介绍了,要用全局钩取,有两种办法:(1)钩取CreateProcessA、CreateProcessW、ZwQuerySystemInformation。(2)钩取ZwResumeThread(不公开的API)。本节中使用方法(2),即,钩取ntdll!
ZwResumeThread,创建进程之后,主线程被Resume(恢复运行)时,可以钩取目标API。
常规API钩取与全局API钩取,图示如下:


 Explorer.exe是Windows操作系统的基本shell,可以创建子进程。
 当使用CreateProcessW创建进程时(钩取任何一个都能实现全局API钩取),其调用流程如下:

 那么,如果我们钩取ntdll!ZwResumeThread,就可以在子进程的EP代码运行之前,拦截获取控制权。ntdll!ZwResumeThread的定义如下:(ntdll!ZwResumeThread=ntdll!NtResumeThread)

练习示例
 向目标进程注入redirect.dll来实现API钩取,redirect.dll钩取下面2个API:
1  | wininet!InternetConnectW: 可以控制IE进程的连接地址  | 
 由于IE进程以父子进程的形式运行,只要钩取父进程的ntdll!ZwResumeThread API,那么后面生成的所有子IE进程都会自动钩取。实现的效果是,使用InjDll.exe,向IE进程iexplore.exe中注入redirect.dll。此后,打开IE浏览器,访问www.naver.com、www.daum.net、wwwnate.com、www.yahoo.com时,会直接访问www.ReverseCore.com。(上述34.1的实验不成功,但是这个代码却可以正常运行。)
 相应代码&注释如下:(InjDll.exe与之前的代码类似,不做赘述。)
1  | // redirect.cpp -> redirect.dll  | 
留言
- 文章链接: https://wd-2711.tech/
 - 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!