编译与链接
环境配置
masm32
masm32 是微软的 masm32 的民间工具集合。该工具集合除了 asm32 本身的汇编器 ml
外还提供了:
- SDK 对应的函数声明头文件和
lib
库。 - 32 位版本的
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 onml /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.datag_szTitle db "hello,world!",0.code
ENTRY:invoke MessageBoxA, NULL, offset g_szTitle, NULL, MB_OK
end ENTRYends
RadASM
RadASM 是一款 ASM 的编辑器。相比与 VSCode ,RadASM 支持结构体成员和 API 的代码提示。这里推荐下载 RadASM2.2.1.2汉化增强版,该版本集成了调试器、编译器等环境。
常见设置位置及注意事项:
- 修改调试器路径:工程 → 工程选项 → 调试运行
- 修改编辑器主题:选项 → 颜色及关键字 → 选择主题并点击载入
- 虽然 RadASM 可以设计对话框,但不会自动生成对应的头文件,需要手动定义相关的消息编号。
- 以
printf
函数为例,使用 C 运行库时如果用的是msvcrt.lib
则函数名为crt_printf
,属于动态链接。
16 位与 32 位汇编区别
源文件格式
- 文件三件套
.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
- 分段
32 位汇编取消了分段,改用内存属性来划分,称作节(section)、内存区或内存块。节 可读 可写 可执行 备注 .DATA
√ √ 初始化的全局变量 .CONST
√ 只读数据区 .DATA?
√ √ 未初始化的全局变量 .CODE
√ √ 代码
调试
32 位汇编调试一般使用 OD 或者 x64dbg 。
操作或者快捷键 | 位置 | 说明 |
---|---|---|
选项 → 添加到右键资源管理器 | 可以在 exe 右键使用 od 打开 | |
F7 | 单步步入 | |
F8 | 单步步过 | |
F9 | 运行 | |
Ctrl + F2 | 重新打开 | |
F2 或双击 | 机器码 | 设置断点 |
空格或双击 | 反汇编 | 汇编 |
选中 + 空格 | 内存 | 修改内存 |
Ctrl + G | 内存,反汇编堆栈 | 转到指定地址 |
* | 堆栈 | 转到栈顶 |
寄存器
- 长度
寄存器扩展到了 32 位,命名为 Exx,低 16 位对应原来的 8086 的寄存器名称。
- 寄存器组
32 位中,3 环可用寄存器。16 位 32 位 通用寄存器 EAX
,ECX
,EBX
,EDX
ESI
,EDI
,EBP
,ESP
√ √ 标志寄存器 EFLAGS
√ √ 指令指针寄存器 EIP
√ √ 段寄存器 CS
,DS
,ES
,SS
√ - 指令
8086 所有指令在 32 位保持原有功能不变,操作数长度扩充到 32 位。
寻址
相较于 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:NONEinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szClassName db "MyWindowClass", 0g_szTitle db "My first asm32 window", 0g_szTip db "Failed to create window", 0.code
; 过程函数
MainWndProc proc hWnd:HWND, nMsg:UINT, wParam:WPARAM, lParam:LPARAM.IF nMsg == WM_DESTROYinvoke PostQuitMessage, 0.ENDIFinvoke DefWindowProc, hWnd, nMsg, wParam, lParamret
MainWndProc endpWinMain proc hInstance:HINSTANCElocal @wc: WNDCLASSlocal @hWnd:HWNDlocal @msg:MSG; 注册窗口类mov @wc.style, CS_HREDRAW or CS_VREDRAWmov @wc.lpfnWndProc, offset MainWndProcmov @wc.cbClsExtra, 0mov @wc.cbWndExtra, 0mov eax, hInstancemov @wc.hInstance, eaxinvoke LoadIcon, NULL, IDI_APPLICATIONmov @wc.hIcon, eaxinvoke LoadCursor, NULL, IDC_ARROWmov @wc.hCursor, eaxinvoke GetStockObject, WHITE_BRUSHmov @wc.hbrBackground, eaxmov @wc.lpszMenuName, NULLmov @wc.lpszClassName, offset g_szClassNameinvoke 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, NULLmov @hWnd, eax.if eax == NULLinvoke MessageBox, NULL, offset g_szTip, offset g_szTitle, MB_OKret.endif; 显示窗口invoke ShowWindow, @hWnd, SW_SHOW; 更新窗口invoke UpdateWindow, @hWnd; 消息循环.WHILE TRUEinvoke GetMessage, addr @msg, NULL, 0, 0.IF eax == 0.break.ENDIFinvoke TranslateMessage, addr @msginvoke DispatchMessage, addr @msg.ENDWret
WinMain endpENTRY:invoke GetModuleHandle, NULLinvoke WinMain, eaxinvoke ExitProcess, 0
end ENTRYends
在调试程序的时候 OD 可以自动查找到窗口的过程函数。
资源的使用
这里以对话框为例介绍汇编如何使用资源。
首先在 VisualStuduo 中创建项目,并且在项目中创建一个对话框资源。
编写汇编程序使用对话框资源。
.386
.model flat, stdcall
option casemap:NONE
include windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.libIDD_DIALOG1 equ 101.datag_szClassName db "MyWindowClass", 0g_szTitle db "My first asm32 window", 0g_szTip db "Failed to create window", 0
.codeDlgProc proc hWnd:HWND, nMsg:UINT, wParam:WPARAM, lParam:LPARAM.IF nMsg == WM_CLOSEinvoke EndDialog, hWnd, 0.ENDIFmov eax, FALSEret
DlgProc endpWinMain proc hInstance:HINSTANCEinvoke DialogBoxParam, hInstance, IDD_DIALOG1, NULL, offset DlgProc, 0ret
WinMain endpENTRY:invoke GetModuleHandle, NULLinvoke WinMain, eaxinvoke ExitProcess, 0
end ENTRYends
编译的时候注意要添加相关库的路径到环境变量中,其中链接的时候需要将资源链接到最终的可执行文件中。
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 onrc RC.rc
ml /c /coff test.asm
link /subsystem:windows /OUT:test.exe RC.RES test.obj
联合编译
与 dll
不同,obj
编译是直接链接到目标程序中的。
汇编调用 C
汇编只能在链接阶段实现对 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:noneMyAdd proto C:DWORD, :DWORD.code
start:invoke MyAdd, 4, 5invoke ExitProcess, 0
end start
C 调用汇编
创建 Sub.asm
文件,编写如下汇编代码,编译生成 Sub.obj
。因为这里只需要 MySub
函数,因此不需要指定程序入口。
.586
.model flat,stdcall
option casemap:none.code
MySub proc nVal1:DWORD, nVal2:DWORDmov eax, nVal1sub eax, nVal2retMySub endpend
这里想要编译新添加的 asm
文件需要在 项目 → 工程选项 → 编译
中添加文件名,不过我这里貌似不行,可能 cmd 版本问题。可以把 RadASM 的编译命令复制出来手动修改。
将编译生成的 Sub.obj
放到 VC 项目目录下,在项目设置中添加该 obj
文件。
这时候就可以在 C 语言中调用汇编函数了。
extern "C" int __stdcall MySub(int,int);int main() {MySub(2,3);return 0;
}
DLL 调用
联合编译可能会因版本问题识别,因此汇编与 C 之间的调用最好通过 DLL 实现。
汇编调用 C
首先创建一个 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:noneincludelib Msg.lib
Msg proto C :DWORD.datag_szHello db "Hello,World!",0.code
start:invoke Msg, offset g_szHello
end start
C 调用汇编
新建 DLL 类型的汇编项目,项目代码如下:
.586
.model flat,stdcall
option casemap:noneinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.codeAsmMsg proc C szMsg:LPSTRinvoke MessageBox,NULL, szMsg, NULL, MB_OKretAsmMsg endpDllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOIDmov eax, TRUEretDllMain endpend DllMain
在汇编中如果想要导出 AsmMsg
函数,需要在项目中的 .def
文件中添加该函数:
EXPORTSAsmMsg
编译后将生成的 .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<stdio.h>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 nArymov eax, type nArymov eax, length nAry}return 0;
}
裸函数
裸函数通常与内联汇编配合使用,该种函数通过 _declspec(naked)
来声明,函数中无任何预留汇编代码。
#include <stdio.h>_declspec(naked) void func() {__asm {push ebpmov ebp, espsub esp, __LOCAL_SIZE}int n;scanf_s("%d", &n);__asm {leaveretn}
}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
函数处理消息。
patch 程序添加功能
由于扫雷程序没有开启 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
- 如果用户点击的是取消按钮则跳转至 0x010021BB ,由窗口处理函数完成平栈工作。
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:NONEinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datahInstance dd ?CommandLine LPSTR ?g_szWinMineCap db "扫雷",0g_szUser32 db "user32.dll",0g_szMessageBox db "MessageBoxA",0.code
CODE_BEG:jmp MSG_CODEg_szText db "注入代码",0g_szCaption db "温情提示",0g_pfnMessageBox dd 0MSG_CODE:call NEXT
NEXT:pop ebxsub ebx, offset NEXTpush MB_OKmov eax, offset g_szCaptionadd eax, ebxpush eaxmov eax, offset g_szTextadd eax, ebxpush eaxpush NULLmov eax, offset g_pfnMessageBoxadd eax, ebxcall dword ptr [eax]retCODE_END:g_dwCodeSize dd offset CODE_END - offset CODE_BEGWinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORDLOCAL @hWindWinmine:HWNDLOCAL @dwProcId:DWORDLOCAL @hProc:HANDLELOCAL @pBuff:LPVOIDLOCAL @dwBytesWrited:DWORDLOCAL @hUser32:HMODULELOCAL @dwOldProc:DWORDinvoke VirtualProtect, offset g_pfnMessageBox, size g_pfnMessageBox, PAGE_EXECUTE_READWRITE, addr @dwOldProcinvoke LoadLibrary, offset g_szUser32mov @hUser32, eaxinvoke GetProcAddress, @hUser32, offset g_szMessageBoxmov g_pfnMessageBox, eaxinvoke VirtualProtect, offset g_pfnMessageBox, size g_pfnMessageBox, @dwOldProc, addr @dwOldProcinvoke FindWindow, NULL, offset g_szWinMineCapmov @hWindWinmine, eaxinvoke GetWindowThreadProcessId, @hWindWinmine, addr @dwProcIdinvoke OpenProcess, PROCESS_ALL_ACCESS, FALSE, @dwProcIdmov @hProc, eaxinvoke VirtualAllocEx, @hProc, NULL, g_dwCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITEmov @pBuff, eaxinvoke WriteProcessMemory, @hProc, @pBuff, offset CODE_BEG, g_dwCodeSize, addr @dwBytesWritedinvoke CreateRemoteThread, @hProc, NULL, 0, @pBuff, NULL, NULL, NULLxor eax, eaxretWinMain endpstart:invoke GetModuleHandle,NULLmov hInstance, eaxinvoke GetCommandLinemov CommandLine, eaxinvoke WinMain,hInstance, NULL, CommandLine, SW_SHOWDEFAULTinvoke ExitProcess, 0end start
shellcode 注入的其中一个作用就是调用游戏中某个特定的函数实现某些特定的功能。不过在这种情境下只是传参和调用函数,不需要考虑重定位问题。要实现这个工具,需要解决的是如何进行汇编。这里我使用的是 XEDParse 。
#include <iostream>
#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
*/
API Hook
基本原理
以 MessageBoxW
为例,windows 的 API 往往都是以 mov edi, edi; push ebp; mov ebp, esp
开头的,这是微软为 API Hook 准备的。因为这段汇编对应的机器码长度恰好是 5 字节,与 jmp
指令的长度相等,并且函数开头一定是某一条指令的开头,不会出现 Hook 到某条指令中间的情况。
76D3A270 > 8BFF mov edi,edi ; winmine2.<ModuleEntryPoint>
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:noneinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szUser32 db "user32",0g_szMessageBoxW db "MessageBoxW", 0g_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, 00hg_pfnMessageBoxW dd 0
.codeHOOKCODE:mov edi, edipush ebpmov ebp, espmov [ebp + 10h], offset g_szNewTitlemov eax, g_pfnMessageBoxWadd eax, 5 jmp eaxInstallHook proc uses ebxLOCAL @hUser32:HMODULELOCAL @dwOldProc:DWORD; 获取 MessageBox 地址invoke GetModuleHandle, offset g_szUser32mov @hUser32, eaxinvoke GetProcAddress, @hUser32, offset g_szMessageBoxWmov g_pfnMessageBoxW, eax; 计算跳转偏移mov eax, offset HOOKCODEsub eax, g_pfnMessageBoxWsub eax, 5push eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc; 修改跳转pop eaxmov ebx, g_pfnMessageBoxWmov byte ptr [ebx], 0e9h ; jmpmov dword ptr [ebx + 1], eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProcretInstallHook endpDllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID.if fdwReason == DLL_PROCESS_ATTACHinvoke InstallHook.endifmov eax, TRUEretDllMain endpend DllMain
测试发现标题成功修改。
重入问题
如果想要在 Hook 代码中调用被 Hook 的函数会出现无限递归,其中一种解决方法是在调用被 Hook 的函数前去除函数上的钩子,调用完之后再将钩子重新加上。
.586
.model flat,stdcall
option casemap:noneinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szUser32 db "user32",0g_szMessageBoxW db "MessageBoxW", 0g_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, 00hg_pfnMessageBoxW dd 0
.codeInsertJmp proc uses ebxLOCAL @dwOldProc:DWORD; 计算跳转偏移mov eax, offset HOOKCODEsub eax, g_pfnMessageBoxWsub eax, 5push eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc; 修改跳转pop eaxmov ebx, g_pfnMessageBoxWmov byte ptr [ebx], 0e9h ; jmpmov dword ptr [ebx + 1], eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProcretInsertJmp endpRemoveJmp procLOCAL @dwOldProc:DWORDinvoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProcmov ebx, g_pfnMessageBoxWmov byte ptr [ebx], 8bhmov dword ptr [ebx + 1], 0ec8b55ffhinvoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProcretRemoveJmp endpHOOKCODE:mov edi, edipush ebpmov ebp, espmov [ebp + 10h], offset g_szNewTitle; 修复invoke RemoveJmp; 重入push MB_OKpush offset g_szNewTitlepush offset g_szNewTitlepush NULLcall g_pfnMessageBoxW; 修改invoke InsertJmpmov eax, g_pfnMessageBoxWadd eax, 5 jmp eaxInstallHook proc uses ebxLOCAL @hUser32:HMODULELOCAL @dwOldProc:DWORD; 获取 MessageBox 地址invoke GetModuleHandle, offset g_szUser32mov @hUser32, eaxinvoke GetProcAddress, @hUser32, offset g_szMessageBoxWmov g_pfnMessageBoxW, eaxinvoke InsertJmpretInstallHook endpDllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID.if fdwReason == DLL_PROCESS_ATTACHinvoke InstallHook.endifmov eax, TRUEretDllMain endpend DllMain
不过这种方法多次修改原 API 可能出现同步问题。一种解决方法是创建一个新的函数入口作为未被 Hook 的函数使用。
.586
.model flat,stdcall
option casemap:noneinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szUser32 db "user32",0g_szMessageBoxW db "MessageBoxW", 0g_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, 00hg_pfnMessageBoxW dd 0
.codeMyMessageBoxW proc hWnd:HWND, lpText:DWORD, lpCaption:DWORD, uType:DWORDmov eax, g_pfnMessageBoxWadd eax, 5 jmp eaxMyMessageBoxW endpHOOKCODE:mov edi, edipush ebpmov ebp, espmov [ebp + 10h], offset g_szNewTitleinvoke MyMessageBoxW, NULL, offset g_szNewTitle, offset g_szNewTitle, MB_OKmov eax, g_pfnMessageBoxWadd eax, 5 jmp eaxInstallHook proc uses ebxLOCAL @hUser32:HMODULELOCAL @dwOldProc:DWORD; 获取 MessageBox 地址invoke GetModuleHandle, offset g_szUser32mov @hUser32, eaxinvoke GetProcAddress, @hUser32, offset g_szMessageBoxWmov g_pfnMessageBoxW, eax; 计算跳转偏移mov eax, offset HOOKCODEsub eax, g_pfnMessageBoxWsub eax, 5push eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, PAGE_EXECUTE_READWRITE, addr @dwOldProc; 修改跳转pop eaxmov ebx, g_pfnMessageBoxWmov byte ptr [ebx], 0e9h ; jmpmov dword ptr [ebx + 1], eaxinvoke VirtualProtect, g_pfnMessageBoxW, 1, @dwOldProc, addr @dwOldProcretInstallHook endpDllMain proc hinstDll:HINSTANCE, fdwReason:DWORD, lpvReserved:LPVOID.if fdwReason == DLL_PROCESS_ATTACHinvoke InstallHook.endifmov eax, TRUEretDllMain endpend 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)
异常时才会用的到,此时:- 数组第一个元素是个读写标志位,0 是读异常,1 是写异常。
- 数组第二个元素表示读写异常发生时读写的内存地址。
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:NONEinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szMsg db "异常来了,是否跳过异常代码?", 0g_szMsg1 db "异常已被跳过", 0.codeMyUnhandledExceptionFilter proc pEP:ptr EXCEPTION_POINTERS LOCAL @pEr:ptr EXCEPTION_RECORDLOCAL @pCtx:ptr CONTEXTmov eax, pEPassume eax:ptr EXCEPTION_POINTERSmov ebx, [eax].pExceptionRecordmov @pEr, ebxmov ebx, [eax].ContextRecordmov @pCtx, ebxinvoke MessageBox, NULL, offset g_szMsg, NULL, MB_OKCANCEL.if eax == IDOKmov ebx, @pCtxassume ebx:ptr CONTEXTadd [ebx].regEip, 2assume ebx:nothingmov eax, EXCEPTION_CONTINUE_EXECUTION.elsemov eax, EXCEPTION_CONTINUE_SEARCH.endifretMyUnhandledExceptionFilter endpstart:invoke SetUnhandledExceptionFilter, offset MyUnhandledExceptionFiltermov eax, 123hmov dword ptr [eax], eaxinvoke MessageBox, NULL, offset g_szMsg1, NULL, MB_OKinvoke ExitProcess, 0end start
自单步反软件断点
自单步反软件断点是利用单步异常在指令执行前来的特性检查每一条要执行的指令是否是 int3
断点来实现反调试。
.386
.model flat, stdcall
option casemap:NONEinclude windows.inc
include user32.inc
include gdi32.inc
include kernel32.incincludelib user32.lib
includelib gdi32.lib
includelib kernel32.lib.datag_szMsg db "异常来了,是否跳过异常代码?", 0g_szMsg1 db "未发现 INT3 断点", 0g_szTip db "发现 INT3 断点", 0.codeMyUnhandledExceptionFilter proc pEP:ptr EXCEPTION_POINTERS LOCAL @pEr:ptr EXCEPTION_RECORDLOCAL @pCtx:ptr CONTEXTmov eax, pEPassume eax:ptr EXCEPTION_POINTERSmov ebx, [eax].pExceptionRecordmov @pEr, ebxmov ebx, [eax].ContextRecordmov @pCtx, ebxmov ebx, @pErassume ebx:ptr EXCEPTION_RECORDmov esi, @pCtxassume esi:ptr CONTEXT.if [ebx].ExceptionCode == EXCEPTION_ACCESS_VIOLATIONadd [esi].regEip, 2or [esi].regFlag, 100h.elseif [ebx].ExceptionCode == EXCEPTION_SINGLE_STEPmov eax, [esi].regEip.if byte ptr [eax] == 0cchinvoke MessageBox, NULL, offset g_szTip, NULL, MB_OKmov eax, EXCEPTION_EXECUTE_HANDLERret.endif.if [esi].regEip != offset CODE_ENDor [esi].regFlag, 100h.endif.endifassume esi:nothingassume ebx:nothingmov eax, EXCEPTION_CONTINUE_EXECUTIONretMyUnhandledExceptionFilter endpstart:invoke SetUnhandledExceptionFilter, offset MyUnhandledExceptionFiltermov eax, 123hmov dword ptr [eax], eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxxor eax, eaxCODE_END:invoke MessageBox, NULL, offset g_szMsg1, NULL, MB_OKinvoke ExitProcess, 0end start
如果下完断点之后直接继续执行,那么在异常中设置的设置的单步异常不会被调试器接管后清除,因此程序的异常处理函数会检查指令从而发现断点。
如果是在调试器中单步调试单步步过第一条异常指令避免进入异常处理函数设置单步异常,那么后续程序就无法自动为每条指令设置单步异常。并且调试器单步时设置的单步异常会在调试器接管单步异常之后清除,因此此时该反调试方法失效。
OD 插件
前面调试筛选器异常的时候如果使用 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;
}