masm32 是微软的 masm32 的民间工具集合。该工具集合除了 asm32 本身的汇编器 ml
外还提供了:
lib
库。link
(原版本是 16 位,这里的 32 位版本的 link
来自 VC 6.0)rc
(同样来自 VC 6.0)环境配置:
masm32
。masm32
下的 bin
目录添加到 path
。include
,将 masm32
目录下的 include
目录添加进去。lib
,将 masm32
目录下的 lib
目录添加进去。编译链接:
ml /c /coff %1.asm
link /subsystem:windows %1.obj
编译脚本:
echo off
set path=%path%;C:\masm32\bin
set include=%include%;C:\masm32\include
set lib=%lib%;C:\masm32\lib
echo on
ml /c /coff test.asm
link /subsystem:windows test.obj
测试程序:
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
includelib user32.lib
.data
g_szTitle db "hello,world!",0
.code
ENTRY:
invoke MessageBoxA, NULL, offset g_szTitle, NULL, MB_OK
end ENTRY
ends
RadASM 是一款 ASM 的编辑器。相比与 VSCode ,RadASM 支持结构体成员和 API 的代码提示。这里推荐下载 RadASM2.2.1.2汉化增强版,该版本集成了调试器、编译器等环境。
常见设置位置及注意事项:
printf
函数为例,使用 C 运行库时如果用的是 msvcrt.lib
则函数名为 crt_printf
,属于动态链接。.386
.model flat, stdcall
option casemap:NONE
.386
:汇编使用的指令集,同样也可以选择 .386P
,.486
等等。这里默认选 .386
。flat
:指定汇编程序所使用的内存模型。32 位只能选 flat
,因为 32 位程序可以访问 4GB 内存空间,不需要分段。stdcall
:指定函数默认调用约定为 stdcall
。option
:其它选项,和 ml
或 link
的命令行选项等价。32 位汇编一般只用 casemap
。
casemap | 对应的 ml 选项 |
英文 | 解释 |
---|---|---|---|
ALL |
/Cu |
Map all identifiers to upper case | 所有标识符转大写(大小写不敏感) |
NONE |
/Cp |
Reverse case of user identifies | 所有标识符维持原有大小写(大小写敏感) |
NOTPULIC |
/Cx |
Preserve case in publics,externs |
节 | 可读 | 可写 | 可执行 | 备注 |
---|---|---|---|---|
.DATA |
√ | √ | 初始化的全局变量 | |
.CONST |
√ | 只读数据区 | ||
.DATA? |
√ | √ | 未初始化的全局变量 | |
.CODE |
√ | √ | 代码 |
32 位汇编调试一般使用 OD 或者 x64dbg 。
操作或者快捷键 | 位置 | 说明 |
---|---|---|
选项 → 添加到右键资源管理器 | 可以在 exe 右键使用 od 打开 | |
F7 |
单步步入 | |
F8 |
单步步过 | |
F9 |
运行 | |
Ctrl + F2 |
重新打开 | |
F2 或双击 |
机器码 | 设置断点 |
空格或双击 | 反汇编 | 汇编 |
选中 + 空格 | 内存 | 修改内存 |
Ctrl + G |
内存,反汇编堆栈 | 转到指定地址 |
* |
堆栈 | 转到栈顶 |
16 位 | 32 位 | |
---|---|---|
通用寄存器EAX ,ECX ,EBX ,EDX ESI ,EDI ,EBP ,ESP |
√ | √ |
标志寄存器EFLAGS |
√ | √ |
指令指针寄存器EIP |
√ | √ |
段寄存器CS ,DS ,ES ,SS |
√ |
相较于 16 位汇编,32 位汇编的寻址变得宽松了,除了原有的寻址方式之外,额外增加了比例因子寻址。
EA = { 无 EAX EBX ECX EDX ESI EDI EBP ESP } 基址寄存器 + { 无 EAX EBX ECX EDX ESI EDI EBP } 变址寄存器 × { 1 2 4 8 } 比例因子 + { 无 8 位 16 位 } 偏移常量 \text{EA}=\underset{\text{基址寄存器}}{{\color{Green} \begin{Bmatrix} \text{无}\\ \text{EAX}\\ \text{EBX}\\ \text{ECX}\\ \text{EDX}\\ \text{ESI}\\ \text{EDI}\\ \text{EBP}\\ \text{ESP} \end{Bmatrix}}} + \underset{\text{变址寄存器}}{{\color{Blue} \begin{Bmatrix} \text{无}\\ \text{EAX}\\ \text{EBX}\\ \text{ECX}\\ \text{EDX}\\ \text{ESI}\\ \text{EDI}\\ \text{EBP} \end{Bmatrix}}} \times \underset{\text{比例因子}}{{\color{Blue} \begin{Bmatrix} {\color{Purple} 1} \\ {\color{Purple} 2} \\ {\color{Purple} 4} \\ {\color{Purple} 8} \end{Bmatrix}}} + \underset{\text{偏移常量}}{{\color{Tan} \begin{Bmatrix} \text{无} \\ \text{8 位}\\ \text{16 位} \end{Bmatrix}} } EA=基址寄存器⎩ ⎨ ⎧无EAXEBXECXEDXESIEDIEBPESP⎭ ⎬ ⎫+变址寄存器⎩ ⎨ ⎧无EAXEBXECXEDXESIEDIEBP⎭ ⎬ ⎫×比例因子⎩ ⎨ ⎧1248⎭ ⎬ ⎫+偏移常量⎩ ⎨ ⎧无8 位16 位⎭ ⎬ ⎫
16 位 | 32 位 | 说明 |
---|---|---|
CBW CWD |
CBW CWD CWDE CDQ |
符号扩充 |
LODSB LODSW |
LODSB LODSW LODSD |
串读取 |
STOSB STOSW |
STOSB STOSW STOSD |
串存储 |
MOVSB MOVSW |
MOVSB MOVSW MOVSD |
串读取 |
MOVSX reg, reg MOVSW reg, mem |
符号扩展 | |
MOVZX reg, reg MOVZW reg, mem |
无符号扩展 | |
移位指令 cl /1 |
移位指令 cl /1移位指令 reg, imm8 移位指令 mem, imm8 |
RCL ,RCR ,ROL ,ROR SAL /SHL ,SAR ,SHR |
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szClassName db "MyWindowClass", 0
g_szTitle db "My first asm32 window", 0
g_szTip db "Failed to create window", 0
.code
; 过程函数
MainWndProc proc hWnd:HWND, nMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF nMsg == WM_DESTROY
invoke PostQuitMessage, 0
.ENDIF
invoke DefWindowProc, hWnd, nMsg, wParam, lParam
ret
MainWndProc endp
WinMain proc hInstance:HINSTANCE
local @wc: WNDCLASS
local @hWnd:HWND
local @msg:MSG
; 注册窗口类
mov @wc.style, CS_HREDRAW or CS_VREDRAW
mov @wc.lpfnWndProc, offset MainWndProc
mov @wc.cbClsExtra, 0
mov @wc.cbWndExtra, 0
mov eax, hInstance
mov @wc.hInstance, eax
invoke LoadIcon, NULL, IDI_APPLICATION
mov @wc.hIcon, eax
invoke LoadCursor, NULL, IDC_ARROW
mov @wc.hCursor, eax
invoke GetStockObject, WHITE_BRUSH
mov @wc.hbrBackground, eax
mov @wc.lpszMenuName, NULL
mov @wc.lpszClassName, offset g_szClassName
invoke RegisterClass, addr @wc
; 创建窗口
invoke CreateWindowEx, NULL, offset g_szClassName, offset g_szTitle, WS_OVERLAPPEDWINDOW, \
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, \
NULL, NULL, hInstance, NULL
mov @hWnd, eax
.if eax == NULL
invoke MessageBox, NULL, offset g_szTip, offset g_szTitle, MB_OK
ret
.endif
; 显示窗口
invoke ShowWindow, @hWnd, SW_SHOW
; 更新窗口
invoke UpdateWindow, @hWnd
; 消息循环
.WHILE TRUE
invoke GetMessage, addr @msg, NULL, 0, 0
.IF eax == 0
.break
.ENDIF
invoke TranslateMessage, addr @msg
invoke DispatchMessage, addr @msg
.ENDW
ret
WinMain endp
ENTRY:
invoke GetModuleHandle, NULL
invoke WinMain, eax
invoke ExitProcess, 0
end ENTRY
ends
这里以对话框为例介绍汇编如何使用资源。
首先在 VisualStuduo 中创建项目,并且在项目中创建一个对话框资源。
编写汇编程序使用对话框资源。
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
IDD_DIALOG1 equ 101
.data
g_szClassName db "MyWindowClass", 0
g_szTitle db "My first asm32 window", 0
g_szTip db "Failed to create window", 0
.code
DlgProc proc hWnd:HWND, nMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF nMsg == WM_CLOSE
invoke EndDialog, hWnd, 0
.ENDIF
mov eax, FALSE
ret
DlgProc endp
WinMain proc hInstance:HINSTANCE
invoke DialogBoxParam, hInstance, IDD_DIALOG1, NULL, offset DlgProc, 0
ret
WinMain endp
ENTRY:
invoke GetModuleHandle, NULL
invoke WinMain, eax
invoke ExitProcess, 0
end ENTRY
ends
编译的时候注意要添加相关库的路径到环境变量中,其中链接的时候需要将资源链接到最终的可执行文件中。
echo off
set path=%path%;C:\masm32\bin
set include=%include%;C:\masm32\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\um\;C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\shared
set lib=%lib%;C:\masm32\lib
echo on
rc RC.rc
ml /c /coff test.asm
link /subsystem:windows /OUT:test.exe RC.RES test.obj
与 dll
不同,obj
编译是直接链接到目标程序中的。
汇编只能在链接阶段实现对 C 函数的调用。
在 VC6.0(新版 VS 貌似不太兼容)的项目中新建一个源文件并编写 MyAdd
函数:
extern "C" int MyAdd(int nVal1, int nVal2) {
return nVal1 + nVal2;
}
这里要注意:
extern "C"
避免 C++ 名称修饰。C
调用约定。然后选中该文件右键选择 Compile
将其编译成 obj
文件。这里可能会报 C1010
错误,这是因为找不到项目设置的预编译文件头,需要在 Project → Settings
中设置不使用预编译头。
奖编译好的 Add.obj
复制到 RadASM 项目目录下,并在 RadASM 的 项目 → 工程选项 → 连(链)接
中添加 Add.obj
。
之后汇编程序中声明 MyAdd
函数后就可以直接调用。
.586
.model flat,stdcall
option casemap:none
MyAdd proto C:DWORD, :DWORD
.code
start:
invoke MyAdd, 4, 5
invoke ExitProcess, 0
end start
创建 Sub.asm
文件,编写如下汇编代码,编译生成 Sub.obj
。因为这里只需要 MySub
函数,因此不需要指定程序入口。
.586
.model flat,stdcall
option casemap:none
.code
MySub proc nVal1:DWORD, nVal2:DWORD
mov eax, nVal1
sub eax, nVal2
ret
MySub endp
end
这里想要编译新添加的 asm
文件需要在 项目 → 工程选项 → 编译
中添加文件名,不过我这里貌似不行,可能 cmd 版本问题。可以把 RadASM 的编译命令复制出来手动修改。
将编译生成的 Sub.obj
放到 VC 项目目录下,在项目设置中添加该 obj
文件。
这时候就可以在 C 语言中调用汇编函数了。
extern "C" int __stdcall MySub(int,int);
int main() {
MySub(2,3);
return 0;
}
联合编译可能会因版本问题识别,因此汇编与 C 之间的调用最好通过 DLL 实现。
首先创建一个 DLL 项目(本质是在生成 exe
的链接命令中添加一个 /DLL
参数来生成 dll
,另外需要用 /DEF
参数指明 def
文件来指明要导出哪些函数),编写并导出函数。
#include "pch.h"
extern "C" __declspec(dllexport) void Msg(char* szMsg) {
MessageBoxA(NULL, szMsg, NULL, MB_OK);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
将 DLL 项目编译产生的 dll
和 lib
文件复制到汇编工程目录下,然后 includelib
掉入库并声明函数即可调用。
.586
.model flat,stdcall
option casemap:none
includelib Msg.lib
Msg proto C :DWORD
.data
g_szHello db "Hello,World!",0
.code
start:
invoke Msg, offset g_szHello
end start
新建 DLL 类型的汇编项目,项目代码如下:
.586
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.code
AsmMsg proc C szMsg:LPSTR
invoke MessageBox,NULL, szMsg, NULL, MB_OK
ret
AsmMsg endp
DllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID
mov eax, TRUE
ret
DllMain endp
end DllMain
在汇编中如果想要导出 AsmMsg
函数,需要在项目中的 .def
文件中添加该函数:
EXPORTS
AsmMsg
编译后将生成的 .dll
文件复制到 C/C++ 项目中可执行文件生成的目录,.lib
文件复制到项目中源文件所在的目录。在源文件中导入 lib
文件并声明函数即可使用。(这可能会编译失败,可能是因为高版本的 SafeSEH 机制与导入的 dll 冲突导致的,关闭 SafeSEH 即可。)
#pragma comment(lib, "AsmDll.lib")
extern "C" void AsmMsg(const char *);
int main() {
AsmMsg("Hello,World!");
return 0;
}
内联汇编即在 C/C++ 中使用的汇编,在 VC 中需要用 __asm
关键字指明。如果需要写多条汇编语句可以用 {}
表示汇编范围,然后汇编语句按行进行分隔。另外,在内联汇编中同时支持汇编和 C/C++ 注释
int main() {
// 一条汇编语句
__asm mov eax, eax
// 多条汇编语句
__asm {
/* 这是注释 */
mov ebx, ebx
; 这是注释
mov ecx, ecx
// 这是注释
}
return 0;
}
在内联汇编中可以直接使用 C/C++ 中定义的变量。需要注意,例如下面代码中汇编里面的 nAry[2*4]
和 C 代码中的 nAry[2]
指的是同一个地址。
#include
int main() {
int nAry[10]{};
__asm {
mov eax, 0x114514;
mov nAry[2 * 4], eax
}
printf("%x\n", nAry[2]);
return 0;
}
在内联汇编中,原本 masm 的宏不能使用,但是属性还是可以使用的,例如下面的代码。
int main() {
int nAry[10]{};
__asm {
mov eax, size nAry
mov eax, type nAry
mov eax, length nAry
}
return 0;
}
裸函数通常与内联汇编配合使用,该种函数通过 _declspec(naked)
来声明,函数中无任何预留汇编代码。
#include
_declspec(naked) void func() {
__asm {
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
int n;
scanf_s("%d", &n);
__asm {
leave
retn
}
}
int main() {
func();
return 0;
}
关于裸函数有如下注意事项:
[ebp - xxx]
位置,因此需要手动抬栈为局部变量预留栈空间,避免裸函数内部函数调用破坏局部变量。可以使用内置宏 __LOCAL_SIZE
让编译器自动计算需要开辟的栈空间。补丁即在二进制程序中通过 patch 的方式添加功能的技术。
这里我们以在扫雷程序中添加退出提示弹框为例讲解 Win32 程序的调试分析和打补丁的过程。
Win32 程序的核心逻辑大多在窗口过程处理函数或者通过该函数调用的函数中,因此我们首先要做的就是寻找窗口过程处理函数。
这里提供提供两个寻找窗口过程处理函数的思路。
第一种思路在过创建窗口的函数下断点,然后根据参数来确定窗口过程处理函数。首先在在 Executable modules
窗口中选择模块然后 Ctrl + n
查看模块的导入导出表,然后在 RegisterClassW
和 DialogBoxParam
等关键函数下断点。下断点的方式可以是右键函数选择查看参考找到所有调用函数的位置然后手动下断点,也可以右键选择在每个参考上设置断点。之后继续运行程序,结果程序在 RegisterClassW
函数断下来:
通过分析参数可知窗口的过程函数地址为 0x1001BC9 ,可以在内存窗口中选择地址数据然后右键选择反汇编窗口跟随 DWORD 跳转到过程函数。
另一种方法是 F9
运行程序,然后再 Windows
窗口右键选择刷新即可看到程序注册的所有窗口。
右键对应的 ClsProc
选择跟随 ClsProc
即可跳转到对应的窗口过程处理函数。
过程窗口虽然找到了,但是过程窗口处理大量的消息,如果在过程窗口下断点很难调试到关闭消息。
我们首先知道过程 WM_CLOSE
消息对应的值为 0x10 且为窗口过程函数的第二个参数。因此我们可以通过 IDA 找到 0x10 对应的代码即可。
另一种方法是下条件断点。在过程处理函数处 Shift + F2
添加条件断点,条件为 dword ptr [esp + 8] == 0x10
,即第二个参数为 0x10 。如果断点处变为粉色说明条件断点添加成功。
之后 F9
继续运行程序然后点击窗口关闭按钮可以成功断下。
最终的结果是对于 WM_CLOSE
消息程序在 0x1001C16 地址处直接跳转到 0x010021A9 调用 DefWindowProcW
函数处理消息。
由于扫雷程序没有开启 DEP 保护,因此程序加载到内存的所有段都具有可执行权限。
这里选择在 0x01001C16 位置处修改代码跳转到 01004A60 地址处,然后在该地址处实现确认对话框。
跳转的位置:
01001C03 . 8BC2 mov eax,edx
...
01001C13 . 83E8 38 sub eax,0x38
01001C16 . E9 452E0000 jmp winmine.01004A60
01001C1B 90 nop
跳转到的位置:
01004A60 > \83FA 10 cmp edx,0x10 ; Default case of switch 01001C05
01004A63 .^ 0F85 40D7FFFF jnz winmine1.010021A9
01004A69 . 6A 01 push 0x1 ; /Style = MB_OKCANCEL|MB_APPLMODAL
01004A6B . 68 A04A0001 push winmine1.01004AA0 ; |Title = "sky123的补丁"
01004A70 . 68 904A0001 push winmine1.01004A90 ; |Text = "是否需退出?"
01004A75 . 6A 00 push 0x0 ; |hOwner = NULL
01004A77 . FF15 B8100001 call dword ptr ds:[<&USER32.MessageBoxW>>; \MessageBoxW
01004A7D . 83F8 01 cmp eax,0x1
01004A80 .^ 0F84 23D7FFFF je winmine1.010021A9
01004A86 .^ E9 30D7FFFF jmp winmine1.010021BB
跳回的位置:
010021A9 > FF75 14 push dword ptr ss:[ebp+0x14] ; /lParam = 0x0; Default case of switch 01001F5F
010021AC . |FF75 10 push dword ptr ss:[ebp+0x10] ; |wParam = 10 (16.)
010021AF . |FF75 0C push dword ptr ss:[ebp+0xC] ; |Message = MSG(0x17B193E)
010021B2 . |FF75 08 push dword ptr ss:[ebp+0x8] ; |hWnd = 01001BC9
010021B5 . |FF15 24110001 call dword ptr ds:[<&USER32.DefWindowProcW>] ; \DefWindowProcW
010021BB > |5F pop edi ; user32.76CF2BC3
010021BC . |5E pop esi ; user32.76CF2BC3
010021BD . |5B pop ebx ; user32.76CF2BC3
010021BE . |C9 leave
010021BF . |C2 1000 retn 0x10
参考的 MessageBox 调用代码:
010039CB |. 6A 10 push 0x10 ; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL
010039CD |. 8D85 00FFFFFF lea eax,[local.64] ; |
010039D3 |. 50 push eax ; |Title = FFFFFFC9 ???
010039D4 |. 8D85 00FEFFFF lea eax,[local.128] ; |
010039DA |. 50 push eax ; |Text = FFFFFFC9 ???
010039DB |. 6A 00 push 0x0 ; |hOwner = NULL
010039DD |. FF15 B8100001 call dword ptr ds:[<&USER32.MessageBoxW>>; \MessageBoxW call dword ptr ds:[0x10010B8]
字符串位置:
01004A90 2F 66 26 54 00 97 00 90 FA 51 1F FF 00 00 00 00 是否需退出?..
01004AA0 73 00 6B 00 79 00 31 00 32 00 33 00 84 76 65 88 sky123的补
01004AB0 01 4E 00 00 00 00 00 00 00 00 00 00 00 00 00 丁.......
patch 时有以下几点需要注意:
edx
寄存器存放的是消息号,因此可以直接在补丁代码中用 edx
寄存器判断是否是 WM_CLOSE
消息。MessageBoxW
函数是通过导入表进行调用的,这里可以参考程序中其他调用 MessageBoxW
函数的代码。MessageBoxW
的返回值来判断用户点击的是确认还是取消按钮。#define IDOK 1
#define IDCANCEL 2
MessageBoxW
需要 UNICODE 字符串,OD 编辑 UNICODE 字符串的时候不能用输入法输入中文,而是通过右键 UNICODE 框选择粘贴将提前复制好的字符串粘贴进去。完成修改后随便选中一处修改,然后 右键 → 复制到可执行文件 → 所有修改 → 全部复制
,然后在弹出的窗口 右键 → 保存文件
即可将 patch 好的程序 dump 下来。
目前的 patch 程序虽然点击关闭按钮后弹出的对话框功能正常,但是通过 Game → 退出(X)
退出时无论选择确定还是取消窗口都会消失。在 SDK 学习中我们知道点击菜单上的退出时发送的是 WM_COMMAND
消息,因此我们在 OD 的 Windows
窗口右键窗口处理函数选择在 ClassProc
上设置消息断点来监控 WM_COMMAND
消息。
通过调试我们发现程序在处理该 WM_COMMAND
消息时执行的代码如下。程序会先调用 ShowWindow
将窗口隐藏起来,然后进行后续操作。
01001E9F . 57 push edi ; /ShowState = SW_HIDE
01001EA0 . FF35 245B0001 push dword ptr ds:[0x1005B24] ; |hWnd = 007F075A ('扫雷',class='扫雷')
01001EA6 . FF15 34110001 call dword ptr ds:[<&USER32.ShowWindow>] ; \ShowWindow
01001EAC > 57 push edi ; /lParam = 0x0
01001EAD . 68 60F00000 push 0xF060 ; |wParam = 0xF060
01001EB2 . 68 12010000 push 0x112 ; |Message = WM_SYSCOMMAND
01001EB7 . FF35 245B0001 push dword ptr ds:[0x1005B24] ; |hWnd = 0x7F075A
01001EBD . FF15 00110001 call dword ptr ds:[<&USER32.SendMessageW>; \SendMessageW
01001EC3 .^\E9 96FDFFFF jmp winmine2.01001C5E
...
01001C5E > /33C0 xor eax,eax
01001C60 . |E9 56050000 jmp winmine2.010021BB
...
010021BB > 5F pop edi
010021BC . |5E pop esi
010021BD . |5B pop ebx
010021BE . |C9 leave
010021BF . |C2 1000 retn 0x10
我们将 ShowWindow
函数的调用代码 nop 掉后发现功能正常了。
向目标进程注入 shellcode 时由于位置不确定,因此需要对代码进行重定位。重定位可以先通过 call + pop
的方式获取 shellcode 地址,然后修正地址即可。
下面的例子是向扫雷程序中注入一段弹窗代码并执行。
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
hInstance dd ?
CommandLine LPSTR ?
g_szWinMineCap db "扫雷",0
g_szUser32 db "user32.dll",0
g_szMessageBox db "MessageBoxA",0
.code
CODE_BEG:
jmp MSG_CODE
g_szText db "注入代码",0
g_szCaption db "温情提示",0
g_pfnMessageBox dd 0
MSG_CODE:
call NEXT
NEXT:
pop ebx
sub ebx, offset NEXT
push MB_OK
mov eax, offset g_szCaption
add eax, ebx
push eax
mov eax, offset g_szText
add eax, ebx
push eax
push NULL
mov eax, offset g_pfnMessageBox
add eax, ebx
call dword ptr [eax]
ret
CODE_END:
g_dwCodeSize dd offset CODE_END - offset CODE_BEG
WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL @hWindWinmine:HWND
LOCAL @dwProcId:DWORD
LOCAL @hProc:HANDLE
LOCAL @pBuff:LPVOID
LOCAL @dwBytesWrited:DWORD
LOCAL @hUser32:HMODULE
LOCAL @dwOldProc:DWORD
invoke VirtualProtect, offset g_pfnMessageBox, size g_pfnMessageBox, PAGE_EXECUTE_READWRITE, addr @dwOldProc
invoke LoadLibrary, offset g_szUser32
mov @hUser32, eax
invoke GetProcAddress, @hUser32, offset g_szMessageBox
mov g_pfnMessageBox, eax
invoke VirtualProtect, offset g_pfnMessageBox, size g_pfnMessageBox, @dwOldProc, addr @dwOldProc
invoke FindWindow, NULL, offset g_szWinMineCap
mov @hWindWinmine, eax
invoke GetWindowThreadProcessId, @hWindWinmine, addr @dwProcId
invoke OpenProcess, PROCESS_ALL_ACCESS, FALSE, @dwProcId
mov @hProc, eax
invoke VirtualAllocEx, @hProc, NULL, g_dwCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE
mov @pBuff, eax
invoke WriteProcessMemory, @hProc, @pBuff, offset CODE_BEG, g_dwCodeSize, addr @dwBytesWrited
invoke CreateRemoteThread, @hProc, NULL, 0, @pBuff, NULL, NULL, NULL
xor eax, eax
ret
WinMain endp
start:
invoke GetModuleHandle,NULL
mov hInstance, eax
invoke GetCommandLine
mov CommandLine, eax
invoke WinMain,hInstance, NULL, CommandLine, SW_SHOWDEFAULT
invoke ExitProcess, 0
end start
shellcode 注入的其中一个作用就是调用游戏中某个特定的函数实现某些特定的功能。不过在这种情境下只是传参和调用函数,不需要考虑重定位问题。要实现这个工具,需要解决的是如何进行汇编。这里我使用的是 XEDParse 。
#include
#include "XEDParse.h"
#pragma comment(lib, "XEDParse_x86.lib")
XEDPARSE xed{};
void MyAsm(const char* ins) {
printf_s("%s ", ins);
strcpy_s(xed.instr, ins);
XEDParseAssemble(&xed);
for (int i = 0; i < (int)xed.dest_size; i++) {
printf_s("%02X%c", xed.dest[i], i == xed.dest_size - 1 ? '\n' : ' ');
}
xed.cip += xed.dest_size;
}
int main() {
xed.x64 = false;
xed.cip = 0x01003e21;
MyAsm("push 70");
MyAsm("push 01001390");
MyAsm("call 0100400C");
return 0;
}
/*
push 70 6A 70
push 01001390 68 90 13 00 01
call 0100400C E8 DF 01 00 00
*/
以 MessageBoxW
为例,windows 的 API 往往都是以 mov edi, edi; push ebp; mov ebp, esp
开头的,这是微软为 API Hook 准备的。因为这段汇编对应的机器码长度恰好是 5 字节,与 jmp
指令的长度相等,并且函数开头一定是某一条指令的开头,不会出现 Hook 到某条指令中间的情况。
76D3A270 > 8BFF mov edi,edi ; winmine2.
76D3A272 /. 55 push ebp
76D3A273 |. 8BEC mov ebp,esp
76D3A275 |. 833D 94ACD676>cmp dword ptr ds:[0x76D6AC94],0x0
76D3A27C |. 74 22 je short user32.76D3A2A0
76D3A27E |. 64:A1 1800000>mov eax,dword ptr fs:[0x18]
76D3A284 |. BA 10B3D676 mov edx,user32.76D6B310
76D3A289 |. 8B48 24 mov ecx,dword ptr ds:[eax+0x24]
76D3A28C |. 33C0 xor eax,eax
76D3A28E |. f0:0fb10a lock cmpxchg dword ptr ds:[edx],ecx
76D3A292 |. 85C0 test eax,eax
76D3A294 |. 75 0A jnz short user32.76D3A2A0
76D3A296 |. C705 30ADD676>mov dword ptr ds:[0x76D6AD30],0x1
76D3A2A0 |> 6A FF push -0x1
76D3A2A2 |. 6A 00 push 0x0
76D3A2A4 |. FF75 14 push [arg.4]
76D3A2A7 |. FF75 10 push [arg.3]
76D3A2AA |. FF75 0C push [arg.2]
76D3A2AD |. FF75 08 push [arg.1]
76D3A2B0 |. E8 0BFEFFFF call user32.MessageBoxTimeoutW
76D3A2B5 |. 5D pop ebp ; kernel32.76E87BA9
76D3A2B6 \. C2 1000 retn 0x10
API Hook 的思路和前面打补丁类似,只不过进行打补丁的是加载进去的 DLL 而不是负责代码注入的进程。
这里我们通过 API Hook 修改前面打过补丁的扫雷程序的弹框的标题。
.586
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szUser32 db "user32",0
g_szMessageBoxW db "MessageBoxW", 0
g_szNewTitle db 41h, 00h, 50h, 00h, 49h, 00h, 20h, 00h, 48h, 00h, 6Fh, 00h, 6Fh, 00h, 6Bh, 00h, 20h, 00h, 4Bh, 6Dh, 0D5h, 8Bh, 00h, 00h
g_pfnMessageBoxW dd 0
.code
HOOKCODE:
mov edi, edi
push ebp
mov ebp, esp
mov [ebp + 10h], offset g_szNewTitle
mov eax, g_pfnMessageBoxW
add eax, 5
jmp eax
InstallHook proc uses ebx
LOCAL @hUser32:HMODULE
LOCAL @dwOldProc:DWORD
; 获取 MessageBox 地址
invoke GetModuleHandle, offset g_szUser32
mov @hUser32, eax
invoke GetProcAddress, @hUser32, offset g_szMessageBoxW
mov g_pfnMessageBoxW, eax
; 计算跳转偏移
mov eax, offset HOOKCODE
sub eax, g_pfnMessageBoxW
sub eax, 5
push eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc
; 修改跳转
pop eax
mov ebx, g_pfnMessageBoxW
mov byte ptr [ebx], 0e9h ; jmp
mov dword ptr [ebx + 1], eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProc
ret
InstallHook endp
DllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID
.if fdwReason == DLL_PROCESS_ATTACH
invoke InstallHook
.endif
mov eax, TRUE
ret
DllMain endp
end DllMain
如果想要在 Hook 代码中调用被 Hook 的函数会出现无限递归,其中一种解决方法是在调用被 Hook 的函数前去除函数上的钩子,调用完之后再将钩子重新加上。
.586
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szUser32 db "user32",0
g_szMessageBoxW db "MessageBoxW", 0
g_szNewTitle db 41h, 00h, 50h, 00h, 49h, 00h, 20h, 00h, 48h, 00h, 6Fh, 00h, 6Fh, 00h, 6Bh, 00h, 20h, 00h, 4Bh, 6Dh, 0D5h, 8Bh, 00h, 00h
g_pfnMessageBoxW dd 0
.code
InsertJmp proc uses ebx
LOCAL @dwOldProc:DWORD
; 计算跳转偏移
mov eax, offset HOOKCODE
sub eax, g_pfnMessageBoxW
sub eax, 5
push eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc
; 修改跳转
pop eax
mov ebx, g_pfnMessageBoxW
mov byte ptr [ebx], 0e9h ; jmp
mov dword ptr [ebx + 1], eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProc
ret
InsertJmp endp
RemoveJmp proc
LOCAL @dwOldProc:DWORD
invoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc
mov ebx, g_pfnMessageBoxW
mov byte ptr [ebx], 8bh
mov dword ptr [ebx + 1], 0ec8b55ffh
invoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProc
ret
RemoveJmp endp
HOOKCODE:
mov edi, edi
push ebp
mov ebp, esp
mov [ebp + 10h], offset g_szNewTitle
; 修复
invoke RemoveJmp
; 重入
push MB_OK
push offset g_szNewTitle
push offset g_szNewTitle
push NULL
call g_pfnMessageBoxW
; 修改
invoke InsertJmp
mov eax, g_pfnMessageBoxW
add eax, 5
jmp eax
InstallHook proc uses ebx
LOCAL @hUser32:HMODULE
LOCAL @dwOldProc:DWORD
; 获取 MessageBox 地址
invoke GetModuleHandle, offset g_szUser32
mov @hUser32, eax
invoke GetProcAddress, @hUser32, offset g_szMessageBoxW
mov g_pfnMessageBoxW, eax
invoke InsertJmp
ret
InstallHook endp
DllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID
.if fdwReason == DLL_PROCESS_ATTACH
invoke InstallHook
.endif
mov eax, TRUE
ret
DllMain endp
end DllMain
不过这种方法多次修改原 API 可能出现同步问题。一种解决方法是创建一个新的函数入口作为未被 Hook 的函数使用。
.586
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szUser32 db "user32",0
g_szMessageBoxW db "MessageBoxW", 0
g_szNewTitle db 41h, 00h, 50h, 00h, 49h, 00h, 20h, 00h, 48h, 00h, 6Fh, 00h, 6Fh, 00h, 6Bh, 00h, 20h, 00h, 4Bh, 6Dh, 0D5h, 8Bh, 00h, 00h
g_pfnMessageBoxW dd 0
.code
MyMessageBoxW proc hWnd:HWND, lpText:DWORD, lpCaption:DWORD, uType:DWORD
mov eax, g_pfnMessageBoxW
add eax, 5
jmp eax
MyMessageBoxW endp
HOOKCODE:
mov edi, edi
push ebp
mov ebp, esp
mov [ebp + 10h], offset g_szNewTitle
invoke MyMessageBoxW, NULL, offset g_szNewTitle, offset g_szNewTitle, MB_OK
mov eax, g_pfnMessageBoxW
add eax, 5
jmp eax
InstallHook proc uses ebx
LOCAL @hUser32:HMODULE
LOCAL @dwOldProc:DWORD
; 获取 MessageBox 地址
invoke GetModuleHandle, offset g_szUser32
mov @hUser32, eax
invoke GetProcAddress, @hUser32, offset g_szMessageBoxW
mov g_pfnMessageBoxW, eax
; 计算跳转偏移
mov eax, offset HOOKCODE
sub eax, g_pfnMessageBoxW
sub eax, 5
push eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc
; 修改跳转
pop eax
mov ebx, g_pfnMessageBoxW
mov byte ptr [ebx], 0e9h ; jmp
mov dword ptr [ebx + 1], eax
invoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProc
ret
InstallHook endp
DllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID
.if fdwReason == DLL_PROCESS_ATTACH
invoke InstallHook
.endif
mov eax, TRUE
ret
DllMain endp
end DllMain
筛选器异常即最终异常,Windows 会为提供一个 API 来设置一个回调函数来处理异常,这个 API 即 SetUnhandledExceptionFilter
,具体定义如下:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
该函数的参数是一个函数指针,即注册的异常处理的回调函数,会被 UnhandledExceptionFilter
函数调用:
LONG UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *ExceptionInfo );
异常处理函数参数同样是一个 _EXCEPTION_POINTERS
类型的结构体指针,_EXCEPTION_POINTERS
结构体定义如下:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
PEXCEPTION_RECORD ExceptionRecord
:指向 EXCEPTION_RECORD
结构体的指针。EXCEPTION_RECORD
结构体用于描述发生的异常的详细信息。
ExceptionCode
:异常代码,表示特定类型的异常。ExceptionFlags
,ExceptionRecord
:与异常嵌套有关,通常用不到。ExceptionAddress
:异常发生时的指令地址。NumberParameters
:ExceptionInformation
中元素的个数。ExceptionInformation
:一个包含异常参数的数组,只有 EXCEPTION_ACCESS_VIOLATION(0xC0000005)
异常时才会用的到,此时:
PCONTEXT ContextRecord
:指向 CONTEXT
结构体的指针。CONTEXT
结构体用于保存异常发生时的线程上下文,包括寄存器值、堆栈指针等。UnhandledExceptionFilter
的返回值有以下三种:
// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION (-1)
EXCEPTION._EXECUTE_HANDLER
:表示该异常已经处理(已经记录异常信息了),进程可以结束了。EXCEPTION_CONTINUE_SEARCH
:表示不处理该异常,请继续寻找其他处理程序。如果有调试器则交给调试器,否则结束进程。EXCEPTION_CONTINUE_EXECUTION
:表示该异常已被修复,请回到异常现场再次执行。筛选器异常需要使用安装了 sharpOD 插件的 x64dbg 调试。
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szMsg db "异常来了,是否跳过异常代码?", 0
g_szMsg1 db "异常已被跳过", 0
.code
MyUnhandledExceptionFilter proc pEP:ptr EXCEPTION_POINTERS
LOCAL @pEr:ptr EXCEPTION_RECORD
LOCAL @pCtx:ptr CONTEXT
mov eax, pEP
assume eax:ptr EXCEPTION_POINTERS
mov ebx, [eax].pExceptionRecord
mov @pEr, ebx
mov ebx, [eax].ContextRecord
mov @pCtx, ebx
invoke MessageBox, NULL, offset g_szMsg, NULL, MB_OKCANCEL
.if eax == IDOK
mov ebx, @pCtx
assume ebx:ptr CONTEXT
add [ebx].regEip, 2
assume ebx:nothing
mov eax, EXCEPTION_CONTINUE_EXECUTION
.else
mov eax, EXCEPTION_CONTINUE_SEARCH
.endif
ret
MyUnhandledExceptionFilter endp
start:
invoke SetUnhandledExceptionFilter, offset MyUnhandledExceptionFilter
mov eax, 123h
mov dword ptr [eax], eax
invoke MessageBox, NULL, offset g_szMsg1, NULL, MB_OK
invoke ExitProcess, 0
end start
自单步反软件断点是利用单步异常在指令执行前来的特性检查每一条要执行的指令是否是 int3
断点来实现反调试。
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.inc
includelib user32.lib
includelib gdi32.lib
includelib kernel32.lib
.data
g_szMsg db "异常来了,是否跳过异常代码?", 0
g_szMsg1 db "未发现 INT3 断点", 0
g_szTip db "发现 INT3 断点", 0
.code
MyUnhandledExceptionFilter proc pEP:ptr EXCEPTION_POINTERS
LOCAL @pEr:ptr EXCEPTION_RECORD
LOCAL @pCtx:ptr CONTEXT
mov eax, pEP
assume eax:ptr EXCEPTION_POINTERS
mov ebx, [eax].pExceptionRecord
mov @pEr, ebx
mov ebx, [eax].ContextRecord
mov @pCtx, ebx
mov ebx, @pEr
assume ebx:ptr EXCEPTION_RECORD
mov esi, @pCtx
assume esi:ptr CONTEXT
.if [ebx].ExceptionCode == EXCEPTION_ACCESS_VIOLATION
add [esi].regEip, 2
or [esi].regFlag, 100h
.elseif [ebx].ExceptionCode == EXCEPTION_SINGLE_STEP
mov eax, [esi].regEip
.if byte ptr [eax] == 0cch
invoke MessageBox, NULL, offset g_szTip, NULL, MB_OK
mov eax, EXCEPTION_EXECUTE_HANDLER
ret
.endif
.if [esi].regEip != offset CODE_END
or [esi].regFlag, 100h
.endif
.endif
assume esi:nothing
assume ebx:nothing
mov eax, EXCEPTION_CONTINUE_EXECUTION
ret
MyUnhandledExceptionFilter endp
start:
invoke SetUnhandledExceptionFilter, offset MyUnhandledExceptionFilter
mov eax, 123h
mov dword ptr [eax], eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
xor eax, eax
CODE_END:
invoke MessageBox, NULL, offset g_szMsg1, NULL, MB_OK
invoke ExitProcess, 0
end start
如果下完断点之后直接继续执行,那么在异常中设置的设置的单步异常不会被调试器接管后清除,因此程序的异常处理函数会检查指令从而发现断点。
如果是在调试器中单步调试单步步过第一条异常指令避免进入异常处理函数设置单步异常,那么后续程序就无法自动为每条指令设置单步异常。并且调试器单步时设置的单步异常会在调试器接管单步异常之后清除,因此此时该反调试方法失效。
前面调试筛选器异常的时候如果使用 OD 会出现异常交给 OD 后被 OD 处理了而没有交给程序的情况,导致程序执行流程与实际不符,无法调试异常处理函数。
这里我们实现一个 OD 插件,通过修改关键跳转使得筛选器异常直接交给程序而不是交给调试器,从而实现对筛选器异常处理函数的调试。
实际调试发现我的环境中的 KernelBase.dll
中的 UnhandledExceptionFilter
函数中调用 BasepIsDebugPortPresent
函数判断是否存在调试器之后存在一个关键跳转。只要我们将该跳转的 jnz
修改为 jz
就可以确保筛选器异常不会交给调试器。
if ( BasepIsDebugPortPresent() )
return 0;
.text:10212791 E8 E7 FB FF FF call _BasepIsDebugPortPresent@0 ; BasepIsDebugPortPresent()
.text:10212791
.text:10212796 85 C0 test eax, eax
.text:10212798 0F 85 E8 00 00 00 jnz loc_10212886
插件代码如下(OD 插件实在加载不上去就鸽了 )
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "Plugin.h"
#pragma comment(lib, "Ollydbg.lib")
int ODBG_Plugindata(char* shortname) {
strcpy_s(shortname, 31, "MyOdPlug");
return PLUGIN_VERSION;
}
int ODBG_Plugininit(int ollydbgversion, HWND hw, ulong* features) {
return 0;
}
int ODBG_Paused(int reason, t_reg* reg) {
if (reason == PP_EVENT) {
HMODULE hKernel = GetModuleHandleA("kernelbase.dll");
LPBYTE pAddr = (LPBYTE) GetProcAddress(hKernel, "UnhandledExceptionFilter");
pAddr += 0xA8;
BYTE btCode = 0x84;
Writememory(&btCode, (ulong) pAddr, sizeof(btCode), MM_SILENT);
}
return NULL;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}