逆向工程核心原理-32

逆向工程核心原理-第32章-计算器显示中文数字

通过DLL注入钩取API, 是将DLL注入目标进程后, 修改IAT更改进程中调用的特定API的功能

32.1 技术图表

以下注入DLL实现API钩取的所需要的技术, 画下划线就是使用的技术

Untitled

32.2 选定目标API

再进行钩取API之前, 首先我们要确定钩取哪个API

我们本次实例的目标是”计算器显示的文本”, 能实现这个功能的API: SetWindowTextW(), SetDlgItemTextW(). 而SetDlgItemTextW()中又调用了SetWindowTextW(). 所以我们可以锁定目标API—SetWindowTextW()

我们看看SetWindowTextW()的函数原型

1
2
3
4
BOOL SetWindowTextW(
HWND hWnd, //窗口句柄
LPCTSTR lpString //窗口要输出的字符串指针
);

我们想要实现将阿拉伯数字改为中文数字

我们首先使用OD来验证上面的猜想: SetWindowTextW()就是我们要修改的API

首先用OD打开calc

Untitled

然后查看所有模块引用, 并找到SetWindowTextW()(我的OD加载不出引入函数的符号, 所以换了x32dbg来调试)

Untitled

在这两个地方设下断点.随后我们F9运行, 先进入了EntryPoint, 然后再F9就停在了SetWindowTextW()前

Untitled

观察上图的栈区, 可以看到两个参数逆序压入栈中, 我们可以看到第二个参数是”0.”, 这正是计算器程序一开始要输出的内容. 我们再一次F9, 就可以看见计算器的窗口弹出, 并输出了”0.”

Untitled

现在我们输入7

Untitled

可以看到第二个参数也变成了L”7.”, 我们尝试把7改成”七”, 首先先转到我们的数据缓冲区中

Untitled

将0x37改为4e03, 由于小端, 所以在内存中是034e

Untitled

我们再次运行

Untitled

可以看到输出变成了”七.”.

由此我们可以确定我们需要钩取的API就是SetWindowTextW()

接下来就正式开始钩取IAT

32.3 IAT钩取工作原理

进程的IAT中保存着程序中调用API的地址

IAT钩取通过修改IAT中保存的API地址来钩取某个API

下面是正常使用IAT调用函数的流程

Untitled

  • 首先1002628处使用了CALL指令调用01001110处的值
  • 而01001110处的值为我们API的地址77D0960E
  • 实际上调用的是77D0960E

下面是IAT被钩取后的流程

Untitled

  • 01002628地址处调用了[01001110]跟上面是一样的
  • 但是[01001110]处存储的实际调用地址被修改成了10001000
  • 于是控制流转移到了10001000, 这里就是我们的钩取函数MySetWindowTextW()
  • 执行完其语句后再结尾调用了[1000B6B8]
  • 而1000B6B8处存储的是实际调用地址77D0960E, 这个地址就是我们原来的SetWindowTextW()
  • 随后调用了SetWindowTextW()
  • 返回到原来的位置

整个流程就是把原本调用SetWindowTextW()的位置替换成了MySetWindowTextW(), 随后再MySetWindowTextW()中调用了SetWindowTextW().

核心思想是: 在保持运行代码不变的前提下, 将IAT中保存的API起始地址变为我们的钩取函数的起始地址.

32.3.0 注意

我想在这里提一下DLL注入跟IAT钩取之间的关系, DLL是IAT钩取实现的基础, 而IAT钩取是API钩取的实现思路之一(前面的调试钩取也是思路之一). 千万不要把DLL注入跟这些概念搞混了. 后面还会提到调试钩取API跟钩取IAT之间的区别.

32.4 练习示例

我们首先打开calc

Untitled

查找其PID

Untitled

随后使用管理员身份打开终端

Untitled

其中第二个参数表示Inject(注入)

成功注入时, 我们回到calc中可以看到输出不一样了

Untitled

