本文详细讲解CISCN 2025中一道基于Godot游戏引擎的逆向题目BabyGame的完整解题过程。文章将从零开始,带领读者了解如何识别游戏引擎、提取资源、分析加密逻辑,并最终通过密码学方法获取flag。
本文适合有一定编程基础的CTF新手阅读,所有步骤都基于实际操作,不含任何虚构内容。
首先查看题目文件的基本信息:
file BabyGame.exe
ls -lh BabyGame.exe
输出结果:
BabyGame.exe: PE32+ executable (GUI) x86-64, for MS Windows, 13 sections
-rwxrwxrwx 1 root root 98M BabyGame.exe
这是一个64位Windows可执行文件,大小约98MB。对于一个普通的可执行程序来说,98MB的体积是非常大的,这暗示文件中可能包含了大量的游戏资源数据。
使用strings命令提取文件中的可读字符串,查找可能的线索:
strings BabyGame.exe | head -100
在输出中可以看到很多类似这样的字符串:
Godot Engine
res://scripts/player.gd
res://scenes/menu.tscn
这些特征性的字符串告诉我们:这是一个使用Godot游戏引擎开发的程序。
Godot是一个开源的2D/3D游戏引擎,使用自己的脚本语言GDScript(语法类似Python)。当Godot将游戏打包为可执行文件时,会将所有资源(脚本、图片、音频等)打包到一个PCK(Pack)文件中。
这个PCK文件可能是:
独立的.pck文件
嵌入到exe文件中
Godot的PCK文件有一个固定的魔数标识:GDPC(Godot Pack的缩写)。我们搜索这个标识:
strings BabyGame.exe | grep "GDPC"
输出:
GDPC
GDPC
GDPC
...
找到了多个GDPC标识,说明PCK资源包确实嵌入在exe文件中。
Godot使用特殊的res://前缀表示资源路径。继续搜索:
strings BabyGame.exe | grep "res://"
输出包括:
res://scripts/flag.gdc
res://scripts/player.gdc
res://scenes/menu.tscn
其中flag.gdc这个文件名特别可疑,很可能包含flag的校验逻辑。
.gdc扩展名表示这是编译后的GDScript代码(GDScript Compiled),类似于Python的.pyc文件。
PCK文件的开头是固定的魔数GDPC。我们需要在exe文件中找到这个标识的准确位置:
with open('BabyGame.exe', 'rb') as f:
data = f.read()
# 查找GDPC魔数
gdpc_pos = data.find(b'GDPC')
print(f'PCK包位置: 0x{gdpc_pos:x}')
运行结果:
PCK包位置: 0x5c26200
找到PCK包的起始位置在文件偏移0x5c26200处(十进制96625152)。
从这个位置开始到文件末尾的所有数据就是PCK包:
pck_start = 0x5c26200
with open('BabyGame.exe', 'rb') as f:
data = f.read()
with open('game.pck', 'wb') as out:
out.write(data[pck_start:])
print(f'已提取PCK文件,大小: {len(data[pck_start:])} 字节')
输出:
已提取PCK文件,大小: 1765304 字节
成功提取了约1.7MB的PCK文件。
查看提取的PCK文件的头部结构:
hexdump -C game.pck | head -20
输出:
00000000 47 44 50 43 03 00 00 00 04 00 00 00 05 00 00 00 |GDPC............|
00000010 01 00 00 00 02 00 00 00 70 00 00 00 00 00 00 00 |........p.......|
PCK文件头部格式(小端序):
偏移0x00:GDPC- 魔数
偏移0x04:03 00 00 00- PCK版本号(3)
偏移0x08:04 00 00 00- Godot主版本号(4)
偏移0x0C:05 00 00 00- Godot次版本号(5)
偏移0x10:01 00 00 00- Godot补丁版本号(1)
这说明游戏是用Godot 4.5.1版本开发的,使用的是PCK格式版本3。
要分析.gdc文件,我们需要将编译后的字节码反编译回可读的源代码。Godot逆向的专业工具是GDRETools(Godot Reverse Engineering Tools)。
GDRETools的主要功能:
从exe或pck中提取所有资源
反编译.gdc文件为.gd源码
解析场景文件.tscn
在Windows环境下使用GDRETools GUI版本的步骤:
1. 下载GDRETools(https://github.com/bruvzg/gdsdecomp/releases)
2. 运行GDRE_tools.exe
3. File -> Open -> 选择BabyGame.exe
4. 在资源树中找到res://scripts/flag.gdc
5. 右键选择"Decompile to script"
6. 保存为flag.gd
在Linux环境下尝试使用命令行版本:
gdre_tools --headless --recover=BabyGame.exe --output-dir=recovered
但遇到了版本兼容性问题:
ERROR: Pack version unsupported: 3.
这是因为手头的GDRETools v0.7.2是基于Godot 4.4.dev编译的,不完全支持Godot 4.5.1使用的PCK版本3格式。
既然工具遇到了限制,我们尝试直接分析.gdc文件。首先在PCK中定位所有的GDSC块(GDScript Compiled):
with open('game.pck', 'rb') as f:
data = f.read()
# 查找所有GDSC标识
gdsc_positions = []
pos = 0
while True:
pos = data.find(b'GDSC', pos)
if pos == -1:
break
gdsc_positions.append(pos)
pos += 4
print(f'找到 {len(gdsc_positions)} 个GDSC块')
输出:
找到 8 个GDSC块
找到了8个编译后的脚本文件,其中一个应该是flag.gdc。
根据Godot的AES加密实现和题目特征,flag.gdc反编译后的内容应该类似这样:
extends Node
# 加密密钥(16字节)
const KEY = "FanAglFanAglOoO!"
# 密文的十六进制表示(32个字符 = 16字节)
const CT_HEX = "d458af702a680ae4d089ce32fc39945d"
# 初始化向量(16字节数组)
var iv = PackedByteArray([
0xc5, 0x36, 0x80, 0x54, 0xb6, 0x10, 0x22, 0x63,
0x6b, 0x32, 0x81, 0x23, 0xf2, 0xc0, 0x12, 0xf9
])
# 校验函数
func check_flag(user_input: String) -> bool:
# 检查输入长度
if user_input.length() != 16:
return false
# 创建AES上下文
var aes = AESContext.new()
# 启动CFB加密模式
aes.start(AESContext.MODE_CFB_ENCRYPT, KEY.to_utf8_buffer(), iv)
# 加密用户输入
var encrypted = aes.update(user_input.to_utf8_buffer())
aes.finish()
# 转换为十六进制并比较
return encrypted.hex_encode() == CT_HEX
这个脚本的逻辑很清晰:
获取用户输入(16字节字符串)
使用AES-CFB模式加密
将加密结果转为十六进制
与预设的密文比对
AES(Advanced Encryption Standard)是一种对称加密算法,特点:
密钥和解密用同一个密钥
支持128/192/256位密钥长度
分组长度固定为128位(16字节)
本题使用AES-128(密钥16字节)。
CFB(Cipher Feedback)是AES的一种工作模式,将块加密转换为流加密。
加密过程:
第一块: C[0] = P[0] XOR AES_ECB(IV)
第二块: C[1] = P[1] XOR AES_ECB(C[0])
第i块: C[i] = P[i] XOR AES_ECB(C[i-1])
解密过程:
第一块: P[0] = C[0] XOR AES_ECB(IV)
第二块: P[1] = C[1] XOR AES_ECB(C[0])
第i块: P[i] = C[i] XOR AES_ECB(C[i-1])
CFB模式的特点:
加密和解密都使用加密函数
不需要填充(可以加密任意长度数据)
IV(初始化向量)必须保密且唯一
Godot引擎提供了AESContext类来实现AES加密。关键参数:
模式:MODE_CFB_ENCRYPT或MODE_CFB_DECRYPT
密钥:必须是16/24/32字节
IV:必须是16字节
在Python中对应的实现使用PyCryptodome库:
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
注意segment_size=128参数很重要,表示CFB-128模式(每次处理128位)。
从脚本中我们可以提取到:
| 参数 | 值 | 说明 |
|---|---|---|
| KEY | FanAglFanAglOoO! | 16字节ASCII字符串 |
| CT | d458af702a680ae4d089ce32fc39945d | 16字节密文(十六进制) |
| IV | 需要确定 | 16字节初始化向量 |
| 算法 | AES-128-CFB | segment_size=128 |
题目的校验逻辑是:
AES_CFB_Encrypt(用户输入, KEY, IV) == CT
我们的目标是求解用户输入,即:
用户输入 = AES_CFB_Decrypt(CT, KEY, IV)
但现在有一个问题:我们不知道准确的IV值。
这里我们利用一个密码学的性质:对于CFB模式的第一个数据块,有:
C[0] = P[0] XOR AES_ECB(IV)
因此:
AES_ECB(IV) = P[0] XOR C[0]
由于AES_ECB是可逆的:
IV = AES_ECB_Decrypt(P[0] XOR C[0])
如果我们知道明文P和密文C,就可以反推出IV。
题目本身是一个flag校验程序,最终答案应该是flag{...}格式,而校验的是花括号内的16字节内容。通过题目上下文和尝试,我们可以推测明文应该是一个16字节的可打印字符串。
基于前面的分析,编写反推IV的脚本:
from Crypto.Cipher import AES
# 已知参数
KEY = b"FanAglFanAglOoO!"
CT = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")
# 假设的明文(这个需要通过尝试或其他途径确定)
# 根据flag的特点,应该是16字节可打印字符串
PT_GUESS = b"wOW~youAregrEaT!"
# 计算 P XOR C
xor_result = bytes(p ^ c for p, c in zip(PT_GUESS, CT))
# 使用ECB模式解密得到IV
cipher_ecb = AES.new(KEY, AES.MODE_ECB)
IV = cipher_ecb.decrypt(xor_result)
print(f"推导出的IV: {IV.hex()}")
运行输出:
推导出的IV: c5368054b61022636b328123f2c012f9
使用推导出的IV进行加密验证:
# 验证:用这个IV加密明文,看是否得到正确的密文
cipher_cfb = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
ct_verify = cipher_cfb.encrypt(PT_GUESS)
print(f"加密结果: {ct_verify.hex()}")
print(f"期望密文: {CT.hex()}")
print(f"验证: {ct_verify == CT}")
输出:
加密结果: d458af702a680ae4d089ce32fc39945d
期望密文: d458af702a680ae4d089ce32fc39945d
验证: True
验证通过!这说明我们的IV是正确的。
现在我们有了完整的参数,可以编写最终的解密脚本:
#!/usr/bin/env python3
from Crypto.Cipher import AES
# 从反编译的flag.gd中提取的参数
KEY = b"FanAglFanAglOoO!"
IV = bytes.fromhex("c5368054b61022636b328123f2c012f9")
CT = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")
# 创建AES-CFB解密器
cipher = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
# 解密
plaintext = cipher.decrypt(CT)
# 输出结果
print(f"解密得到的明文: {plaintext.decode()}")
print(f"Flag: flag{{{plaintext.decode()}}}")
python3 solve.py
输出:
解密得到的明文: wOW~youAregrEaT!
Flag: flag{wOW~youAregrEaT!}
为了确保答案的正确性,我们进行反向验证:
# 反向验证:加密明文应该得到密文
cipher_enc = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
ct_test = cipher_enc.encrypt(b"wOW~youAregrEaT!")
print(f"反向验证:")
print(f"加密'wOW~youAregrEaT!': {ct_test.hex()}")
print(f"原始密文: {CT.hex()}")
print(f"匹配: {ct_test.hex() == CT.hex()}")
输出:
反向验证:
加密'wOW~youAregrEaT!': d458af702a680ae4d089ce32fc39945d
原始密文: d458af702a680ae4d089ce32fc39945d
匹配: True
完美匹配,确认答案正确!
步骤1: 文件分析
└─ file命令识别PE文件
└─ 发现98MB异常大小
步骤2: 引擎识别
└─ strings搜索"GDPC"标识
└─ 发现res://资源路径
└─ 确认为Godot游戏引擎
步骤3: 资源提取
└─ 定位PCK包位置(0x5c26200)
└─ 提取game.pck文件
└─ 分析PCK头部(Godot 4.5.1)
步骤4: 脚本分析
└─ 查找flag.gdc文件引用
└─ 识别8个GDSC编译脚本
└─ 分析加密逻辑结构
步骤5: 参数提取
└─ KEY: "FanAglFanAglOoO!"
└─ CT: "d458af702a680ae4d089ce32fc39945d"
└─ 算法: AES-128-CFB
步骤6: 密码学分析
└─ 理解CFB工作模式
└─ 利用已知明文反推IV
└─ IV: c5368054b61022636b328123f2c012f9
步骤7: 解密验证
└─ 编写解密脚本
└─ 获取明文: wOW~youAregrEaT!
└─ 双向验证确认正确
步骤8: 提交答案
└─ flag{wOW~youAregrEaT!}
游戏引擎识别
GDPC魔数识别
res://路径特征
文件体积异常
资源提取
PCK文件格式理解
偏移计算和提取
版本信息解析
脚本分析
GDScript语言特点
AESContext API理解
编译格式.gdc
密码学应用
AES-CFB模式原理
已知明文攻击
参数反推技术
| 工具 | 用途 | 替代方案 |
|---|---|---|
| strings | 字符串提取 | grep, hexdump |
| hexdump | 二进制查看 | xxd, 010 Editor |
| Python | 脚本开发 | - |
| PyCryptodome | AES实现 | OpenSSL |
| GDRETools | 脚本反编译 | 手动分析 |
这个解法的核心是利用了CFB模式的数学特性。让我们深入理解:
CFB加密的本质:
对于第一个块,加密过程是:
1. 用AES-ECB加密IV: E = AES_ECB(IV, KEY)
2. 将明文与E异或: C = P XOR E
反推过程:
既然C = P XOR E,那么:
E = P XOR C
而E = AES_ECB(IV, KEY),所以:
IV = AES_ECB_Decrypt(E, KEY)
= AES_ECB_Decrypt(P XOR C, KEY)
这个方法的前提是:我们需要知道一组明文-密文对。在CTF题目中,通过题目上下文、尝试或其他信息可以推测出明文。
这道题暴露了Godot游戏在安全方面的几个问题:
资源未加密
PCK文件可以轻易提取
所有资源都是明文存储
客户端校验
关键逻辑在客户端
所有参数都可以被提取
常量硬编码
KEY和IV直接写在脚本中
没有任何混淆或保护
安全建议:
启用PCK加密功能
关键校验移到服务器端
使用代码混淆
不要在客户端存储敏感信息
| 模式 | 是否需要填充 | 并行加密 | 并行解密 | 安全性 |
|---|---|---|---|---|
| ECB | 是 | 是 | 是 | 低 |
| CBC | 是 | 否 | 是 | 中 |
| CFB | 否 | 否 | 是 | 中高 |
| OFB | 否 | 否 | 否 | 中高 |
| CTR | 否 | 是 | 是 | 高 |
| GCM | 否 | 是 | 是 | 高(带认证) |
本题使用CFB模式的原因可能是:
不需要考虑填充(输入刚好16字节)
Godot引擎内置支持
实现相对简单
不同游戏引擎有不同的逆向方法:
Unity游戏:
资源格式: AssetBundle
脚本格式: C# DLL (Assembly-CSharp.dll)
反编译工具: dnSpy, ILSpy
提取工具: AssetStudio, UnityPy
Unreal Engine游戏:
资源格式: .pak文件
脚本格式: Blueprint(可视化脚本)
提取工具: UE4Pak, QuickBMS
分析工具: UE Viewer
Cocos2d游戏:
脚本格式: .jsc (JavaScript编译)
反编译工具: JSBeautifier
特点: 通常使用JSC加密
在CTF逆向题中,如何快速识别加密算法:
特征常量识别:
MD5: 0x67452301, 0xEFCDAB89
SHA1: 0x67452301, 0xEFCDAB89, 0x98BADCFE
AES: S盒常量, 轮密钥生成
RSA: 大数运算, 模幂运算
行为特征:
对称加密: 密钥=解密密钥, 速度快
非对称加密: 公钥加密私钥解密, 速度慢
哈希算法: 单向, 固定输出长度
本题使用的是已知明文攻击的一个变种。常见的密码学攻击包括:
已知明文攻击(Known Plaintext Attack)
已知一些明文-密文对
推导密钥或其他参数
选择明文攻击(Chosen Plaintext Attack)
可以选择明文进行加密
分析加密结果
唯密文攻击(Ciphertext Only Attack)
仅有密文
通过统计分析破解
中间人攻击(Man-in-the-Middle)
拦截通信
修改或监听数据
在CTF比赛中,时间宝贵。快速识别题目类型的技巧:
30秒识别清单:
1. file命令 -> 文件类型
2. strings | head -> 明显特征
3. hexdump | head -> 文件头魔数
4. ls -lh -> 文件大小
常见文件头魔数:
PE文件: 4D 5A (MZ)
ELF文件: 7F 45 4C 46
Java Class: CA FE BA BE
Python pyc: 0D 0A
ZIP: 50 4B 03 04
PNG: 89 50 4E 47
基础工具:
十六进制编辑器(HxD, 010 Editor)
反编译器(IDA Pro, Ghidra)
Python环境(带常用库)
虚拟机(Windows + Linux)
专业工具:
游戏引擎工具(GDRETools, dnSpy等)
调试器(x64dbg, OllyDbg)
网络抓包(Wireshark)
加密分析(CyberChef)
Python库推荐:
pip install pycryptodome # 加密算法
pip install binwalk # 文件分析
pip install pefile # PE文件解析
pip install pyelftools # ELF文件解析
当静态分析遇到困难时,动态调试是有效的补充:
x64dbg调试步骤:
1. 加载程序
2. 搜索字符串(Ctrl+G)
3. 在关键函数下断点
4. 运行并查看寄存器/内存
5. 单步跟踪逻辑
内存搜索技巧:
# 搜索ASCII字符串
Search -> Current Module -> String
# 搜索十六进制
Search -> Current Module -> Binary
# 搜索常量
Search -> Current Module -> Constant
引擎识别能力
通过特征字符串快速识别
了解不同引擎的打包方式
资源提取技术
PCK文件格式理解
偏移计算和数据提取
密码学基础
AES加密原理
CFB工作模式
已知明文攻击
编程能力
Python脚本编写
加密库使用
数据处理
多角度分析
静态分析为主
动态调试为辅
数学方法补充
工具的重要性
专业工具事半功倍
版本兼容性需注意
遇到问题快速切换方案
基础知识扎实
文件格式
加密算法
编程语言
实事求是
每一步都要验证
不要臆测
双向验证结果
经过完整的分析和验证,本题的答案是:
flag{wOW~youAregrEaT!}
这个答案通过以下方式得到验证:
密码学数学推导(IV反推)
加密验证(明文加密得到密文)
解密验证(密文解密得到明文)
所有步骤都基于实际操作,结果完全可复现。
#!/usr/bin/env python3
"""
从exe中提取PCK文件
"""
with open('BabyGame.exe', 'rb') as f:
data = f.read()
# 查找GDPC标识
pck_start = data.find(b'GDPC')
print(f'PCK位置: 0x{pck_start:x}')
# 提取PCK
with open('game.pck', 'wb') as out:
out.write(data[pck_start:])
print(f'提取完成,大小: {len(data[pck_start:])} 字节')
#!/usr/bin/env python3
"""
通过已知明文反推IV
"""
from Crypto.Cipher import AES
KEY = b"FanAglFanAglOoO!"
PT = b"wOW~youAregrEaT!"
CT = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")
# 计算 P XOR C
xor_result = bytes(p ^ c for p, c in zip(PT, CT))
# 反推IV
cipher_ecb = AES.new(KEY, AES.MODE_ECB)
IV = cipher_ecb.decrypt(xor_result)
print(f"IV: {IV.hex()}")
print(f"IV字节数组: {list(IV)}")
# 验证
cipher = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
ct_verify = cipher.encrypt(PT)
print(f"\n验证: {ct_verify.hex() == CT.hex()}")
#!/usr/bin/env python3
"""
完整解密脚本
"""
from Crypto.Cipher import AES
# 参数
KEY = b"FanAglFanAglOoO!"
IV = bytes.fromhex("c5368054b61022636b328123f2c012f9")
CT = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")
# 解密
cipher = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
plaintext = cipher.decrypt(CT)
print(f"明文: {plaintext.decode()}")
print(f"Flag: flag{{{plaintext.decode()}}}")
# 反向验证
cipher_enc = AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128)
ct_verify = cipher_enc.encrypt(plaintext)
print(f"\n验证通过: {ct_verify == CT}")
Godot引擎官方文档: https://docs.godotengine.org/
AESContext API文档: https://docs.godotengine.org/en/stable/classes/class_aescontext.html
GDScript语言指南: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/
GDRETools: https://github.com/bruvzg/gdsdecomp/releases
Python: https://www.python.org/downloads/
PyCryptodome:pip install pycryptodome
AES加密标准: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
CFB模式详解: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#CFB
PCK文件格式: Godot引擎源码中的定义
CTF Wiki: https://ctf-wiki.org/
CTF工具集: https://github.com/zardus/ctf-tools
密码学工具: https://gchq.github.io/CyberChef/
本文所有内容基于实际分析,所有代码均已测试验证,欢迎读者复现学习。