0%

TencentGameSecurity2026-Windows

混淆没解全靠猜

真解不动混淆了。

拿地图信息

第一步:找驱动基址(绕过 DKOM 摘链)

驱动摘掉了自己的链表(lm m ShadowGateSys 返回空),所以不能用常规模块枚举找基址,改走设备对象反查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
1: kd> !object \Device\ShadowGate
Object: ffffc68c1eb4e6a0 Type: (ffffc68c19cb1400) Device
ObjectHeader: ffffc68c1eb4e670 (new version)
HandleCount: 0 PointerCount: 2
Directory Object: ffff800f81c44dc0 Name: ShadowGate

1: kd> dt nt!_DEVICE_OBJECT ffffc68c1eb4e6a0
+0x000 Type : 0n3
+0x002 Size : 0x150
+0x004 ReferenceCount : 0n0
+0x008 DriverObject : 0xffffc68c`201b8e30 _DRIVER_OBJECT
+0x010 NextDevice : (null)
+0x018 AttachedDevice : (null)
+0x020 CurrentIrp : (null)
+0x028 Timer : (null)
+0x030 Flags : 0x44
+0x034 Characteristics : 0x100
+0x038 Vpb : (null)
+0x040 DeviceExtension : (null)
+0x048 DeviceType : 0x22
+0x04c StackSize : 1 ''
+0x050 Queue : <anonymous-tag>
+0x098 AlignmentRequirement : 0
+0x0a0 DeviceQueue : _KDEVICE_QUEUE
+0x0c8 Dpc : _KDPC
+0x108 ActiveThreadCount : 0
+0x110 SecurityDescriptor : 0xffff800f`81cf4a60 Void
+0x118 DeviceLock : _KEVENT
+0x130 SectorSize : 0
+0x132 Spare1 : 0
+0x138 DeviceObjectExtension : 0xffffc68c`1eb4e7f0 _DEVOBJ_EXTENSION
+0x140 Reserved : (null)

1: kd> dt nt!_DRIVER_OBJECT 0xffffc68c`201b8e30
+0x000 Type : 0n4
+0x002 Size : 0n336
+0x008 DeviceObject : 0xffffc68c`1eb4e6a0 _DEVICE_OBJECT
+0x010 Flags : 0x12
+0x018 DriverStart : 0xfffff803`05c80000 Void
+0x020 DriverSize : 0x40b000
+0x028 DriverSection : 0xffffc68c`20f686a0 Void
+0x030 DriverExtension : 0xffffc68c`201b8f80 _DRIVER_EXTENSION
+0x038 DriverName : _UNICODE_STRING "\Driver\ShadowGate"
+0x048 HardwareDatabase : 0xfffff803`04f2e990 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x050 FastIoDispatch : (null)
+0x058 DriverInit : 0xfffff803`05c88000 long +fffff80305c88000
+0x060 DriverStartIo : (null)
+0x068 DriverUnload : 0xfffff803`05c81840 void +fffff80305c81840
+0x070 MajorFunction : [28] 0xfffff803`05c814b0 long +fffff80305c814b0

