IAT hook与inline hook的区别——learning malware analysis这本书里提到了这两种rootkit的检测方法
IAT hook
导入表hook原理:修改导入表中某函数的地址到自己的补丁函数。IATHook
通过GetProcAddress获取目标函数地址
在程序内存中找到所在dll的导入表
查找目标函数地址保存的位置
把地址修改为自己补丁函数
问题:当该函数递归调用时,不会被hook
为解决这个问题,可以使用inline hook
inline hook
原理跟导入表相同,步骤也差别不大
但是二者都存在一个问题,就是当目标函数是一个递归函数时,只有第一次能hook成功,函数内部调用自身时不会执行补丁代码,因此最好的方法是Inline hook,如微软的detour库采用的方法,修改函数开头几个字节的机器码,使执行流程一进入函数就调到补丁代码,执行完补丁代码后在另外一个地方执行函数原来几个字节的代码(此处并不还原开头几个字节机器码),然后然后跳转到原函数patch后面的指令继续执行,永久的改变了函数内容。因为微软detour库有自带反汇编引擎,因此无需担心会破坏接下来执行的完整性,如果自己手动实现的话,需要先观察函数开头的执行,做到指令对齐,以免破坏patch后指令的完整导致执行崩溃。
https://terrorgum.com/tfox/books/learningmalwareanalysis_ebook.pdf 这本书里专门讲到了两种hook的区别:
当程序的IAT被hook时,IAT中DeleteFileA()的地址被替换 带有恶意函数的地址,如下所示。 现在,当合法程序 调用 DeleteFileA(),调用被重定向到恶意软件中的恶意函数 模块。 然后恶意函数调用原始的 DeleteFileA() 函数,以 让它看起来一切正常。 介于两者之间的恶意函数可以 防止合法程序删除文件,或监视参数(文件 被删除),然后采取一些行动: 除了阻止和监视之外,这通常发生在调用 原始函数,恶意函数还可以过滤输出参数,从而发生 重新调用后。 这样,恶意软件就可以挂钩显示进程列表的 API, 文件、驱动程序、网络端口等,并过滤输出以隐藏使用的工具 这些 API 函数。
使用此技术的攻击者的缺点是,如果 程序正在使用运行时链接,或者如果攻击者希望挂钩的函数已作为ordinal被导入则失效(见下原文)。 攻击者的另一个缺点是 IAT 挂钩可以 容易被发现。
在正常情况下,IAT 中的条目应位于 其对应模块的地址范围。 比如DeleteFile()的地址 应该在kernel32.dll的地址范围内。 为了检测这种挂钩技术, 安全产品可以识别 IAT 中位于其模块地址之外的条目 范围。
在 64 位 Windows 上,名为 PatchGuard 的技术会阻止修补调用 表,包括 IAT。 由于这些问题,恶意软件作者使用略有不同的挂钩技术,接下来将讨论。==》也就是说inline hook使用更为广泛!
英文原文:
IAT hook依赖于交换函数指针,而在inline hook中,API 函数本身被修改(修补)以将 API 重定向到恶意代码。 与 IAT 一样 挂钩,这种技术允许攻击者拦截、监视和阻止由 具体应用,以及滤波器输出参数。 在内联挂钩中,目标 API 函数的前几个字节(指令)通常被一个跳转语句覆盖 将程序控制重新路由到恶意代码。 然后恶意代码可以拦截 输入参数,过滤输出,并将控件重定向回原始函数。 为了帮助您理解,我们假设攻击者想要挂钩 DeleteFileA() 由合法应用程序进行的函数调用。 通常,当合法应用程序的 线程遇到对DeleteFileA()的调用,线程从头开始执行 DeleteFileA() 函数,如下所示: 要用跳转替换函数的前几条指令,恶意软件需要选择 替换哪个指令。 jmp 指令至少需要 5 个字节,因此恶意软件 需要选择占用5字节以上的指令。 在上图中,它是 可以安全地替换前三个指令(使用不同颜色突出显示),因为它们 恰好占用 5 个字节,而且,这些指令除了设置 栈帧。 复制 DeleteFileA() 中要替换的三个指令,并且 然后替换为某种跳转语句,将控制权转移给恶意软件 功能。
恶意函数做它想做的事,然后执行原来的 DeleteFileA() 的三个指令并跳回到位于下面的地址 patch(跳转指令下方),如下图所示。 被取代的 指令以及返回目标函数的跳转语句是已知的 作为trampoline:
这种技术可以通过在开始时寻找意外的跳转指令来检测 API 函数,但请注意,恶意软件可能会通过插入 在 API 函数中跳得更深,而不是在函数的开头。 而不是使用 jmp 指令,恶意软件可能使用 call 指令,或 push 和 ret 的组合 指令,重定向控制;
这种技术绕过了安全工具,这些工具只看 对于 jmp 指令。 了解内联挂钩后,让我们看一个恶意软件示例(Zeus 机器人)使用这种技术。 Zeus bot 挂钩各种 API 函数; 其中之一是 Internet Explorer (iexplore.exe) 中的 HttpSendRequestA()。
通过挂钩这个函数, 恶意软件可以从 POST 负载中提取凭据。 在挂钩之前,恶意的 可执行文件(包含各种功能)被注入到互联网的地址空间 探索者。 以下屏幕截图显示地址 0x33D0000,可执行文件位于 注入:
注入可执行文件后,HttpSendRequestA() 被挂钩以重定向程序 控制注入的可执行文件中的恶意功能之一。 在我们看之前 钩子函数,让我们看看合法的 HttpSendRequestA() 的前几个字节 功能(此处显示): 前三个指令(占用 5 个字节,在前面的屏幕截图中突出显示)是 替换为重定向控制。 以下屏幕截图显示了 HttpSendRequestA() 上钩后。 前三个指令替换为jmp指令(占用 5 个字节); 注意跳转指令如何将控制重定向到地址处的恶意代码 0x33DEC48,落在注入的可执行文件的地址范围内:
下面这个文章写得更详细,可以专门去看下!
https://bbs.pediy.com/thread-270901.htm
Inline HOOK
API函数都保存在操作系统提供的DLL文件中,当在程序中调用某个API函数并运行程序后,程序会隐式地将API函数所在的DLL文件加载入内存中,这样,程序就会像调用自己的函数一样调用API。Inline Hook这种方法是在程序流程中直接进行嵌入jmp指令来改变流程的。
简而言之,就是将函数开头修改为jmp指令,跳转到我们自定义的函数上去。
首先用CreateProcessA API写一个测试程序,功能很简单,程序启动后,按下任意键,调用CreateProcessA创建进程,为了直观这里是直接弹一个计算器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <windows.h> #include <stdio.h> #include "createprocess.h" #define EXE_PATH "C:\\Windows\\System32\\calc.exe" BOOL CreateProcessR(char * szExePath) { SECURITY_ATTRIBUTES psa = { 0 }; SECURITY_ATTRIBUTES tsa = { 0 }; STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; BOOL Ret; Ret = CreateProcessA(szExePath, NULL, &psa, &tsa, false, 0 , NULL, NULL, &si, &pi); / / TerminateProcess(pi.hProcess, 0 ); / / 结束进程 return Ret; } int main( int argc,char * argv[]) { system( "pause" ); CreateProcessR(EXE_PATH); return 0 ; } |
下面就HOOK CreateProcessA:
将生成的EXE拖到Xdbg中,定位到CreateProceessA这里,在调用CreateProcessA前有一段汇编代码:
mov edi edi,
push ebp
mov ebp,esp
16进制:8B FF 55 8B EC
HOOK的话肯定要准备一个自定义的函数, call addr ,假如它的地址为12345678,那么我们HOOK的操作就是将上面的命令替换为:JMP 12345678,也就是e9 addr。
解除的HOOK的话就是将替换的字节恢复。
Inline Hook流程
- 构造跳转指令。
- 在内存中找到欲Hook函数地址,并保存欲Hook位置处的前5字节。
- 将构造的跳转指令写入需Hook的位置处。
- 当被Hook位置被执行时会转到自己的流程执行。
- 如果要执行原来的流程,那么取消Hook,也就是还原被修改的字节。
- 执行原来的流程。
- 继续Hook住原来的位置。
代码如下,关键步骤已注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
/ / Myhook.cpp #include "Myhook.h" CHOOK::CHOOK() { MyFuncaAddress = NULL; memset(MyOldBytes, 0 , 5 ); memset(MyNewBytes, 0 , 5 ); } CHOOK::~CHOOK() { UnHOOK(); MyFuncaAddress = NULL; memset(MyOldBytes, 0 , 5 ); memset(MyNewBytes, 0 , 5 ); } BOOL CHOOK::Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc) { HMODULE hModule = GetModuleHandle(pszModuleName); MyFuncaAddress = (PROC)GetProcAddress(hModule, pszFuncName); if (MyFuncaAddress = = NULL) { return FALSE; } / / 读地址 将原来的 5 个字节的数据保存 ReadProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes, 5 , 0 ); / / JMP ADDRESS JMP 123456789 E9 MyNewBytes[ 0 ] = '\xE9' ; * (DWORD * )(MyNewBytes + 1 ) = (DWORD)pfnHookFunc - (DWORD)MyFuncaAddress - 5 ; WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes, 5 , 0 ); return TRUE; } VOID CHOOK::UnHOOK() { if (MyFuncaAddress ! = NULL) { WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes, 5 , 0 ); } return VOID(); } BOOL CHOOK::ReHook() { if (MyFuncaAddress ! = NULL) { WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes, 5 , 0 ); } return 0 ; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
/ / dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "Myhook.h" CHOOK MyHookObject; BOOL WINAPI MyCreateProcessA( _In_opt_ LPCSTR lpApplicationName, _Inout_opt_ LPSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCSTR lpCurrentDirectory, _In_ LPSTARTUPINFOA lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation ) { if (MessageBox(NULL, "是否拦截" , "Notice" , MB_YESNO) = = IDYES) { MessageBox(NULL, "程序已拦截" , "Notice" , MB_OK); } else { MyHookObject.UnHOOK(); CreateProcessA( lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation ); MyHookObject.ReHook(); } return true; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MyHookObject.Hook((LPSTR) "Kernel32.dll" , (LPSTR) "CreateProcessA" ,(PROC)MyCreateProcessA); break ; case DLL_THREAD_ATTACH: break ; case DLL_THREAD_DETACH: break ; case DLL_PROCESS_DETACH: MyHookObject.UnHOOK(); break ; } return TRUE; } |
效果如下:
注入Hook
Hook的核心思想就是修改API的代码,但是,比如我A进程要Hook一个B进程的CreateProcess函数,A是没有权限修改B内存中的代码的,怎么办?这时候使用DLL注入技术就可以解决这问题,我们将Hook的代码写入一个DLL(或直接一个shellcode),将此DLL注入到B进程中,此时因为DLL在B进程的内存中,所以就有权限直接修改B内存中的代码了。
5.1 IAT Hook
IAT Hook顾名思义就是通过修改IAT里的函数地址对API进行Hook。
5.1.1 技术原理
如下,左图红框内是IAT修改前的状态,指明SetWindowTextW()的地址为0x77D0960E,所以calc.exe执行call SetWindowTextW(dword ptr[01001110])实质上就是执行call 0x77D0960E。
右图是被Hook后的状态,IAT中的SetWinowTextW()的地址已被修改为0x10001000,calc.exe执行call
SetWindowTextW(dword ptr[01001110])实质变成了执行call
0x10001000(也就是恶意代码的起始地址),这时候就可以做我们想做的操作了。
5.1.2 代码实现
下图是Hook IAT的代码实现,核心代码很少,大部分代码在计算IAT的位置。这里值得注意的是,我们把SetWindowTextW()替换为我们的恶意函数后,我们的恶意函数执行完后必须要调用回SetWindowTextW()(在Hook之前我们保存了SetWindowTextW()的地址),这样才能保证功能的完整性。
IAT Hook
IAT Hook是 Address Hook的一种方式,顾名思义就是通过修改函数的地址进行Hook。
IAT(Import Address Table,输入表)是PE中的一种结构,如图:
再用一张图来理解导入表结构:
需要注意的是:因为IAT具体指某个PE模块的IAT,所以他的作用范围只针对被Hook的模块,且必须在以静态链接的方式调用API时才会被Hook,所以它的作用范围只针对被Hook的模块,且必须以静态链接的方式调用API时才会被Hook,在使用Loadlibrary或GetProcAddress进行动态调用时不受影响。要想对已加载的所有模块起作用,就必须遍历进程内的所有模块,对目标API进行Hook。
现在开始Hook,这里是通过注入来Hook其他的程序,将代码写在DLL里面
为了保证原来的函数不受影响,我们先将要被Hook的函数的地址保存下来:
1
|
OldMesageBoxA = (FncMessageBoxA)GetProcAddress(GetModuleHandleA( "user32.dll" ), "MessageBoxA" ); |
然后就是解析PE文件,获取函数导入表中的函数地址表,并替换:
解析PE文件前面已经学习过了,下面过程直接走一遍:
- 获取DOS头
1
2
|
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)GetModuleHandleA(NULL); / / 当传入参数为NULL时,获取的是PE文件的imagebase,通过类型强制转换就获取到了dos_header |
- 获取NT头
1
|
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader - >e_lfanew); |
pDosHeader->e_lfanew的数据类型为DWORD,前面定义了pDosHeader的数据类型为PIMAGE_DOS_HEADER,要和pDosHeader相加需将pDosHeader类型转换为DWORD,最后再将得到的结果转换为PIMAGE_NT_HEADERS。
-
获取扩展头
12PIMAGE_OPTIONAL_HEADER pOptionalHeader
=
(PIMAGE_OPTIONAL_HEADER)&pNtHeader
-
>OptionalHeader;
/
/
扩展头是NT头的一个成员
-
从扩展头中获取数据目录表中的导入表
获取导入表前,要先找到它的偏移:
1
|
DWORD dwImportTableOffset = pOptionalHeader - >DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; / / 获取导入表偏移 |
获取导入表:
1
|
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(DWORD)pDosHeader + dwImportTableOffset; |
- 遍历导入表
有多个导入表结构,所以要遍历每个导入表:
因为导入表是依靠一个全零的结构来判断结束的,所以我们就采取对比pImportTable->Characteristics为0和pImportTable->FirstThunk为NULL时,来判断结束,然后在其中判断函数地址是否与我们所得到的原函数地址一致,如果一致说明找到了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
DWORD * pFirstThunk; / / 遍历导入表结构 while (pImprotTable - >Characteristics && pImprotTable - >FirstThunk ! = NULL) { pFirstThunk = (DWORD * )(pImprotTable - >FirstThunk + (DWORD)pDosHeader); while ( * (DWORD * )pFirstThunk ! = NULL) { / / 如果相当了,就说明当前的数组元素就是我们要找的函数地址表中的函数地址 if ( * (DWORD * )pFirstThunk = = (DWORD)OldMesageBoxA) { DWORD oldProtected; VirtualProtect(pFirstThunk, 0x1000 , PAGE_EXECUTE_READWRITE, &oldProtected); DWORD dwFuncAddr = (DWORD)MyMessageBoxA; memcpy(pFirstThunk, (DWORD * )&dwFuncAddr, 4 ); VirtualProtect(pFirstThunk, 0x1000 , oldProtected, &oldProtected); } pFirstThunk + + ; } pImprotTable + + ; } |
最后,为了保证程序的稳定,我们需要构造与被 HOOK 的函数一样结构的函数,同时为了保证原函数功能的正常运行,再定义一个函数指针,在自己的功能执行完成后,调用原来程序正常的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
typedef int (WINAPI * FncMessageBoxA)( _In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType); FncMessageBoxA OldMesageBoxA = NULL; int WINAPI MyMessageBoxA( _In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType) { SECURITY_ATTRIBUTES psa = { 0 }; SECURITY_ATTRIBUTES tsa = { 0 }; STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; CreateProcessA(EXE_PATH, NULL, &psa, &tsa, false, 0 , NULL, NULL, &si, &pi); return 0 ; } |
要HOOK的API是MessageBoxA (X32),写一个简单的程序:
1
2
3
4
5
6
7
|
#include <windows.h> int main() { system( "pause" ); MessageBoxA( 0 , 0 , 0 , 0 ); system( "pause" ); return 0 ; } |
要达到的目的是当测试程序运行时,注入DLL,Hook住MessageBoxA,使其指向CreateProcessA api:
正常运行:
Hook后: