Windows逆向进阶

6.2k 词

Hook基础

Hook的概念无需多言,简单来讲就是监听某个API函数,来对这个函数做相关操作,之后再跳转回到原本的函数继续执行。

两种Hook方式

Hook的方式有inline和address两种。

inline Hook

即内联Hook,直接在要Hook的函数处修改机器码,让程序执行跳转到我们写好的地址,来执行我们的函数,执行完后再跳转回到原函数处执行。

address Hook

即通过修改call的地址实现Hook。然后修改地址处的代码,执行完我们写好的代码后再跳转回到原本的函数。

实际上两种方式是一样的。

两种跳转JMP(E9)与CALL(E8)的区别

Hook的基础是跳转,有两种汇编指令实现,分别是jmp与call。

Call

call在执行时会改变ESP和EIP寄存器的值,并将call指令的下一条指令的地址压入栈中。

如果使用call,因为会改变栈,所以有两种解决:

  1. 直接在call完后加一条popad指令,将入栈的地址弹出
  2. 在call之前对栈指针ESP做操作,加一条ADD esp,4(32位地址),就可以将ESP指针修改回来。

Jmp

jmp更简单粗暴,跳转对寄存器没有改变。

但要注意的是:jmp后的参数跳转地址都是相对地址(RVA),需要计算。

例如jmp 0x09,即向下跳转到九个字节后的地址,如果是负值则向上跳转,例如jmp 0xF9FFFFFF

jmp参数相对地址计算

跳转地址计算一般可以使用这个公式:

目标地址 - 当前地址 - 5

为什么会减五呢?其实是减去你写的跳转代码的长度,即jmp 0x12345678长度为五个字节。

使用OD或者x32dbg会帮你计算,所以用地址就可以,但如果写成代码需要自己写好。

其他跳转方式

也可以用栈实现跳转:

1
2
push 0x12345678
ret

这种方式不会改变栈和寄存器,很常用。

栈平衡

我们hook后的函数必须要实现栈平衡,否则会影响原程序正常执行。

栈平衡有多种策略,取决于函数调用约定,常用的有三种:

  1. cdecl(C/C++默认调用约定)

    (1)参数从右向左依次压入堆栈.
    (2)由调用者恢复堆栈,称为手动清栈。
    (3)函数名自动加前导下划线。

  2. stdcall(Windows API默认方式)

    (1)参数从右向左依次压入堆栈.
    (2)由被调用函数自己来恢复堆栈,称为自动清栈。
    (3)函数名自动加前导下划线,后面紧跟着一个@,其后紧跟着参数的大小。

  3. fastcall

    通过 CPU 寄存器来传递参数。此方式的函数的第一个和第二个DWORD参数通过ecx和edx传递,后面的参数从右向左的顺序压入栈。被调用函数清理堆栈。

Hook的目标函数,与你写好的Hooked函数必须调用约定一致,否则会造成栈不平衡。

Hook任意位置

改写Hook点

首先需要修改内存属性,由可读可执行改为可读可写可执行:一般使用VirtulProtect()

写入内存一般有两种方式:memcpy()WriteProcessMemory()

注意的Hook流程如下:

1
2
3
4
5
6
7
8
9
VirtualProtect((LPVOID)orgAddr, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect);

DWORD rvaAddr = tarAddr - orgAddr - 0x5;

BYTE code[5] = { 0xE9, 0x90, 0x90, 0x90, 0x90 };

memcpy(&code[1], &rvaAddr, 0x4);

memcpy((void*)orgAddr, code, 0x5);

实例: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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <Windows.h>
#include "test.hpp"

void WINAPI myTest(int a, int b, int c)
{
}

void __declspec(naked) My_fun()
{
__asm mov eax, eax
__asm ret
}

DWORD orgAddr = 0x0041247E; // 0x0041243E;
DWORD retAddr = 0x00412483;

void __declspec(naked) MyHook()
{
// 需要注意栈平衡
__asm
{
pushad
pushfd
}

MessageBoxW(NULL, L"Hello!", L"this is a test box", MB_OK);

// 注意push和pop顺序
__asm
{
popfd
popad
}

__asm push 0xF1F4
__asm jmp retAddr
}

