本帖最后由 你的沐公子 于 2026-4-14 17:53 编辑
该帖子为定时发布 编写2026-4-11
一、题目理解
这个题表面上是一个 Godot 小游戏,场景里有黄色方块和绿色方块。黄色方块只是演示,向前开过去就能触发;真正计分的是绿色方块。题目要求不只是拿到绿色方块的flag,还要把 flag 的生成算法和逆算法都写出来,而且必须是 C/C++ 实现。
二、整体思路
1. 先把 Godot 脚本侧的逻辑摸清楚,确认绿色分支到底调了什么。
2. 再去跟原生 GDExtension,直接把生成 flag 的那个 `Process` 方法调用起来。
最后事实证明,绿色 flag 的计算被拆成了两层:
- 1.第一层在 GDScript 里,先对 token 做一轮 `xor_enc`
- 2.第二层在 native 里,再和一个固定 8 字节 key 异或
复制代码 右上角显示的那串内容,本质上就是最终 8 字节结果的大写十六进制。
三、先解决 token 从哪里来题目左上角会显示一个随机 token,我注意到 token 对应的脚本会直接把内容打印到 Godot 日志里,所以最稳的方法其实是直接读 logcat: - 04-11 04:32:52.144 6682 6726 I godot : Token: 37b8c7e2
复制代码所以当前这一局的 token 是: 后面所有动态验证,我都以这个 token 为主样本。 四、绿色分支并不是单纯的 GDScript静态看资,可以确认绿色方块的触发逻辑不是直接在脚本里把 flag 算完,而是走到了一个原生扩展对象。关键关系可以概括成下面这句: - flag1 = obj.Process(xor_enc(token))
复制代码最后界面显示的是: - flag{sec2026_PART1_<flag1>}
复制代码这里有两个关键点: 所以分析重点自然就变成了:Process 到底是谁、怎么注册、具体做了什么。 五、定位原生类和方法这个题是 Godot + GDExtension 结构,native 逻辑不是直接暴露在 Java 层,而是通过 Godot 的扩展接口注册进来的。我的处理方式是用 Frida 在运行时 hook 它的注册过程。 做法比较直接: - hook extension_init
- hook module_init
- hook classdb_register_extension_class5
- hook classdb_register_extension_class_method
- 把类名、方法名、对象地址、方法调用入口都记下来
复制代码实际抓到的 native 类名是: GameEx父类是: Node真正要的 native 方法是: Process动态抓到的关键参数如下: - get_proc = 0x7542e187bc
- object = 0xb4000076b7c077d0
- method_userdata = 0xb400007627d07050
- call_func = 0x7535e6fde4
- ptrcall_func = 0x7535e6fe3c
复制代码
这些值本身不是答案,但它们证明了一件事:Process 不是伪线索,确实是运行时注册出来并被正常调用的 native 方法。到这里,就可以跳过“碰到绿色方块”这件事,直接模拟绿色分支的调用过程。 六、脚本层算法:xor_encProcess 的输入不是原始 token,而是 xor_enc(token) 的结果。 这个预处理不复杂,输入是 token 的 8 个 ASCII 字节,处理规则如下: - out = bytes(token)
- for i = 0..6:
- out = out ^ out[i + 1]
- out[7] = out[7] ^ out[0]
复制代码
如果把 token 记成 t0..t7,输出记成 x0..x7,那么它们之间的关系是: - x0 = t0 ^ t1
- x1 = t1 ^ t2
- x2 = t2 ^ t3
- x3 = t3 ^ t4
- x4 = t4 ^ t5
- x5 = t5 ^ t6
- x6 = t6 ^ t7
- x7 = t7 ^ x0
复制代码拿当前 token 37b8c7e2 去算,得到: - xor_enc("37b8c7e2") = 04 55 5A 5B 54 52 57 36
复制代码这里我的思路是直接在动态调用 Process 之前,把传进去的 8 字节也打印了出来,和脚本推导完全一致。 七、直接调用 Process有了 GameEx 的实例地址和 Process 的调用入口后,直接在 Frida 里构造 Godot 的参数对象,然后原地调用 Process。 对 token 37b8c7e2 的调用结果如下: - token = 37b8c7e2
- xor_enc = [04,55,5A,5B,54,52,57,36]
- result = 7E4A09122764744A
复制代码于是当前绿色 flag 就直接出来了: - flag{sec2026_PART1_7E4A09122764744A}
复制代码 八、算法还原我接着喂了几组不同 token 给同一个 native Process,得到下面这些样本: - 00000000 -> 7A1F53497336234C
- ffffffff -> 7A1F53497336231A
- 12345678 -> 791E544870372C47
- 37b8c7e2 -> 7E4A09122764744A
- 37b8c7e3 -> 7E4A09122764754B
- deadbeef -> 7B1B564F7436201B
复制代码这些样本放在一起看,规律非常明显:native 不是在做复杂加密,而是对 xor_enc(token) 的每个字节又做了一次固定异或。 把 key 反推出来以后,得到: - K = [7A, 1F, 53, 49, 73, 36, 23, 7C]
复制代码所以 Process 的真实逻辑其实很简单: - y = x ^ K
- flag_suffix = HEX_UPPER(y[0..7])
复制代码也就是说,Part1 的正向算法就是: - flag_suffix = HEX_UPPER(xor_enc(token) XOR K)
复制代码完整 flag 格式为: - flag{sec2026_PART1_<flag_suffix>}
复制代码
九、逆算法既然正向算法已经拆成两层,那么逆向也按相反顺序来做就行。 1. 先去掉 native 固定异或把 flag 后缀的 16 个十六进制字符解成 8 字节 y0..y7,然后: 这样就恢复出了 xor_enc(token) 的结果。 2. 再反解 xor_enc前面已经知道: - x0 = t0 ^ t1
- x1 = t1 ^ t2
- x2 = t2 ^ t3
- x3 = t3 ^ t4
- x4 = t4 ^ t5
- x5 = t5 ^ t6
- x6 = t6 ^ t7
- x7 = t7 ^ x0
复制代码把这组关系整理一下,可以写出一个很干净的恢复公式: - t0 = x7 ^ x1 ^ x2 ^ x3 ^ x4 ^ x5 ^ x6
- t = t[i - 1] ^ x[i - 1], i = 1..7
复制代码恢复完 8 个字节后,再检查它们是不是合法十六进制字符。如果是,就得到了原始 token。 拿当前样本验证: - 7E4A09122764744A -> 37b8c7e2
复制代码和日志完全吻合
|