首页 > 基础资料 博客日记
第十九届全国大学生信息安全竞赛_babygame:Godot 游戏逆向与 AES 运行时密钥替换
2026-04-01 21:00:02基础资料围观1次
正文开始前,在此夹带一点点“私货”:我最近动手搭了一个属于自己的个人博客!虽然目前还是个“毛坯房”,主题和排版还在慢慢打磨中(可能有些地方功能还不全面、甚至可能报错😅),但这毕竟是我在互联网上的独立小天地。这篇文章也同步归档在我的小站里,除了长篇 WP,我以后也会在那边发一些零碎的踩坑记录。欢迎大家来我的小站串门、挑错。
我的个人博客:https://beini-faxianl.github.io/
一、分析附件
题目给了一个 .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.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="babygame"
run/main_scene="uid://4oheuinvn0ol"
config/features=PackedStringArray("4.5", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
Music="*res://scenes/music.tscn"
Flag="*res://scenes/flag.tscn"
[dotnet]
project/assembly_name="first-game"
[input]
jump={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
[rendering]
textures/canvas_textures/default_texture_filter=0
textures/vram_compression/import_etc2_astc=true
关键信息:
[autoload]
Music="*res://scenes/music.tscn"
Flag="*res://scenes/flag.tscn"
autoload 单例会在游戏启动就创建,直到游戏关闭才销毁。任何场景、任何脚本都可以直接用
Flag.xxx访问它的变量。
找到文件 /scenes/flag.tscn(只能找到 /scenes/flag.tscn.remap):
[remap]
path="res://.godot/exported/133200997/export-ed01e640138f262d3a3519429431a67d-flag.scn"
从之前的后缀说明可以看出这是起重定向的作用,接着找 /.godot/exported/133200997/export-ed01e640138f262d3a3519429431a67d-flag.scn 这个文件,其代码:
[gd_scene load_steps=2 format=3 uid="uid://dkq0of6k4nvab"]
[ext_resource type="Script" uid="uid://dguwugfoefc7s" path="res://scripts/flag.gd" id="1"]
[node name="Flag" type="CenterContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "Please input your flag:"
[node name="FlagTextEdit" type="TextEdit" parent="PanelContainer/VBoxContainer"]
custom_minimum_size = Vector2(800, 42)
layout_mode = 2
theme_override_font_sizes/font_size = 32
[node name="Label2" type="Label" parent="PanelContainer/VBoxContainer"]
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.886275, 0.937255, 0.478431, 1)
theme_override_font_sizes/font_size = 24
text = "you are great~!"
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
[node name="Button" type="Button" parent="PanelContainer/VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(100, 42)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Submit"
[node name="Button2" type="Button" parent="PanelContainer/VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(100, 42)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Back"
[connection signal="ready" from="." to="." method="_on_ready"]
[connection signal="pressed" from="PanelContainer/VBoxContainer/HBoxContainer/Button" to="." method="submit"]
[connection signal="pressed" from="PanelContainer/VBoxContainer/HBoxContainer/Button2" to="." method="back"]
根据内部的英文提示词:
- "you are great~!"
- "Please input your flag:"
我们大致可以猜出获得 flag 的流程应该是:输入 $\to$ 校验 $\to$ 输出
聚焦:
[node name="Label2" type="Label" parent="PanelContainer/VBoxContainer"]
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.886275, 0.937255, 0.478431, 1)
theme_override_font_sizes/font_size = 24
text = "you are great~!"
Label2 并不可见(visible = false),说明游戏中并不能看到关键信息(顺带一提,我的一个队友按要求通关游戏之后,屏幕显示了“flag”,之后的事情,想必大家都知道了 -)。
聚焦文件的开头:
res://scripts/flag.gd
这就是我们在 010 中看到的那个,并且根据后缀可知,这是 GDScript 源码,必然成为重点。
三、AES
点开文件 /scripts/flag.gd(工具中看到的是 flag.gdc,点开后自动反编译成 flag.gd,后面不再提及此时,请自行注意):
extends CenterContainer
@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit
@onready var label2: Node = $PanelContainer / VBoxContainer / Label2
static var key = "FanAglFanAglOoO!"
var data = ""
func _on_ready() -> void :
Flag.hide()
func get_key() -> String:
return key
func submit() -> void :
data = flagTextEdit.text
var aes = AESContext.new()
aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer())
var encrypted = aes.update(data.to_utf8_buffer())
aes.finish()
if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d":
label2.show()
else:
label2.hide()
func back() -> void :
get_tree().change_scene_to_file("res://scenes/menu.tscn")
关键信息:
- AES,模式为 ECB,并且告知密钥为
FanAglFanAglOoO! label2.show():我们期望看到的秘密- 检验方式:
if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d"
总结一下就是,需要对 ciphertext 进行 AES 解密,将解密后的结果作为输入,以此来使验证成立,最终显示出秘密数据。
四、坑点
坑点出现了,根据已有信息进行解密输出的结果是不可读的:
尝试:
from Crypto.Cipher import AES
key = b"FanAglFanAglOoO!"
ciphertext = "d458af702a680ae4d089ce32fc39945d"
cipher = AES.new(key,AES.MODE_ECB)
ciphertext = bytes.fromhex(ciphertext)
plaintext = cipher.decrypt(ciphertext)
print(plaintext)
输出:
┌──(penv)─(zyf㉿zhengyifeng)-[~/Templates]
└─$ python decode.py
b'i1$\xa2\xf1\x84\xb3\xe5\xda\x0f\x89\\!bC\xf7'
密文是写死的,但是回想初始 key 是 static var,挂在全局单例 Flag 上,理论上可以被任何脚本修改。因此不能只看 flag.gd,需要排查所有可能修改 Flag.key 的脚本。
回到题目的提示信息上"请找出隐藏的Flag。请注意只有收集了所有的金币,才能验证flag。"
难道和金币有关系?
金币的英文 coin,通过文件名找到 coin.gdc:
extends Area2D
@onready var game_manager: Node = %GameManager
@onready var animation_player: AnimationPlayer = $AnimationPlayer
func _on_body_entered(body: Node2D) -> void :
game_manager.add_point()
animation_player.play("pickup")
这里调用了 add_point() 这个方法,去找找 game_manager 这个类。
根据文件名可以直接定位(game_manager,.gdc),其代码:
extends Node
@onready var fan = $"../Fan"
var score = 0
func add_point():
score += 1
if score == 1:
Flag.key = Flag.key.replace("A", "B")
fan.visible = true
拨云见日,原来当金币+1的时候,就会将 Flag.key 中的"A"都换成"B",那么吃完所有的金币 score 值必然大于等于 1,换言之 key 必然变成:
FanBglFanBglOoO!
五、获得 Flag
修改脚本:
from Crypto.Cipher import AES
key = b"FanBglFanBglOoO!"
ciphertext = "d458af702a680ae4d089ce32fc39945d"
cipher = AES.new(key,AES.MODE_ECB)
ciphertext = bytes.fromhex(ciphertext)
plaintext = cipher.decrypt(ciphertext)
print(plaintext)
输出:
┌──(penv)─(zyf㉿zhengyifeng)-[~/Templates]
└─$ python decode.py
b'wOW~youAregrEaT!'
成功!
六、启发
- 看到校验逻辑时,永远问"这个关键变量在运行时会被改吗"。静态初始值不等于运行时实际值。
- 工具选择依赖目标识别,目标识别先于工具选择。不知道目标是什么之前,用最轻量的侦察手段,而不是上最重的工具。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
相关文章
最新发布
- OpenAI 官方出手:把 Codex 接进 Claude Code
- 【OpenClaw】通过 Nanobot 源码学习架构---(2)外层控制逻辑
- 【Pwn】堆学习之glibc2.31下的__free_hook劫持
- Spring with AI (6): 记忆保持——会话与长期记忆
- 实际的 c++2026
- 第十九届全国大学生信息安全竞赛_babygame:Godot 游戏逆向与 AES 运行时密钥替换
- 关于列式存储(Column-base Storage)的几个要点解读
- 同样都是九年义务教育,他知道的AI算力科普好像比我多耶
- Slickflow 与 OpenClaw 结合实践:技术原理、集成方式与 Skill 说明
- 如何使用 UEFI Shell 执行 Hello World 程序

