本题需要围绕游戏中的 PART1 / PART2 / PART3 完成三件事:确认每一部分 flag 的真实生成路径,恢复正向算法,给出逆向恢复方法,并最终用 C / C++ 写成可复现工具。这题的难点不在于单独逆一段 libsec2026.so,而在于要把 Godot 脚本、场景资源、资源加密、引擎 native 和题目扩展这几层关系先串起来。只有先把入口、触发条件和调用关系理顺,后面的算法还原才不容易偏离方向。因此本文不按零散线索来展开,而是按照实际解题顺序来组织内容:先确认程序结构与扩展注册,再恢复脚本和场景,接着把 PART1 / PART2 分析清楚,然后处理最难的 PART3,最后补上为运行时验证服务的反调试与反 Frida 绕过部分,把整条分析链收完整。截至当前,已经完成:本文重点放在“算法、资源恢复、行为验证”这三部分。目标不是把每一层运行时都铺开,而是把 Godot 脚本、场景入口、引擎 native 和 libsec2026.so 串成一条连续证据链,最后统一到一份 C++ 实现中。如果只看交付结果,本报告已经覆盖题目要求的四类内容:入口定位、算法恢复、逆向恢复、运行验证。后文再按“先把路径讲清楚,再把算法写清楚”的顺序展开。拿到 APK 之后,首先确认整体结构。样本为 arm64-v8a,核心文件实际上只有两层:libgodot_android.so 是 Godot 4.5.1 引擎本体,libsec2026.so 则是题目自定义的 native 扩展。也就是说,后续所有分析都要回答一个基础问题:libsec2026.so 在 Godot 工程里到底暴露了什么能力,它是不是全部逻辑的唯一入口。图 1 APK 结构与关键 so。样本真正需要重点分析的部分主要集中在 libgodot_android.so 与 libsec2026.so。本节先不给算法结论,只做一件事:确认 native 扩展在 Godot 里以什么形式暴露给脚本。后续哪些逻辑回脚本层看,哪些逻辑继续追 native,都建立在这个前提上。顺着这条注册链,可以确认 libsec2026.so 在 Godot 中注册的是:证据 1 是类注册主链 sub_A07F4。这里可以直接看到“类名对象”和“父类名对象”同时被装入注册描述体。整理成伪代码后大致如下:因此 GameExtension : Node 不是字符串偶然出现,而是沿着注册链可以明确确认的结果。同时它对脚本层暴露了两个关键方法:证据 2 是 bind_methods 主链 sub_97B6C。虽然外层是 OLLVM 状态机,但关键绑定动作仍然能整理成下面这段伪代码:其中:Tick 更像逐帧入口,Process(input) -> String 更符合 flag 计算接口形态,因此后续优先沿 Process 追 PART2。本节结论可以概括成一句话:libsec2026.so 不是脱离引擎独立运行的算法黑盒,而是以 GameExtension : Node 的形式接入 Godot,并向脚本暴露 Tick / Process 两个关键入口。上一节只把扩展入口确定下来,但还没有回答真正和出题目标直接相关的问题:哪个 Trigger 对应 PART1,哪个 Trigger 对应 PART2,左上角 token 从哪里来,结果最终写到哪里,以及为什么有些 Trigger 在场景里明明存在却根本碰不出来。要回答这些问题,分析必须从 native 注册链回到 Godot 脚本和资源层。这里采用了两条互相印证的恢复路线。第一条路线是在 libgodot_android.so 的 .gdc 加载链上做运行时 dump,再离线解析;第二条路线则是在 libgodot_android.so 中定位 Godot 加密资源 key,用 GDRETools 恢复完整资源。两条路线分工并不相同:前者解决“资源加载后真实长什么样”,后者解决“如何离线拿到完整脚本与场景结构”。两条结果互相校验后,后面的脚本结论和 Trigger 场景结论就不再依赖单点猜测。恢复出的关键资源包括:token.gdc 直接说明左上角 token 是脚本生成的,而不是 native 层生成的;它的长度固定为 8,字符集固定为 0123456789abcdef。因此后续三部分算法的输入可以统一定义为:对应脚本片段如下:label2.gdc 负责把 Trigger 的输出结果真正显示到界面上。也就是说,只要脚本或 native 最终把 flag 写进 Label2.text,就会在游戏里显示出来。这个结论后面会和 Trigger 脚本直接对上。对应脚本片段如下:资源 key 的定位依赖两条硬证据:其一,FileAccess.open_encrypted() 明确要求 key 长度为 32;其二,上层初始化逻辑会把同一处 32 字节常量复制进 key 缓冲区,再传给 open_encrypted()。因此可以把 libgodot_android.so 中的这组常量认定为实际资源 key,而不是“像 key 的数据”。如果把这条链整理成伪代码,核心关系可以写成:关键判断依据是调用关系本身:0x400EF18 处的 32 字节常量被复制进 key 缓冲区,并作为参数传给 open_encrypted()。最终恢复出的 key 为:使用该 key 跑 GDRETools 后,可以成功恢复 Trigger 脚本和场景资源,这说明前面的判断不是“像 key”,而是“确实能把资源外层解开”。这里也要说明边界:key 的作用是解开加密资源包,把其中的 .gdc / .scn 文件取出来;它并不能一步把 .gdc 直接变成源码 .gd。恢复结果的关键目录结构如下:图 2 使用资源 key 配合 GDRETools 恢复出的关键资源目录。后续脚本与场景分析都可以直接落在这些文件上。脚本语义恢复最终依赖两条分工明确的链路:到这里,本节已经完成四件事:拿到 Trigger 脚本,拿到场景资源,确认 token 来源,确认 flag 的界面落点。后续 PART1 / PART2 / PART3 的对应关系分析都建立在这组恢复结果上。脚本和场景拿到手之后,下一步不是平铺直叙地介绍四个 Trigger,而是先把 PART1 和 PART2 的对应关系彻底确定下来。只有把哪一块对应哪一部分 flag 说清楚,后面算法分析和场景验证才有明确对象。先看 Trigger2 对应脚本。这里关键不只是前缀里写了 sec2026_PART1_,而是核心函数 _fe() 完全停留在脚本层内部实现,轮函数 key 也直接写成了 Sec2026_Godot,整个流程里没有调用 _gx.Process。这说明 PART1 的本体就在脚本中,而不是 native 层的 Process。因此可以明确写成:这里真正完成的是“解耦”:通过 Trigger 脚本本身,就可以把 PART1 和 native 算法明确区分开。再看 Trigger3 对应脚本。这里真正的关键点不是 _PART2_ 前缀本身,而是输入来源、参数转换和 _gx.Process(_buf) 三件
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290971.htm
[原创 ]2026 腾讯游戏安全竞赛决赛 - Android WriteUp
153 浏览
0 回复
暂无回复,快来抢沙发吧!