获取移动码和checksum等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1: kd> bp 0xfffff80305f974c6 ".printf \"[DIR] dir=%02x token=%08x checksum=%08x expect=%08x\\n\",  by(@r14), dwo(@r14+4), dwo(@r14+8), @edx; gc"
breakpoint 0 redefined
1: kd> u 0xfffff80305f97502 L6
fffff803`05f97502 4881c1c0010000 add rcx,1C0h
fffff803`05f97509 e900000000 jmp fffff803`05f9750e
fffff803`05f9750e ff151ccbceff call qword ptr [fffff803`05c84030]
fffff803`05f97514 488b0d9ddbceff mov rcx,qword ptr [fffff803`05c850b8]
fffff803`05f9751b 8ad0 mov dl,al
fffff803`05f9751d ff81bc000000 inc dword ptr [rcx+0BCh]
1: kd> g
[DIR] dir=d3 token=00000000 checksum=dead13e4 expect=dead13e4 S
[DIR] dir=53 token=00000001 checksum=dead1365 expect=dead1365 A
[DIR] dir=d0 token=00000002 checksum=dead13e5 expect=dead13e5 D
[DIR] dir=52 token=00000003 checksum=dead1366 expect=dead1366 W

拿地图信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1: kd> r @$t1 = poi(fffff80305c850b8)
1: kd> ? @$t1
Evaluate expression: -63169849477728 = ffffc68c`1f8fb5a0
1: kd> dq @$t1 L3B
ffffc68c`1f8fb5a0 01000000`00000000 01010100`00000000
ffffc68c`1f8fb5b0 00010101`00010101 01000000`00000001
ffffc68c`1f8fb5c0 00000100`00000000 01010101`00010101
ffffc68c`1f8fb5d0 00000100`00010101 01000000`00000000
ffffc68c`1f8fb5e0 01010001`00010000 01000001`00010101
ffffc68c`1f8fb5f0 01000000`01000100 01010001`00000100
ffffc68c`1f8fb600 00010101`00010001 01000000`00000100
ffffc68c`1f8fb610 01010000`00000100 00010001`01010101
ffffc68c`1f8fb620 00000100`00000101 01000100`01000100
ffffc68c`1f8fb630 00010001`00010001 00000000`00010001
ffffc68c`1f8fb640 00000000`01000100 00000000`00000000
ffffc68c`1f8fb650 00000000`00000000 00000002`00000002
ffffc68c`1f8fb660 00000000`00000000 00000000`00000000
ffffc68c`1f8fb670 00000000`00000000 00000000`00000000
ffffc68c`1f8fb680 00000000`00000000 00000000`00000000
ffffc68c`1f8fb690 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6a0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6b0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6c0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6d0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6e0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb6f0 00000000`00000000 00000000`00000000
ffffc68c`1f8fb700 00000000`00000000 00000000`00000000
ffffc68c`1f8fb710 00000000`00000000 00000000`00000000
ffffc68c`1f8fb720 00000000`00000000 00000000`00000000
ffffc68c`1f8fb730 00000000`00000000 00000000`00000000
ffffc68c`1f8fb740 00000000`00000000 00000000`00000000
ffffc68c`1f8fb750 00000000`00000000 00000000`00000000
ffffc68c`1f8fb760 00000000`00000000 00000000`000007ec
ffffc68c`1f8fb770 00000000`00000e94

写bfs求最短路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3
"""
ShadowGate 迷宫解析 + 最短路径求解

用法:
python maze_solve.py maze.txt # 从文件读取 WinDbg dq 输出
python maze_solve.py # 从 stdin 粘贴 dq 输出,Ctrl+Z 结束

WinDbg 操作步骤:
r @$t1 = poi(fffff80305c850b8)
dq @$t1 L3B
(把输出复制保存为 maze.txt)
"""

import sys
import struct
from collections import deque

CHECKSUM_MAGIC = 0xDEAD1337
DIR_CODE = {'W': 0x52, 'A': 0x53, 'S': 0xD3, 'D': 0xD0}
DIR_DELTA = {'W': (-1,0), 'S': (1,0), 'A': (0,-1), 'D': (0,1)}
DELTA_DIR = {v: k for k, v in DIR_DELTA.items()}


def parse_dq(text):
"""把 WinDbg dq 输出转成 bytearray(自动跳过地址前缀)"""
raw = bytearray()
for line in text.splitlines():
line = line.strip()
if not line:
continue
parts = line.split()
# 跳过第一列(地址)
for token in parts[1:]:
token = token.replace('`', '')
if len(token) != 16:
continue
if '?' in token:
raw += b'\x00' * 8
continue
v = int(token, 16)
for i in range(8):
raw.append((v >> (i * 8)) & 0xFF)
return raw


def bfs(grid, W, H, start, end):
queue = deque([(start, [start])])
visited = {start}
while queue:
(r, c), path = queue.popleft()
if (r, c) == end:
return path
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < H and 0 <= nc < W and grid[nr][nc] == 0 and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append(((nr, nc), path + [(nr, nc)]))
return None


def print_maze(grid, W, H, path_set, start, end):
for r in range(H):
row = ''
for c in range(W):
if (r, c) == start: row += 'S'
elif (r, c) == end: row += 'E'
elif (r, c) in path_set: row += '*'
elif grid[r][c]: row += '#'
else: row += '.'
print(row)


def main():
# 读取 dq 数据
if len(sys.argv) > 1:
with open(sys.argv[1], 'r') as f:
text = f.read()
else:
print('[*] 粘贴 WinDbg dq 输出,结束后按 Ctrl+Z (Windows) / Ctrl+D (Linux):')
text = sys.stdin.read()

raw = parse_dq(text)
if len(raw) < 169:
print(f'[!] 数据不足,只解析到 {len(raw)} 字节,至少需要 169 字节(13×13)')
sys.exit(1)

# 迷宫参数(硬编码本次确认值,可按需改)
W, H = 13, 13
start = (0, 0) # (row, col) = EntryY, EntryX
end = (12, 12) # ExitY, ExitX

maze_bytes = raw[:W * H]
grid = [list(maze_bytes[r * W:(r + 1) * W]) for r in range(H)]

# 验证起终点是否可通
if grid[start[0]][start[1]] != 0:
print('[!] 警告: 起点是墙,数据可能有误')
if grid[end[0]][end[1]] != 0:
print('[!] 警告: 终点是墙,数据可能有误')

# BFS
path = bfs(grid, W, H, start, end)
if not path:
print('[!] BFS 未找到路径,请检查地图数据')
sys.exit(1)

moves = [DELTA_DIR[(path[i][0]-path[i-1][0], path[i][1]-path[i-1][1])]
for i in range(1, len(path))]

# 输出地图
print(f'\n=== 迷宫地图 {W}x{H} (#=墙 .=通 *=路径 S=起 E=终) ===')
print_maze(grid, W, H, set(path), start, end)

# 输出路径
print(f'\n最短步数: {len(moves)}')
print(f'方向序列: {"".join(moves)}')

# 输出 IOCTL 发包表(token 固定 0)
print('\n=== IOCTL_MAZE_MOVE 发包序列 (token=0) ===')
print(f'{"步":>4} {"向":>2} {"dir":>4} {"checksum":>10} {"12字节包(hex)":}')
print('-' * 60)
for i, d in enumerate(moves):
code = DIR_CODE[d]
ck = code ^ CHECKSUM_MAGIC
pkt = struct.pack('<BxxxII', code, 0, ck)
print(f'{i+1:4d} {d:>2} 0x{code:02X} 0x{ck:08X} {pkt.hex()}')


if __name__ == '__main__':
main()

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
python .\maze_solve.py .\maze.txt

=== 迷宫地图 13x13 (#=墙 .=通 *=路径 S=起 E=终) ===
S******#..***
######*###*#*
.....#*****#*
.###.#######*
.#.........#*
.#.#.#####.#*
.#.#.#...#.#*
.#.###.#.###*
.#.....#.#***
.#######.#*##
...#...#.#*#.
##.#.#.#.#*#.
.....#.#..**E

最短步数: 32
方向序列: DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD

=== IOCTL_MAZE_MOVE 发包序列 (token=0) ===
步 向 dir checksum 12字节包(hex)
------------------------------------------------------------
1 D 0xD0 0xDEAD13E7 d000000000000000e713adde
2 D 0xD0 0xDEAD13E7 d000000000000000e713adde
3 D 0xD0 0xDEAD13E7 d000000000000000e713adde
4 D 0xD0 0xDEAD13E7 d000000000000000e713adde
5 D 0xD0 0xDEAD13E7 d000000000000000e713adde
6 D 0xD0 0xDEAD13E7 d000000000000000e713adde
7 S 0xD3 0xDEAD13E4 d300000000000000e413adde
8 S 0xD3 0xDEAD13E4 d300000000000000e413adde
9 D 0xD0 0xDEAD13E7 d000000000000000e713adde
10 D 0xD0 0xDEAD13E7 d000000000000000e713adde
11 D 0xD0 0xDEAD13E7 d000000000000000e713adde
12 D 0xD0 0xDEAD13E7 d000000000000000e713adde
13 W 0x52 0xDEAD1365 52000000000000006513adde
14 W 0x52 0xDEAD1365 52000000000000006513adde
15 D 0xD0 0xDEAD13E7 d000000000000000e713adde
16 D 0xD0 0xDEAD13E7 d000000000000000e713adde
17 S 0xD3 0xDEAD13E4 d300000000000000e413adde
18 S 0xD3 0xDEAD13E4 d300000000000000e413adde
19 S 0xD3 0xDEAD13E4 d300000000000000e413adde
20 S 0xD3 0xDEAD13E4 d300000000000000e413adde
21 S 0xD3 0xDEAD13E4 d300000000000000e413adde
22 S 0xD3 0xDEAD13E4 d300000000000000e413adde
23 S 0xD3 0xDEAD13E4 d300000000000000e413adde
24 S 0xD3 0xDEAD13E4 d300000000000000e413adde
25 A 0x53 0xDEAD1364 53000000000000006413adde
26 A 0x53 0xDEAD1364 53000000000000006413adde
27 S 0xD3 0xDEAD13E4 d300000000000000e413adde
28 S 0xD3 0xDEAD13E4 d300000000000000e413adde
29 S 0xD3 0xDEAD13E4 d300000000000000e413adde
30 S 0xD3 0xDEAD13E4 d300000000000000e413adde
31 D 0xD0 0xDEAD13E7 d000000000000000e713adde
32 D 0xD0 0xDEAD13E7 d000000000000000e713adde

成功拿到并解密flag:

1
2
3
4
5
6
  =============================================
ACCESS GRANTED - Credential extracted!
=============================================
[CREDENTIAL] flag{SHAD0WNT_HYPERVMX}
[*] Your input (32 ops sent): RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR
[*] Note: blocked ops are included above; the driver only counts successful ones.

ShadowGate — 隐匿信道逆向分析过程(4信道详解)

分析对象:ShadowGateSys.sys
IDA 静态基址:0x140000000
运行时基址:0xfffff80305c80000(WinDbg 动态确认)
分析工具:IDA Pro(MCP 接入)、WinDbg 内核调试、手工 XOR 解密


一、工具链与分析方法论

本次逆向采用"静态优先、动态验证"的三段式方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IDA Pro 静态分析
├── MCP 工具导出反编译/反汇编结果
├── 导入表 → 锁定高价值 API 调用点
├── 全局变量槽(qword_140005080/90/98)→ 动态解析函数指针
└── 加密字符串 → 手工 XOR 0x4B 解密

↓ 确认候选函数后

WinDbg 断点验证
├── bp <运行时地址> → 确认函数被调用
├── dt/dq → 确认结构体字段读写
└── !process / !thread → 确认进程附加路径

↓ 逻辑完整后

用户层检测实现(channel_probe.c)
└── 对应每个信道部署检测原语,Reset 后前5步逐一验证

混淆手段汇总(影响分析路径的关键干扰)

手段 位置 影响
jmp $+5E9 00 00 00 00)跳花指令 全段 线性反汇编断裂,IDA 函数识别失败
cmc/clc/stc/rcl/rcr 无意义标志操作 IOCTL handler 混淆段 干扰符号执行和反编译类型推断
66 前缀 16 位寄存器混用 混淆段 IDA 类型推断失效,变量被错误截断
_guard_dispatch_icall_fptr() 间接跳转 CH4 信道函数 IDA 无法静态确定调用目标,显示为 CFG 保护调用
qword_14000508x 全局函数指针槽 CH2/3/4 共用 真实 API 名称运行时才写入,静态全为 0
XMM XOR 字符串加密 信号量名称数据段 静态看不到明文,需手工解密

关键结论:因混淆严重,信道函数的发现路径是"导入表 API 反查 xref → 候选函数 → 反编译验证逻辑 → WinDbg 确认执行",而非直接从 DriverEntry 顺序追踪。


二、驱动整体结构(快速定位框架)

DriverEntry 链

1
2
3
4
5
6
7
8
9
10
11
12
13
0x140008000  DriverEntry(IDA 识别的导出入口,仅 0x2C 字节)
├── call sub_14000802C ← 全局初始化(解密函数指针槽、初始化 SpinLock)
└── call sub_140003208 ← 真实 DriverEntry 实现

0x140003208 sub_140003208(真实初始化)
├── ExAllocatePool2(64, 472, 'ezaM') ← 分配 472 字节 MazeContext,Tag='MazeTag'
├── P = Pool2 ← P 是全局 MazeContext 指针
└── sub_1400018A0(Pool2) ← 注册 DriverObject/DeviceObject/符号链接

0x140001840 sub_140001840(DriverUnload)
├── ExFreePoolWithTag(P, 'MazeTag')
├── IoDeleteSymbolicLink(&SymbolicLinkName)
└── IoDeleteDevice(DeviceObject)

IOCTL Dispatch 链

1
2
3
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]
→ 0x140001540 sub_140001540(thunk,仅 5 字节)
└── jmp sub_1403172AB ← 混淆段真实 dispatch 入口(base+0x3172AB)

0x1403172AB 是关键混淆段入口,大量花指令包裹,但 IDA MCP 反编译可提取出清晰的 switch 结构(见第三节)。

动态解析函数指针槽(运行时 DriverEntry 写入)

1
2
3
4
// 静态值全为 0,运行时由 MmGetSystemRoutineAddress 填入
qword_140005080 ← PsGetThreadTeb
qword_140005090 ← ZwSetInformationObject
qword_140005098 ← KeReleaseSemaphore(或信号量对象结构指针)

分析时遇到 call qword_140005080 形式的间接调用,结合导入表和 WinDbg 运行时读槽值来确定真实目标。


三、IOCTL 协议逆向(sub_1403172AB)

发现路径

  1. IDA 导入表中有 IofCompleteRequest,xref 追到 sub_140001000(IRP 完成封装)
  2. sub_140001000 的调用者集中在 sub_1403172AB
  3. sub_1403172AB 反编译,IDA 提取出完整 switch-case:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// IDA 反编译(地址:0x1403172AB,size=0x52A)
switch (IoControlCode) { // v12 = IRP stack 参数中的 IOCTL code

case 0x80012004: // IOCTL_MAZE_MOVE
// 地址 0x140317488
if (v15 && InputBufferLength >= 0xC && OutputBufferLength >= 0x84) {
// 地址 0x1403174CF:checksum 验证
if (*(DWORD*)(v15+8) == ((BYTE)*v15 ^ HIDWORD(*(QWORD*)v15) ^ 0xDEAD1337)) {
// checksum OK → 递增步进计数器,调用信道分发
++MazeContext->StepCount; // MazeContext+0xBC
v22 = ROTL8(direction, 5) ^ 0x5A; // 内部方向编码
sub_140002161(MazeContext, v22); // ★ 信道分发函数
}
// 无论 checksum 是否通过,都填充输出缓冲区(LCG 噪声)
sub_1400038C0(v15, 0, 132); // 清零 132 字节
sub_140002038(v15); // 填 LCG 噪声(见下)
return_bytes = 132;
}
break;

case 0x80012008: // IOCTL_RESET_POS(重置位置)
// 地址 0x14031744C
sub_140001A7C(MazeContext, ...); // 清零 MazeContext 状态字段
sub_14031A53E(); // 重置全局步进计数器
return_bytes = 0; // ★ 无输出
break;

case 0x8001200C: // IOCTL_GET_INFO(获取迷宫信息)
// 地址 0x1403173DD
if (v15 && OutputBufferLength >= 0x18) {
// 直接写 6 个 DWORD 到输出缓冲
v15[0] = 13; // Width
v15[1] = 13; // Height (注:以 int64 写入,高32位=0)
v15[3] = 0; // EntryY
v15[4] = 12; // ExitX
v15[5] = 12; // ExitY
return_bytes = 24; // ★ 返回 24 字节
}
break;
}

关键发现

  • 0x80012008(RESET)无输出;0x8001200C(GET_INFO)返回24字节——与原始文档记录相反,以反编译代码为准。
  • 输出缓冲区的132字节全部是 LCG 噪声,不含任何信道信号(见下)。

LCG 噪声函数(sub_140002038)

1
2
3
4
5
6
7
8
9
10
11
// IDA 地址:0x140002038,size=0x3F
void sub_140002038(unsigned int *OutBuffer) {
// 种子 = KeTickCount(KUSER_SHARED_DATA+0x320) ^ 0xBAADF00D
DWORD seed = MEMORY[0xFFFFF78000000320] ^ 0xBAADF00D;
OutBuffer[0] = seed; // 前4字节 = 种子明文(可见但无信道价值)
BYTE *p = (BYTE*)OutBuffer + 1;
for (int i = 56; i > 0; i--) {
seed = 1103515245 * seed + 12345; // 标准 glibc LCG
*p++ = BYTE2(seed); // 取第2字节
}
}

含义:每次 IOCTL 响应的132字节输出是纯随机噪声,用于干扰基于输出差异的信道检测。真实信号通过完全独立的5条隐匿路径传递。

checksum 验证与 BSOD

1
2
3
4
5
6
7
8
9
IOCTL 输入(12字节):  [ direction(1B) | pad(3B) | token(4B) | checksum(4B) ]

验证逻辑(0x1403174CF):
expected = (uint8_t)direction ^ token ^ 0xDEAD1337
if checksum != expected → 不调用 sub_140002161

实际 BSOD 触发点在 0x1403174C6 附近的汇编路径中(混淆段,IDA 反编译未完整捕获):
cmp [r14+8], edx ; edx = expected checksum
jne → call KeBugCheckEx

自动化程序固定 token=0,则 checksum = direction ^ 0xDEAD1337,无需维护序列号。


四、信道分发机制(sub_140002161)

发现路径

IOCTL MAZE_MOVE 的 checksum 验证通过后调用 sub_140002161(MazeContext, v22)。该函数内部维护一个步进计数器,每次调用递增,模5循环,决定本轮激活哪个信道函数。

1
2
3
4
5
6
7
8
9
sub_140002161(地址 0x140002161):
├── 大量花指令(jmp+junk、pushfq/popfq、无效常量运算)
├── 核心:读取 MazeContext 内部计数器(StepCount mod 5)
└── 根据计数器路由到对应信道函数:
0 → sub_1400022B0 CH1: 命名事件
1 → sub_140319A37 CH2: 命名信号量
2 → sub_140316ADF CH3: TEB 直写
3 → sub_14031857E CH4: 句柄标志
4 → (CH5 QVM,待确认)

:此函数因混淆严重,IDA 反编译失败(Decompilation failed at 0x1400021b7),分发逻辑通过 WinDbg 断点在各信道函数入口逐一触发确认。


五、信道1:命名内核事件(CH1)

IDA 静态分析

发现路径:导入表有 ZwOpenEvent0x140004098)和 ZwSetEvent0x1400040D8),反查 xref → 唯一调用点在 sub_1400022B00x1400022B0,base+0x22B0)。

