第28章 使用汇编语言编写注入代码

本文最后更新于:2022年5月19日 晚上

第28章使用汇编语言编写注入代码
本章将学习使用汇编语言编写注入代码的相关知识与技术。

28.1 目标

首先借助OllyDbg的汇编功能,使用汇编语言编写注入代码(ThreadProc()函数)。使用汇编语言能够生成比C语言更自由、更灵活(非标准化的)的代码(如:直接访问栈、寄存器的功能),然后将纯汇编语言编写的ThreadProc()函数注入notepad.exe进程。学习本章要注意与前面讲过的(用C语言编写的)ThreadProc()比较,了解它们的不同点。

28.2 汇编编程

大家通常使用C/C++语言编写程序,其中具有代表性的开发工具有Microsoft Visual C++与Borland C++ Builder。使用汇编语言编写程序时,常用的开发工具(Assembler)有MASM(Microsoft Macro Assembler)、TASM (Borland Turbo Assembler )、FASM (Flat Assembler)等。

就我个人而言,用C/C++语言编写程序时使用MS Visual C++开发工具,用汇编语言编写程序时使用MASM编译器。MASM编译器支持多样化的Macro函数以及库文件,编程时使用起来非常方便,其便捷程度几乎与C语言相当。

设置好MASM编译器后,就可以正常进行汇编编程(Assembly Programming)了。当然还可以在类似Visual C++的C语言开发工具中使用“内联汇编”(Inline Assembly),将汇编指令插入C语言代码。这是非常适合开发入员的方式。本章将向各位介绍一种更适合代码逆向分析的方式,就是使用OllyDbg中的汇编功能编写汇编程序。

OllyDbg的汇编功能支持简单的汇编语言编程,这在代码逆向分析中相当有用(因为调试时经常需要修改各处代码)。

28.3 OllyDbg的汇编命令

本节将使用OllyDbg的汇编命令编写汇编语言程序。首先使用OllyDbg打开asmtest.exe示例文件(asmtest.exe是为进行汇编语言编程测试而编写的可执行文件(无任何功能))。

从代码区域的顶端部分( 401000 )开始看起,先向大家介绍一个OllyDbg的新命令New origin here,它把EIP更改为指定地址。在OllyDbg的代码窗口中移动光标到401000地址处,在鼠标右键菜单中选择New origin here(Ctrl+Gray*)菜单命令,如图28-2所示。

图28-2 New origin here(Ctrl+Gray*)菜单命令

执行菜单命令后,EIP地址变为401000。

调试时常常需要更改EIP值,所以New origin here菜单非常有用,希望各位记住它。

New origin here命令仅用来改变EIP值,与直接通过调试方式转到指定地址是不一样的,因为寄存器与栈中内容并未改变。

在401000地址处执行汇编命令(快捷键:Space),将弹岀输入汇编命令的窗口,如图28-4所示。

图28-4 汇编功能

接下来就可以在OllyDbg中编写简单的汇编语言程序了

建议大家在图28-4中取消(uncheck)对“Fill with NOP’s”的复选。OllyDbg的汇编命令用来向相应地址输入用户代码。若“Fill with NOP’s”项处于复选状态,输入代码长度短于已有代码时,剩余长度会填充为NOP (No Operation)指令,以整体对齐代码(Code Alignment)。为便于本章说明,我取消了对“Fill with NOP’s”项目的复选。

28.3.1 编写 ThreadProc()函数

下面使用汇编语言编写ThreadProc()函数。与前面使用C语言编写的ThreadProc()函数相比,其不同之处在于,需要的Data (字符串)已包含在Code中。请各位参考图28-5输入汇编指令。各条汇编指令的作用将在后面讲解(输入时注意取消对“Fill with NOP’s”的复选,若出现误录,转到相应地址处重新输入即可)。

图28-5 输入ThreadProc()

自上而下依次输入汇编指令,直到40102E地址处的CALL指令为止,各位的输入都正确吗?接下来,继续输入字符串。请先关闭Assemble窗口,在OllyDbg代码窗口中,移动光标至401033地址处,打开Edit窗口(快捷键:Ctrl+E),如图28-6所示。

