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使用更为广泛!

英文原文:

The disadvantage for an attacker using this technique is that it does not work if the
program is using run time linking, or if the function(the attacker wishes to hook) has been
imported as an ordinal.==》言下之意就是说运行时链接的函数无法生效,或者已经导入的函数。
Another disadvantage for the attacker is that IAT hooking can be
easily detected. Under normal circumstances, the entries in the IAT should lie within the
address range of its corresponding module. For example, the address of DeleteFile()
should be within the address range of kernel32.dll. To detect this hooking technique, a
security product can identify the entry in the IAT that falls outside of its module's address
range. On 64-bit Windows, a technology named PatchGuard prevents patching the call
tables, including IAT. Due to these problems, malware authors use a slightly different
hooking technique, which is discussed next.

 

 

 

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

 

image-20211217113006811

 

HOOK的话肯定要准备一个自定义的函数, call addr ,假如它的地址为12345678,那么我们HOOK的操作就是将上面的命令替换为:JMP 12345678,也就是e9 addr。

 

解除的HOOK的话就是将替换的字节恢复。

 

Inline Hook流程

  1. 构造跳转指令。
  2. 在内存中找到欲Hook函数地址,并保存欲Hook位置处的前5字节。
  3. 将构造的跳转指令写入需Hook的位置处。
  4. 当被Hook位置被执行时会转到自己的流程执行。
  5. 如果要执行原来的流程,那么取消Hook,也就是还原被修改的字节。
  6. 执行原来的流程。
  7. 继续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;
}

效果如下:

 

image-20211220091318443

注入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(也就是恶意代码的起始地址),这时候就可以做我们想做的操作了。

7

8

5.1.2 代码实现

下图是Hook IAT的代码实现,核心代码很少,大部分代码在计算IAT的位置。这里值得注意的是,我们把SetWindowTextW()替换为我们的恶意函数后,我们的恶意函数执行完后必须要调用回SetWindowTextW()(在Hook之前我们保存了SetWindowTextW()的地址),这样才能保证功能的完整性。

 

IAT Hook

​ IAT Hook是 Address Hook的一种方式,顾名思义就是通过修改函数的地址进行Hook。

 

​ IAT(Import Address Table,输入表)是PE中的一种结构,如图:

 

image-20211220162556261

 

再用一张图来理解导入表结构:

 

导入表.png

 

需要注意的是:因为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

 

image-20211220164816840

  • 获取扩展头

    1
    2
    PIMAGE_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:

 

正常运行:

 

image-20211221135030995

 

Hook后:

 

image-20211221135121759

 

 

posted @ 2023-01-02 21:13  bonelee  阅读(411)  评论(0编辑  收藏  举报