2026腾讯游戏安全技术竞赛PC决赛分析报告
腾讯游戏安全技术竞赛 PC决赛 分析报告
今年终于是完整的都做出来了,想到去年连让驱动成功运行的任务都做不到,真是感慨。
结算画面,擦边第五名

任务一:成功运行程序
检测Hyper-V环境
部署失败会输出Code…之类的错误信息,搜索字符串找到该处,是通过一个间接Call返回数据来输出Code的。

dbg动调发现会调用到如下Call,可以比较容易看出这是在检测系统是否启用了Hyper-V功能。

禁用物理机的Hyper-V,然后给虚拟机启用虚拟化功能,再在虚拟机Powershell运行Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -All安装Hyper-V,即可通过phase 0。


2. 判断驱动是否写出并加载成功
通过上面函数里用到的全局状态值,这边可以找到共四个。

找交叉调用可以找到以下函数,是写出了一个驱动文件,然后启动服务,最后判断是否写出&启动成功。

3. 判断驱动可通信成功
可以看到这边使用cpuid通信,根据上文大概就能知道加载的驱动是hypervisor之类的,是判断是否通信成功。


任务二:分析程序对操作系统底层攻击
1. 获取隐藏sys文件
通过上文有看到写出一个sys,在此处可以断点拿到隐藏的驱动文件。

断点在CloseHandle后,在C:\Windows\System32\drivers底下可以找到随机名称的sys。

2. 初步分析sys文件
2.1 间接Call地址还原
存在大量ollvm平坦化,不考虑全部分析。
这边发现许多地方都有这种计算间接跳转Call地方,可以看到内存中存在大量的这种数据,猜测是程序内函数地址或API地址。


借助AI帮忙一键计算分析所有的Call地址。

同时AI帮忙命名了几个全局变量和函数指针具体的意义,有助于后续的代码分析。



以上AI记录截图见AI记录\记录1.png和AI记录\记录2.png
注释函数地址idapython脚本见annotate_apis.py
2.2 Pattern
sub_140018260,该函数可以看到是根据系统版本,选择不同的特征字节返回。

我系统使用的特征字节转汇编如下,是hvix64.exe的VMExit handler特征字节。
1 | 65 C6 04 25 6D 00 00 00 00 → mov byte ptr gs:[6Dh], 0 |
2.3 Dma
sub_140009530这边看到是和设备通信的函数。

断点开头,看第一个参数,也就是设备的handle,windbg查到的设备是\Driver\disk

查看交叉调用一共有两个函数调用了(sub_140003E10/sub_140004FB0),从第一个函数分析,其中有一个PreparePageAccess(sub_140003EB0)函数调用,对a2地址做了什么。


sub_140003EB0分析,总体功能就是获取传入的虚拟地址的物理地址,然后设置权限位允许设备访问。


回到上一层,根据上面搜到的handle设备等线索,可以知道就是vhdmp虚拟磁盘设备,通过SCSI通信直接读写,可以绕过EPT保护。
下面是将a2物理页内容写入磁盘,然后再将磁盘里的内容页读入到a1,这样达到了copy a2地址内存到a1的效果,然后最后再恢复磁盘。


2.4 ManualMapPE
翻到一个ManualMapPE(sub_14000E630)函数,也就是手动映射加载PE文件,比较普通的映射加载,这里多了将内存映射到0x327fffE00000,也就是重定位的时候将基址设置为了0x327fffE00000。

2.5 扫描特征获取VmExit handler地址
找CopyPage函数的交叉调用,有且只有一个sub_14000BEB0。
这边是先遍历物理页,必须内存全都为0xff才符合条件,其实就是在搜hypervisor的保护页。

然后映射保护页,用Disk读取出被保护的内存,这样能够读取到hv相关的代码。

判断了一下CPU厂商

这边可以看到调用了上面分析到的BuildSigPattern,然后在刚刚读取的hv代码页里面去扫VmExit Handler的特征。

扫到后通过加上BuildSigPattern提供的一个固定偏移量,得到了目标Hook地址。

2.6 Hook VmExit handler
在ManualMapPE函数下方会找到一个函数,sub_14000FC40,被ollvm混淆了,但是可以看到代码量很大,而且存在下图类似Hook一样的代码块,可以直接使用D810去除控制流平坦化。


开头先在目标地址后面找连续的0xCC内存段,用来放shellcode。

申请了内存,将shellcode模板复制到内存中。


然后搜索shellcode中的两个占位符,替换成映射加载驱动的payload地址,以及a5参数。


再把shellcode复制到目标padding地址。

然后这边是在shellcode结尾写入执行原代码以及jmp回原始代码下一条,也就是标准inline hook的处理方式,然后调用CopyPage,写入hv物理页,然后再写一个jmp到shellcode的字节。