image-20220224104028783

在图28-6的Edit窗口向ASCII项输入“ReverseCore”。因字符串必须以NULL结束,故在HEX项的最后输入00值(取消Keep size选项)。像这样完成全部输入后,在OllyDbg中査看代码,如 图28-7所示。

图28-7 “ReverseCore”字符串区域

图28-7中灰色部分即是“ReverseCore”字符串区域,可以看到字符串使用非常奇怪的指令进行显示。这样显示的原因在于,OllyDbg的Disassembler (反汇编器)将字符串误认为IA-32指令了。其实,这是由于输入者在Code位置输入字符串引起的,是输入者的错,而不能怪罪OllyDbg的反汇编器。

调试时常常会遇到这种反汇编(Disassemble)问题,有些反调试技术正是利用了这一点,后面讲解反调试时再向大家介绍。

如图28-7所示,选中字符串后再执行Analysis命令(快捷键;Ctrl+A ),得到图28-8。

图28-8 执行Analysis命令

OllyDbg的Analysis命令用来再次分析代码,再分析Unpack (解码的)代码时经常用到。

图28-8中的代码是执行了Analysis命令之后的形式。在401033地址处可以清晰地看到前面输入的字符串“ReverseCore”,但是401000地址之后的指令却解析有误(OllyDbg 2.0也无法将代码与数据100%区分开来。事实上,机器本身很难分清它是字符串还是指令)。在图28-8中难以查看代码,使用鼠标右键菜单中的Analysis-Remove analysis from module命令可以将代码恢复原样。使用Remove analysis命令恢复代码,如图28-9所示。

图28-9 Remove analysis from module菜单

接下来,使用汇编命令从40103F地址处(位于401033地址的“ReverseCore”字符串后面的地址)开始继续输入指令如图28-10所示。

图28-10 从40103F地址处开始输入指令

然后使用编辑命令,在401044地址处输入字符串(“www.reversecore.com”,不要忘记在最后添上NULL)如图28-11所示。

图28-11 输入字符串

再次使用汇编命令从401058地址处开始输入指令,如图28-12所示。

图28-12 汇编代码输入完成

至此已将ThreadProc()代码全部输入。图28-13显示了所有输入的代码,请各位对照查看自己输入的代码是否正确

图28-13 ThreadProc()代码

401033、401044地址中的内容不是指令,而是字符串。由于OllyDbg会将字符串识别为指令,所以字符串看上去有些怪异。

28.3.2 保存文件

编好代码后要保存。在OllyDbg代码菜单中,选择鼠标右键Copy to executable-All modifications菜单(参考图28-14)。

图28-14 Copy to executable-All modifications菜单

如图28-15所示,弹岀确认消息框,单击“Copy all”按钮。

图28-15 单击“Copy all”按钮

然后弹出窗口,显示所有修改内容,在鼠标右键菜单中选择Save file项目,如图28-16所示。

图28-16 Save file菜单项目

之后弹岀保存文件对话框,输入文件名称(asmtest_patch.exe )后保存。

Assemble(Space):输入汇编代码。
Analysis(Ctrl+A):再次分析代码。
New origin here(Ctrl+Gray*):更改 EIP。

28.4 编写代码注入程序

本节将使用前面创建好的汇编代码编写代码注入程序(Injector)。

28.4.1 获取ThreadProc()函数的二进制代码

首先,使用OllyDbg工具打开前面创建的asmtest_patch.exe文件。我们前面编写的ThreadProc()函数地址为401000,在内存窗口中转到401000地址处(转移命令Ctrl+G),如图28-17所示。

图28-17 转到401000地址处

ThreadProc()函数的地址区间为401000~401061。如图28-17所示,选中该地址区域,在鼠标
右键菜单中依次选择Copy-To file项目(参考图28-18 )。

图28-18 Copy-To file菜单

接着,使用文本编辑器打开刚刚保存的文件(我使用的文本编辑器为GVIM,各位也可以使用自己熟悉的文本编辑器)。