IDA 反编译(完整,0x1400022B0,size=0xD5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int sub_1400022B0(__int64 MazeContext, int n2)
{
const WCHAR *name;
UNICODE_STRING us;
OBJECT_ATTRIBUTES oa;
void *EventHandle;

// n2=0(成功)或 n2=2(出口)→ MazeMoveOK
// n2=1(碰墙) → MazeMoveWall
if (!n2 || n2 == 2) // 0x140002311
name = L"\\BaseNamedObjects\\MazeMoveOK"; // 0x140002313
else
name = L"\\BaseNamedObjects\\MazeMoveWall";

RtlInitUnicodeString(&us, name); // 0x14000231E
oa.Length = 48; // 0x140002328
oa.ObjectName = &us; // 0x140002332
oa.RootDirectory = NULL; // 0x14000233A
oa.Attributes = 576; // OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE // 0x140002347

EventHandle = NULL;
ZwOpenEvent(&EventHandle, EVENT_MODIFY_STATE, &oa); // 0x14000235F
if (NT_SUCCESS(result)) {
ZwSetEvent(EventHandle, NULL); // 0x14000236F(★ 信号发出)
ZwClose(EventHandle); // 0x140002379
}
}

关键观察

  • 对象名明文硬编码(无加密),IDA 直接可读。
  • ZwOpenEvent 而非 NtCreateEvent:驱动不创建对象,依赖用户层先创建,否则 ZwOpenEvent 失败,信道静默跳过。
  • Attributes=576OBJ_CASE_INSENSITIVE(0x40) | OBJ_KERNEL_HANDLE(0x200)

WinDbg 验证

1
2
3
4
5
6
bp 0xfffff80305c822B0       ; 断在信道函数入口
运行迷宫,第1步移动触发
→ r rdx ; 查看 n2 参数
→ dq rsp+0x20 L1 ; 确认 EventHandle 被填入
继续执行至 ZwSetEvent 返回
→ !object \BaseNamedObjects\MazeMoveOK ; 确认事件已触发

用户层检测原语

1
2
3
4
5
6
7
8
9
10
11
// 必须在 IOCTL 之前创建(手动重置型,初始未触发)
HANDLE hOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
HANDLE hWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");

// 每步前清除基线
ResetEvent(hOK);
ResetEvent(hWall);

// IOCTL 后采样
BOOL ev_ok = (WaitForSingleObject(hOK, 0) == WAIT_OBJECT_0);
BOOL ev_wall = (WaitForSingleObject(hWall, 0) == WAIT_OBJECT_0);

必须用手动重置(bManualReset=TRUE:自动重置型会在 WaitForSingleObject 返回时消费信号,后续步骤的基线判断会被污染。


六、信道2:命名内核信号量(CH2)

IDA 静态分析

发现路径:导入表有 ObReferenceObjectByName0x1400040E0)和 KeReleaseSemaphore0x140004018),反查 xref → 均指向 sub_140319A370x140319A37,base+0x319A37,位于混淆大段内)。