void WriteHookCode(DWORD tarAddr, DWORD orgAddr)
{
DWORD oldProtect = 0;
VirtualProtect((LPVOID)orgAddr, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect);

DWORD rvaAddr = tarAddr - orgAddr - 0x5;

BYTE code[5] = { 0xE9, 0x90, 0x90, 0x90, 0x90 };

memcpy(&code[1], &rvaAddr, 0x4);

// 将hook代码写入目标地址
memcpy((void*)orgAddr, code, 0x5);
}

int main()
{
WriteHookCode((DWORD)MyHook, orgAddr);
RunMethod1();

std::cout << "Hello, world!\n";
return 0;
}

思路其实很清楚,用vs或者x32dbg调一下也能搞明白。

注意:如果要直接写好RVA地址做Hook的话,必须把地址随机化修改掉,在Visual Studio项目属性->链接器->高级的编译选项中修改即可,否则每次编译都是不同的ImageBase。

但是有点奇怪的是这里:

1
2
DWORD orgAddr = 0x0041247E; // 0x0041243E;
DWORD retAddr = 0x00412483;

Hook点的地址会根据MyHook()中内容改变,多半是MyHook函数写好的汇编代码,会占一部分内存空间导致的。

retAddr实际上就是orgAddr + 0x5,Hook点之后的五个字节,即修改为jmp 0xXXXXXXXX后的下一条指令。

另外:x32dbg 用来调试程序还方便的,如果直接用vs调试,需要先清理再重新生成,否则可能地址对不上。

x32dbg快捷键

快捷键 功能
f7 单步调试,遇到函数调用,会进入函数内部
f8 单步调试,遇到函数调用也会执行,不过会直接跳到执行后的语句
ctrl+f9 程序会一直运行,直到遇见第一个返回语句
alt+f9 若进入系统领空,此命令可瞬问回到应用程序领空
alt+b 打开断点
ctrl+g 搜索函数或者表达式
ctrl+n 查看程序调用的所有API函数

API Hook

程序调用Windows API时,会将系统提供的DLL中的函数实现的调用路径复制到本程序中。但实际上,任何API函数的实现在系统中只有一份,其他所有程序都是调用这一份。

具体的实现是:编译器发现你调用了某个API时,会在生成的PE文件中写好导入表,操作系统在运行时,发现导入表中有某个DLL,会将对应的库函数加载到程序。

大部分API函数,只要在代码中写好函数名,就自动编译成地址,可以很容易的获取到地址。

另外某些函数实际上调用的是,例如MessageBox,实际上不是函数,而且系统根据你是否是x64判断后调用的MessageBoxA或者MessageBoxW,查看源码会发现:

1
2
3
4
5
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE

当然还有未公开API,不能直接获取到地址。

未公开API介绍

公开的WIndows API可以直接调用,但某些API是不能被代码直接调用的,例如MessageBoxTimeOut(),在函数导入表中可以发现,但在程序中写好调用会报错不能编译。

未公开API的设计其实很有趣:如果所有API都公开,那微软就不能修改任何API的函数签名,否则会造成之前写好的代码不能运行,但未公开的话,只要修改公开调用的未公开函数即可,相当于有更好的向下兼容,可以减少因为版本修改造成的影响。

那如何获取到未公开API的地址呢?很容易,手动调用LoadLibraryW或者GetModuleHandle()而不是用编译器获取就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DWORD GetProcAddr(LPCWSTR dllName, LPCSTR procName)
{
// 大小写都可以
HMODULE hDll = LoadLibraryW(dllName);
if (hDll == NULL)
{
return FALSE;
}
FARPROC addr = GetProcAddress(hDll, procName);
if (addr == NULL)
{
return NULL;
}
FreeLibrary(hDll);

return (DWORD)addr;
}

注意:获取到对应句柄或者地址后,需要Free掉。

实例:Hook公开API(MessageboxA)

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
65
66
67
68
69
70
71
#include <iostream>
#include <Windows.h>

DWORD orgMsgBoxAddr;
DWORD retMsgBoxAddr;
DWORD GetProcAddr(LPCWSTR dllName, LPCSTR procName)
{
// 大小写都可以
HMODULE hDll = LoadLibraryW(dllName);
if (hDll == NULL)
{
return FALSE;
}
FARPROC addr = GetProcAddress(hDll, procName);
if (addr == NULL)
{
return NULL;
}
FreeLibrary(hDll);
return (DWORD)addr;
}