图28-19中显示的文本内容即为以Hex值形式表示的ThreadProc()函数,它们其实是一系列的IA-32指令,也是要注入目标进程的代码。

图28-19文本编辑器

IA-32指令解析方法请参考第49章。

如图28-20所示编辑文本文件,去除不必要的部分,每个字节前面加上前缀0x,各字节以逗号(,)分隔。适当应用文本编辑器的编辑功能(选择列、修改字符串)将带来很大便利。

图28-20 编辑内容

观察图28-20中编辑的文本内容,它们看上去就像C语言中的字节数组,这就是要注入的Hex代码(CodeInjection2.cpp文件中会用到)。

28.4.2 Codelnjection2.cpp

本节看看代码注入程序的源代码(CodeInjection2.cpp )。从代码28-1中可以看到,图28-20中使用文本编辑器编辑的Hex代码被保存到名为g_InjectionCode的字节数组。

以下源代码使用 MS Visual C++2010 Express Edition 编写而成,在 Windows XP/7 32位操作系统中通过测试。为便于说明,代码中的返回值检查与异常处理代码均已省略。

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
// CodeInjection2.cpp
// reversecore@gmail.com
// http://www.reversecore.com

#include "windows.h"
#include "stdio.h"

typedef struct _THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

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
};

/*
004010ED 55 PUSH EBP
004010EE 8BEC MOV EBP,ESP
004010F0 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8] ; ESI = pParam
004010F3 68 6C6C0000 PUSH 6C6C
004010F8 68 33322E64 PUSH 642E3233
004010FD 68 75736572 PUSH 72657375
00401102 54 PUSH ESP ; - "user32.dll"
00401103 FF16 CALL DWORD PTR DS:[ESI] ; LoadLibraryA("user32.dll")
00401105 68 6F784100 PUSH 41786F
0040110A 68 61676542 PUSH 42656761
0040110F 68 4D657373 PUSH 7373654D
00401114 54 PUSH ESP ; - "MessageBoxA"
00401115 50 PUSH EAX ; - hMod
00401116 FF56 04 CALL DWORD PTR DS:[ESI+4] ; GetProcAddress(hMod, "MessageBoxA")
00401119 6A 00 PUSH 0 ; - MB_OK (0)
0040111B E8 0C000000 CALL 0040112C
00401120 <ASCII> ; - "ReverseCore", 0
0040112C E8 14000000 CALL 00401145
00401131 <ASCII> ; - "www.reversecore.com", 0
00401145 6A 00 PUSH 0 ; - hWnd (0)
00401147 FFD0 CALL EAX ; MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
00401149 33C0 XOR EAX,EAX
0040114B 8BE5 MOV ESP,EBP
0040114D 5D POP EBP
0040114E C3 RETN
*/


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");

// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

// Open Process
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, // dwDesiredAccess
FALSE, // bInheritHandle
dwPID)) ) // dwProcessId
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for THREAD_PARAM
if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
sizeof(THREAD_PARAM), // dwSize
MEM_COMMIT, // flAllocationType
PAGE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[0], // lpBaseAddress
(LPVOID)&param, // lpBuffer
sizeof(THREAD_PARAM), // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for ThreadProc()
if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
sizeof(g_InjectionCode), // dwSize
MEM_COMMIT, // flAllocationType
PAGE_EXECUTE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[1], // lpBaseAddress
(LPVOID)&g_InjectionCode, // lpBuffer
sizeof(g_InjectionCode), // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !(hThread = CreateRemoteThread(hProcess, // hProcess
NULL, // lpThreadAttributes
0, // dwStackSize
(LPTHREAD_START_ROUTINE)pRemoteBuf[1],
pRemoteBuf[0], // lpParameter
0, // dwCreationFlags
NULL)) ) // lpThreadId
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

