魔改Godot引擎,加上虚拟机,真做吐了。混淆我是一个没去、就说这思路能不能跑吧主要操作:模拟执行,过反调试,Frida Hook(相关脚本不放了、太丑,思路肯定是对的就是了)FLAG1: FLAG2: FLAG3(模拟跑通了、没时间扒下来) 几个不同的触发题触发条件分别是:frida远程调用直接查出来的属性先解包并还原 GDScript,确认三个 Trigger 各自负责什么;再定位 GameExtension.Process() / Tick() 的 native 入口;最后分别处理 PART2 的 watchdog / anti-hook,以及 PART3 的 Godot 事件构造链。改改初赛的解包脚本就能用,并通过 GDC 文件头确定解的是对的,但这次旧脚本直接按上游 Godot token 表解释会全乱。后面确认不是多了一层“token 解混淆函数”,而是决赛包把引擎内部 token 编号表直接改了,所以正确做法是去 libgodot_android.so 的 GDScript 解析链上做动态 dump,把触发器挂载的脚本先还原出来。我不知道那个字节码混淆是怎么搞出来的,多半是在libgodot里面有什么混淆的操作,对Unity开发经验比较多、肯定有一套解释器运行的是去掉混淆过后的脚本,猜测从那个位置去dump基本就可以无视混淆了,类似于il2cppdump一样。trigger1:trigger2:trigger3:trigger4:Trigger4非常神奇,没玩过Godot研究了一整天,我一直以为flag回传是在tick里面做得操作,甚至怀疑dump下来的内容是不是有误,检查了好多遍,最后发现写死在引擎内测(libgodot.so)的0x25f6718函数里,真的吐血。这一段关键地址大致是:把 trigger2.gd 还原出来后,PART1 的脚本逻辑就比较直白了:命中对应 Trigger 的碰撞关系后,脚本去读 /root/TownScene/Label,去掉前缀 "Token: " 拿到 8 位十六进制字符串,再进脚本内 _fe() 做 8 轮 Feistel 风格变换,最后写到 Label2。所以 PART1 的 FLAG 处理算法可以概括成(还原见样例程序PART1_flag):例如样本 3a5e9ecd 最后可以还原出 flag{sec2026_PART1_EEC57451}。unicorn模拟执行分析vmp、平坦化混淆还是花指令什么的,常规操作了,关键思路是找到入口、出口、拿到中间的执行流,只要有执行流混淆基本就只剩下各种花指令,相对于死磕静态分析还是比较划算的。binary ninja mcp插件见样例(框架改的面目全非)。prompt: 通过 Hook 游戏侧的 Process 调用,可以把 PART2 的 native 主链基本钉死。这里自然触发同样不是单纯“谁进了 Area3D 都算”,而是要先满足对应 Trigger 的碰撞关系;移车脚本专门有 trigger_probe / trigger_arm 去查 Trigger 的 monitoring、monitorable 和子 CollisionShape3D.disabled,再把 car 下真正参与物理的 body 传送过去。命中后,GameExtension.Process() 的真实绑定落点(怎么拿到拓展函数的地址见初赛操作)是 libsec2026.so+0x97704,往下马上推进到 0xA936C,核心 16 字节块变换在 0xA7194。调用链大致可以记成:这里几个关键点是:主算法整体很像 AES,但不是标准 AES。静态参数块可以确定:轮函数层大概可以这样记:也就是说,这条链虽然长得像 AES 风格 SPN,但实际是“自定义 S-box + 自定义轮常量 + 自定义 whitening/mask”的一套魔改版本。真实样本里,trigger3 处理 token 48cde866 可以得到 flag{sec2026_PART2_2e15242437e16b2ec0d472561c75018b},可以拿来做结果校对。这里截一眼执行链路: 这条路模拟执行出奇地顺,可能就是预留来这样做的吧,出题人高抬贵手只给20分不是没理由的。同时调试的时候发现有多个奇怪的线程在 native loader 阶段被拉起,所以顺着 extension_init 和 watchdog 启动块往下看,能把保护面大概分成三条:其中 inline_hook_guardian 的主链比较关键:它会枚举模块的可执行 PT_LOAD 段做完整性校验,所以直接在 libsec2026.so 代码页上做 inline hook 很容易触发。再往里还能看到:ptrace_guardian 可以直接拦掉不管,真正更稳定会炸进程的是 Tick() 里的 watchdog。GameExtension.Tick() 的绑定落点是 0x9AD68,全局时间戳在 qword_1834B8,当 delta_us > 0x989680,也就是超过 10 秒时,会跳到 0x9AE70/0x9AE74 的毒分支。最后绕过的办法就没必要硬 patch 代码页了,直接在 tools/frida_bypass_antidebug_final2026.js 里定时刷新 qword_1834B8,把 Tick() 一直维持在正常返回路径即可。这种 data-only keepalive 比直接改 libsec2026.so 文本段稳很多。找了 libsec2026.so 半天没找着,最后发现真正的 PART3 拼 flag 路在 libgodot_android.so 内部。思路是去 Hook Label2 / 信号发射相关的写入链,最后确认真正直接构造 flag{sec2026_PART3_...} 的地方在 sub_25F6718。字符串搜不到,但是可以翻得到提示。 这一段关键地址可以记成:主要逻辑是:说人话:整个保护最精髓的地方就在这里,在引擎源码内部自己实现了一套和拓展方法与游戏场景内通讯的办法,并在引擎侧和拓展侧上依赖保护,拓展侧通过回调、共享内存的办法来传递信息到场景内的gameObject中,相当于绕过了gdc、C#这种字节解释的弱保护,同时又保证拓展层不被卸载能够保护游戏运行的反作弊。具体操作见下(在libsec2026.so内部的虚拟机处理算法,实在没时间写思路了、以下内容直接给ai帮跑日志的,主要是模拟执行。)这里一开始最容易误判的是 sub_1FD9B60(a1, &out) 或 a1 + 480,看起来像是在喂中间串,但后面证实那只是旁路;真正参与 sec2026_PART3_ 中间段构造的是 qword_40541E0 以及它后面的运行时变换结果。在libsec2026中0x4A9A7C 是主要处理函数,一个扁平状态机/小型解释器,不是普通的直接字符串算法。也就是说,这里硬啃全静态当然能做,但性价比不高;更稳的做法是直接 Hook 三个点:如果还想在静态侧再往前推一步,性价比最高的不是直接硬啃 0x4A9A7C 全部状态,而是先把它
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290882.htm
[原创]南极动物游戏安全2026-安卓决赛
321 浏览
1 回复
tql