逆向工程核心原理-28

逆向工程核心原理-第28章-使用汇编语言编写注入代码

28.1 目标

借助OD的汇编功能, 使用汇编语言编写注入代码(ThreadProc()函数), 汇编语言能够生成比C语言更自由, 更灵活的代码(如: 直接访问栈, 寄存器的功能等), 然后将汇编语言编写的ThreadProc()函数注入notepad.exe进程

28.2 汇编编程

常用开发工具:

  • MASM
  • TASM
  • FASM
  • OD的汇编功能

也可以使用OD的汇编功能进行编程, 我们下面将使用OD

28.3 OD的汇编命令

我们首先使用OD打开asmtest.exe(该程序没有实现任何功能)

Untitled

然后从顶部开始(滚轮直接往上滑), 使用New origin here(此处为新EIP)将EIP设置到最顶端, 快捷键是(Ctrl + Gray*, Gray就是小键盘, Gray就是小键盘上的键)

Untitled

然后在该处进行汇编(快捷键: Space)

Untitled

注意, 要取消勾选”使用 NOP 填充”, 这个的功能是当我们输入的代码短于原先代码的时候, 多出来的字节使用NOP来填充

28.3.1 编写 ThreadProc() 函数

Untitled

在401033(紧跟在上面汇编的后面)地址处使用Edit选项(Ctrl + E)开始编写字符串

Untitled

注意后面一定要有一个’\0’

编辑完成后

Untitled

可以看到OD把这个字符串识别成了代码, 因为这里是代码段, 所以识别为代码非常正常

选中字符串开头后, 执行Analysis命令(快捷键: Ctrl + A)得到下图

Untitled

这时在401033地址处可以清晰看到我们的字符串”ReverseCore”

但是上面从401000地址处的代码却分析错误, 我们再使用右键 → 分析 → 从模块中删除分析(Remove analysis from module)

Untitled

又恢复原来的样子了, 我们接着上面的字符串, 继续编写汇编指令

下面感觉识别错误, 导致开头对不齐, 所以直接使用的编辑

下面是编辑完成后的结果

Untitled

28.3.2 保存文件

Untitled

保存为asmtest_patch.exe

Untitled

使用OD打开asmtest_patch.exe, 再内存窗口中跳转至401000处

Untitled

选中该区域右键

Untitled

经过处理以后我们得到了机器码序列

Untitled

然后就是照着书上的进行编写

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
#include <Windows.h>//注意这个头文件要放在开头, 否则会报架构错误
#include <iostream>
#include <windef.h>

typedef struct _Thread_PARAM
{
FARPROC pFunc[2];//参数结构体跟前面的代码注入不同,只包含了两个函数指针
} THREAD_PARAM, * PTHREAD_PARAM;//分别是LoadLibraryA(), GetProcAddress()

BYTE g_InjectionCode[] =
{
0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00, 0x00, 0x68, 0x33, 0x32, 0x2E, 0x64,
0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68, 0x61, 0x67,
0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54, 0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C,
0x00, 0x00, 0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F, 0x72, 0x65, 0x00, 0xE8,
0x14, 0x00, 0x00, 0x00, 0x77, 0x77, 0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00, 0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5,
0x5D, 0xC3
};

BOOL InjectCode(DWORD dwPID)
{
HMODULE hMod = NULL; //模块句柄
THREAD_PARAM param = { 0, }; //参数结构体
HANDLE hProcess = NULL; //进程句柄
HANDLE hThread = NULL; //线程句柄
LPVOID pRemoteBuf[2] = { 0, }; //用于记录目标进程的内存指针

hMod = GetModuleHandleA("kernel32.dll");

//获取线程过程函数所需要的函数地址, 并存储到参数结构体传入该函数中
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

//打开进程
if(!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))){
printf("未发现该进程PID: err_code = %d\n", GetLastError());
return FALSE;
}

//为数据分配目标进程内存中的内存
if (!(pRemoteBuf[0] = VirtualAllocEx(hProcess,
NULL,
sizeof(THREAD_PARAM),
MEM_COMMIT,
PAGE_READWRITE))) {
printf("在目标内存中分配数据空间失败: err_code = %d\n", GetLastError());
return FALSE;
}

//写入代码所需的数据
if (!(WriteProcessMemory(hProcess,
pRemoteBuf[0],
(LPVOID)&param,
sizeof(THREAD_PARAM),
NULL))) {
printf("写入目标进程的内存数据空间时发生错误: err_code = %d\n", GetLastError());
return FALSE;
}

//为代码分配目标进程内存中的内存
if (!(pRemoteBuf[1] = VirtualAllocEx(hProcess,
NULL,
sizeof(g_InjectionCode),
MEM_COMMIT,
PAGE_EXECUTE_READWRITE))) {
printf("在目标内存中分配代码空间失败: err_code = %d\n", GetLastError());
return FALSE;
}