代码28-1中的注入代码与前面讲过的Codelnjection.cpp代码类似(调用的API也一样)。但它们最大的不同在于,代码28-1中的注入代码本身同时包含着代码所需的字符串数据。所以_THREAD_PARAM结构体中并不包含字符串成员,并且图28-20中的指令字节数组(g_InjectionCode )替代了用C语言编写的ThreadProc()函数。若程序编写得更巧妙一些,甚至都可以不用_THREAD_PARAM结构体。每个人的具体实现细节都是不同的,可以根据自己的需要调整。重要的是,方法的实质都是一样的,即编写汇编代码,将生成的指令内联在注入程序的源代码中,进而
将其注入目标进程。将代码成功注入目标进程后,下面我们将通过调试来分析各汇编代码的含义。

将上面的CodeInjection2.cpp与上一章中的Codeinjection.cpp源代码比较,可以明显看出它们的不同之处。

28.5 调试练习

本节将notepad.exe进程注入使用汇编语言编写的代码,并通过调试了解其工作原理。

28.5.1 调试 notepad.exe

使用OllyDbg工具打开notepad.exe文件开始调试。按F9运行键,使notepad.exe处于Running (运行)状态。

一定要使notepad.exe处于Running (运行)状态!!

28.5.2 设置OllyDbg 选项

进行代码注入时要在目标进程创建新线程,如图28-22所示,设置好OllyDbg的选项即可从注入的线程代码开始调试。

图28-22 复选Break on new thread项

这样,notepad.exe进程中有新线程生成时,调试器就会暂停在相应线程函数的开始代码处。

28.5.3 运行 Codelnjection2.exe

首先,使用Process Explorer查看notepad.exe进程的PID,如图28-23所示。

图28-23 notepad.exe进程的PID

以PID值作为参数,在命令行窗口中运行CodeInjection2.exe(以管理员身份运行),如图28-24所示。

图28-24 运行CodeInjection2.exe

28.5.4 线程起始代码

运行CodeInjection2.exe程序完成代码注入后,调试器会暂停在被注入的线程代码的起始位置,如图28-25所示。

图28-25 被注入的代码:ThreadProc()

不同运行环境下代码起始地址( 1C0000 )不同。

下面详细分析图28-25中的代码。

28.6 详细分析

28.6.1 生成栈帧

1
2
001C0000    55              push ebp
001C0001 8BEC mov ebp,esp

上面是2条典型的生成栈帧指令,对它们感到陌生的朋友可以趁此机会记住: 55 8BEC。后面出现的指令使用压字符串入栈的技术,生成栈帧就可以在ThreadProc()函数终止时将栈清理干净。

28.6.2 THREAD_PARAM 结构体指针

1
001C0003    8B75 08         mov esi,dword ptr ss:[ebp+0x8]

生成栈帧后,[EBP+8]是传入函数的第一个参数,这里指THREAD_PARAM结构体指针。下面是THREAD_PARAM结构体的定义,它的成员是2个函数指针,分别用来保存LoadLibraryA()与GetProcAddress()的函数指针(谁获取了函数指针并保存呢?对,就是前面讲过的CodeInjection2.exe程序,它获取了函数的指针,向notepad.exe注入完成并运行线程时以参数形式保存)。

1
2
3
4
typedef struct _THREAD_PARAM 
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

执行完1C0003地址处的MOV ESI,DWORD PTR SS:[EBP+8]指令后,进入ESI寄存器存储的地址查看,如图28-26所示。

图28-26 查看THREAD_PARAM结构体

寄存器ESI中存储的地址为1A0000,该地址是CodeInjection2.exe为THREAD_PARAM结构体在notepad.exe进程内存空间中分配的内存缓冲区地址。

不同用户系统环境下THREAD_PARAM结构体地址( 1A0000 )不同。

观察图28-26中的内存窗口可以看到,280000地址处存储着2个4字节的值,它们就是函数LoadLibraryA()与GetProcAddress()的起始地址。为了更直观地查看函数的起始地址,需要设置OllyDbg内存窗口的视图选项。先移动光标到内存窗口,然后在鼠标右键菜单中依次选择Long-Address项目,如图28-27所示。

图28-27 Long-Address菜单项目