void WriteHookCode(DWORD tarAddr, DWORD orgAddr)
{
DWORD oldProtect = 0;
VirtualProtect((LPVOID)orgAddr, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect);
DWORD rvaAddr = tarAddr - orgAddr - 0x5;
BYTE code[5] = { 0xE9, 0x90, 0x90, 0x90, 0x90 };
memcpy(&code[1], &rvaAddr, 0x4);
// 将hook代码写入目标地址
memcpy((void*)orgAddr, code, 0x5);
}

VOID __declspec(naked) MyMessageBox(_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType)
{
__asm push ebp
__asm mov ebp,esp

__asm
{
pushad
pushfd
}
lpText = "MyHook";

__asm
{
popfd
popad
jmp retMsgBoxAddr
}
}

VOID Hook()
{
orgMsgBoxAddr = GetProcAddr(L"user32.dll", "MessageBoxA");
retMsgBoxAddr = orgMsgBoxAddr + 0x5;
WriteHookCode((DWORD)MyMessageBox, orgMsgBoxAddr);
}

int main()
{
Hook();
DWORD addrMSG1 = (DWORD)MessageBoxA;

MessageBoxA(NULL, "Yoruko", "hello", NULL);
std::cout << "Hello World!\n";
}

注意:我们写好的Hook后的函数中,如果调用了原函数的参数列表,在还原函数时必须首先执行这两行:

1
2
push ebp
mov ebp,esp

这两行是MessageBoxA原函数中被我们覆盖掉的五个字节中的三个字节,这个函数在user32.dll中的实现最开头三行如下:

pFPj7Is.png

move esi,esi是编译器优化的占位符,实际上无操作(所以x32dbg这里标成了灰色),所以不需要恢复。

push ebpmov ebp, esp 组合起来用于建立函数的调用帧,保存并设置函数的基址指针,以便函数能够正确地访问栈上的数据。这是典型的函数开头的常见操作。所以必须恢复,否则后面继续执行栈会出问题。

当然不直接查看库函数的头几行,然后手动恢复,而是用ReadMemory动态获取,也是可以的,具体这部分可以参见我上一篇博客中提供的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
const char* str[] = "myhook";

VOID __declspec(naked) myMsgBoxA()
{
__asm
{
pushad
pushfd
}
__asm
{
mov dword ptr[esp+0x2c],offset str
}
__asm
{
popdf
popad
push ebp
mov ebp,esp
jmp retMessageBoxA
}
}

当然需要调试查看具体传参的偏移量来修改esp+0x2c的值。

未公开API Hook

这里要想一个问题:如果程序中调用了很多版本的MessageBox,或者你不知道具体用到了哪个版本的API(例如MessageBoxA,MessageBoxW,MessageBoxEx,MessageBoxExA,MessageBoxExW),怎么样才能一口气Hook到呢?

实际上大部分公开API,都调用了非公开API,例如MessageBoxA和MessageBoxW,就都调用了未公开的MessageBoxTimeOutW,所以有一个比较方便的办法,就是Hook未公开的API

那么问题就又回到获取未公开API的地址了。

