文章总结: 本文解析了第五届鹏城杯Reverse-MDriver驱动逆向赛题。作者通过VMP脱壳与IDA分析,还原了驱动基于WFP的ICMP流量监听机制,并详解了利用MSRLSTAR寄存器与系统路径生成的双密钥校验逻辑。文章包含反调试绕过方法及最终生成Payload并发送ICMP包获取flag的完整利用代码,实战指导性强。
综合评分: 93
文章分类: CTF,二进制安全,逆向分析,实战经验
第五届“鹏城杯”初赛 Reverse-MDriver-赛题解析
relost
看雪学苑
2026年1月8日 18:05
上海
驱动分析
驱动被vmp保护,因此首先需要dump驱动以完成脱壳,对KeRevertToUserAffinityThread下断点后dump驱动程序并分别修复SectionAlignment,SizeOfHeaders,PointerToRawData以便其能被ida识别。
入口点逻辑
脱完壳后首先定位 security_init_cookie找真正的驱动入口点
这里直接用特征码48 8B 05 ?? ?? ?? ?? 48 85 C0 74 1B 48
由此可以得到驱动真实入口点逻辑,我已经恢复部分导入
_I@vmravY$@vmrav$mjmp$wqggaww%经xor爆破发现是驱动成功加载的调试信息
*(_QWORD *)(RCX + 104) = FwpmFilterDeleteById0_0;RCX 是DRIVER_OBJECT指针。偏移104(0x68) 在 x64DRIVER_OBJECT结构中对应DriverUnload域。它将FwpmFilterDeleteById0_0注册为卸载函数。当驱动停止时,这个函数会被调用以清理 WFP 过滤器。
因此必定存在初始化wfp的环境,即函数sub_FFFFF80541012AA0,接下来主要分析这个函数。
Wfp初始化
可以看到以下都是我恢复符号后的版本:
FwpmEngineOpen0_0: 建立与过滤引擎的会话。
FwpmSubLayerAdd0_0: 创建一个新的子层 (SubLayer),权重设为 64。所有的过滤器都将在这个子层中运行,这有助于控制过滤器的优先级。
sub_FFFFF80541012C90: 调用封装好的辅助函数来注册 Callout 并添加过滤器。
参数传递了层 GUID (&a1)、Callout GUID (&unk_...141C0) 以及回调函数sub_FFFFF80541012440。
FwpmTransactionCommit0_0: 如果一切顺利,提交事务,使过滤规则生效
分析得出sub_FFFFF80541012C90将回调函数注册到内核,并绑定到特定的过滤层
回调函数分析
根据前面的分析 我们知道了sub_FFFFF80541012440是wfp注册的回调函数,用来对流量做处理。
搜索资料得到其函数原型 为 标准WFPClassifyFn
voidClassifyFn(
const FWPS_INCOMING_VALUES0 *inFixedValues, // a1
const FWPS_INCOMING_METADATA_VALUES0 *inMetaValues, // a2
void *layerData, // a3
constvoid *classifyContext, // a4
const FWPS_FILTER1 *filter, // a5
UINT64 flowContext, // a6 (IDA识别为 _DWORD* 有误)
FWPS_CLASSIFY_OUT0 *classifyOut // a7
);
根据layerid的过滤可以确定监听范围为layrid=52
即 ALE_FLOW_ESTABLISHED_V4 当一个新的网络流(Flow)建立时触发
inFixedValues->incomingValue[5].value.uint8是过滤协议的操作。
第五个是protocol所以检查 v9[2] == 1 是过滤icmp的操作,所以此回调函数响应的是ICMP包。
ICMP的Type=8 对应驱动检查 ptr == 8,意味着ICMP包type需要=8
之后解密字符串,打印调试信息并进入最终的工作线程sub_FFFFF805410118A0
校验线程分析
效验线程内执行了四个用于效验的函数,最后执行资源清理工作。
sub_FFFFF80541011120取出发送方的pid备用
sub_FFFFF80541011DC0实际上为key1的生成逻辑 ,其生成一个key v10 来和传入的明文(a1)异或进行加密。
key1的生成逻辑如下:
读取 GS 段寄存器 偏移 0x50 处的值。
与常数 0x020B0000 异或。
循环右移 (ROR) 18 位,计算出 MSR 索引。
最后rdmsr 0xC0000082拿到地址,这个寄存器存放的是系统调用入口函数KiSystemCall64的内存地址,所以key就是开头的几个字节。
1: kd> rdmsr C0000082
msr[c0000082] = fffff801`0c211900
1: kd> db fffff801`0c211900 l4
fffff801`0c211900 0f 01 f8 65(key1!!)
sub_FFFFF80541011750调用sub_FFFFF80541011C10生成key,做进一步处理后与明文异或。
sub_FFFFF80541011C10实际上为key2的生成逻辑
key2的生成逻辑如下:
根据当前irql去计算一个地址(计算地址的脚本就不放了,irql=0),从中取出字符串最终取出的是 L”C:\Windows”,结合前面的部分可以写脚本生成key2并取前0x14字节。
def generate_v4_key():
# 1. 准备种子字符串 "C:\WINDOWS" (宽字符, 大写)
# 来源于之前的分析:KUSER_SHARED_DATA.NtSystemRoot
seed_str = "C:\\WINDOWS"
seed_bytes = seed_str.encode('utf-16le')
seed_bytes = seed_bytes[:0x14]
print(f"[*] 种子数据 (Hex): {seed_bytes.hex().upper()}")
v4 = bytearray(256)
for k in range(len(seed_bytes)):
v4[k] = seed_bytes[k]
print("[*] 开始计算密钥流 v4...")
for i in range(256):
not_i = (~i) & 0xFF
v4[i] ^= not_i
# 5. 输出结果
print("-" * 30)
print(f"[+] 最终密钥 v4 (256 bytes) 生成完成。")
print("-" * 30)
print("前 40 字节预览 (Hex):")
print(v4[:40].hex(' ').upper())
print("\n[C 语言数组格式]:")
c_array = ', '.join([f'0x{b:02X}' for b in v4])
print(f"unsigned char v4[256] = {{ {c_array} }};")
return v4
if __name__ == "__main__":
key_stream = generate_v4_key()
工作线程的第四次函数调用进入了最终效验环节,将加密后的输入和密文比对。
如果成功就将flag做一个异或(虚拟化了),申请一块内存写入到发送包的进程中并打印成功提示(用了 sub_FFFFF80541011120取出的pid)
驱动分析总结
驱动注册了一个 WFP Callout (sub_FFFFF80541012C90),并在ClassifyFn(sub_FFFFF80541012440) 中处理网络包。
代码检查LayerId=52( ALE_FLOW_ESTABLISHED_V4)。
检查incomingValue[5] == 1,对应IProtocol == ICMP。
驱动读取 Payload 数据,进入密钥校验逻辑。
key1: 通过__readgsbyte和 XOR 运算算出 MSR 索引,最终读取LSTAR(0xC0000082)寄存器指向的地址。LSTAR指向 KiSystemCall64,这是系统调用的入口。读取该地址的前 4 字节机器码。标准 Windows 环境下为0F 01 F8 65(swapgs; mov gs:[10], rsp)。
key2: 读取KUSER_SHARED_DATA的NtSystemRoot字段(即C:\WINDOWS),转换为大写宽字符。然后进行Key2[i] = SeedBytes[i] ^ (~i)。
将输入与key1key2异或后与密文比对
当 Payload 校验通过后,驱动执行以下操作:
PsLookupProcessByProcessId获取 EPROCESS。
KeStackAttachProcess挂靠到发送者进程空间。
NtAllocateVirtualMemory在发送者进程中申请 4KB RW内存。
将黑箱运算后的 flag 写入该内存。
DbgPrint输出成功信息。
#
反调试分析
分析完驱动后已经注意到有几处反调试,尝试绕过以便在虚拟机成功加载驱动,打印
[MDriver] Driver init success! 即视为加载成功。
反调试1
在初始化回调函数的末尾被调用
使用dpc广播检查内核 KdDebuggerEnabled 和KdDebuggerNotPresent 标志位,并再次检查KUSER_SHARED_DATA+2d4
检测到调试器则0xdeaddb9蓝屏
反调试2
此反调试位于key2的生成逻辑中
正常程序运行在 PASSIVE_LEVEL (0)计算出来的地址是正确的,能成功取出字符串。
一旦windbgDbgBreakPoint触发 则会提升到 DISPATCH_LEVEL 导致地址计算失败,0x9999999蓝屏。这避免了被动态调试出地址计算流程(实际上并不影响动调获取key2)。
绕过方法
没什么好说的 base+10A0 直接ret就行,反调试2完全不用管,我们已经分析完了驱动逻辑得到了2个key,接下来加载驱动发包即可。
ret反调试后成功加载驱动
EXP
已经在上文静态分析得到了key1和key2,写个脚本异或就行。
def solve_payload():
# 1. 密文 (v41) - 20 字节
cipher = [
0xD4, 0x90, 0x60, 0xED, 0xC7, 0xA4, 0x30, 0xF4, 0xDF, 0x93,
0x1C, 0xE5, 0xD0, 0x96, 0x19, 0xF3, 0xDB, 0x8E, 0x21, 0xA8
]
# 2. Key 1 (MSR Key) - 4 字节循环
# 来源于 KiSystemCall64 前4字节
key1 = [0x0F, 0x01, 0xF8, 0x65]
# 3. Key 2 (Seed Key / v4) - 取前 20 字节
# 来源于 C:\WINDOWS 异或处理
key2 = [
0xBC, 0xFE, 0xC7, 0xFC, 0xA7, 0xFA, 0xAE, 0xF8, 0xBE, 0xF6,
0xBB, 0xF4, 0xB7, 0xF2, 0xBE, 0xF0, 0xB8, 0xEE, 0xBE, 0xEC
]
payload = []
for i in range(len(cipher)):
c = cipher[i]
k2 = key2[i]
k1 = key1[i % 4] # 循环取 Key1
p = c ^ k2 ^ k1
payload.append(p)
print("-" * 60)
# 输出最终结果
payload_str = "".join([chr(x) for x in payload])
payload_hex = " ".join([f"{x:02X}" for x in payload])
print(f"[+] 最终 Key (Hex) : {payload_hex}")
print(f"[+] 最终 Key (String): {payload_str}")
if __name__ == "__main__":
solve_payload()
得到payload
写个exp去发imcp包,格式按照前面分析的来
import socket
import struct
import time
import os
def checksum(data):
if len(data) % 2:
data += b'\x00'
res = sum(struct.unpack('!%sH' % (len(data) // 2), data))
res = (res >> 16) + (res & 0xffff)
res += res >> 16
return (~res) & 0xffff
def send_final_exploit():
target_ip = "127.0.0.1"
print(f"[*] Target IP: {target_ip}")
key = b'go_to_find_the_flag!'
icmp_type = 8
icmp_code = 0
icmp_id = os.getpid() & 0xFFFF
icmp_seq = 1
header_no_chk = struct.pack('!BBHHH', icmp_type, icmp_code, 0, icmp_id, icmp_seq)
chk = checksum(header_no_chk + key)
# 填入校验和
header = struct.pack('!BBHHH', icmp_type, icmp_code, chk, icmp_id, icmp_seq)
packet = header + key
try:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
s.sendto(packet, (target_ip, 0))
print(f"[+] Packet Sent! Type={icmp_type}, Payload='{key.decode()}'")
print("[*] 驱动触发")
except Exception as e:
print(f"[X] 发送失败: {e}")
if __name__ == "__main__":
send_final_exploit()
input("\n[!] 进程已暂停。\n按回车键退出...")
打包一下放虚拟机运行即可。
最终结果
flag{Y0r_Ar3_W1nKern3l_Mas7er!*}
#
看雪ID:relost
https://bbs.kanxue.com/user-home-1006061.htm
*本文为看雪论坛优秀文章,由 relost 原创,转载请注明来自看雪社区
往期推荐
量子安全 quantum ctf Global Hyperlink Zone Hack the box
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 relost《第五届“鹏城杯”初赛 Reverse-MDriver-赛题解析》