-
-
[原创]2026 年腾讯游戏安全初赛 Android方向
-
发表于: 3小时前 56
-
注:本人草台班子出身,属于那种“能把效果整出来,但文章过程写得一坨”的类型。这篇题解也是靠 AI 帮忙润色过的。看了论坛里各位大佬的文章后,才发现自己还有不少明显的丢分点,只能说下次继续努力了。
另外,本文仅代表个人解法,最终答案请以官方题解为准。
一、题目理解
这题表面上是一个 Godot 小游戏,场景里有黄色方块和绿色方块。黄色方块更像是演示流程,车直接开过去就能触发;真正和计分相关的是绿色方块。
这题要求也不只是拿到绿色方块对应的 flag,而是还要:
- 找出 flag 的生成算法;
- 给出对应的逆算法;
- 并且要求使用 C/C++ 实现。
所以这题本质上不是单纯“跑出一个结果”,而是要把整条链路都分析清楚。

二、整体思路
我的做法分成两步:
- 先把 Godot 脚本侧的逻辑摸清楚,确认绿色分支到底调用了什么;
- 再去跟 native 的 GDExtension,直接把生成 flag 的
Process方法调用起来。
最后可以确认,绿色 flag 的计算实际上被拆成了两层:
- 第一层在 GDScript 中,先对 token 做一轮
xor_enc - 第二层在 native 中,再和一个固定的 8 字节 key 做异或
右上角显示的那串内容,本质上就是最终 8 字节结果转换成的大写十六进制字符串。
三、先解决 token 从哪里来
题目左上角会显示一个随机 token。我这里注意到,token 对应的脚本会直接把内容打印到 Godot 日志里,所以最稳的方法其实是直接读 logcat:
04-11 04:32:52.144 6682 6726 I godot : Token: 37b8c7e2
所以当前这一局对应的 token 是:
37b8c7e2
后面的动态验证,我都以这个 token 作为主样本。
四、绿色分支并不是单纯的 GDScript
静态分析后可以确认,绿色方块的触发逻辑并不是在脚本里直接把 flag 算完,而是走到了一个原生扩展对象。
整个关键关系可以概括为:
flag1 = obj.Process(xor_enc(token))
最后界面显示的是:
flag{sec2026_PART1_<flag1>}这里有两个关键点:
xor_enc是脚本层做的预处理;- 真正决定 flag 的核心逻辑在 native 的
Process方法里。
所以分析重点自然就变成了: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_enc
Process 的输入并不是原始 token,而是 xor_enc(token) 的结果。
这个预处理不复杂。输入是 token 的 8 个 ASCII 字节,处理规则如下:
out = bytes(token) for i = 0..6: out[i] = out[i] ^ 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[i] = x[i] ^ K[i] 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,然后计算:
x[i] = y[i] ^ K[i]
这样就恢复出了 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[i] = t[i - 1] ^ x[i - 1], i = 1..7
恢复出 8 个字节后,再检查它们是否都是合法的十六进制字符。如果是,就得到了原始 token。
拿当前样本验证:
7E4A09122764744A -> 37b8c7e2
结果与日志中的 token 完全吻合。
十、C++ 实现说明
我把正向和逆向都写在了同一个 C++ 文件里:
tx.cpp
主要支持两种用法:
tx.exe --flag 37b8c7e2 tx.exe --token 7E4A09122764744A
其中:
--flag:根据 token 计算 flag 后缀--token:根据 flag 后缀反推 token
这样题目要求的正向算法和逆算法,都能用 C/C++ 方式完整跑通。
十一、最终结果
当前样本下,Part1 绿色方块对应的 flag 为:
flag{sec2026_PART1_7E4A09122764744A}对应的 token 为:
37b8c7e2
十二、总结
这题我最后的感觉是:表面上看是个小游戏,实际上核心在于把 Godot 脚本层和 native GDExtension 串起来看。
真正有用的信息链路是:
- 从日志里拿到 token;
- 在脚本层确认
xor_enc; - 在运行时定位并调用 native 的
Process; - 通过多组样本反推出固定 key;
- 最后补出正向和逆向的 C++ 实现。
整体上不算特别重型,但如果一开始没意识到绿色分支是走 native,那确实容易绕很久。