然后脱钩

Untitled

第二参数e表示end结束

随后键入一个值

Untitled

可以看到字符恢复了

32.5 源代码分析

InjectDll.cpp跟前面的DLL注入的InjectDll.cpp时类似的: 通过查找PID获取进程句柄, 同时获取LoadLibrary()API, 然后通过像目标进程(我们已有进程句柄)创建远程线程调用LoadLibrary()API来加载我们像注入的DLL.

32.5.1 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
#include "pch.h"

LPVOID g_pOrgFunc;

BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//保存原始API地址, 并保存在全局变量中, 便于后面挂钩和脱钩
g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"), "SetWindowTextW");

//挂钩
hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);//MySetWindowTextW因为已经存在在DLL中, 所以可以直接使用函数名代表其函数地址
break;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:

case DLL_PROCESS_DETACH:
//脱钩
hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
break;
}
return TRUE;
}

流程:

  • 首先注入时使用标签DLL_PROCESS_ATTACH: 全局变量保存SetWindowTextW()的API地址, 并挂钩, 即替换IAT
  • 当结束钩取时使用标签DLL_PROCESS_DETACH: 脱钩, 即将IAT中的内容复原

这里还是跟上一章一样, 要记住全局变量的内容, 后面还会用到.

32.5.2 MySetWindowTextW()

MySetWindowTextW()为钩取函数

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
//此时g_pOrgFunc = SetWindowTextW()函数的起始地址

typedef BOOL(WINAPI* PFSETWINDOWTEXTW)(HWND hWnd, LPWSTR lpString);//定义函数指针, 便于后面调用SeiWindowTextW

BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)//这个被替换后的函数正常接收参数, 第二参数就是输出字符串的指针
{
const wchar_t* pNum = L"零一二三四五六七八九";//定义需要替换的字符, 类型为宽字符
wchar_t temp[2] = { 0, };//为什么要使用数组: 因为后面_wtoi()是将<字符串>转换为<整数>,
//单个字符无法转换, 所以需要利用数组在结尾添加上'\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];
}
}

//g_pOrgFunc存储的是SetWindowTextW函数指针
//修改完缓冲区数据以后, 调用原来的SetWindowTextW()
return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

至此我们在回头看看整个的思路

  • 首先是InjectDll.exe负责注入DLL到目标进程中, 用的方法应该是远程创建线程
  • 于是我们来到了DllMain中, 注意此时我们已经进入了目标进程的内存中, 可以对其数据进行修改
  • 在DllMian中我们实现了挂钩—将目标进程内存中的IAT进行修改, 使其跳转到DLL中的数据处理函数(该函数也随着DLL的注入而进入了目标进程的内存中, 所以可以正常调用)
  • 在钩取函数中修改数据, 然后再调用原来的函数, 就像是加了一个中间加工的过程.
  • 然后在DLL卸载的时候, 我们进行了脱钩—将目标进程内存中的IAT复原

我们已经解决了DllMain和数据处理函数, 最关键的挂钩和脱钩操作将在下面的进行分析, 之所以在这里提到这个是因为在书中对源代码的分析不是按照逻辑顺序进行的, 而是从简到难, 所以不梳理以下整个流程会有些混乱.

32.5.2 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
//    "user32.dll"的字符串指针  需要替换的函数指针  替换的函数指针
BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
HMODULE hMod;
LPCSTR szLibName;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;//导入描述符IID结构体指针
PIMAGE_THUNK_DATA pThunk;//IID结构体中的一个成员, 用于之处IAT的起始地址
DWORD dwOldProtect, dwRVA;//dwOldProtect存储之前的内存段保护属性, dwRVA存储RVA
PBYTE pAddr;