其实除了上面提到的手动加载,还有一个办法,就是让非公开API变成公开API(需要知道微软文档不会提供的非公开API的参数列表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern "C"
{
int WINAPI MessageBoxTimeoutA(
IN HWND hWnd,
IN LPCSTR LpText,
IN LPCSTR LpCaption,
IN UINT uType,
WORD wLanguageId,
DWORD dnMilliseconds);

int WINAPI MossageBoxTimeoutWs(
IN HWND hWnd,
IN LPCWSTR LpText,
IN LPCWSTR LpCaption,
IN UINT uType,
IN WORD wLanguageId,
IN DWORD dnMilliseconds);
}

但不是通用的方法。

言归正传,既然要加载非公开API,就需要定义这个API的参数列表。

1
2
3
4
5
6
7
8
9
10
11
typedef int (WINAPI* pMessageBoxTimeoutW)(
IN HWND hWnd,
IN LPCWSTR lppText,
IN LPCWSTR lpCaption,
IN UINT uType,
IN WORD wLanguageId,
IN DWORD dwMilliseconds);

pMessageBoxTimeoutW orgMessageBoxTimeoutW;
// 写成这种形式也可以
typedef int (WINAPI* pMessageBoxTimeoutW)(HWND, LPCWSTR, LPCWSTR, UINT, WORD, DWORD);

然后获取地址就可以直接使用了:

1
2
orgMessageBoxTimeoutW = (pMessageBoxTimeoutW)GetProcAddr(L"user32.dll", "MessageBoxTimeoutW");
orgMessageBoxTimeoutW(NULL, L"hello", L"test", NULL, NULL, 5000);

注意:这个函数的原型是MessageBoxTimeoutW,其中的out的o是小写,这里不能错一点,否则获取不到之前的函数地址。

到x32dbg里查看,发现甚至都没有加载user32.dll:

pFiCs61.png

因为没有在导入表中引入,而且我们手动加载的,在函数执行时会看到加载进来了:

load DLL

获取到地址后,再执行正常的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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <Windows.h>

DWORD orgMsgBoxAddr;
DWORD retMsgBoxAddr;

typedef int (WINAPI* pMessageBoxTimeoutW)(
IN HWND hWnd,
IN LPCWSTR lppText,
IN LPCWSTR lpCaption,
IN UINT uType,
IN WORD wLanguageId,
IN DWORD dwMilliseconds);
pMessageBoxTimeoutW orgMessageBoxTimeoutW;
DWORD GetProcAddr(LPCWSTR dllName, LPCSTR procName)
{
HMODULE hDll = LoadLibraryW(dllName);
if (hDll == NULL)
{
return FALSE;
}
FARPROC addr = GetProcAddress(hDll, procName);
if (addr == NULL)
{
return NULL;
}
FreeLibrary(hDll);
return (DWORD)addr;
}

void WriteHookCode(DWORD tarAddr, DWORD orgAddr)
{
DWORD oldProtect = 0;
VirtualProtect((LPVOID)orgAddr, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect);
DWORD rvaAddr = tarAddr - orgAddr - 0x5;
BYTE code[5] = { 0xE9, 0x90, 0x90, 0x90, 0x90 };
memcpy(&code[1], &rvaAddr, 0x4);
memcpy((void*)orgAddr, code, 0x5);
}

VOID __declspec(naked) MyMessageBoxTimeout(_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
__asm push ebp
__asm mov ebp, esp
__asm
{
pushad
pushfd
}
lpText = L"MyHook";
__asm
{
popfd
popad
jmp retMsgBoxAddr
}
}

VOID Hook()
{
retMsgBoxAddr = (DWORD)orgMessageBoxTimeoutW + 0x5;
WriteHookCode((DWORD)MyMessageBoxTimeout, (DWORD)orgMessageBoxTimeoutW);
}

int main()
{
orgMessageBoxTimeoutW = (pMessageBoxTimeoutW)GetProcAddr(L"user32.dll", "MessageBoxTimeoutW");
Hook();
orgMessageBoxTimeoutW(NULL, L"hello", L"test", NULL, NULL, 5000);

MessageBoxA(NULL, "Yoruko", "hello-A", NULL);
MessageBoxW(NULL, L"Yoruko", L"hello-W", NULL);
std::cout << "Hello World!\n";
}

此时测试发现任何版本的MessageBox都被Hook到了。

注意:这种方法有时不行,例如在LoadLibraryW时,是因为我们获取到函数地址中,对应的机器码中有跳转,导致hook到的函数仅仅是中间跳转部分:

1
2
3
4
mov edi,edi
push ebp
mov ebp,esp
jmp dword ptr ds:[<&LoadLibraryW>]

这种写法的跳转部分机器码开头为FF25,所以我们可以对这个特征码做单独判断就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HMODUL hDll = LoadLibraryW(moduleName);
BYTE jmp[6] = {0};

if(hDll != NULL)
{
orgAddr = GetProcAddress(hDll, orgName);
ReadProcessMemory(GetCurrentProcess(), orgAddr, jmp, 6, NULL);
1f(jmp[0] == 0xFF && jmp[1] == 0x25)
{
FARPROC correctAddr = 0;
BYTE readAddr[4] = {jmp[2], jmp[3], jmp[4], jmp[5]};
DWORD* pReadAddr = (DWORD*)readAddr;
ReadProcessMemory(GetCurrentProcess(), (LPVOID)*pReadAddr, &correctAddr, 4, NULL);
orgAddr = correctAddr;
}

if(orgAddr != NULL)
{
retAddr = (FARPRCO)((DWORD)orgAddr + 5);
FreeLibrary(hDll);
return TRUE;
}
return FALSE;
}

DLL注入

如上所述的Hook只能操作自己进程的API,但大多数时候,是要hook目标进程的API,所以需要用到DLL注入。

因为每个进程的内存空间都是私有的,不能通过一个进程读写另一个进程的内存。那么我们如何Hook呢?很容易,把hook代码写进动态链接库,然后让目标进程加载这个动态链接库即可。

动态链接库基础

加载动态链接库会造成线程创建的问题。创建dll时需要写一个DllMain,来判断出现某种消息时需要执行的代码。示例如下:

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
#include <Windows.h>
VOID WINAPI NewThreadFunc(LPVOID lpParamate)
{
MessageBox(NULL, L"NewThreadFunc", NULL, NULL);
}

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxW(NULL, L"DLL_PROCESS_ATTACH", NULL, NULL);
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)NewThreadFunc, NULL, NULL, NULL);
break;
case DLL_THREAD_ATTACH:
MessageBoxW(NULL, L"DLL_THREAD_ATTACH", NULL, NULL);
break;
case DLL_THREAD_DETACH:
MessageBoxW(NULL, L"DLL_THREAD_DETACH", NULL, NULL);
break;
case DLL_PROCESS_DETACH:
MessageBoxW(NULL, L"DLL_PROCESS_DETACH", NULL, NULL);
break;
}
return TRUE;
}