选中图28-27中的菜单后,OllyDbg的内存窗口显示形式改变,如图28-28所示。

图28-28 API起始地址

函数地址如图显示就更直观了。并且,Comment栏中与各行地址对应的API名称也一同出现(在鼠标右键菜单中选择Hex-Hex/ASCII(16bytes)菜单,可以重新显示为Hex形式)。

28.6.3 “User32.dll” 字符串

1
2
3
001C0006    68 6C6C0000     push 0x6C6C
001C000B 68 33322E64 push 0x642E3233
001C0010 68 75736572 push 0x72657375

上面3行代码将“User32.dll”字符串压入栈,这种独特技术仅用于使用汇编语言编写的程序。地址1C0006处的PUSH 6C6C指令用来将6C6C压入栈,其中6C是ASCII码,对应字母1,所以该指令最终压入栈的是字符串\0\0ll。紧接着,1C000B与1C0010地址处的PUSH指令分别将字符串d.23resu压入栈。由于x86 CPU采用小端序标记法,再加上栈的逆向扩展特性,所 以字符串被逆向压入栈,请重点注意这个调试时必须掌握的内容。
自上而下跟踪代码到1C0015地址处,查看栈,如图28-29所示。

图28-29 栈中存储着“user32.dll”字符串

像这样,使用PUSH指令可以把指定字符串压入栈。并且,注入代码时不必另外注入字符串数据,只要把它们包含到代码中,只注入代码即可。

  • 还有一种将字符串数据包含进代码的方法,后面会单独介绍。

  • 32位的OS中,PUSH指令一次只能将4字节大小的数据压入栈。

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

1
001C0015    54              push esp

LoadLibraryA() API拥有1个参数,用来接收1个字符串的地址,该字符串是其要加载的DLL文件的名称。

1
2
3
4
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName
// address of filename of executable module
);

从图28-29中可知,当前ESP的值为A3FF7C,它是“user32.dll”字符串的起始地址。地址1C0015处的PUSH ESP指令用来将“user32.dll”字符串的起始地址(A3FF7C)压入栈(参考图28-30 )。

图28-30 1C0015地址处的PUSH指令

28.6.5 调用 LoadLibraryA( “user32.dll”)

1
001C0016    FF16            call dword ptr ds:[esi]		; kernel32.LoadLibraryA

如图28-28所示,ESI寄存器中存储的地址值为1A0000,该地址中保存着LoadLibraryA() API的起始地址( 77E2DC65 ),请看图28-31。

图28-31 ESI寄存器

对汇编语言内存引用语法感到陌生的朋友,可以借此机会记住它。为了帮助大家更好地理解这一个过程,我们把上面的CALL指令展开,形式如下([]类似于C语言中的指针引用):

*[ESI]=[470000]=77E2DC65 (address of kernel32.LoadLibraryA)

=存储在280000地址中的值

*CALL[ESI]=CALL[470000]=CALL 77E2DC65 =CALL Kernel32.LoadLibraryA

执行位于1C0016地址处的CALL DWORD PTR DS:[ESI]指令后,就会调用LoadLibraryA() API,同时加载作为参数传入的user32.dll文件。由于notepad.exe进程运行时已经加载了user32.dll,所以它只会返回加载的地址

图28-32 USER32.dll加载地址

函数的返回地址保存在EAX中,所以从图28-32中可以看到EAX=77D10000。选择OllyDbg菜单中的View-Executable modules[ALT+E]菜单项,可以查看加载到进程内存的所有DLL,如图28-23所示。可以清楚看到,user32.dll的加载地址就是77D10000。

图28-33 査看USER32.dll加载地址

28.6.6 “MessageBoxA” 字符串

上面3条PUSH指令将字符串“MessageBoxA”压入栈(与前面将字符串“user32.dll”压入栈的方法相同)。调试到1C0022地址处的PUSH指令,字符串“MessageBoxA”被存储到栈中,如图28-34所示。

1
2
3
001C0018    68 6F784100     push 0x41786F		;""\0Axo"
001C001D 68 61676542 push 0x42656761 ; "Bega"
001C0022 68 4D657373 push 0x7373654D ; "sseM"