//获取当前进程句柄
hMod = GetModuleHandle(NULL);//参数为NULL是, 返回当前调用该函数的进程的句柄, 也就是文件的基地址(开头)
//通过进程句柄获得进程基址
pAddr = (PBYTE)hMod;//但是hMod类型为HMODULE, 要作为位置指针的话需要强制类型转换为字节指针
//此时pAddr就是我们的进程当前起始位置的虚拟地址VA
//获取NT头地址
pAddr += *((DWORD*)&pAddr[0x3C]);//首先右边的整个部分就是<进程偏移为0x3C上大小为4字节的内容>, 而根据前面我们PE所学的部分, 这个内容是下一个节区头的偏移
//所以pAddr += 下一个节区头的偏移, 此时pAddr指向的是NT头开头

//获取导入表RVA
dwRVA = *((DWORD*)&pAddr[0x80]);//跟上面类似, 此时pAddr指向的是NT头起始地址, 在此基础上偏移0x80所指向的内容是IMPORT TABLE(导入表)的RVA
//所以dwRVA所代表的是导入表的RVA

//首先pImportDesc的类型是IID结构体指针, 而前面dwRVA所指向的地址就是导入表, 所以类型转换后就是第一个IID结构体指针
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod + dwRVA);//这里加上hMod是因为dwRVA是导入表的RVA, 要转换成准确的虚拟地址要加上Base基址, 而示例中的基址就是hMod

//我们要知道结尾为NULL结构体, 所以这个条件的意思就是要遍历导入表的每个IID
for (; pImportDesc->Name; pImportDesc++); {//IID指针++就是指向下一个结构体
szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);//可能会觉得奇怪为什么pImportDesc前面已经转换为准确的VA了, 为什么还是要加上hMod基址
//因为Name成员表示的是模块名称字符串的RVA, 所以还是得加上hMod才能得到准确的VA

if (!stricmp(szLibName, szDllName)) {//szDllName即是我们传入hook_iat的参数, 是我们要查找的模块名称
//当检验发现查到对应的"user32.dll"时, 获取描述user32.dll的pThunk, pThunk指向的时IAT的指针, 注意要类型转换, 因为转换为ITD的指针才能获取并操控ITD结构体
pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + pImportDesc->FirstThunk);//成员FirstThunk所记录的IAT地址也是RVA形式, 所以要加上hMod基址

//遍历IAT, 成员u1联合体Function为函数指针
for (; pThunk->u1.Function; pThunk++)
{
//如果遍历到的函数指针就是我们需要替换的函数起始地址的话, 说明这就是我们需要修改的IAT
if (pThunk->u1.Function == (DWORD)pfnOrg) {
//将该IAT地址区域保护属性修改为可读可写, 并将原来的保护属性保存在dwOldProtect中
VirtualProtect((LPVOID)&pThunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);

//修改IAT
pThunk->u1.Function = (DWORD)pfnNew;

//恢复内存属性
VirtualProtect((LPVOID)&pThunk->u1.Function, 4, dwOldProtect, &dwOldProtect);

//如果成功修改了IAT, 则返回TURE
return TRUE;
}
}
}
}

//如果没有找到对应的IAT, 则返回FLASE
return FALSE;
}

32.6 调试被注入的DLL文件

首先打开calc(计算器), 并使用process explorer查看其PID

Untitled

接下来使用OD(这里换成了x32dbg)附加到calc上

Untitled

F9使其处于running状态

并设置调试选项

Untitled

随后开始通过注入DLL来修改IAT从而实现API钩取

Untitled

调试器会停在hookiat.dll的入口点随后即可开始调试

Untitled

这里明显不是DllMian, 所以我们需要先找到DllMian的位置, 而这里最简单的方式就是字符串查找

我们已经知道DllMian中在调用GetProcAddress函数时, 使用了两个字符串指针”user32.dll”和”SetWindowTextW”, 所以可以通过这个来找到我们的DllMian

Untitled

确定了这里是DllMian后我们在该函数的开头下下断点并F9

即可开始调试DLL

文章作者: LamのCrow
文章链接: http://example.com/2022/04/19/逆向工程核心原理-第 e28be/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow