逆核系列No.14--SetWindowsText_Hook

上节内容是使用的调试器Hook,通过修改目标代码第一个字节为0xCC来设置调试事件,随后当相关API被调用,由于此时第一个字节被设置为断点异常调试事件,可以被调试器捕获到,此时获取程序执行上下文的CONTEXT结构可以获取到对应API的参数地址,做相应的修改后,恢复API第一个字节,之后在正常调用,由于参数被修改了,借此达到Hook的目的。

本节采取的是另外一种方法,Hook IAT中的函数。

展示书中Hook的实例,钩取的目标API是user32.SetWindowTextW,函数原型:

1
2
3
4
BOOL SetWindowTextW(
[in] HWND hWnd, //窗口句柄
[in, optional] LPCWSTR lpString //显示内容所在地址
);

钩取后修改第二个参数内容,可以达到修改Windows窗口即将显示的内容,比如修改计算器的显示字符为中文:(由于计算器可能因OS版本存在差异,这里笔者使用的为书的作者的计算器)


PE格式回顾

明确本节的内容目的在于Hook IAT中的API,需要从程序中读取IAT中的目标API。

这里回顾下PE文件格式的内容,建议读者和我一样,尽可能按照手动查找的方式,不过于依赖工具,假以时日PE格式解读会更上一层路。话不多说,我们开始。

在HxD下,找到相关字段:

读取出IAT的位置为0x00012B80(RVA),查看区段表,发现该地址位于节区.text

节区.text的节区头信息如下:

地址转换,RVA -> RAW : 0x00012B80 - 0x00001000 + 0x00000400 = 0x00011F80,文件中跟随该地址:

以0x14 byte为单位进行读取(这是由于IAT表项指向单元为IMAGE_IMPORT_DESCRIPTOR,大小占0x14 bytes),上图已经划分好,可以看到,calc.exe载入6个DLL文件,最后一个是结束标志,以全0结构体IMAGE_IMPORT_DESCRIPTOR为标识。

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to original unbound INT
} DUMMYUNIONNAME;
DWORD TimeDateStamp;

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //name
DWORD FirstThunk; // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR;

一次查看,发现了我们的目标函数SetWindowsText所在的DLL文件—user32.dll,对应倒数第二个结构项。

1
B0 2C 01 00 FF FF FF FF FF FF FF FF A4 36 01 00 A4 10 00 00

对应的存放着IAT地址为RVA = 0x00012CB0 -> RAW = 0x00120B0,[0x00120B0]是IAT的地址 = 0x0001335C -> 0x0001275C

查看到了目标API是存在与IAT中的。于是开始下边的实验部分。

实验部分

从源码入手分析,该Hook方法的实施细节,这里贴一下完整代码(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
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
// include
#include "stdio.h"
#include "wchar.h"
#include "windows.h"


// typedef
typedef BOOL (WINAPI *PFSETWINDOWTEXTW)(HWND hWnd, LPWSTR lpString);


// globals
FARPROC g_pOrgFunc = NULL;


// »ç¿ëÀÚ ÈÄÅ· ÇÔ¼ö
BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)
{
wchar_t* pNum = L"¿µÀÏÀÌ»ï»ç¿ÀÀ°Ä¥Æȱ¸";
wchar_t temp[2] = {0,};
int i = 0, nLen = 0, nIndex = 0;

nLen = wcslen(lpString);
for(i = 0; i < nLen; i++)
{
// '¼ö'¹®ÀÚ¸¦ 'ÇѱÛ'¹®ÀÚ·Î º¯È¯
// lpString Àº wide-character (2 byte) ¹®ÀÚ¿­
if( L'0' <= lpString[i] && lpString[i] <= L'9' )
{
temp[0] = lpString[i];
nIndex = _wtoi(temp);
lpString[i] = pNum[nIndex];
}
}

// user32!SetWindowTextW() API È£Ãâ
// (À§¿¡¼­ lpString ¹öÆÛ ³»¿ëÀ» º¯°æÇÏ¿´À½)
return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}