IDA 反编译(0x140319A37,size=0x2E4,含大量混淆残留):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__int64 sub_140319A37(char _CL, int n2)
{
WCHAR SourceString[76]; // 栈上明文缓冲区(解密输出)

// 混淆噪声:_BitScanForward64, rcl di cl 等(无实际作用)

if (qword_140005098 && *(_QWORD*)qword_140005098) { // 0x140319A7E
// 选择加密数据源
// n2=0 或 n2=2 → unk_140004160(Sem1 加密名)
// n2=1 → unk_1400041E0(Sem2 加密名)
_BYTE *src = (n2 == 0 || n2 == 2) ? &unk_140004160 // 0x140319B3D
: &unk_1400041E0;

// ★ XOR 解密循环(0x140319B68~0x140319B91)
// 57 次迭代,每次解密一个 WCHAR(2字节),key=0x4B
WCHAR *dst = SourceString;
for (int i = 57; i > 0; i--) {
*dst = *(WCHAR*)src ^ 0x4B; // XOR 0x4B 解密
dst++;
src++; // 实际步进由 v9 偏移控制
}

// 用解密出的名称打开信号量对象
RtlInitUnicodeString(&DestinationString_, SourceString); // 0x140319C07
ObReferenceObjectByName(&DestinationString_, 64, NULL); // 0x140319C76
// KeReleaseSemaphore 通过 qword_140005098 间接调用(CFG 保护)

memset(SourceString, 0, 0x72); // 清除栈上明文(防内存扫描) // 0x140319C93
}
}

