kr第三阶段(二)32 位汇编

编译与链接

环境配置

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

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:其它选项,和 mllink 的命令行选项等价。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 的寄存器名称。
    kr第三阶段(二)32 位汇编_第1张图片
  • 寄存器组
    32 位中,3 环可用寄存器。
    16 位 32 位
    通用寄存器
    EAXECXEBXEDX
    ESIEDIEBPESP
    标志寄存器
    EFLAGS
    指令指针寄存器
    EIP
    段寄存器
    CSDSESSS
  • 指令
    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 +偏移常量 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
RCLRCRROLROR
SAL/SHLSARSHR

汇编版第一个窗口

.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

在调试程序的时候 OD 可以自动查找到窗口的过程函数。
在这里插入图片描述

资源的使用

这里以对话框为例介绍汇编如何使用资源。

首先在 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

汇编只能在链接阶段实现对 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 中设置不使用预编译头。
kr第三阶段(二)32 位汇编_第2张图片
奖编译好的 Add.obj 复制到 RadASM 项目目录下,并在 RadASM 的 项目 → 工程选项 → 连(链)接 中添加 Add.obj
kr第三阶段(二)32 位汇编_第3张图片

之后汇编程序中声明 MyAdd 函数后就可以直接调用。

.586
.model flat,stdcall
option casemap:none

MyAdd proto C:DWORD, :DWORD

.code
start:
	invoke MyAdd, 4, 5
	invoke ExitProcess, 0
end start

C 调用汇编

创建 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 文件。
kr第三阶段(二)32 位汇编_第4张图片
这时候就可以在 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 项目编译产生的 dlllib 文件复制到汇编工程目录下,然后 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

C 调用汇编

新建 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 的方式添加功能的技术。
kr第三阶段(二)32 位汇编_第5张图片

这里我们以在扫雷程序中添加退出提示弹框为例讲解 Win32 程序的调试分析和打补丁的过程。

寻找窗口过程处理函数

Win32 程序的核心逻辑大多在窗口过程处理函数或者通过该函数调用的函数中,因此我们首先要做的就是寻找窗口过程处理函数。

这里提供提供两个寻找窗口过程处理函数的思路。

第一种思路在过创建窗口的函数下断点,然后根据参数来确定窗口过程处理函数。首先在在 Executable modules 窗口中选择模块然后 Ctrl + n 查看模块的导入导出表,然后在 RegisterClassWDialogBoxParam 等关键函数下断点。下断点的方式可以是右键函数选择查看参考找到所有调用函数的位置然后手动下断点,也可以右键选择在每个参考上设置断点。之后继续运行程序,结果程序在 RegisterClassW 函数断下来:
kr第三阶段(二)32 位汇编_第6张图片
通过分析参数可知窗口的过程函数地址为 0x1001BC9 ,可以在内存窗口中选择地址数据然后右键选择反汇编窗口跟随 DWORD 跳转到过程函数。

另一种方法是 F9 运行程序,然后再 Windows 窗口右键选择刷新即可看到程序注册的所有窗口。
在这里插入图片描述
右键对应的 ClsProc 选择跟随 ClsProc 即可跳转到对应的窗口过程处理函数。

寻址关闭窗口消息的处理代码

过程窗口虽然找到了,但是过程窗口处理大量的消息,如果在过程窗口下断点很难调试到关闭消息。

我们首先知道过程 WM_CLOSE 消息对应的值为 0x10 且为窗口过程函数的第二个参数。因此我们可以通过 IDA 找到 0x10 对应的代码即可。

另一种方法是下条件断点。在过程处理函数处 Shift + F2 添加条件断点,条件为 dword ptr [esp + 8] == 0x10 ,即第二个参数为 0x10 。如果断点处变为粉色说明条件断点添加成功。
kr第三阶段(二)32 位汇编_第7张图片
之后 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 消息。
kr第三阶段(二)32 位汇编_第8张图片
通过调试我们发现程序在处理该 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
*/

API Hook

基本原理

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 而不是负责代码注入的进程。
kr第三阶段(二)32 位汇编_第9张图片
这里我们通过 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

测试发现标题成功修改。
kr第三阶段(二)32 位汇编_第10张图片

重入问题

如果想要在 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

kr第三阶段(二)32 位汇编_第11张图片
不过这种方法多次修改原 API 可能出现同步问题。一种解决方法是创建一个新的函数入口作为未被 Hook 的函数使用。
kr第三阶段(二)32 位汇编_第12张图片

.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:异常代码,表示特定类型的异常。
    • ExceptionFlagsExceptionRecord:与异常嵌套有关,通常用不到。
    • ExceptionAddress:异常发生时的指令地址。
    • NumberParametersExceptionInformation 中元素的个数。
    • 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: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

如果下完断点之后直接继续执行,那么在异常中设置的设置的单步异常不会被调试器接管后清除,因此程序的异常处理函数会检查指令从而发现断点。
kr第三阶段(二)32 位汇编_第13张图片
如果是在调试器中单步调试单步步过第一条异常指令避免进入异常处理函数设置单步异常,那么后续程序就无法自动为每条指令设置单步异常。并且调试器单步时设置的单步异常会在调试器接管单步异常之后清除,因此此时该反调试方法失效。

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

你可能感兴趣的:(c++,安全,汇编)