// hook_iat
// ÇöÀç ÇÁ·Î¼¼½ºÀÇ IAT ¸¦ °Ë»öÇؼ­
// pfnOrg °ªÀ» pfnNew °ªÀ¸·Î º¯°æ½ÃÅ´
BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
HMODULE hMod;
LPCSTR szLibName;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
PIMAGE_THUNK_DATA pThunk;
DWORD dwOldProtect, dwRVA;
PBYTE pAddr;

// hMod, pAddr = ImageBase of calc.exe
// = VA to MZ signature (IMAGE_DOS_HEADER)
hMod = GetModuleHandle(NULL);
pAddr = (PBYTE)hMod;

// pAddr = VA to PE signature (IMAGE_NT_HEADERS)
pAddr += *((DWORD*)&pAddr[0x3C]);

// dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table
dwRVA = *((DWORD*)&pAddr[0x80]);

// pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod+dwRVA);

for( ; pImportDesc->Name; pImportDesc++ )
{
// szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name
szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);
if( !_stricmp(szLibName, szDllName) )
{
// pThunk = IMAGE_IMPORT_DESCRIPTOR.FirstThunk
// = VA to IAT(Import Address Table)
pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod +
pImportDesc->FirstThunk);

// pThunk->u1.Function = VA to API
for( ; pThunk->u1.Function; pThunk++ )
{
if( pThunk->u1.Function == (DWORD)pfnOrg )
{
// ¸Þ¸ð¸® ¼Ó¼ºÀ» E/R/W ·Î º¯°æ
VirtualProtect((LPVOID)&pThunk->u1.Function,
4,
PAGE_EXECUTE_READWRITE,
&dwOldProtect);

// IAT °ªÀ» º¯°æ
pThunk->u1.Function = (DWORD)pfnNew;

// ¸Þ¸ð¸® ¼Ó¼º º¹¿ø
VirtualProtect((LPVOID)&pThunk->u1.Function,
4,
dwOldProtect,
&dwOldProtect);

return TRUE;
}
}
}
}

return FALSE;
}



BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
// original API ÁÖ¼Ò ÀúÀå
g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"),
"SetWindowTextW");

// # hook
// user32!SetWindowTextW() ¸¦ hookiat!MySetWindowText() ·Î ÈÄÅ·
hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
break;

case DLL_PROCESS_DETACH :
// # unhook
// calc.exe ÀÇ IAT ¸¦ ¿ø·¡´ë·Î º¹¿ø
hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
break;
}

return TRUE;
}

DllMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
// original API ÁÖ¼Ò ÀúÀå
g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"),
"SetWindowTextW");

// # hook
// user32!SetWindowTextW() ¸¦ hookiat!MySetWindowText() ·Î ÈÄÅ·
hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
break;

case DLL_PROCESS_DETACH :
// # unhook
// calc.exe ÀÇ IAT ¸¦ ¿ø·¡´ë·Î º¹¿ø
hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
break;
}
  • 第7、8行,获取目标API的地址,存放在g_pOrgFunc

  • 调用hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);,根据下边的函数原型,可以看到传入的参数大致情况:

    • 第一个参数,目标API所在的DLL文件
    • 第二个参数,目标API所在DLL中的地址
    • 第三个参数,钩子函数地址。

hook_iat

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
BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
HMODULE hMod;
LPCSTR szLibName;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
PIMAGE_THUNK_DATA pThunk;
DWORD dwOldProtect, dwRVA;
PBYTE pAddr;

hMod = GetModuleHandle(NULL);
pAddr = (PBYTE)hMod;

// pAddr = VA to PE signature (IMAGE_NT_HEADERS)
pAddr += *((DWORD*)&pAddr[0x3C]);

// dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table
dwRVA = *((DWORD*)&pAddr[0x80]);

// pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod+dwRVA);