加密数据定位与手工解密

加密数据存放于数据段:

  • unk_140004160:Sem1 名称密文(57 × WCHAR = 114字节)
  • unk_1400041E0:Sem2 名称密文(57 × WCHAR = 114字节)

解密方法(每字节 XOR 0x4B):

1
2
3
4
# 以 WCHAR 为单位,低字节 XOR 0x4B,高字节(通常为0)不变
# 解密结果:
Sem1: "Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}"
Sem2: "Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}"

为何用 XOR 加密:防止字符串扫描工具(如 Strings.exe)直接找到信号量名称,增加静态分析难度。memset 清除栈帧则防止运行时内存扫描。

WinDbg 验证

1
2
3
4
5
bp 0xfffff80305f99A37       ; 断在信道函数入口(运行时 = base+0x319A37)
触发第2步移动
→ p(单步至 RtlInitUnicodeString 调用后)
→ du rcx ; 读 UNICODE_STRING.Buffer,确认解密后的 GUID 字符串
→ p(至 ObReferenceObjectByName 返回后查 rax 是否成功)

用户层检测原语

1
2
3
4
5
6
7
8
// 先创建信号量(初始计数=0),驱动 KeReleaseSemaphore 后计数变为1
HANDLE hS1 = CreateSemaphoreW(NULL, 0, 16, L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
HANDLE hS2 = CreateSemaphoreW(NULL, 0, 16, L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}");

// 信道触发后采样(0超时,非阻塞)
BOOL s1 = (WaitForSingleObject(hS1, 0) == WAIT_OBJECT_0);
BOOL s2 = (WaitForSingleObject(hS2, 0) == WAIT_OBJECT_0);
// s1=TRUE 表示成功/出口,s2=TRUE 表示碰墙

注意WaitForSingleObject 在信号量上成功会自动将计数减1(消费),无需手动 Reset。但若信号量计数已经 > 0(前一步未消费),则采样结果会有误——每步后必须消费干净。


七、信道3:TEB LastError 直写(CH3)

IDA 静态分析

发现路径:导入表有 KeStackAttachProcess0x1400040B0)、ProbeForWrite0x140004058)、PsLookupProcessByProcessId0x1400040C0)、PsLookupThreadByThreadId0x1400040C8),四个 API 的 xref 均汇聚于 sub_140316ADF0x140316ADF,base+0x316ADF)。

IDA 反编译(0x140316ADF,size=0x295):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int sub_140316ADF(__int64 MazeContext, int n2)
{
// MazeContext+0x1D0 = 存储的 ThreadId(调用 IOCTL 的线程)
// MazeContext+0x1C8 = 存储的 ProcessId
void *ThreadId = *(void**)(MazeContext + 464); // 464=0x1D0 // 0x140316B32

if (ThreadId && *(QWORD*)(MazeContext + 456)) { // 456=0x1C8 // 0x140316B45/B4B
if (qword_140005080) { // = PsGetThreadTeb(运行时填入)
PETHREAD Thread = NULL;
PsLookupThreadByThreadId(ThreadId, &Thread); // 0x140316B82
if (NT_SUCCESS) {
PEPROCESS Process = NULL;
PsLookupProcessByProcessId(*(HANDLE*)(MazeContext+0x1C8), &Process); // 0x140316BC1

KeStackAttachProcess(Process, &ApcState); // 0x140316BD9(进入用户进程地址空间)

// qword_140005080 = PsGetThreadTeb(CFG 间接调用)
__int64 Teb = qword_140005080(Thread); // 0x140316BFD

// ★ 核心:TEB+0x68 = LastErrorValue(Win32 LastError)
int *Address = (int*)(Teb + 104); // 104=0x68 // 0x140316C11

// 根据移动结果写入不同魔数
int v10;
if (n2 == 0) v10 = -1059192831; // = 0xC0DE0001(成功)
else if (n2 == 2) v10 = -1059192830; // = 0xC0DE0002(出口)
else v10 = -1059192832; // = 0xC0DE0000(碰墙) // 0x140316CA8~CCD

ProbeForWrite(Address, 4, 4); // 0x140316CFF(安全性校验)
*Address = v10; // 0x140316D05(★ 写入)

KeUnstackDetachProcess(&ApcState); // 0x140316D11
ObfDereferenceObject(Process); // 0x140316D1C
}
ObfDereferenceObject(Thread);
}
}
}

关键常数推导

IDA 反编译中看到的有符号十进制值,换算为十六进制魔数:

IDA 显示值(有符号 int32) 十六进制 含义
-1059192831 0xC0DE0001 成功(n2=0)
-1059192830 0xC0DE0002 出口(n2=2)
-1059192832 0xC0DE0000 碰墙(n2=1)

换算方式:0xC0DE0001 = 32357744653235774465 - 4294967296 = -1059192831

TEB+0x68 = LastErrorValue 是 Windows TEB 的固定偏移,GetLastError() 的本质就是读取此偏移。驱动通过 PsGetThreadTeb 获取 TEB 指针后直接修改,完全绕过用户层 API 调用链。

MazeContext 结构(由此信道确认的字段)

1
2
3
4
5
6
7
8
// 从 sub_140316ADF 推导:
struct MazeContext {
// ...(前部字段待确认)
/* +0x1C8 */ HANDLE CallerProcessId; // PsGetCurrentProcessId() 存入
/* +0x1D0 */ HANDLE CallerThreadId; // PsGetCurrentThreadId() 存入
// ...
};
// MazeContext 大小:472字节(ExAllocatePool2 参数确认)

WinDbg 验证

