混淆没解全靠猜
真解不动混淆了。
拿地图信息
第一步:找驱动基址(绕过 DKOM 摘链)
驱动摘掉了自己的链表(lm m ShadowGateSys 返回空),所以不能用常规模块枚举找基址,改走设备对象反查:
1 | 1: kd> !object \Device\ShadowGate |
获取移动码和checksum等:
1 | 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" |
拿地图信息:
1 | 1: kd> r @$t1 = poi(fffff80305c850b8) |
写bfs求最短路径:
1 | #!/usr/bin/env python3 |
结果:
1 | python .\maze_solve.py .\maze.txt |
成功拿到并解密flag:
1 | ============================================= |
ShadowGate — 隐匿信道逆向分析过程(4信道详解)
分析对象:
ShadowGateSys.sys
IDA 静态基址:0x140000000
运行时基址:0xfffff80305c80000(WinDbg 动态确认)
分析工具:IDA Pro(MCP 接入)、WinDbg 内核调试、手工 XOR 解密
一、工具链与分析方法论
本次逆向采用"静态优先、动态验证"的三段式方法:
1 | IDA Pro 静态分析 |
混淆手段汇总(影响分析路径的关键干扰)
| 手段 | 位置 | 影响 |
|---|---|---|
jmp $+5(E9 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 | 0x140008000 DriverEntry(IDA 识别的导出入口,仅 0x2C 字节) |
IOCTL Dispatch 链
1 | DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] |
0x1403172AB 是关键混淆段入口,大量花指令包裹,但 IDA MCP 反编译可提取出清晰的 switch 结构(见第三节)。
动态解析函数指针槽(运行时 DriverEntry 写入)
1 | // 静态值全为 0,运行时由 MmGetSystemRoutineAddress 填入 |
分析时遇到 call qword_140005080 形式的间接调用,结合导入表和 WinDbg 运行时读槽值来确定真实目标。
三、IOCTL 协议逆向(sub_1403172AB)
发现路径
- IDA 导入表中有
IofCompleteRequest,xref 追到sub_140001000(IRP 完成封装) sub_140001000的调用者集中在sub_1403172AB- 对
sub_1403172AB反编译,IDA 提取出完整 switch-case:
1 | // IDA 反编译(地址:0x1403172AB,size=0x52A) |
关键发现:
0x80012008(RESET)无输出;0x8001200C(GET_INFO)返回24字节——与原始文档记录相反,以反编译代码为准。- 输出缓冲区的132字节全部是 LCG 噪声,不含任何信道信号(见下)。
LCG 噪声函数(sub_140002038)
1 | // IDA 地址:0x140002038,size=0x3F |
含义:每次 IOCTL 响应的132字节输出是纯随机噪声,用于干扰基于输出差异的信道检测。真实信号通过完全独立的5条隐匿路径传递。
checksum 验证与 BSOD
1 | IOCTL 输入(12字节): [ direction(1B) | pad(3B) | token(4B) | checksum(4B) ] |
自动化程序固定 token=0,则 checksum = direction ^ 0xDEAD1337,无需维护序列号。
四、信道分发机制(sub_140002161)
发现路径
IOCTL MAZE_MOVE 的 checksum 验证通过后调用 sub_140002161(MazeContext, v22)。该函数内部维护一个步进计数器,每次调用递增,模5循环,决定本轮激活哪个信道函数。
1 | sub_140002161(地址 0x140002161): |
注:此函数因混淆严重,IDA 反编译失败(Decompilation failed at 0x1400021b7),分发逻辑通过 WinDbg 断点在各信道函数入口逐一触发确认。
五、信道1:命名内核事件(CH1)
IDA 静态分析
发现路径:导入表有 ZwOpenEvent(0x140004098)和 ZwSetEvent(0x1400040D8),反查 xref → 唯一调用点在 sub_1400022B0(0x1400022B0,base+0x22B0)。
IDA 反编译(完整,0x1400022B0,size=0xD5):
1 | int sub_1400022B0(__int64 MazeContext, int n2) |
关键观察:
- 对象名明文硬编码(无加密),IDA 直接可读。
ZwOpenEvent而非NtCreateEvent:驱动不创建对象,依赖用户层先创建,否则ZwOpenEvent失败,信道静默跳过。Attributes=576:OBJ_CASE_INSENSITIVE(0x40) | OBJ_KERNEL_HANDLE(0x200)。
WinDbg 验证
1 | bp 0xfffff80305c822B0 ; 断在信道函数入口 |
用户层检测原语
1 | // 必须在 IOCTL 之前创建(手动重置型,初始未触发) |
必须用手动重置(bManualReset=TRUE):自动重置型会在 WaitForSingleObject 返回时消费信号,后续步骤的基线判断会被污染。
六、信道2:命名内核信号量(CH2)
IDA 静态分析
发现路径:导入表有 ObReferenceObjectByName(0x1400040E0)和 KeReleaseSemaphore(0x140004018),反查 xref → 均指向 sub_140319A37(0x140319A37,base+0x319A37,位于混淆大段内)。
IDA 反编译(0x140319A37,size=0x2E4,含大量混淆残留):
1 | __int64 sub_140319A37(char _CL, int n2) |
加密数据定位与手工解密
加密数据存放于数据段:
unk_140004160:Sem1 名称密文(57 × WCHAR = 114字节)unk_1400041E0:Sem2 名称密文(57 × WCHAR = 114字节)
解密方法(每字节 XOR 0x4B):
1 | # 以 WCHAR 为单位,低字节 XOR 0x4B,高字节(通常为0)不变 |
为何用 XOR 加密:防止字符串扫描工具(如 Strings.exe)直接找到信号量名称,增加静态分析难度。memset 清除栈帧则防止运行时内存扫描。
WinDbg 验证
1 | bp 0xfffff80305f99A37 ; 断在信道函数入口(运行时 = base+0x319A37) |
用户层检测原语
1 | // 先创建信号量(初始计数=0),驱动 KeReleaseSemaphore 后计数变为1 |
注意:WaitForSingleObject 在信号量上成功会自动将计数减1(消费),无需手动 Reset。但若信号量计数已经 > 0(前一步未消费),则采样结果会有误——每步后必须消费干净。
七、信道3:TEB LastError 直写(CH3)
IDA 静态分析
发现路径:导入表有 KeStackAttachProcess(0x1400040B0)、ProbeForWrite(0x140004058)、PsLookupProcessByProcessId(0x1400040C0)、PsLookupThreadByThreadId(0x1400040C8),四个 API 的 xref 均汇聚于 sub_140316ADF(0x140316ADF,base+0x316ADF)。
IDA 反编译(0x140316ADF,size=0x295):
1 | int sub_140316ADF(__int64 MazeContext, int n2) |
关键常数推导
IDA 反编译中看到的有符号十进制值,换算为十六进制魔数:
| IDA 显示值(有符号 int32) | 十六进制 | 含义 |
|---|---|---|
-1059192831 |
0xC0DE0001 |
成功(n2=0) |
-1059192830 |
0xC0DE0002 |
出口(n2=2) |
-1059192832 |
0xC0DE0000 |
碰墙(n2=1) |
换算方式:0xC0DE0001 = 3235774465,3235774465 - 4294967296 = -1059192831。
TEB+0x68 = LastErrorValue 是 Windows TEB 的固定偏移,GetLastError() 的本质就是读取此偏移。驱动通过 PsGetThreadTeb 获取 TEB 指针后直接修改,完全绕过用户层 API 调用链。
MazeContext 结构(由此信道确认的字段)
1 | // 从 sub_140316ADF 推导: |
WinDbg 验证
1 | bp 0xfffff80305f96ADF ; 断在信道函数入口(base+0x316ADF) |
用户层检测原语
1 | // IOCTL 前设置明确的哨兵值(区分"驱动未触发"和"返回0") |
GetLastError() 在 IOCTL 后可能被 Win32 内部机制覆写,因此 DeviceIoControl 使用同步模式(无 OVERLAPPED),返回后立即读取,时间窗口极小。
八、信道4:TEB 句柄标志(CH4)
IDA 静态分析
发现路径:导入表有 ProbeForRead(0x140004050)、已知 qword_140005080=PsGetThreadTeb、qword_140005090=ZwSetInformationObject(均动态解析)。在 sub_14031857E(0x14031857E,base+0x31857E)找到这三者的组合使用。
IDA 反编译(0x14031857E,size=0x1F5):
1 | void sub_14031857E() |
关键偏移推导
5960 = 0x1748:TEB+0x1748在 Windows 10/11 x64 中是Win32ClientInfo扩展区域的槽位,非标准 TEB 字段,驱动借用此偏移传递句柄。_guard_dispatch_icall_fptr():IDA 对 CFG 保护间接调用的标准表示,实际调用目标由qword_14000508x中的函数指针决定,运行时才可知。
驱动侧行为(WinDbg 确认)
驱动读取 TEB+0x1748 处的值(视为 HANDLE),然后:
1 | ZwSetInformationObject( |
效果:将该 handle 的 HANDLE_FLAG_INHERIT 位置1,用户层通过 GetHandleInformation 可观察到变化。
用户层检测原语
1 | // 步骤1:创建一个普通 event 作为"靶子句柄" |
为何选 HANDLE_FLAG_INHERIT:ZwSetInformationObject 的 ObjectHandleFlagInformation 只控制两个位(Inherit 和 ProtectFromClose),初始清零后任何变化都说明驱动修改了句柄属性。
使用后清理:
1 | // 探测完毕后清除 TEB 槽,防止后续步骤干扰 |
九、四信道对比汇总
| 维度 | 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_POS(0x80012008)调用sub_14031A53E()重置全局步进计数器归零- 每次 checksum 验证通过的 MAZE_MOVE 调用,计数器 +1,传入
sub_140002161 - 计数器 mod 5 决定激活信道,顺序固定:
1 | Reset → 计数器=0 |
“合法移动”:checksum 验证通过的移动,无论是否实际移动(碰墙也算步进计数)
信道五呢:猜测是ZwQueryVirtualMemory,没了,做不动。
完整检测和自动化:
1 | /* |