上面3条PUSH指令将字符串“MessageBoxA”压入栈(与前面将字符串“user32.dll”压入栈的方法相同)。调试到1C0022地址处的PUSH指令,字符串“MessageBoxA”被存储到栈中,如图28-34所示。

图28-34 存储在栈中的“MessageBoxA”字符串

28.6.7 调用 GetProcAddress(hMod, “MessageBoxA”)

1
2
3
001C0027    54              push esp
001C0028 50 push eax ; user32.77D10000
001C0029 FF56 04 call dword ptr ds:[esi+0x4] ; kernel32.GetProcAddress

当前ESP的值为95FF70(参考图28-34 ),所以1C0027地址处的PUSH ESP指令用来将‘MessageBoxA”字符串的地址(95FF70)压入栈(该字符串的地址被用作GetProcAddress() API的第二个参数,在1C0029地址处调用此API)。而当前EAX的值为77D10000,它是user32.dll模块的加载地址(参考图28-32),所以1C0028地址处的PUSH EAX指令用来将user32.dll的起始地址(hMod)压入栈(该字符串的地址被用作GetProcAddress() API的第一个参数,在1C0029地址处调用此API)。调试至此查看栈,如图28-35所示。

图28-35 栈内情形

ESI寄存器的值为1A0000,所以可以将[ESI+4]如下展开(参考图28-28、图28-31 ):

*[ESI+4]=[1A0004]=77E2CC94(address of kernel32.GetProcAddress)

=存储在280004地址的值

*CALL [ESI+4]=CALL [1A0004]=CALL 77E2CC94=CALL Kernel32.GetProcAddress

所以1C0029地址处的 CALL DWORD PTR DS:[ESI+4]指令用来调用 GetProcAddress(77D10000,“MessageBoxA”)API函数。执行该条CALL指令后,user32.MessageBoxA() API的起始地址就会保存到EAX寄存器(系统环境不同,地址会有所不同。在我的系统环境下,EAX=7793EA71 ),如图28-36所示。

图28-36 MessageBoxA() API的起始地址

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

1
001C002C    6A 00           push 0x0

PUSH 0指令将0压入栈,0为MessageBoxA() API (后面会调用该API)的第四个参数(uType )MessageBoxA() API共有4个参数,函数原型如代码28-3所示。

1
2
3
4
5
6
7
8
9
10
int MessageBox(
HWND hWnd,
// handle of owner window
LPCTSTR lpText,
// address of text in message box
LPCTSTR lpCaption,
// address of title of message box
UINT uType
// style of message box
);

uType值为0,表示弹出的消息对话框为MB_OK,仅显示一个OK (确定)按钮。

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

1
2
3
4
5
6
7
8
001C002E    E8 0C000000     call 001C003F
001C0033 52 push edx
001C0034 65:76 65 jbe short 001c009c
001C0037 72 73 jb short 001C00AC
001C0039 65:43 inc ebx
001C003B 6f outs dx,dword ptr ds:[esi]
001C003C 72 65 jb short 001C00A3
001C003E 00E8 add al,ch

下面介绍“使用CALL指令将包含在代码间的字符串数据地址压入栈”的技术,该技术也仅能用在使用汇编语言编写的程序中。很明显,1C0033~1C003E地址区域是程序代码区域,但其内容实为“ReverseCore”字符串数据。也就是说,“ReverseCore”字符串的首地址为1C0033,它被用作MessageBoxA() API的第三个参数(lpCaption )。

将字符串作为参数传递给函数前,需要先把字符串地址压入栈,那么采用哪种方式好呢?继续调试位于1C002E地址处的CALL指令(StepIn(F7)),查看栈,如图28-37所示。

图28-37 1C003F代码