1
2
3
4
5
6
7
bp 0xfffff80305f96ADF       ; 断在信道函数入口(base+0x316ADF)
触发第3步移动
→ dq @rcx+0x1C8 L1 ; 确认 ProcessId 被存储
→ dq @rcx+0x1D0 L1 ; 确认 ThreadId 被存储
→ p(单步至 *Address = v10 之后)
→ dd <Teb+0x68> ; 直接读 TEB 确认写入值
用户层同时执行 GetLastError(),观察到 0xC0DE0001 / 0xC0DE0000 / 0xC0DE0002

用户层检测原语

1
2
3
4
5
6
7
8
9
// IOCTL 前设置明确的哨兵值(区分"驱动未触发"和"返回0")
SetLastError(0xDEADBEEF);

// IOCTL 后立即采样
DWORD lerr = GetLastError();
if (lerr == 0xC0DE0001) // 成功
else if (lerr == 0xC0DE0000) // 碰墙
else if (lerr == 0xC0DE0002) // 出口
else if (lerr == 0xDEADBEEF) // 未触发(非本轮信道)

GetLastError() 在 IOCTL 后可能被 Win32 内部机制覆写,因此 DeviceIoControl 使用同步模式(无 OVERLAPPED),返回后立即读取,时间窗口极小。


八、信道4:TEB 句柄标志(CH4)

IDA 静态分析

发现路径:导入表有 ProbeForRead0x140004050)、已知 qword_140005080=PsGetThreadTebqword_140005090=ZwSetInformationObject(均动态解析)。在 sub_14031857E0x14031857E,base+0x31857E)找到这三者的组合使用。

IDA 反编译(0x14031857E,size=0x1F5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void sub_14031857E()
{
// qword_140005080 = PsGetThreadTeb(CFG 保护的间接调用)
if (qword_140005080) {
if (qword_140005090) { // = ZwSetInformationObject

// ★ 调用 PsGetThreadTeb(CurrentThread) → 获取调用者 TEB 地址
__int64 Teb = _guard_dispatch_icall_fptr(); // 0x1403185E4

if (Teb) {
// ★ TEB+0x1748(= 5960 = 0x1748)
_QWORD *v1 = (_QWORD*)(Teb + 5960); // 0x1403185EF

// 安全读取:确认该地址可读(在用户地址空间内)
ProbeForRead(v1, 8, 8); // 0x140318621

// ★ 若用户层在此槽放置了 handle,则调用 ZwSetInformationObject
if (*v1) { // 0x140318627
// qword_140005090 = ZwSetInformationObject(CFG 间接调用)
_guard_dispatch_icall_fptr(); // 0x140318750
// 效果:修改 *v1(句柄值)所对应的句柄表项的 Inherit 标志位
}
}
}
}
}

关键偏移推导

  • 5960 = 0x1748TEB+0x1748 在 Windows 10/11 x64 中是 Win32ClientInfo 扩展区域的槽位,非标准 TEB 字段,驱动借用此偏移传递句柄。
  • _guard_dispatch_icall_fptr():IDA 对 CFG 保护间接调用的标准表示,实际调用目标由 qword_14000508x 中的函数指针决定,运行时才可知。

驱动侧行为(WinDbg 确认)

驱动读取 TEB+0x1748 处的值(视为 HANDLE),然后:

1
2
3
4
5
6
ZwSetInformationObject(
*(TEB+0x1748), // 用户层放置的 handle
ObjectHandleFlagInformation, // class = 4
&{Inherit=1, ProtectFromClose=0},
sizeof(OBJECT_HANDLE_FLAG_INFORMATION) // = 2
)

效果:将该 handle 的 HANDLE_FLAG_INHERIT 位置1,用户层通过 GetHandleInformation 可观察到变化。

用户层检测原语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 步骤1:创建一个普通 event 作为"靶子句柄"
HANDLE hFlagProbe = CreateEventW(NULL, FALSE, FALSE, NULL);
// 步骤2:清除初始标志(建立基线:flags=0)
SetHandleInformation(hFlagProbe, HANDLE_FLAG_INHERIT | HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
// 步骤3:将 handle 值放入本线程 TEB+0x1748
BYTE *teb = (BYTE*)NtCurrentTeb();
*(HANDLE*)(teb + 0x1748) = hFlagProbe;

// --- IOCTL 前:每步重置基线 ---
SetHandleInformation(hFlagProbe, HANDLE_FLAG_INHERIT | HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);

// --- IOCTL 后:采样 ---
DWORD hflags = 0;
GetHandleInformation(hFlagProbe, &hflags);
BOOL hflag_changed = (hflags != 0); // INHERIT 位被置1 = 信道触发

为何选 HANDLE_FLAG_INHERITZwSetInformationObjectObjectHandleFlagInformation 只控制两个位(Inherit 和 ProtectFromClose),初始清零后任何变化都说明驱动修改了句柄属性。

使用后清理

1
2
// 探测完毕后清除 TEB 槽,防止后续步骤干扰
*(HANDLE*)(teb + 0x1748) = NULL;

九、四信道对比汇总

维度 CH1:命名事件 CH2:命名信号量 CH3:TEB 直写 CH4:句柄标志
IDA 偏移 base+0x22B0 base+0x319A37 base+0x316ADF base+0x31857E
核心内核 API ZwOpenEvent + ZwSetEvent ObReferenceObjectByName + KeReleaseSemaphore KeStackAttachProcess + PsGetThreadTeb + ProbeForWrite PsGetThreadTeb + ZwSetInformationObject
对象名来源 明文硬编码 XOR 0x4B 加密,运行时解密后清零 无对象名(直接写内存) 无对象名(用户层预置 handle)
信号传递介质 内核事件对象状态 信号量计数 TEB+0x68 LastErrorValue 句柄表 Inherit 标志位
用户层检测 API WaitForSingleObject(hEvent, 0) WaitForSingleObject(hSem, 0) GetLastError() GetHandleInformation()
用户层前置操作 CreateEvent(手动重置) + ResetEvent CreateSemaphore(初始0) SetLastError(哨兵值) 预置 handle + SetHandleInformation 清零
静态分析难度 ★☆☆☆(明文,逻辑清晰) ★★★☆(XOR加密+混淆残留+CFG间接调用) ★★☆☆(偏移推导+CFG间接调用) ★★★★(全CFG+动态槽+TEB非标偏移)
WinDbg 验证方式 断点确认 ZwSetEvent 调用 断点确认解密字符串内容 断点后读 TEB+0x68 断点后查 handle 标志位

十、Reset 后轮转顺序与步进计数器

题目提示:“after each reset, the first five successful moves reveal each flaw exactly once, in a fixed order”。

  • IOCTL_RESET_POS0x80012008)调用 sub_14031A53E() 重置全局步进计数器归零
  • 每次 checksum 验证通过的 MAZE_MOVE 调用,计数器 +1,传入 sub_140002161
  • 计数器 mod 5 决定激活信道,顺序固定:
1
2
3
4
5
6
7
Reset → 计数器=0
第1次合法移动 → 计数器=1 → CH1(事件)
第2次合法移动 → 计数器=2 → CH2(信号量)
第3次合法移动 → 计数器=3 → CH3(TEB直写)
第4次合法移动 → 计数器=4 → CH4(句柄标志)
第5次合法移动 → 计数器=5 → CH5(QVM,待确认)
第6次合法移动 → 计数器=0(循环)→ CH1

“合法移动”:checksum 验证通过的移动,无论是否实际移动(碰墙也算步进计数)

信道五呢:猜测是ZwQueryVirtualMemory,没了,做不动。

完整检测和自动化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
* channel_probe.c — ShadowGate 信道探测
*
* 编译: gcc channel_probe.c -o channel_probe.exe -lkernel32
*
* Reset 后前5次成功移动,顺序固定地各激活一个隐匿信道:
* 轮1: EVT — ZwSetEvent(MazeMoveOK)
* 检测: WaitForSingleObject(event, 0)
* 轮2: SEM — KeReleaseSemaphore(Sem1)
* 检测: WaitForSingleObject(sem, 0)
* 轮3: WRITE — PsGetThreadTeb → TEB+0x68 = LastError
* 检测: GetLastError() == 0xC0DE0001
* 轮4: HFLAG — ZwSetInformationObject(*(TEB+0x1748), ObjectHandleFlagInformation, ...)
* 检测: 在 TEB+0x1748 放真实 handle,IOCTL 后查 GetHandleInformation
* 轮5: QVM — ZwQueryVirtualMemory(机制待确认)
*/

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

/* ───── 常量 ──────────────────────────────────────────────────────── */
#define IOCTL_MAZE_MOVE 0x80012004
#define IOCTL_RESET_POS 0x80012008
#define IOCTL_GET_INFO 0x8001200C
#define CHECKSUM_MAGIC 0xDEAD1337
#define WIN_MAGIC 0x57494E21

/* WRITE 信道魔数 */
#define CODE_SUCCESS 0xC0DE0001u
#define CODE_WALL 0xC0DE0000u
#define CODE_EXIT 0xC0DE0002u

/* HFLAG 信道:driver 从 TEB+0x1748 读出 handle,对该 handle 调 ZwSetInformationObject */
#define TEB_HANDLE_OFFSET 0x1748

/* ───── 方向码 ──────────────────────────────────────────────────── */
#define DIR_W 0x52
#define DIR_A 0x53
#define DIR_S 0xD3
#define DIR_D 0xD0

/* ───── 命名对象 ────────────────────────────────────────────────── */
#define EVT_OK L"Global\\MazeMoveOK"
#define EVT_WALL L"Global\\MazeMoveWall"
#define SEM1 L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}"
#define SEM2 L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}"

/* ───── 最短路径(32步)──────────────────────────────────────────── */
static const uint8_t PATH[32] = {
DIR_D,DIR_D,DIR_D,DIR_D,DIR_D,DIR_D,
DIR_S,DIR_S,
DIR_D,DIR_D,DIR_D,DIR_D,
DIR_W,DIR_W,
DIR_D,DIR_D,
DIR_S,DIR_S,DIR_S,DIR_S,
DIR_S,DIR_S,DIR_S,DIR_S,
DIR_A,DIR_A,
DIR_S,DIR_S,DIR_S,DIR_S,
DIR_D,DIR_D
};

/* ───── 发送单步移动 ──────────────────────────────────────────── */
static BOOL send_move(HANDLE hDev, uint8_t dir,
uint8_t out[132], uint64_t *us_out)
{
uint8_t in[12] = {0};
in[0] = dir;
*(uint32_t*)(in+4) = 0;
*(uint32_t*)(in+8) = (uint32_t)dir ^ CHECKSUM_MAGIC;
DWORD br;
LARGE_INTEGER t1, t2, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&t1);
BOOL ok = DeviceIoControl(hDev, IOCTL_MAZE_MOVE,
in, 12, out, 132, &br, NULL);
QueryPerformanceCounter(&t2);
if (us_out)
*us_out = (uint64_t)(t2.QuadPart - t1.QuadPart)
* 1000000ULL / (uint64_t)freq.QuadPart;
return ok;
}