//写入代码
if (!(WriteProcessMemory(hProcess,
pRemoteBuf[1],
(LPVOID)&g_InjectionCode,
sizeof(g_InjectionCode),
NULL))) {
printf("写入目标进程的内存代码空间时发生错误: err_code = %d\n", GetLastError());
return FALSE;
}

if (!(hThread = CreateRemoteThread(hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)pRemoteBuf[1],
pRemoteBuf[0],
0,
NULL))) {
printf("创建线程失败: err_code = %d", GetLastError());
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

int main(int argc, char* argv[])
{
DWORD dwPID = 0;

if (argc != 2)
{
printf("\n USAGE : %s pid\n", argv[0]);
}

dwPID = (DWORD)atol(argv[1]);
if (InjectCode(dwPID))
printf("注入成功\n");
else
printf("注入失败\n");

return 0;
}

书上没有给出主函数, 但是我们可以参考上一章的主函数进行编写, 然后就是书上省略了对异常情况的处理这里我们添加上即可(g_InjectionCode中间有一个数0x14写成了0x15, 汇编代码一注入程序直接关闭了, 找了好久才发现)

然后我们生成exe, 注意要使用Release和x86选项生成的exe才能成功, 试了其他的选项都不行

Untitled

检验一下

首先打开notepad.exe

Untitled

然后使用ProcessExplorer查看其PID

Untitled

得到PID = 13456

然后在CodeInjection2.exe的文件中以管理员身份打开终端

Untitled

输入参数运行我们的CodeInjection2.exe

Untitled

可以看到弹出了我们想要的窗口

Untitled

28.5 调试练习

28.5.1 调试notepad.exe

使用OD工具打开notepad.exe, F9使其处于运行中状态

28.5.2 设置OllDbg选项

Untitled

这样当我们的CodeInjection2.exe产生新线程时, 我们就是停下来调试了

28.5.3 运行CodeInjection2.exe

使用ProcessExplorer查看notepad的进程PID

Untitled

然后以进程PID作为参数, 运行CodeInjection2.exe

Untitled

28.5.4 线程起始代码

当运行CodeInjection2.exe时, 我们来到OD界面, 可以看到停在了我们的线程函数部分

Untitled

28.6 详细分析汇编指令

28.6.1 生成栈帧

PUSH EBP

MOV EBP, ESP

以上两句为生成栈帧指令

28.6.2 THREAD_PARAM 结构体指针

MOV ESI, DWORD PTR [EBP+8]

生成栈帧以后, [EBP + 8] 是传入函数的第一个参数, 这里指THREAD_PARAM结构体指针. 为什么是EBP + 8我们看看栈图即可知道(此时执行完了MOV EBP, ESP指令)

Untitled

参数结构体总共有8个字节, 前面四个字节保存的是LoadLibraryA()函数的指针, 后面四个字节保存的是GetProcAddress()函数的指针.

28.6.3 “User32.dll” 字符串

PUSH 6C6C “\0\0ll” 逆序

PUSH 642E3233 “d.23” 逆序

PUSH 72657375 “resu” 逆序

上面三行代码将”User32.dll”字符串压入栈中, 这种独特的方法仅用于汇编语言编写的程序, 采用的是逆序的方式压入, 因为栈是从高地址向低地址排列, 且整个数据是以小端排序, 当一整个数据块压入的时候, 会以字节为单位逆序存储, 所以”resu”压入栈中是排列其实是”user”

这样将所需字符串压入栈中的方式, 注入代码时就不需要另外注入数据, 直接使用栈即可.

我们再看看运行完上述语句后的寄存器情况

Untitled

可以看到此时的ESP就相当于我们的字符串指针

28.6.4 压入”user32.dll”字符串参数

PUSH ESP

LoadLibraryA()API需要一个参数, 用来接收一个字符串的地址, 该字符串为需要加载的DLL文件的名称. 所以我们在上面已经知道了ESP此时相当于”user32.dll”的字符串指针, 所以这里PUSH了ESP来作为下面调用的参数

28.6.5 调用LoadLibraryA()(”user32.dll”)

CALL DWORD PTR [ESI]

Untitled

前面已经将参数结构体的地址给了ESI, 而ESI所指向的内存则保存着LoadLibraryA的地址, 所以这里调用了LoadLibraryA()函数, 而其参数就是ESP(”user32.dll”的字符串指针)

在调用完成后, 查看其返回值, 就是该模块的句柄

Untitled

验证一下

Untitled

可以看到user32.dll的基址就是EAX中的76380000

28.6.6 “MessageBoxA” 字符串

PUSH 41786F “\0Axo”

PUSH 42656761 “Bega”

PUSH 7373654D “sseM”

跟28.6.3同理也是通过把字符串压入栈中, 然后得到了ESP就是该字符串的指针

Untitled

28.6.7 调用GetProcAddress(hMod, “MessageBoxA” )

PUSH ESP

PUSH EAX

CALL DWORD PTR DS : [ESI+4]

我们要知道汇编中的参数时逆序要入栈中的, 所以我们先PUSH”MessageBoxA”的指针, 然后再PUSH模块的句柄. 首先, PUSH”MessageBoxA”的字符串指针, 就是PUSH了ESP; 其次, 我们前面调用了LoadLibraryA()函数的返回值就是hMod模块句柄, 存储在EAX当中, 所以这里PUSH了EAX.

随后调用了[ESI + 4]即是结构体中的第二个成员GetProcAddress()函数的地址.

而它的返回值就是MessageBoxA的函数地址, 存放再了EAX当中

Untitled

28.6.8 压入MessageBoxA()函数的参数 1 - MB_OK

紧接着就开始了调用MessageBoxA()的参数准备, 参数是逆序压入的, 所以首先是压入MB_OK参数

PUSH 0

压入的时MessageBoxA的第四个参数

28.6.9 压入MessageBoxA()函数的参数2 - “ReverseCore”

0041002E E8 0C000000 call 0041003F 00410033 52 push edx 00410034 65:76 65 jbe short 0041009c 00410037 72 73 jb short 004100AC 00410039 65:43 inc ebx 0041003B 6f outs dx, dword ptr [esi] 0041003C 72 65 jb short 004100A3 0041003E 00E8 add al, ch

很明显, 410033 ~ 41003E地址处为我们的字符串”ReverseCore”, 只不过被识别成了代码. 也就是说, “ReverseCore”字符串的首地址为410033, 它被用作MessageBoxA()的第三个参数

将字符串作为参数传递给函数前, 要先把字符串的首地址压入栈中

下面, 我们将介绍”使用CALL指令将包含再代码键的字符串数据地址压入栈中”的方法.

Untitled

根据途中的介绍我们可以知道字符串的首地址被压入了栈中, 也就代表着第三个参数被成功压入了栈中, 同时通过CALL指令我们的EIP变成了43003F, 跳过了中间的字符串来到了接下去的代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
个人的理解:  这个方法起始就是把CALL指令拆分成了JMP和PUSH两个指令,  首先使用JMP跳转到下一个
指令处, 然后PUSH将需要的数据压入栈中(只不过条件较为苛刻, 只能将紧贴在CALL的下面的数据压入
栈中).

然后是JMP的一点理解: 在刚开始我有点疑惑的地方是这里CALL的是一个明确的地址43003F, 可是在notepad
中申请的内存是随机, 怎么会跳转到准确的位置呢???
答: 其实之前CSAPP就已经学过了, 这里的CALL在机器码中是根据当前的位置的偏移跳转的, 意思是虽然
我们写入汇编的时候用的绝对的地址, 但是实际CALL的地址一直都是以当前CALL地址为基准点,然后填入
偏移量
0041002E E8 0C000000 call 0041003F

E8 就是CALL指令的机器码
0C就是偏移量
41002E + 0C = 跳转的目标地址

28.6.10 压入MessageBoxA()函数的参数 3 - “www.reversecore.com”

0043003E 00E8 add al, ch 00430040 14 00 adc al, 0 00430042 0000 add byte ptr [eax], al 00430044 77 77 ja short 004300BD 00430046 77 2E ja short 00430076 00430048 72 65 jb short 004300AF 0043004A 76 65 jbe short 004300B1 0043004C 72 73 jb short 004300C1 0043004E 65:636F 72 arpl word ptr gs:[edi+72], bp 00430052 65 gs: 00430053 2E:636F 6D arpl word ptr cs:[edi+6D], bp 00430057 006A 00 add byte ptr [edx], ch

注意, 第一条指令, 由于我的OD分析偏差, 所以跟上面”ReverseCore”结尾的’\0’是连在一起的

实际上的第一条指令机器码为E8 14,意思是CALL 当前位置 + 0x14 , 原理跟上面是一样的

28.6.11 压入MessageBoxA()函数的参数 4 - NULL

00430057 006A 00 add byte ptr [edx], ch

这里同样是识别错误, 跟上面”www.reversecore.com”结尾的’\0’连在了一起

实际上的机器码为6A 00, 汇编为: PUSH 00,压入了第一个参数NULL

28.6.13 调用了MessageBoxA()

0043005A FFD0 call eax

我们可能忘记了EAX是从哪里来的(反正我是忘了), 看看汇编窗口

Untitled

你可能感到奇怪的就是中间我们压入那两个字符串的时候不是也调用了CALL吗, 为什么不会影响EAX?

因为使用的这两个CALL指令根本就没有返回, 一直执行到了最后的RETN程序就结束了, 从头到尾都没有返回.

回到正题, 我们可以知道CALL EAX, 就是调用了MessageBoxA()函数

28.6.13 设置ThreadProc()函数的返回值

0043005C 33C0 xor eax, eax

该汇编代码完成之前, 我们还需要做一些准备工作, XOR指令将线程函数的返回值设置为0

28.6.14 删除栈帧及函数返回

0043005E 8BE5 mov esp, ebp 00430060 5D pop ebp 00430061 C3 retn

删除ThradProc()函数一开始生成的栈帧, 然后RETN

至此我们完成了整个调试过程

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