其中这个switch判断了四种消息,或者说四种事件。分别是:进程创建,线程创建,线程退出,进程退出

然后我们在进程创建的同时又创建了一个线程(即多线程程序)。

把这个动态链接库用显式引用的方式加载进来:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <Windows.h>

int main()
{
LoadLibraryW(L"Dll1.dll");
MessageBoxW(NULL, L"LoadLibraryW", NULL, NULL);

std::cout << "Hello World!\n";
}

会发现弹窗依次出现,并且执行了多次,是因为有线程切换时,又触发了线程加载事件。

那么如何让目标进程加载我们的DLL呢?就有很多方法了。

目标进程加载DLL

远程线程创建

最常用的是用远程线程:CreateRemoteThread

最简单的示例如下:

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
#include <iostream>
#include <Windows.h>

using namespace std;

int main()
{
LPCSTR DllPath = "C:\\Simple-DLL-Injection\\C++\\Debug\\testlib.dll"; // The Path to our DLL

HWND hwnd = FindWindowA(NULL, "Tutorial-x86_64"); // HWND (Windows window) by Window Name
DWORD procID; // A 32-bit unsigned integer, DWORDS are mostly used to store Hexadecimal Addresses
GetWindowThreadProcessId(hwnd, &procID); // Getting our Process ID, as an ex. like 000027AC
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID); // Opening the Process with All Access

// Allocate memory for the dllpath in the target process, length of the path string + null terminator
LPVOID pDllPath = VirtualAllocEx(handle, 0, strlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE);

// Write the path to the address of the memory we just allocated in the target process
WriteProcessMemory(handle, pDllPath, (LPVOID)DllPath, strlen(DllPath) + 1, 0);

// Create a Remote Thread in the target process which calls LoadLibraryA as our dllpath as an argument -> program loads our dll
HANDLE hLoadThread = CreateRemoteThread(handle, 0, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA"), pDllPath, 0, 0);

WaitForSingleObject(hLoadThread, INFINITE); // Wait for the execution of our loader thread to finish

cout << "Dll path allocated at: " << hex << pDllPath << endl;
cin.get();

VirtualFreeEx(handle, pDllPath, strlen(DllPath) + 1, MEM_RELEASE); // Free the memory allocated for our dll path

return 0;
}

关键是这一行:

1
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA"), pDllPath, 0, 0);

但实际上获取到的LoadLibraryA的句柄是不太靠谱的,大部分时候可以通用,但没有必然的逻辑联系。

两边地址没有必然联系。加载进内存和所有进程共用一个dll本质上也没什么关系,共享也不是指内存,本质上在一个进程中dll一开始也没有真正载入到内存,只是属于映射。dll是可以重定位的,系统的dll不一定每次都分配固定的地址,我就是把他映射的地址占了,他能怎么样,他只能自己重定位,这个时候地址就不一样了。

当然这确实是最常用也最方便的DLL方法。

IAT注入

IAT表是PE文件用来标识导入表的结构,Windows下PE文件有两张导入表,即IAT和INT,分别标识导入函数的名称和地址,在加载进内存以前两张表是一致的,但加载后IAT表会变成函数地址。

操作系统读到IAT表,会把对应的库链接到该程序中。

注意:通过PE查看器看dll机构中的IAT表,经常发现函数名与代码中写好的不一样,是因为C++为了实现重载,会对函数在编译后做重命名,如果不想被重命名,在定义函数时加一个extern "C"即可。

我们可以通过写好Dll后,再重建目标程序的IAT表,把我们的函数注入进去。

可以通过CFF Explorer重建IAT表。

IAT injection

这种方式就是直接修改静态的PE结构。

当然还有一种不直接修改IAT表的方式:就是在真正的程序执行前,写好一个loader,把我们写好的注入当作内存补丁,来在loader中创建真正的程序线程,然后再修改内存中IAT表。这种方式明显更好,但要复杂一些。

DLL劫持

DLL hijacking。Windows在加载DLL时,有如下优先级:

DLL load order

会首先在程序当前目录下寻找有无目标DLL,然后再去对应路径。所以我们可以找到引入的DLL,然后写好一样的文件名和函数名,放在当前目录下,就实现了DLL劫持。

但是我们写好的DLL中当然没有对应的导出函数,所以需要完成我们的操作后,再去调用原本的DLL中的导出函数 => 即实现了一个中间人的作用。

DLL hijacking

有个比较常用的DLL可以被劫持,即version.dll,即便没有加载这个库都可以。(但在Win11下,如果没有加载version.dll是不行的。)

另外:劫持时要注意DLL是32位还是64位,实际上Windows文件夹中的system32中的DLL,都是x64的。但SysWOW64中的才是x86的32位程序。

那么如何选择劫持哪个DLL呢?有几个考虑:首先需要在程序执行前就已经加载进去,然后程序对此的操作尽量少,最后需要导出表比较小。满足这些条件的DLL比较适合被劫持。

选好要劫持的DLL,就可以写我们的中间人DLL了。因为要写好对应的导出函数,所以这一步比较复杂,可以用AheadLib这个工具根据要劫持的DLL生成C++代码,然后再编译成DLL放在目标程序目录即可,例如劫持msimg32.dll:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <Windows.h>

//
// created by AheadLib
// github:https://github.com/strivexjun/AheadLib-x86-x64
//

#include <windows.h>
#include <Shlwapi.h>

#pragma comment( lib, "Shlwapi.lib")

#pragma comment(linker, "/EXPORT:vSetDdrawflag=_AheadLib_vSetDdrawflag,@1")
#pragma comment(linker, "/EXPORT:AlphaBlend=_AheadLib_AlphaBlend,@2")
#pragma comment(linker, "/EXPORT:DllInitialize=_AheadLib_DllInitialize,@3")
#pragma comment(linker, "/EXPORT:GradientFill=_AheadLib_GradientFill,@4")
#pragma comment(linker, "/EXPORT:TransparentBlt=_AheadLib_TransparentBlt,@5")

PVOID pfnAheadLib_vSetDdrawflag;
PVOID pfnAheadLib_AlphaBlend;
PVOID pfnAheadLib_DllInitialize;
PVOID pfnAheadLib_GradientFill;
PVOID pfnAheadLib_TransparentBlt;
static
HMODULE g_OldModule = NULL;

VOID WINAPI Free()
{
if (g_OldModule)
{
FreeLibrary(g_OldModule);
}
}