static const char *dir_name(uint8_t d) {
switch(d) {
case DIR_D: return "D(R)"; case DIR_W: return "W(U)";
case DIR_S: return "S(D)"; case DIR_A: return "A(L)";
default: return "?";
}
}

/* ───── 主程序 ──────────────────────────────────────────────── */
int main(void)
{
/* 1. 创建命名对象 */
HANDLE hOK = CreateEventW(NULL, TRUE, FALSE, EVT_OK);
HANDLE hWall = CreateEventW(NULL, TRUE, FALSE, EVT_WALL);
HANDLE hS1 = CreateSemaphoreW(NULL, 0, 16, SEM1);
HANDLE hS2 = CreateSemaphoreW(NULL, 0, 16, SEM2);
if (!hOK || !hWall || !hS1 || !hS2) {
printf("[!] Named objects failed (err=%lu)\n", GetLastError());
return 1;
}

/* 2. CH4 信道:创建一个普通 event 作为 handle 放到 TEB+0x1748
* driver 读出此 handle,调 ZwSetInformationObject 修改其 INHERIT 标志 */
HANDLE hFlagProbe = CreateEventW(NULL, FALSE, FALSE, NULL);
if (!hFlagProbe) {
printf("[!] CreateEvent for flag probe failed (err=%lu)\n", GetLastError());
return 1;
}
/* 清除 INHERIT 标志作为初始状态 */
SetHandleInformation(hFlagProbe, HANDLE_FLAG_INHERIT, 0);
/* 把 handle 值放到 TEB+0x1748 */
{
BYTE *teb = (BYTE*)NtCurrentTeb();
*(HANDLE*)(teb + TEB_HANDLE_OFFSET) = hFlagProbe;
}
printf("[+] HFLAG probe: handle=0x%p installed at TEB+0x%X\n",
hFlagProbe, TEB_HANDLE_OFFSET);

/* 3. 打开设备 */
HANDLE hDev = CreateFileW(L"\\\\.\\ShadowGate",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hDev == INVALID_HANDLE_VALUE) {
printf("[!] Cannot open device (err=%lu)\n", GetLastError());
return 1;
}

/* 4. 获取迷宫信息 + 重置位置 */
uint8_t info[24] = {0};
DWORD br;
DeviceIoControl(hDev, IOCTL_GET_INFO, NULL, 0, info, 24, &br, NULL);
printf("[+] Maze %ux%u Exit=(%u,%u)\n",
*(uint32_t*)info, *(uint32_t*)(info+4),
*(uint32_t*)(info+16), *(uint32_t*)(info+20));
DeviceIoControl(hDev, IOCTL_RESET_POS, NULL, 0, NULL, 0, &br, NULL);
printf("[+] Position reset\n\n");

printf("Step | Dir | Time(us) | CH1:EVT | CH2:SEM | CH3:WRITE(LastErr) | CH4:HFLAG | Detected\n");
printf("-----|------|----------|---------|---------|---------------------|----------------|----------\n");

for (int i = 0; i < 5; i++) {
/* 每步前清状态 */
ResetEvent(hOK);
ResetEvent(hWall);
SetLastError(0xDEADBEEF);
/* CH4: 每步前清除所有 handle 标志,建立基线(driver 可能设 INHERIT 或 PROTECT_FROM_CLOSE)*/
SetHandleInformation(hFlagProbe,
HANDLE_FLAG_INHERIT | HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
/* CH5: 保存基线值 */
BYTE *teb5_pre = (BYTE*)NtCurrentTeb();
DWORD page404000_before = *(volatile DWORD*)0x404000;

uint8_t out[132] = {0};
uint64_t us = 0;
send_move(hDev, PATH[i], out, &us);

/* --- 信道1: 命名事件 --- */
BOOL ev_ok = (WaitForSingleObject(hOK, 0) == WAIT_OBJECT_0);
BOOL ev_wall = (WaitForSingleObject(hWall, 0) == WAIT_OBJECT_0);

/* --- 信道2: 命名信号量 --- */
BOOL s1 = (WaitForSingleObject(hS1, 0) == WAIT_OBJECT_0);
BOOL s2 = (WaitForSingleObject(hS2, 0) == WAIT_OBJECT_0);

/* --- 信道3: WRITE → LastErrorValue (TEB+0x68) --- */
DWORD lerr = GetLastError();
char write_buf[24];
if (lerr == CODE_SUCCESS) strcpy(write_buf, "0xC0DE0001 OK ");
else if (lerr == CODE_WALL) strcpy(write_buf, "0xC0DE0000 WALL");
else if (lerr == CODE_EXIT) strcpy(write_buf, "0xC0DE0002 EXIT");
else if (lerr == 0xDEADBEEF) strcpy(write_buf, "(no change) ");
else sprintf(write_buf, "0x%08X ", lerr);

/* --- 信道4: HFLAG → GetHandleInformation 检查 INHERIT 标志 ---
* ZwSetInformationObject(hFlagProbe, ObjectHandleFlagInformation, &flags, 2)
* 驱动根据移动结果修改 Inherit 位,用户层查询验证 */
DWORD hflags = 0;
GetHandleInformation(hFlagProbe, &hflags);
BOOL hflag_changed = (hflags != 0); /* 任何标志位被设置 = 信道触发 */
char hflag_buf[24];
if (hflags & HANDLE_FLAG_PROTECT_FROM_CLOSE)
sprintf(hflag_buf, "flags=0x%02X PROT!", (unsigned)hflags);
else if (hflags & HANDLE_FLAG_INHERIT)
sprintf(hflag_buf, "flags=0x%02X INHERIT!", (unsigned)hflags);
else
sprintf(hflag_buf, "flags=0x%02X no change", (unsigned)hflags);

/* --- 信道5: QVM —— 监测 0x404000 内容 + TEB 附近槽位变化 ---
* 驱动: ZwQueryVirtualMemory(NtCurrentProcess(), 0x404000, MBI, ...)
* 假设: 驱动从 TEB+0x1750/0x1760/... 读用户提供的地址,QVM 验证后写结果 */
BYTE *teb5 = (BYTE*)NtCurrentTeb();
ULONGLONG slot1750_after = *(ULONGLONG*)(teb5 + 0x1750);
ULONGLONG slot1758_after = *(ULONGLONG*)(teb5 + 0x1758);
ULONGLONG slot1760_after = *(ULONGLONG*)(teb5 + 0x1760);
DWORD page404000_after = *(volatile DWORD*)0x404000;
char qvm_buf[48] = "(no change)";
if (slot1750_after != 0 || slot1758_after != 0 || slot1760_after != 0) {
sprintf(qvm_buf, "TEB+0x1750=%llX +58=%llX +60=%llX",
slot1750_after, slot1758_after, slot1760_after);
}

/* --- 推断激活信道 --- */
const char *detected = "CH5:QVM(?)";
if (ev_ok || ev_wall) detected = "CH1:EVT";
else if (s1 || s2) detected = "CH2:SEM";
else if (lerr == CODE_SUCCESS || lerr == CODE_WALL || lerr == CODE_EXIT)
detected = "CH3:WRITE";
else if (hflag_changed) detected = "CH4:HFLAG";

char evt_str[8], sem_str[8];
if (ev_ok) strcpy(evt_str, "OK ");
else if (ev_wall) strcpy(evt_str, "WALL");
else strcpy(evt_str, "----");
if (s1) strcpy(sem_str, "Sem1");
else if (s2) strcpy(sem_str, "Sem2");
else strcpy(sem_str, "----");

printf(" %d | %s | %8llu | %-7s | %-7s | %-19s | %-14s | %s\n",
i+1, dir_name(PATH[i]), us,
evt_str, sem_str, write_buf, hflag_buf, detected);
}

/* 清理 TEB handle 槽 */
{
BYTE *teb = (BYTE*)NtCurrentTeb();
*(HANDLE*)(teb + TEB_HANDLE_OFFSET) = NULL;
}

/* 走完剩余路径,拿 flag */
printf("\n[*] Completing maze (%d remaining moves)...\n", 32 - 5);
for (int i = 5; i < 32; i++) {
uint8_t out[132] = {0};
send_move(hDev, PATH[i], out, NULL);
uint32_t magic;
memcpy(&magic, out + 0x3C, 4);
if (magic == WIN_MAGIC) {
uint32_t clen;
memcpy(&clen, out + 0x80, 4);
if (clen > 0 && clen <= 63) {
char cred[64] = {0};
memcpy(cred, out + 0x40, clen);
printf("[WIN] %s\n", cred);
}
break;
}
}

CloseHandle(hFlagProbe);
CloseHandle(hDev);
CloseHandle(hOK); CloseHandle(hWall);
CloseHandle(hS1); CloseHandle(hS2);
return 0;
}