无法F2断下的弹窗:WOW64 Direct Syscall 逃逸

前言:一个“灵异弹窗”引发的底层追踪

在 Windows 逆向工程与安全研究的实战中,我们遇到了一个行为极其诡异的目标程序(32位):

图片[1]-无法F2断下的弹窗:WOW64 Direct Syscall 逃逸-软件安全逆向社区论坛-技术社区-学技术网

  1. 它可以弹出一个底层的系统提示框。

  2. 弹窗的宿主进程 100% 指向 csrss.exe,且弹窗会在几秒后自动消失。

  3. 即使主进程崩溃或被强杀,弹窗依然残留在屏幕上。

  4. 最关键的是:user32.dll 里的弹窗 API 甚至 ntdll.dll 里的系统调用(Syscall)下满断点,调试器也毫无反应

最开始的时候以为是简单的信息框,常见的信息框断点:

MessageBoxA/W、MessageBoxExA/W、MessageBoxTimeoutA/W全部无效

于是考虑到弹窗是否发生在目标进程?借助工具发现,果然,进程主体是csrss.exe,而不是目标程序

通过综合分析论断,该软件使用的是:现代高级免杀(APT)与顶级强壳(VMP)所使用的常用隐身术——WOW64 Direct Syscall

逃逸原理

在 32 位进程的 TEB(线程环境块)中,fs:[0xC0] 偏移处保存着一个极其致命的指针。这个指针直接指向了 wow64cpu.dll 的架构转换网关(如 X86SwitchTo64BitMode)。

目标程序自己手写了汇编代码,在自己的内存里伪造了系统调用的准备工作,然后直接 Call 这个网关。 它完全没有经过 ntdll.dll 的大门,断点自然成了摆设。

图片[2]-无法F2断下的弹窗:WOW64 Direct Syscall 逃逸-软件安全逆向社区论坛-技术社区-学技术网

复刻代码

完美复现这一逃逸过程:

#include <windows.h>
#include <stdio.h>

// ========== 编译级硬锁:必须是 32 位 ==========
#ifdef _WIN64
#error "[错误] 此代码是 32 位 WOW64 逃逸专用!请切换为 x86/Win32 编译!"
#endif

// 1. 原生 32 位结构体
typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// 全局变量保存 Syscall 号
DWORD g_SyscallNum = 0;

// =====================================================================
// 核心魔法:纯手工打造的 WOW64 系统调用存根 (Stolen Stub)
// __declspec(naked) 告诉编译器:不要加任何额外的汇编指令,全由我自己控制!
// =====================================================================
__declspec(naked) NTSTATUS NTAPI Direct_WOW64_ZwRaiseHardError(
    NTSTATUS ErrorStatus,
    ULONG NumberOfParameters,
    ULONG UnicodeStringParameterMask,
    PULONG_PTR Parameters,
    ULONG ValidResponseOptions,
    PULONG Response
) {
    __asm {
        mov eax, g_SyscallNum        // 将 Syscall 号放入 eax
        mov edx, esp                 // 将当前栈顶指针 (参数列表) 放入 edx
        call dword ptr fs : [0xC0]     // 幽灵跳跃:直接呼叫 WOW64 架构转换层!
        ret 0x18                     // 6个参数 * 4字节 = 24 (0x18) 字节,平栈返回
    }
}

int main() {
    printf("[+] 终极 API 逃逸测试:WOW64 Direct Syscall\n");

    // 动态提取当前的 Syscall 号,以防硬编码失效
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    BYTE* pZw = (BYTE*)GetProcAddress(hNtdll, "ZwRaiseHardError");

    // 32 位 ntdll 存根特征:B8 [Syscall号]
    if (pZw && pZw[0] == 0xB8) {
        g_SyscallNum = *(DWORD*)(pZw + 1);
        printf("[+] 成功从 ntdll 中窃取到 Syscall 号: 0x%X\n", g_SyscallNum);
    }
    else {
        g_SyscallNum = 0x167; // Fallback
        printf("[!] 提取失败,使用备用 Syscall 号: 0x%X\n", g_SyscallNum);
    }

    const wchar_t* msg = L"你抓不到我了!\n\n此弹窗由程序在自己的内存中纯手工构造寄存器状态,\n直接跳转至 WOW64 转换网关。\n\n即使你在 ntdll.dll 下满了断点,也无法阻止它的发生!";
    UNICODE_STRING uMsg;
    uMsg.Length = (USHORT)(wcslen(msg) * 2);
    uMsg.MaximumLength = uMsg.Length + 2;
    uMsg.Buffer = (PWSTR)msg;

    ULONG_PTR params[1] = { (ULONG_PTR)&uMsg };
    ULONG response = 0;

    printf("[+] 准备就绪。在调试器里随便下断点吧,我要起飞了...\n");
    system("pause"); // 下断点测试时刻

    // 呼叫我们自己的内联汇编函数!完全不经过 ntdll.dll!
    NTSTATUS status = Direct_WOW64_ZwRaiseHardError(
        0x40000015,
        1,
        1,
        params,
        1,
        &response
    );

    printf("[+] 降维打击完成!NTSTATUS: 0x%X\n", status);

    system("pause");
    return 0;
}

运行效果: 这段代码将无视你在 R3 层下的所有软件断点,如同幽灵般顺利弹出错误框

如何反制

面对 WOW64 Direct Syscall,传统的 R3 调试器(x32dbg/OD)已经彻底沦为“瞎子”。因为一旦 CPU 通过 fs:[0xC0] 切入 64 位空间,32 位调试器将失去单步追踪的能力。

如何反制?安全专家通常拥有两把神兵利器:

反制 1:盲打硬件断点 (Hardware Breakpoint)

虽然它绕过了函数名,但它绕不开物理地址。

在 x32dbg 中,按下 Ctrl+G,输入表达式 [fs:C0] 跳转。在这个被系统隐藏的 wow64cpu.dll 的入口处,下达硬件执行断点

当程序断下时,查看堆栈的返回地址,那个指向程序 .text 段的无名地址,就是最开始的调用位置

图片[3]-无法F2断下的弹窗:WOW64 Direct Syscall 逃逸-软件安全逆向社区论坛-技术社区-学技术网

图片[4]-无法F2断下的弹窗:WOW64 Direct Syscall 逃逸-软件安全逆向社区论坛-技术社区-学技术网

反制 2:内核级降维打击 (WinDbg)

既然目标把任务交给了内核,我们就站在内核(Ring 0)等它。

配置 VMware 双机调试,使用 WinDbg 连接。直接在内核态执行:

bp nt!NtRaiseHardError

无论 R3 层的花样有多少(混淆、VMP、Heaven’s Gate),只要它敢向内核发出中断,WinDbg 就会瞬间冻结整个系统,让你把目标进程的底裤看得一清二楚。

结语

这场逆向之旅深刻地告诉我们:底层安全攻防,从来都不是单纯的 API 对抗,而是对操作系统物理法则、内存布局以及架构演进的深刻理解。 只有懂得了系统是如何运行的,你才能知道系统是如何被欺骗的。

请登录后发表评论

    没有回复内容