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 许可协议。转载请注明出处!