省流: 被算法卡脖子了 学到虚脱
(2)能在双机环境运行驱动并调试(1分)
直接加载驱动会报错:
DriverEntry failed 0xc0000001 for driver \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\ACE2025
代码高度混淆 静态也看不出东西 这里尝试patch驱动强制下断让windbg接管:
但是调着调着会进到KeBugCheckEx()
然后蓝屏:
估计就是这里检测双机调试或程序完整性的 写个驱动hook这个函数 同时观察.text段中需要解密的字节是否在此时已经解密了:
1 |
|
到加载驱动的入口时这段还是待解密的:
到达hook到的kebugcheck里之后已经解密过了:
现在dump出来方便静态分析 .writemem "D:\dump.bin" ACEDriver L012CE000
用CFF重建PE头:
在IDA数据库中rebase为0方便分析 找到了可能可以指示真正入口点的字符串 但是交叉引用没有结果 应该是隐藏了 用bn试试能不能找到在哪里引用了这个字符串:
找到DEFB
处进行了引用 去除几处花指令就在IDA中发现了可能为程序入口点的位置 在此处下硬件断点(ba e1 ACEDriver+0xde2c
)看看Windbg能不能断在此处:
成功断下:
调试到后面发现进入了这个函数:
1 | __int64 __fastcall sub_E304(__int64 a1) |
其中的RtlFailFast()
启发了我主动造成蓝屏的原因 如果要造成蓝屏且错误代码是0x414345
那么就一定会将这个参数计算或者直接传入寄存器作为参数 先尝试搜索:
直接搜到了两个结果 在这两个函数开头下断看看会不会到这里 结果直接断下来了 并且还可以发现是先清除了栈然后调用的是KeBugCheckEx()
:
在IDA中向上交叉引用还能发现反调试使用的函数:
hook这个函数看看是否能正常进行双机调试了:
1 |
|
先运行hook驱动再运行题目驱动成功让调试器正常附加到内核上了:
(1)在intel CPU/64位Windows10系统上运行sys 成功加载驱动(0.5分)
能够调试驱动过后找到导致驱动加载失败的位置 现在IDA中静态分析:
E3B8
的返回值应该就是驱动加载成功的关键 最终定位到这个函数:
如果要正确加载驱动这个函数应该返回0 也就是需要一直执行到important()
中间没有跳转 调试到此处通过修改标志位来让它返回0 最后成功加载驱动:
(3)优化驱动中的耗时算法 并给出demo能快速计算得出正确的key(1分)
在上面的调试过程中发现important()
调用了位于0x9930
处的函数 而这个函数中间提示了它应该就是需要改进的耗时算法:
在进入耗时函数之前还执行了位于0x670C
处的函数 开始就先解密了一个字符串:
dump出解密结果得到是\Registry\Machine\System\CurrentControlSet\Services\ACEDriver\2025ACECTF
:
1 | text = """5C 00 52 00 65 00 67 00 69 00 73 00 74 00 72 00 |
那驱动大概率通过注册表来获取了一些值 在ZwOpenKey
和ZwQueryValueKey
下断点发现在进入程序时就读取了\Registry\Machine\System\CurrentControlSet\Services\ACEDriver\2025ACECTF
的内容:
先随便创建一个对应的注册表键值对在下一次调试时看看具体读取的是哪个键:
reg add "HKLM\System\CurrentControlSet\Services\ACEDriver\2025ACECTF" /v "Flag" /t REG_SZ /d "flag{testflaggggggggggggg}" /f
在ZwQueryValueKey
处的断点还得知了它要读取的键名为Key
并且第2次调用时可以得知大小至少为8字节 应该是一个QWORD:
那么先创建这个键看看下一次如果读取成功的话会对key进行什么操作:
reg add "HKLM\System\CurrentControlSet\Services\ACEDriver\2025ACECTF" /v "Key" /t REG_QWORD /d 0xACE6ACE6ACE6ACE6 /f
后续又读取了Flag
:
在0x6a61
处读取到了flag:
再执行时发现驱动就启动成功了 且不需要修改返回值:
也就是说驱动加载成功的关键是注册表中有Key和Flag两项
后续调试发现如果要让程序自己计算正确的Key就需要不传入Key 手动修改跳转条件使其进入待改进的算法 :
1 | __int64 __fastcall to_promote(__int32 _0x2c, unsigned int _0x16) |
后面对最后返回的v5 即key没有影响 只用分析上面的 初步分析可以定义出下面几个结构体 不知道含义的先随便用xyz之类的顶着:
1 | 00000000 struct data // sizeof=0x20 |
逻辑清晰了一点:
接下来就用调试来分析了
但是算法这块还是太史了 比赛的时候就做到这里 下面是根据网上的wp进行复现的
重点应该是now
的赋值(99EF
)以及那个不是对step
进行判断的特殊的分支(9AA0
) 分别下断点观察一下 取出的data
结构体放在r8
中 经过几轮调试观察取出来的data
的x
和y
可以大致发现规律 下面是一个辅助理解的python程序:
1 | import matplotlib.pyplot as plt |
可以发现是一个求路径和的算法 计算从(44, 22)
到(0, 0)
的一个特殊路径和 只能往左或者左下走走一格 用公式来表示大概是W(x, y) = W(x - 1, y) + W(x - 1, y - 1) + (x % 5)
往左走优先于往左下走 在y = 0
和y = x
两条曲线上的点权重为0
题目给的驱动里面用的是类似递归的算法 可以直接正向求解 时间复杂度可以降至O(n)
:
1 | prev_row = [1] |
正确的Key为0x66711265fd2
(4)分析并给出flag的计算执行流程(1.5分) 能准确说明其串联逻辑(0.5分)
在注册表中填充正确的key:reg add "HKLM\System\CurrentControlSet\Services\ACEDriver\2025ACECTF" /v "Key" /t REG_QWORD /d 0x66711265fd2 /f
后续跟进Key的处理发现了第一步加密:
也就是将flag和上面得到的key(d2 5f 26 11 67 06)进行异或 但是从内存中([r15+RCX]
)取出flag时会发现和用WinDbg查到的不一样 第1位应该是0x66
但是经过0x95df
的mov al后得到的却是0x95 合理怀疑特意为flag分配了一个page是为了进行EPThook 查找vmresume(0F 01 C3)
指令看看能不能找到 VT对虚拟机退出进行异常处理的位置 然后查找到了0x1220
处的ExitHandler 其中调用的0x5150
应该是就分发器 其中_RCX
应该就是从VMCS中取出的Exit Reason:
根据Intel手册(或者直接看linux的vmx.h) EPT violation
对应数值0x30 进行运算后为0x460d
找对对应的handler0x2B78
下面大概是取出了Exit qualification看是不是mov al那条指令导致的 那么就可以确定其中的就是handler:
中间取的大概是Guest的寄存器 按照x86_64的寄存器顺序大概可以判断这几个变量的含义 接下来handler根据取出的当前flag位和其下标进行了一个校验值计算:
1 | char __fastcall ept_violation_handler(char now, char idx) |
大概是根据当前的字节和下标生成一个乱序的set(0~7)并根据这个序列调换当前字节的每个位 这个过程明显是不可逆的 但是可以打印出所有下标和值进行计算的结果最后取表
接下来就是经典TEA(0x93B8):
但是连初赛都上了inline hook 决赛的TEA绝对不是看上去那么简单 果然调试的时候就能发现给delta赋值这一步执行的指令和读到的就已经不一样了:
这里肯定是上了ept hook 但是在处理EPT violation
的handler中能明显看出来根据指令hook的只有上面用key和flag异或那一步根据是否是mov al进行操作 既然没有根据指令hook 那应该就是直接hook了一整个页 这里识别出VT框架就很重要了 看网上的wp说是hv框架的 该框架在处理EPT violation
时会将安装了ept hook的假页面返回 交叉引用install_ept_hook
可以发现该框架可以通过guest执行vmcall主动向host发送安装ept hook的请求
再看看hc::install_ept_hook
:
1 | // install an EPT hook for the CURRENT logical processor ONLY |
要hook的页面和实际执行的页面分别在执行vmcall时存放在RCX和RDX中 在驱动中唯一一个vmcall(0x1557)下断应该就能得到实际执行的函数了:
dump出这个页(4kb)后缝到之前dump出来的驱动上方便用IDA分析:
1 | origin = bytearray(open('.\dumpsys.sys', 'rb').read()) |
得到实际执行的TEA:
1 | __int64 __fastcall TEA(unsigned int *flag, _DWORD *key) |
继续调试可以发现执行完TEA后与密文进行比较前flag又发生了变动 0x94A1
处的rdmsr
指令执行完后flag就发生了变化 read msr导致的退出Exit Reason为0x1f 计算完后为0x690d 对应handler在0x2608 根据某个和0xE8对比的变量可以推测a1在0x2740B8偏移位置的应该就是guest的寄存器指针:
找到hv框架中对应的guset-context 在IDA中定义寄存器结构体 方便后续逆向 后面可以发现就是拆成byte进行异或:
验证一下:
1 | int main(){ |
成功验证:
到这里就能给出flag计算中的串联逻辑了 即:
- 通过ept hook在读取flag内容时触发
EPT Violation
根据flag中每个字节和其下标交换字节中的位 - 与Key循环异或
- 变异TEA加密
- 通过读
MSR
触发Read MSR
对flag进行异或
EXP:
1 |
|
(6)该题目使用了一种外挂常用的隐藏手段 请给出多种检测方法 要求demo程序能在题目驱动运行的环境下进行精确检测 方法越多分数越高(3分)
从上面的分析能看出要检验的应该就是是否开启VT
方法1: cpuid执行的cpu周期
cpuid
作为一条特权指令 在VT环境下执行会触发VM-exit交由host处理 即使题目的VT框架没有特殊处理这条特权指令 但是下面这些代码都是会执行的:
这肯定比执行一条cpuid
所需的时间长 所以可以通过执行和cpuid
所需时间差不多长的指令来找到一个基准 如果执行cpuid
的时间远超这个值基本上就可以确定当前处于VT环境下 先测一下非嵌套VT环境下执行cpuid所需的时间:
1 |
|
可以看到在非嵌套VT环境下cpuid执行的时间大致为800左右:
启动了题目驱动后执行时间翻了十倍有余:
接下来就是找到不被VT影响的指令来对比cpuid进行是否在VT环境的判断:
最后得到检测VT环境的函数:
1 | void PerformTimingTest() { |
未开启题目驱动时:
开启题目驱动后:
成功检测到VT环境
方法2: 检测IA32_APERF_MSR
IA32_APERF_MSR(MSR[0xE8])
用于记录cpu在运行期间的实际执行周期数 在两次读取之间如果cpu执行了一些任务那么理论上第二次读取到的数值会大于第一次读到的 但是上面解flag的时候已经知道了题目驱动hook了读取msr[0xE8]的指令 返回的永远都是0 可以利用这一点检测是否开启了题目驱动 但是因为我的VMware开了虚拟化CPU性能计数器就报错开不了机 所以这里只给出可能可行的代码:
1 | void PerformAPERFTest() { |
参考
腾讯2025游戏安全PC方向决赛题解 - moshuiD https://bbs.kanxue.com/thread-286462.htm
腾讯游戏安全竞赛2025决赛题解 - xia0ji233 https://xia0ji233.pro/2025/04/14/tencent-race-2025-final/
hv github项目 https://github.com/jonomango/hv