论坛首页 密码学讨论区 阅读主题

[原创]2026腾讯游戏安全竞赛Android决赛 wp

365 浏览 7 回复
#1 楼主 2026-06-01 21:08:54
题目与初赛一样,都是godot题目附件:ps:一些部分其实写的不太详细,后面有机会再补充吧,赛中使用了codex加速解题,wp中给的顺序非实际做题顺序,做题时没有处理反调试,因为10s的kill时间足够frida输出信息了,反调试是最后去了混淆再处理的,因此下面使用frida分析的动态调代码都会触发反调试,但是信息正常输出,在反调试章节给出了最终处理反调试的frida代码,和一些其它现象。去混淆部分写的特别乱,主要当时写wp的时候还是有点懵,有空再整理这一部分其实跟初赛一样,甚至简化了首先找脚本加密 key,与初赛相同,Godot 4.5 用 AES-256 CFB 加密 .gdc 文件,key 存在libgodot_android.so 的 .data段中沿用初赛脚本做熵扫描,在段中找连续 32 字节高熵区域(前后被零填充包围)。key在0x4002f18与初赛相同,用初赛的解密脚本报错了,检查了一下发现,GEQ = GDSC ^ {0x00, 0x01, 0x02, 0x03},是标准的CFB-128,没有XOR,微调了下初赛的脚本运行脚本发现提取出来的是类似的乱码,猜测是被混淆了,因为之前在华为杯看到过类似的题可能是改了opcode的映射?通过对比初赛和决赛 .gdc 文件结构发现初赛Godot 4.5 格式:决赛:用新格式解析,AI搓一个脚本输出trigger1,示例有点乱,然后研究了libgodotengine.so发现,不仅改格式,果然重排了 token 类型 ID。完整 token 表从 libgodot_android.so 提取在 0x3ec2548 找到 100 项的 _ZN17GDScriptTokenizer5Token8get_nameEv字符串指针表。AI解释了下说依旧搓个脚本生成的trigger起码能看了让AI综合两版进行总结整理下这其实是最后做的当时Trigger里的Tick一开始以为是part3 最后发现是反调试扎堆的地方工作原理博客,不久前才稍微复习了ollvm,用上了刚好,其实原理差不多,虽然混淆每次不一样,如果不理解请先了解ollvm对抗跟踪到0x9AD68 ,这一块结合codex进行分析0x9AD68 - 0x9ADB0很明显是个序言在分配栈空间和初始化魔数比如0x9AD94 - 0x9AE10在进行一些常量设置,结合后面分析得出大概是这样的核心的分发逻辑在0x9AE30 - 0x9AE6C0x9AE1C - 0x9AE2C进行状态迁移转换Entry 0 (0x9AE14) : slow-path把返回地址向后推 0x30 字节,RET 后 CPU 跳到进入 Tick 时 LR 值 + 0x30 的位置。但 Tick 进入时 LR = 0x9AEFC(BLR llabs 的下一条,被保存在寄存器未被改过),推 0x30 , 跳到 0x9AF2C,那是 Tick 自己函数体内部。LR用来保存函数返回值,跳转到某个寄存器里保存的地址,并把返回地址保存到 LR/X30回到 0x9AF2C 后代码继续执行 entry 5 的后半段,再次 B 到 dispatcher,第二次分发时 W8 不再匹配任何 valid state,落到默认 entry 7。以下分了几个entry,下面要穿起来理解但理解某个entry可能不太好理解Entry 1 (0x9AF44) : baseline 初始化判定首次 Tick:baseline==0 ,选 entry 2 写 baseline非首次:baseline!=0,选 entry 5 检查 10 秒Entry 2 (0x9AE88): 初始化 baseline这是唯一写 qword_1834B8 的地方(在 Tick 内部)。只在首次调用 Tick 时执行。这段是 Tick 反调试的初始化 baseline 时间戳分支(首次运行时建立基准时间)Entry 5 (0x9AEBC) : 10 秒 diff 判定 核心反调试常量:Entry 4 (0x9AF60) : 正常 epilogue(fast path 出口)标准 epilogue stack canary 验证。fast path 出口是这里(指的是没被调试过)。Entry 7 (0x9AE70) traploc_9AE68 实际是 dispatcher 最后两条指令的机器码(LDR X8, [X24, X8] + BR X8):合起来读0xD61F0100F8686B08。这是 ARM64 指令字节被当成数据地址读取,BLR X1 跳到不存在的内存 , MEM_INVALID 导致 SIGSEGV。OLLVM 的骚操作 , 用自己的指令字节做陷阱指针。正常运行流程:本质是一个反向心跳机制: 反调试线程每 3s 证明还活着,如果证明中断 ,Tick 自毁。内联 SVC 的反 hook 细节Tick 里 clock_gettime 不走 libc,而是 inline SVCFrida Interceptor.attach(libc.so, "clock_gettime") 完全拦不到。要拦这个 SVC 必须用 Stalker 级别指令扫描,开销大。发现了 OLLVM 标准结构,实际工作只在 entry block 里。dispatcher 纯粹是混淆。先找 dispatcher 边界 + jump table 基址工作原理博客,不久前才稍微复习了ollvm,用上了刚好简单学习ollvm混淆&polyre例题解析 | Matriy's blogangr符号执行对抗ollvm - Qmeimei's Blog | 探索一切,攻破一切既然是OLLVM CFF ,我们需要找dispatcher,OLLVM CFF 把它变成巨型 switch每个 basic block 变成一个 entry例1:例2:所以可以得到一个通用流程用 Unicorn-style 模拟 dispatcher,建立 state到 entry 映射,拿博客里的改就行这里手动找了序言,放进去了,后面有自动化的版本这里只为了验证输出:魔数对应如上其中的prologue_static_regs 怎么填?看 prologue 里的 MOV W19, ...; MOVK W19, #..., LSL #16 序列,把每个寄存器最终的值算出来。手动算或用:trace cold-start 链知道 state到entry 后,从 prologue 的 INITIAL_STATE 开始 trace,就是之前的流程XOR_K 和 ADD_K 在 dispatcher 入口,比如 sub_97B6C 是 EOR W8, W8, #0x8D; ADD W8, W8, #0x8F。遇到 CSEL 的 entry, 它依赖一个全局 byte(如 byte_183518 = 反调试 flag)。Cold start 时这些 byte 全 0,所以总是走false 分支。trace 一遍 cold-path 即可。这个我的博客里也提到怎么处理Patch dispatcher 短路成线性每个 entry 末尾的 B 0x9

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291005.htm
#2 2026-06-01 21:08:54
tql
#3 2026-06-01 21:08:54
tql
#4 2026-06-01 21:08:54
tql

最后于 2026-4-28 18:54
被东方玻璃编辑

,原因:
#5 2026-06-01 21:08:54
tql
#6 2026-06-01 21:08:54
tql
#7 2026-06-01 21:08:54
tql
#8 2026-06-01 21:08:54
谢谢

请登录后参与讨论

立即登录 注册账号