BOOL WINAPI Load()
{
TCHAR tzPath[MAX_PATH];
TCHAR tzTemp[MAX_PATH * 2];
//
// 这里是否从系统目录或当前目录加载原始DLL
//
//GetModuleFileName(NULL,tzPath,MAX_PATH); //获取本目录下的
//PathRemoveFileSpec(tzPath);

GetSystemDirectory(tzPath, MAX_PATH); //默认获取系统目录的

lstrcat(tzPath, TEXT("\\msimg32.dll"));

g_OldModule = LoadLibrary(tzPath);
if (g_OldModule == NULL)
{
wsprintf(tzTemp, TEXT("无法找到模块 %s,程序无法正常运行"), tzPath);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
}
return (g_OldModule != NULL);
}

FARPROC WINAPI GetAddress(PCSTR pszProcName)
{
FARPROC fpAddress;
CHAR szProcName[64];
TCHAR tzTemp[MAX_PATH];

fpAddress = GetProcAddress(g_OldModule, pszProcName);
if (fpAddress == NULL)
{
if (HIWORD(pszProcName) == 0)
{
wsprintfA(szProcName, "#%d", pszProcName);
pszProcName = szProcName;
}
wsprintf(tzTemp, TEXT("无法找到函数 %hs,程序无法正常运行"), pszProcName);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
ExitProcess(-2);
}
return fpAddress;
}

VOID stratHook()
{
MessageBoxW(NULL, L"DLL hijacking!", NULL, NULL);
}

BOOL WINAPI Init()
{
pfnAheadLib_vSetDdrawflag = GetAddress("vSetDdrawflag");
pfnAheadLib_AlphaBlend = GetAddress("AlphaBlend");
pfnAheadLib_DllInitialize = GetAddress("DllInitialize");
pfnAheadLib_GradientFill = GetAddress("GradientFill");
pfnAheadLib_TransparentBlt = GetAddress("TransparentBlt");
return TRUE;
}

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Load();
Init();
stratHook;
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Free();
break;
}
return TRUE;
}
EXTERN_C __declspec(naked) void __cdecl AheadLib_vSetDdrawflag(void)
{
__asm jmp pfnAheadLib_vSetDdrawflag;
}

EXTERN_C __declspec(naked) void __cdecl AheadLib_AlphaBlend(void)
{
__asm jmp pfnAheadLib_AlphaBlend;
}

EXTERN_C __declspec(naked) void __cdecl AheadLib_DllInitialize(void)
{
__asm jmp pfnAheadLib_DllInitialize;
}

EXTERN_C __declspec(naked) void __cdecl AheadLib_GradientFill(void)
{
__asm jmp pfnAheadLib_GradientFill;
}

EXTERN_C __declspec(naked) void __cdecl AheadLib_TransparentBlt(void)
{
__asm jmp pfnAheadLib_TransparentBlt;
}

这样就实现了DLL劫持。

Detour使用

Detour是用来优雅高效地完成address hook的。

如前所述的inline hook很清楚,但不稳定。原因很简单,如果每次调用目标API函数都有修改一遍机器码,然后在调原本函数的时候再修改回去,这样如果在修改的时候再出现函数调用,就可能出问题。

Detour就方便很多了,具体实现是:将我们hook时覆盖掉的代码保存在另一块内存区域,然后在回复原函数时,跳转到那里再跳回来。

detour address hook

编译

命令行

首先需要下载Detour库

下好之后直接用命令行的nmake编译即可,源码里写好makefile了。

结果在这一步出问题了,搞出来各种各样的报错,最后还是用添加环境变量的方法解决了:

error solve

详细编译错误排除可以参考这两个链接:

设置cl.exe环境变量

VC常见编译链接错误(.obj : error LNK,fatal error)

Visual Studio

但其实还有更好的办法:直接打开源码的vc文件夹里的Detours.sln,用vs2022编译:编译选项使用Release+x86。

这样可以不用配命令行的环境变量,而且依赖和Windows SDK都是配好的。

(早知道这种方法就不会浪费这么多时间配环境了)

当然不编译成.lib也是可以使用的,但是需要把源码都拉进我们写好的项目里,相比之下不如.lib方便。

引入

编译好后把头文件detours.h和库文件detours.lib拉入我们的项目里,然后写好配置:

