Code Patch
本节关注另外一种API Hook的实施细节—–修改API代码(Code Patch)。通过Code Patch方法,展示通过这种hook技术完成向系统隐藏指定进程(Rootkit)。
Rootkit
隐藏进程(stealth process)在代码逆向分析领域中的专业术语为Rootkit,它指的是通过修改(hooking)系统内核来隐藏进程、文件、注册表等的一种技术。
实施细节
相比先前章节的内容,IAT钩取通过操作进程的对应的IAT值来实现API钩取,控制程序的执行流程。代码修改(Code patch)技术则是将API代码的前5个字节修改为JMP xxxxxxxx
指令来钩取API,当调用由Code Patch钩取的API时,由于被钩取的API代码前5个字节被修改为JMP xxxxxxxx
,因此会跳转到 JMP 目的地xxxxxxxx
的位置去执行代码,进而控制程序的执行。(下面图片,来自书中内容)
实验
一些进程查看软件,例如Procexp.exe,可以查看当前系统中运行的进程。而隐藏进程则是钩取进程查看软件的相关API,从中通过Hook Function使得待隐藏的目标进程对于进程查看软件不可视。
相关API
ntdll.ZwQuerySystemInfomation
,API文档,原型如下
1 | NTSTATUS WINAPI ZwQuerySystemInformation( |
第一个参数指向SYSTEM_INFORMATION_CLASS
的枚举类型,指出想要获取的信息。出于本节查看进程信息的目的需要设置为:SYSTEM_INFORMATION_CLASS = SystemProcessInformation
。若获取成功,则第二个参数SystemInformation
则会写入相应获取到进程信息,本节中,该字段会获取到相应的结构体链表信息,单个的结构体如下:
1 | typedef struct _SYSTEM_PROCESS_INFORMATION { |
因此,大致的隐藏进程的思路便有了:通过钩取ZwQuerySystemInformation
,设置好参数,修改第二个参数中返回的系统信息链表,遍历查找出我们需要隐藏的目标进程在链表中对应的节点,将之删除即可达到隐藏进程的目的。
过程分析
下Hook
- 使用的DLL注入技术,将DLL文件注入进程查看软件,注入后可获取和宿主进程一样的权限。
- 通过DllMain修改宿主进程中API
ntdll.ZwQuerySystemInfomation
的前五个字节的指令码,修改为JMP 10001120
,其中0x10001120
的内容是写在DLL文件中的Hook Function
,如此一来即可完成对APIntdll.ZwQuerySystemInfomation
的Hook工作。
Hook工作
完成Hook的工作后,当进程查看软件调用API ntdll.ZwQuerySystemInfomation
试图借此获取当前系统中运行的进程信息时,由于前五bytes的指令字节码被修改为JMP 10001120
,因此会跳转向Hook Function
的地址去执行相应的内容。
- 首先需要对API
ntdll.ZwQuerySystemInfomation
进行unhook操作,再对恢复好的APIntdll.ZwQuerySystemInfomation
进行调用。 - 对API
ntdll.ZwQuerySystemInfomation
返回的包含系统进程信息链表进行遍历,从中删除想隐藏的进程对应的节点,便可使得待隐藏进程对进程查看软件不可视。 - 之后重新对API进行Hook,以便再次使用。
源码分析
HideProc part
1 |
|
下面分部分对HideProc的源码进行分析:
main
1 | int _tmain(int argc, TCHAR* argv[]) |
- 第7-12行,完成的是对参数的检测。第一个参数是启用的模式:一个是隐藏进程模式,一个是取消隐藏模式; 第二个参数是想要隐藏的进程名;第三个参数是注入的DLL文件。
- 第15行,提升权限
- 第18-22行,完成DLL的载入,并指定将隐藏的进程的。
- 第25-28行,根据启用的模式调用
InjectAllProcess
InjectAllProcess
1 | BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath) |
完成的内容是使用CreateToolhelp32Snapshot
& Process32First
& Process32Next
遍历获取到的进程快照,比对是否为目标进程对应的PID。第23-26行,根据nMod的不同,调用不同的接口。
InjectDll
1 | BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) |
- 第8-12行,使用
OpenProcess
以设置好的参数(PROCESS_ALL_ACCESS权限)尝试打开目标进程。 - 第14-18行,当
OpenProcess
成功打开目标进程,则在进程内部开辟一段足够存放待注入DLL路径信息的空间(具备写入等权限)起始地址为pRemoteBuf
,之所以能开辟是由于在main中提升了权限。 - 第20-24行,先是使用
GetProcAddress
获取Kernel32.LoadLibraryw
API的地址存放在pThreadProc
中,随后调用CreateRemoteThread
,开辟的线程去执行LoadLibraryw
调用,传递给LoadLibraryw
的参数是写入DLL路径的地址空间LoadLibraryw
,线程执行即可完成dll的载入。 - 之后的内容就是等待线程执行结束,释放开辟出来的内存空间,关闭相关句柄等
DLL part
1 |
|
接下来逐部分对stealth.dll的源码进行分析。通过HideProc所做的工作,可完成对目标进程注入该DLL
DllMain
1 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) |
分情况调用
hook_by_code
传入待钩取API所在的模块名称,待钩取的API,钩取函数地址,用于保存原来字节码内容的位置。unhook_by_code
传入待恢复的API所在的模块名称,待恢复的API,保存原来字节码内容的位置。
hook_by_code
1 | BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes) |
- 第8-9行,指出需要钩取的API地址,将API地址转化为字节指针,方便后续读取出字节内容。
- 第11-12行,比对API地址的第一个字节内容,判断是否被钩取过(E9代表JMP)
- 第14-15行,修改API所在内存的内存保护权限,因为需要修改字节内容以及执行需要有
PAGE_EXECUTE_READWRITE
,并保存旧的权限,方便后面回复。(可能PE装载器会验证) - 第17-18行,可以说是最关键的内容了。完成对JMP目标地址的计算,计算结果是相对地址,这是JMP指令的操作数决定的。具体计算方法:
XXXXXXXX = 跳转目标地址 - 当前指令地址 - 当前指令长度(5)
- 第20-22行,覆盖原API的前5字节,恢复相关内存的旧权限,完成HOOK
至于unhook_by_code
部分,相信经过上述的部分的分析,读者也能尝试自己阅读这部分源码。
1 | BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes) |
以上便是对于隐藏进程Rookie技术的实施过程。但依旧存在以下问题:
- 需要进行DLL注入完成钩取的进程个数:由于进程查看器这类的软件由很多,上述的方案中,需要将DLL注入进程查看器对应的进程。若只是注入一个进程查看器进程,则再次开启另一进程查看器进行查看,依旧会使得我们想隐藏的进程被观察到。即对于后续即将启用的新的进程查看器的进程依旧看得到我们想隐藏的目标。
结局方案:全局Hook。若设置的钩子是全局的,使得当前系统中无论是否是在下完钩子后的进程调用被Hook的API,依旧会做同样的钩子函数的流程。这部分,我们下节讨论!