for( ; pImportDesc->Name; pImportDesc++ )
{
// szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name
szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);
if( !_stricmp(szLibName, szDllName) )
{
// pThunk = IMAGE_IMPORT_DESCRIPTOR.FirstThunk
// = VA to IAT(Import Address Table)
pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod +
pImportDesc->FirstThunk);

// pThunk->u1.Function = VA to API
for( ; pThunk->u1.Function; pThunk++ )
{
if( pThunk->u1.Function == (DWORD)pfnOrg )
{
// ¸Þ¸ð¸® ¼Ó¼ºÀ» E/R/W ·Î º¯°æ
VirtualProtect((LPVOID)&pThunk->u1.Function,
4,
PAGE_EXECUTE_READWRITE,
&dwOldProtect);

// IAT °ªÀ» º¯°æ
pThunk->u1.Function = (DWORD)pfnNew;

// ¸Þ¸ð¸® ¼Ó¼º º¹¿ø
VirtualProtect((LPVOID)&pThunk->u1.Function,
4,
dwOldProtect,
&dwOldProtect);

return TRUE;
}
}
}
}

return FALSE;
}


  • 第10行,获取本进程的句柄(因为GetModuleHandle的参数是NULL)句柄存放在hMod变量中
  • 第11行,将hMod转为PBYTE类型付给pAddr,这使得pAddr指向文件的A to MZ signature (IMAGE_DOS_HEADER)后续应该是根据这里,查找SetWindowsText API的调用地址(文件IAT)
  • 第14-20行,从PE文件开头定位到PE文件中导入表结构体IMAGE_IMPORT_DESCRIPTOR
  • 第22-57行,遍历结构体IMAGE_IMPORT_DESCRIPTOR,找到目标DLL–user32.dll对应的IMAGE_IMPORT_DESCRIPTOR结构体
    • 里层遍历user32.dll导入的函数,匹配目标API–SetWindowsText pThunk->u1.Function == (DWORD)pfnOrgps:之所以能这样是由于user32.dll是系统级DLL,系统中所有进程共享一份,因此能够这样跨进程(Injectdll.exe -> calc.exe)匹配。找到后实施Hook
      • VirtualProtect修改相应内存的保护权限,需要写入,执行权限。
      • pThunk->u1.Function = (DWORD)pfnNew 覆盖IAT中目标API地址为钩子函数,这样当进程调用它以为是SetWindowsText时,其实调用的是Hook Function
      • 覆盖完IAT后,需要使用VirtualProtect恢复相应内存的旧权限,这是因为可能PE有相关字段对应着,不恢复可能PE装载器装载时可能出现错误

hook function

钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)
{
wchar_t* pNum = L"一二三四五六七八九";
wchar_t temp[2] = {0,};
int i = 0, nLen = 0, nIndex = 0;

nLen = wcslen(lpString);
for(i = 0; i < nLen; i++)
{
//参数替换,使得显示的阿拉伯数字字符为中文字符
if( L'0' <= lpString[i] && lpString[i] <= L'9' )
{
temp[0] = lpString[i];
nIndex = _wtoi(temp);
lpString[i] = pNum[nIndex];
}
}

// user32!SetWindowTextW() API
return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

回顾

  • 定位到PE文件中的OptionalHeader.DataDirectory[1]记录的第一个IMAGE_IMPORT_DESCRIPTOR结构体
  • 遍历所有IMAGE_IMPORT_DESCRIPTOR中的name字段,判断是否是目标DLL
  • 确定目标DLL后,获取目标函数相应在DLL中的位置
  • 修改相应的内存权限,覆盖目标函数的的地址
  • 下钩成功

下钩成功后,程序正常调用被Hook的API时,实际调用的是我们覆写进去的钩子函数的地址,在钩子函数相应的参数修改,由于此时API的参数被修改了,放行程序即可完成一次完整的API Hook

Author: Victory+
Link: https://cvjark.github.io/2022/05/09/逆核系列No-14-SetWindowsText-Hook/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.