1
2
3
4
#include <iostream>
#include <Windows.h>
#include "detours.h"
#pragma comment(lib, "detours.lib")

就可以使用Detours提供的函数了。

使用Detours模板

实际上,使用Detours做Hook与手动inline hook是一样的,只是用代码模板代替了我们自己的hookcode:

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
#include <iostream>
#include <Windows.h>
#include "detours.h"
#pragma comment(lib, "detours.lib")

typedef int (WINAPI* pMessageBoxW)(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType);
pMessageBoxW orgMessageBoxW;

int WINAPI myMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
lpText = L"Yoruko-test";
return orgMessageBoxW(hWnd, lpText, lpCaption, uType);
}


int main()
{
orgMessageBoxW = MessageBoxW;

DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)orgMessageBoxW, myMessageBoxW);
DetourTransactionCommit();

MessageBoxW(NULL, NULL, NULL, NULL);


DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)orgMessageBoxW, myMessageBoxW);
DetourTransactionCommit();

MessageBoxW(NULL, NULL, NULL, NULL);

std::cout << "Hello World!\n";
}

其中核心模板部分是这里:

1
2
3
4
5
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)orgMessageBoxW, myMessageBoxW);
DetourTransactionCommit();

具体文档可以参见微软官方Wiki

Hook目标进程

可以使用Detours的DetourCreateProcessWithDllsW比较方便Hook目标进程,此时需要三个部分:

  1. 目标进程
  2. 我们写好hook代码的DLL
  3. Loader

整体流程:loader加载我们写好的DLL,然后创建目标进程做Hook。

loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <Windows.h>
#include "detours.h"
#pragma comment(lib, "detours.lib")

STARTUPINFOW si;
PROCESS_INFORMATION pi;


int main()
{
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);

LPCSTR dllName[] = { "hook.dll", "hook1.dll" };

DetourCreateProcessWithDllsW(L"dll_injection_test.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi, 1, dllName, NULL);
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}

Hook的DLL:

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
#include <Windows.h>
#include "detours.h"
#pragma comment(lib, "detours.lib")
VOID __declspec(dllexport) MyFunc(){};

typedef int (WINAPI* pMessageBoxW)(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType);
pMessageBoxW orgMessageBoxW;
int WINAPI myMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
lpText = L"Yoruko-test";
return orgMessageBoxW(hWnd, lpText, lpCaption, uType);
}
VOID Hook()
{
orgMessageBoxW = MessageBoxW;

DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)orgMessageBoxW, myMessageBoxW);
DetourTransactionCommit();
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Hook();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

注意:VOID __declspec(dllexport) MyFunc(){};这部分不能省,虽然导出的函数我们没有使用,但如果没有导出函数,加载DLL会报错

调试DLL:手动创建控制台

注入的DLL很难调试,因为在目标进程中运行,没办法打印调试信息到控制台,所以我们可以单独开一个控制台:

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
#include <Windows.h>
#include <stdio.h>
#include <iostream>

static FILE* steamconsole;

DWORD WINAPI SetConsoleTop(LPVOID lpParameter)
{
WCHAR consoleTitle[256] = { 0 };

while (true)
{
GetConsoleTitleW(consoleTitle, 256);
HWND hConsole = FindWindowW(NULL, (LPWSTR)consoleTitle);
if (hConsole != NULL)
{
SetWindowPos(hConsole, HWND_TOPMOST, NULL, NULL, NULL, NULL, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
break;
}
}

return 0;
}


VOID WINAPI SetConsole()
{
AllocConsole();
AttachConsole(ATTACH_PARENT_PROCESS);
freopen_s(&steamconsole, "CONIN$", "r+t", stdin);
freopen_s(&steamconsole, "CONOUT$", "r+t", stdout);
SetConsoleTitleW(L"Hijack Test");

CreateThread(NULL, NULL, SetConsoleTop, NULL, NULL, NULL);

HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
SetConsoleMode(hStdin, mode & ~ENABLE_QUICK_EDIT_MODE);

std::locale::global(std::locale(""));
}

逻辑不复杂,大致就是打开一个控制台,然后把输入输出重定向到这个控制台中。

写在Tools.hpp中,用起来还是很方便的。

留言