第十九届全国大学生信息安全竞赛_babygame:Godot:游戏逆向与AES运行时密钥替换
一、分析附件
题目给了一个 .exe 文件,对于未知二进制的第一步不是上 IDA,而是先识别它是什么。
010 Editor 搜索关键字符串是成本最低的侦察手段,丢到 010 中分析一波:


在查找“flag”字符串的时候,发现了文件 flag.gd、flag.scn 文件,上网搜索之后得知这是一个 Godot 引擎打包的游戏:

与该引擎相关的文件后缀:
| 后缀 | 是什么 | 编译前版本 |
|---|---|---|
project.godot |
项目配置,文本 | - |
project.binary |
项目配置,二进制 | project.godot |
.gd |
GDScript 源码 | - |
.gdc |
GDScript 字节码 | .gd |
.tscn |
场景文件,文本 | - |
.scn |
场景文件,二进制 | .tscn |
.tres |
资源文件,文本 | - |
.res |
资源文件,二进制 | .tres |
.remap |
资源重定向索引 | - |
关于该引擎有专门的工具用于反编译,我这用的是”gdsdecomp”
二、反编译
将游戏文件拖入工具中:

1、查看项目入口
上面提到 .binary 是项目入口,查看其代码(工具会帮我们反编译成 .godot 文件):
; Engine configuration file. |
关键信息:
[autoload] |
autoload 单例会在游戏启动就创建,直到游戏关闭才销毁。任何场景、任何脚本都可以直接用
Flag.xxx访问它的变量。
找到文件 /scenes/flag.tscn(只能找到 /scenes/flag.tscn.remap):
[remap] |
从之前的后缀说明可以看出这是起重定向的作用,接着找 /.godot/exported/133200997/export-ed01e640138f262d3a3519429431a67d-flag.scn 这个文件,其代码:
[gd_scene load_steps=2 format=3 uid="uid://dkq0of6k4nvab"] |
根据内部的英文提示词:
- “you are great~!”
- “Please input your flag:”
我们大致可以猜出获得 flag 的流程应该是:输入 $\to$ 校验 $\to$ 输出
聚焦:
[node name="Label2" type="Label" parent="PanelContainer/VBoxContainer"] |
Label2 并不可见(visible = false),说明游戏中并不能看到关键信息(顺带一提,我的一个队友按要求通关游戏之后,屏幕显示了“flag”,之后的事情,想必大家都知道了 ^-^)。
聚焦文件的开头:
res://scripts/flag.gd |
这就是我们在 010 中看到的那个,并且根据后缀可知,这是 GDScript 源码,必然成为重点。
三、AES
点开文件 /scripts/flag.gd(工具中看到的是 flag.gdc,点开后自动反编译成 flag.gd,后面不再提及此时,请自行注意):
extends CenterContainer |
关键信息:
- AES,模式为 ECB,并且告知密钥为
FanAglFanAglOoO! label2.show():我们期望看到的秘密- 检验方式:
if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d"
总结一下就是,需要对 ciphertext 进行 AES 解密,将解密后的结果作为输入,以此来使验证成立,最终显示出秘密数据。
四、坑点
坑点出现了,根据已有信息进行解密输出的结果是不可读的:
尝试:
from Crypto.Cipher import AES |
输出:
┌──(penv)─(zyf㉿zhengyifeng)-[~/Templates] |
密文是写死的,但是回想初始 key 是 static var,挂在全局单例 Flag 上,理论上可以被任何脚本修改。因此不能只看 flag.gd,需要排查所有可能修改 Flag.key 的脚本。
回到题目的提示信息上”请找出隐藏的Flag。请注意只有收集了所有的金币,才能验证flag。”
难道和金币有关系?
金币的英文 coin,通过文件名找到 coin.gdc:
extends Area2D |
这里调用了 add_point() 这个方法,去找找 game_manager 这个类。
根据文件名可以直接定位(game_manager,.gdc),其代码:
extends Node |
拨云见日,原来当金币+1的时候,就会将 Flag.key 中的”A”都换成”B”,那么吃完所有的金币 score 值必然大于等于 1,换言之 key 必然变成:
FanBglFanBglOoO! |
五、获得 Flag
修改脚本:
from Crypto.Cipher import AES |
输出:
┌──(penv)─(zyf㉿zhengyifeng)-[~/Templates] |
成功!
六、启发
- 看到校验逻辑时,永远问”这个关键变量在运行时会被改吗”。静态初始值不等于运行时实际值。
- 工具选择依赖目标识别,目标识别先于工具选择。不知道目标是什么之前,用最轻量的侦察手段,而不是上最重的工具。