从栈中可以看到,“ReverseCore”字符串的起始地址1C0033被压入其中。也就是说,MessageBoxA()的第三个参数被压入栈。这个“花招”巧妙运用了CALL指令的“动作原理”。执 行1C0033地址处的CALL指令后,函数(1C003F )终止并将返回地址( 1C0033 )压入(PUSH) 栈,然后再跳转到(JMP)相应的函数地址(1C003F)。也就是说,执行一条CALL指令相当于执行了PUSH与JMP两条指令。但1C003F实际并不是函数,不具有以RETN指令返回的形态。此处的CALL指令只是用来将紧接其后的“ReverseCore”字符串地址压入栈,然后转到下一条代码指令。大家现在应该理解这个很有意思的CALL指令用法了。

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

1
2
3
4
5
6
7
8
9
10
001C003F    E8 14000000     call 001C0058
001C0044 77 77 ja short 001C00BD
001C0046 77 2E ja short 001C0076
001C0048 72 65 jb short 001C00AF
001C004A 76 65 jbe short 001C00B1
001C004C 72 73 jb short 001C00C1
001C004E 65:636F 72 arpl word ptr gs:[edi+0x72],bp
001C0052 65 gs:
001C0053 2E:636F 6D arpl word ptr cs:[edi+0x6D],bp
001C0057 006A 00 add byte ptr ds:[edx],ch

与“ReverseCore”字符串类似,上面的代码将MessageBoxA() API的第二个参数lpText字符串 (“www.reversecore.com”)压入栈。上述代码中的1C0044~1C0057地址区域并非代码指令,而是字符串数据(“www.reversecore.com”)。

1C003F地址处的CALL指令(与前面说明的一样)将紧接其后的“www.reversecore.com”字符串的地址(1C0044 )压入栈,然后转到下一条指令的地址处(1C0058 )(参考图28-38 )。

图28-38

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

1
001C0058    6A 00           push 0x0

上面这条指令将MessageBoxA() API的第一个参数hWnd压入栈,该参数用来确定消息对话框所属的窗口句柄,这里压入NULL值,创建一个不属于任何窗口的消息对话框。

28.6.12 调用 MessageBoxA()

1
001C005A    FFD0            call eax       ; user32.MessageBoxA

上面这条CALL指令调用MessageBoxA() API。指令中的EAX寄存器存储着MessageBoxA()API的起始地址(77D6EA11),该地址是前面调用GetProcAddress()后返回的值(参考图28-36、图28-38)。调试1C005A地址处的CALL EAX指令后,查看寄存器与栈,如图28-39所示。

图28-39 调用MessageBoxA()

执行CALL EAX指令即可弹出消息对话框,如图28-40所示。

图28-40 消息对话框

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

1
001C005C    33C0            xor eax,eax

注入notepad.exe进程的代码(ThreadProc()线程函数)执行完之前,还需要做一些准备工作,即用XOR EAX,EAX指令将线程函数的返回值设置为0。前面学过函数的返回值使用EAX寄存器,各位还记得吧?

XOR EAX,EAX指令能够又快又好地将EAX寄存器初始化为0(对CPU而言,它比使用MOV EAX,0指令更简单快捷)。

28.6.14 删除栈帧及函数返回

1
2
3
001C005E    8BE5            mov esp,ebp
001C0060 5D pop ebp
001C0061 C3 retn

最后,删除ThreadProc()函数开始时生成的栈帧,并使用RETN命令返回函数。栈帧在ThreadProc()函数中非常重要。对于前面使用PUSH指令压入栈的字符串,我们不需要费力地用POP命令逐个弹岀,只要使用上面几条删除栈帧的指令即可快速恢复原状。

28.7 小结

对使用汇编语言编写的注入代码的说明到此结束。使用汇编语言编写程序要比使用c语言更加灵活自由,强烈建议大家尝试使用汇编语言编写更多更具创意的代码。对于刚接触汇编语言不久的朋友,我建议使用OllyDbg中的汇编指令,用它编写汇编代码更容易。

参考

《逆向工程核心原理》 第28章


第28章 使用汇编语言编写注入代码
https://m0ck1ng-b1rd.github.io/1999/02/26/逆向工程核心原理/第28章 使用汇编语言编写注入代码/
作者
何语灵
发布于
1999年2月26日
许可协议