最后底下又写了一个跳板,然后在目标Hook点写入Jmp,jmp到跳板处执行,可以看到0x327FFFE00000,就是上面ManualMapPE驱动映射的内存地址。

3. 获取手动映射的PE文件
由于看到了ManualMapPE,就先想着给他加载的payload dump出来看看是什么。
直接断点sys+0xe630,rcx就能看到是MZ头的PE文件,直接.writemem导出。

IDA加载可以看到是一个驱动

分析可以发现以下关键函数:
- sub_1400017A0 (DriverEntry):入口点,初始化payload函数指针
- sub_140001000 (密码验证):8轮XXHash变体算法,seed来自dispatcher页面+0x500,异或”SHAD0WNT”/“HYPERVMX”常量后混合计算

- sub_1400013A0 (VMCALL状态机):处理通信协议,
0xDEADBEEF+0x114514→返回0x1919810,0xDEADBEEF+0x191A710+pw→密码验证

- payload_2 (CPUID拦截):使用vmread/vmwrite操作VMCS,拦截guest的CPUID指令实现通信

- payload_3 (VMCALL处理):拦截VMCALL hypercall,转发到状态机处理

这个PE就是核心payload,通过inline hook劫持了VMExit dispatcher后,所有VMExit都先经过它处理,拦截特定的CPUID/VMCALL请求实现与shadow_panel.exe的通信和密码验证,其余VMExit透传给原始handler。
4. 攻击流程总结
shadow_panel.exe释放驱动到drivers目录加载服务
被加载的驱动:
ManualMapPE手动映射加载了个驱动到0x327FFFE00000地址。
遍历物理内存,筛选出全0xFF的页面(Hyper-V保护的hypervisor代码页)
利用vhdmp虚拟磁盘设备发起读写,借助磁盘DMA绕过Hyper-V的EPT保护,读取到hypervisor真实代码。
根据系统版本特征定位VMExit handler。
将其用同样的方式对VMExit handler的dispatcher call进行inline hook替换成jmp到跳板地址,最终会跳转到手动映射加载的那个驱动入口,劫持VMExit流程。
任务三:编写检测工具
直接模仿原驱动的流程,去通过dma方案暴力扫描所有的物理页,找到VmExit Handler,然后看看是不是E8被改成E9,也就是被Hook了,则检测成功。

代码见detector项目文件夹
AI记录见AI记录\检测
任务四:得到正确终止密码
上文可以知道被加载的驱动就是核心密码验证的地方,代码验证算法可以整理成如下代码,最主要的就是得拿到两个8字节seed,就可以计算出本机的密码。
两个seed值都是从payload1+0x500和0x508得到的。

1 | uint64_t rol64(uint64_t v, int n) { |
在原dump文件中payload3函数最底下是作为回调调用,其实就是相当于原VmExit dispatcher地址,因为hvix64的某个VmExit handler被inline hook了,而inline hook劫持的函数最后是需要运行原函数的。
所以这个payload1就应该是原dispatcher call的地址。

1. 找到被Hook的Handler
由于从ManualMapPE处Dump的是原文件,payload1都是空的,没办法拿到seed值计算密码,需要dump运行时的内存。
但遇到一个问题,该驱动运行在host层,guest层是根本没办法找到该内存,windbg是没办法找到的。
但是有个办法,也就是虚拟机里面运行驱动是在Vmware的host,物理机是可以通过Vmware的文件读到host层代码—-虚拟机的内存都会存在文件夹下的.vmem文件中,可以直接读取该文件扫描到host层的代码。
从.vmem中查驱动里面提供的特征字节,确实发现其中一个handler的call被inline hook成jmp了,验证了上面inline hook代码分析。
find_handler_pattern.py
1 | import struct, sys |

2. 计算Flag
由于知道了这个Handler的偏移是3E3CC,所以可以直接去hvix64.exe该偏移处找到原dispatcher的call地址,

将地址后三位清零然后再加0x500,得到对应seed。

get_password.cpp
1 |
|
得到密码675A4ACF5B1867E85EE7F7226032411A
验证成功

任务五:编写Keygen
从上文得到的目标Handler地址,以及在驱动里面的特征字节搜索到的结果,可以对比看到,实际就是一个地方。


所以可以得到以下密码生成流程:
读取
C:\Users\Windows\System32\hvix64.exe,通过版本号选择不同的特征字节,搜索到Handler。读取到Handler下面的Dispatcher Call的rel32
计算RVA,取page+0x500的16字节seed
计算出16字节密码
版本大于22621的特征码掩码是被异或加密了,需要动调取一下,在构建特征码的地方这边下段,设置rdx也就是版本号大于22621。

然后在箭头处也就是异或解密的Call后面这个mov下断,然后[rdi+8]就是解密出来的掩码了。xxxxxxx????xxxx?xxxxxx?x????x

keygen.cpp
1 |
|
在虚拟机运行keygen得到密码,输入验证成功
