一、题目信息 给了一个apk和说明文档,让小车和方块碰撞可以弹flag(这车是真难开)
题目要求是拿到flag(20)以及逆向flag生成算法(40+40)
运行游戏,通过启动页面判断采用的是Godot引擎
GDScript 是 Godot 开发的编程语言
Godot 可以同时使用 C# 和 GDScript 编程,两者在使用时差别不大,性能差距可以忽略,GDScript 更全能一些,C# 目前还不支持 Android 平台
另外,GDScript 在运行时不会被编译,C# 会被编译。GDScript 在运行时会被 GDScript VM 调用并直接执行
要拿flag得让车碰到绿方块,但它在房顶上,只能开挂拿,黄色方块用于生成测试flag
二、逆向过程 环境:
设备: Google Pixel 4, Android 10, root
工具: IDA Pro, Frida 17.2.11, jadx, gdsdecomp,
apk解包后文件结构如下
1 2 3 4 5 6 7 8 9 10 11 12 preliminary/ ├── assets/ │ ├── token.gdc # Token 生成逻辑 (编译后 GDScript) │ ├── label2.gdc # Flag 显示逻辑 │ ├── Trigger/trigger.gdc # 触发方块逻辑 (核心) │ ├── ext/sec2026.gdextension # GDExtension 配置 │ └── project.binary # Godot 项目配置 ├── lib/arm64-v8a/ │ ├── libsec2026.so # ★ 核心目标: 自定义安全库 (462KB) │ ├── libgodot_android.so # Godot 引擎运行时 (67MB) │ └── libc++_shared.so # C++ 标准库 └── classes.dex # Java/Kotlin 层
首先想到的是使用gdsdecomp 反编译项目直接拿到GDScript
但是报错
1 2 3 4 5 ERROR: The MD5 sum of the decrypted file does not match the expected value. at: FileAccessEncrypted::open_and_parse (core\io\file_access_encrypted.cpp:113) ERROR: Can't open encrypted pack directory (PCK format version 3, engine version 4.5.1). ERROR: FATAL ERROR: Cannot open encrypted pck! (wrong key?) ERROR: Can't load project!
从报错信息得到以下信息
PCK 是加密的 — 用了 Godot 内置的 FileAccessEncrypted 加密机制
PCK 格式版本 3,引擎版本 4.5.1
加密方式是 AES-256-CFB,密钥是编译时嵌入的 script_encryption_key
MD5 校验失败 — 说明提供的 key 不对
意思是现在缺一个key,也就是AES-256-CFB的key
godot是开源项目,应该是魔改了libgodot_android.so,IDA分析发现抹了符号,先去官网godotengine/godot 下一份带符号的so和源码
寻找AES解密的key AI说主要在这两个类里找
其实根据前面的报错信息FileAccessEncrypted::open_and_parse (core\io\file_access_encrypted.cpp:113)也可以确定这个方法
在 core/io/file_access_encrypted.cpp:104看到
搜 script_encryption_key 全局引用,在 core/io/file_access_pack.cpp:300看到PCK 解密直接用全局数组script_encryption_key[32]
在 core/core_builders.py:68 看到:
这个 key 是编译时从环境变量 SCRIPT_AES256_ENCRYPTION_KEY(64 位 hex 字符串)读取,硬编码进 libgodot_android.so 的全局变量里。
由此确定调用链:file_access_pack.cpp → open_and_parse() → ctx.set_encode_key() → 底层 mbedtls_aes_setkey_enc()。
接下来就是找这几个函数在so里的偏移,对比分析下来信息如下:(bindiff失败了,文件太大,电脑直接卡死)
mbedtls_aes_setkey_enc (0x197C210):有明显的AES加密痕迹,sbox等
thunk (0x197BD08) — PLT/GOT 跳板
AESContext::set_encode_key (0x376EDFC)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function waitForModule (name, callback ) { var mod = Process .findModuleByName (name); if (mod) { callback (mod); return ; } var interval = setInterval (function ( ) { mod = Process .findModuleByName (name); if (mod) { clearInterval (interval); callback (mod); } }, 200 ); } waitForModule ("libgodot_android.so" , function (mod ) { var aes_setkey_enc = mod.base .add (0x197C210 ); Interceptor .attach (aes_setkey_enc, { onEnter : function (args ) { var key_ptr = args[1 ]; var keybits = args[2 ].toInt32 (); var keyLen = keybits / 8 ; console .log ("\n[mbedtls_aes_setkey_enc] keybits=" + keybits); console .log (hexdump (key_ptr, { length : keyLen, header : true , ansi : true })); } }); });
key:ce4df8753b59a5a39ade58ac07ef947a3da39f2af75e3284d51217c04d49a061
拿去解密,发现还是不对
没想明白为什么,选择直接去内存dump解密后的gd文件
dump解密后的文件
每个加密的 .gdc/.scn 文件被 Godot 加载时都会经过 FileAccessEncrypted,析构时被 dump 出来。最终 dump 出的 decrypted_0.bin ~ decrypted_N.bin就是解密后的原始 .gdc 文件,可以直接用 gdsdecomp 反编译。
hook 的是 FileAccessEncrypted 的析构函数 ~FileAccessEncrypted,当 Godot 读完一个加密文件后会销毁对象,此时内部的 data 缓冲区里还保存着完整的解密后明文。
file_access_encrypted.h 里的成员声明顺序,在apk的libgodot_android.so里找到对象,分析vtable确认偏移 0x3802984
偏移
类型
字段名
说明
+0
ptr
vtable
虚表指针
+308
byte
big_endian
大端序标志
+400
Vector
data
解密后数据缓冲区
+408
uint64
pos
当前读写位置
1 2 3 4 5 6 7 8 9 10 11 12 13 waitForModule ("libgodot_android.so" , function (mod ) { Interceptor .attach (mod.base .add (0x3802984 ), { onEnter : function (args ) { var inst = ptr (args[0 ]); var data_ptr = inst.add (400 ).readPointer (); var sizeNum = data_ptr.sub (8 ).readU32 (); var fd = new File (DUMP_DIR + "decrypted_" + count + ".bin" , "wb" ); fd.write (data_ptr.readByteArray (sizeNum)); fd.close (); } }); });
GDScript 反编译及分析 使用 gdsdecomp 工具对 dump 出的 .gdc 文件进行反编译:
1 gdre_tools.exe --headless --decompile=token.gdc --bytecode=4.5.1 --output=decompiled/
成功反编译 7 个脚本文件:token.gd, trigger.gd, label2.gd, vehicle.gd, car_select.gd, follow_camera.gd, spedometer.gd
文件
继承
作用
与 flag 的关系
token.gd
Label
启动时生成 8 位随机 hex token
直接相关 — token 是 flag 算法的输入
trigger.gd
Area3D
方块碰撞检测 + 浮动动画,碰撞时调用 GameExtension.Process()
核心 — flag 生成的调用入口
label2.gd
Label
连接 Trigger 的信号,显示 flag 文本
显示层,无计算逻辑
Token 生成逻辑 (token.gd) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 extends Label const TOKEN_LEN = 8 const CHARS = "0123456789abcdef" var rng = RandomNumberGenerator.new() func generate_token(len: int) -> String: var s = "" for i in len: var idx = rng.randi_range(0, CHARS.length() - 1) s += CHARS[idx] return s func _ready() -> void: rng.randomize() text = "Token: " + generate_token(TOKEN_LEN)
Token 是 8 位随机小写 hex 字符串。
核心调用链 (trigger.gd) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 extends Area3D signal collided_with(name) const FLAG_PREFIX: = "sec2026_PART1_" var obj func _ready() -> void : obj = GameExtension.new() func xor_enc(plain: String) -> PackedByteArray: var out_buf = plain.to_utf8_buffer() if out_buf.size() < 8: out_buf.resize(8) var result = out_buf.slice(0, 8) for i in range(7): result[i] = result[i] ^ result[i + 1] result[7] = result[7] ^ result[0] return result func _process(delta): # ... 方块动画 ... var body = get_overlapping_bodies() if body.size() > 0: if str(get_path()) == "/root/TownScene/Trigger2": var label = get_node("/root/TownScene/Label2") var label1 = get_node("/root/TownScene/Label") var flag1 = obj.Process(xor_enc(str(label1.text).substr(7))) label.text = "flag{" + FLAG_PREFIX + flag1 + "} "
总结一下就是
1 2 3 const FLAG_PREFIX: = "sec2026_PART1_" var flag1 = obj.Process(xor_enc(str(label1.text).substr(7))) label.text = "flag{" + FLAG_PREFIX + flag1 + "} "
现在还差Process()方法,里面应该就是核心算法了。
不过还是先开挂拿flag,trigger.gd说明碰了绿方块才会调用Process()方法
改方块坐标拿flag 找指针 trigger.gd 的 _process() 每帧执行 $MeshInstance3D.position.y = height(浮动动画),这会调用引擎的 Node3D::set_position。所以 hook 这个函数,就能在每帧回调里拿到 MeshInstance3D 子节点的 this 指针。
寻找Node::get_parent()的汇编可以直接拿到parent字段在 Node 对象里的绝对偏移
写脚本,主要是把两个方块交换下位置方便触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Interceptor .attach (setPosition, { onEnter : function (args ) { if (done) return ; var thisPtr = args[0 ]; var name = readStringName (thisPtr.add (0x1E0 )); if (name !== "MeshInstance3D" ) return ; var parentPtr = thisPtr.add (0x148 ).readPointer (); var parentName = readStringName (parentPtr.add (0x1E0 )); if (parentName === "Trigger2" ) { var vec = Memory .alloc (12 ); vec.writeFloat (-14.1 ); vec.add (4 ).writeFloat (0.5 ); vec.add (8 ).writeFloat (-3.96 ); setPositionFn (parentPtr, vec); console .log ("[+] Trigger2 moved to ground" ); done = true ; } } });
拿到flag
libsec2026.so 壳保护分析 lib里有一个自实现的so库libsec2026.so
导出了 2 个函数:
extension_init @ 0x56D50 — GDExtension 入口
start @ 0x69870 — .init_array 构造函数,SO 加载时自动执行
start里有明显的自解密痕迹:
AI分析如下
start 实现了一个运行时解壳器,通过直接 syscall (SVC 指令) 绕过 libc
1 2 3 4 5 6 7 8 9 10 void start () { int fd = syscall(__NR_openat, 0 , "/proc/self/auxv" , 0 ); read(fd, buf, 0x200 ); close(fd); sub_69984(&dword_69A6C, 4129 , out_buf, &out_size); int memfd = syscall(__NR_memfd_create, "name" , 0 ); write(memfd, decompressed_data, size); void *base = mmap(NULL , size, PROT_READ|PROT_EXEC, MAP_PRIVATE, memfd, 0 ); ((void (*)())(base + 0x10 ))(); }
方案一: Frida hook syscall 同时 hook memfd_create、write、mmap,在壳写入 memfd 时截获解压后的数据。dump下来的elf由于base和数据段缺失很难看,转动调。
方案二: Patch SO + IDA 远程调试 将 start 函数入口 patch 为死循环 (B .),IDA 附加后手动单步跟踪加载器。
动调 经过1次寄存器跳转来到了
sub_7C4AA1A450负责把真正的 extension SO 加载到内存
memfd_create → write 16字节 header → mmap(RX) → close
循环遍历 ELF Program Headers,筛选 PT_LOAD 段,mmap 映射
写入 ARM64 trampoline 指令 (含 BR X10 跳板)
跳转到加载完成的 extension 入口(extension_init)
跳转过来时在动态解密一个数组
解密结果如下
其中Process是gd要调用的方法,现在需要确定Process方法的地址
壳加载器将 extension ELF 的代码段和数据段分别 mmap 到内存,两者相对偏移由 ELF program header 决定,每次运行固定。
数据段包含 XOR 解密后的明文字符串 “Th1s ls n0t a rea1 key!!@sec2026”,可作为锚点定位数据段地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function findExt ( ) { var pattern = "54 68 31 73 20 6C 73 20 6E 30 74 20 61 20 72 65 61 31 20 6B 65 79 21 21 40 73 65 63 32 30 32 36" ; var found = null ; Process .enumerateRanges ("rw-" ).forEach (function (r ) { if (found || r.size < 0x100 || r.size > 0x100000 ) return ; try { var m = Memory .scanSync (r.base , r.size , pattern); if (m.length > 0 ) found = m[0 ].address ; } catch (e) {} }); if (!found) { console .log ("[-] Not found" ); return ; } var dataStrBase = found.sub (0x12 ); var codeBase = dataStrBase.sub (0xA25C0 ); var processAddr = codeBase.add (0x27A8 ); console .log ("[+] Process() @ " + processAddr); } findExt ();
先跑交换位置的脚本,然后通过上面的js计算得到Process的真实地址,再detach用ida附加动调分析
找到Process具体实现
跟踪调试了一下,发现有MBA和类似于控制流平坦化的混淆,直接trace导出让AI看
trace AI分析 以下全是将trace丢给AI后分析出来的结果
混淆分析 extension 代码使用了多种混淆手法:
地址计算混淆 (恒等变换跳板) : 每次间接跳转前都经过混淆 gadget,纯粹干扰静态分析
无标准函数序言 : 没有 STP X29,X30 序言,全部用 BR/BLR 间接跳转
算术混淆 : XOR 被混淆为 (a|b) ^ -(a&b) + 2*((a|b) & -(a&b)) = a^b;自增被混淆为 n += (n&1) + (~n|0xFE) + 2 = n++
数据段分析 运行时解密后:
1 2 3 4 5 +0x04: "Process" ← GDExtension 方法名 +0x12: "Th1s ls n0t a rea1 key!!@sec2026" ← ★ ChaCha20 密钥 (32字节) +0x33: "012345678901" ← ★ ChaCha20 nonce (12字节) +0x40: "%02X" ← flag 输出格式 +0xC0: "/dev/urandom"
Process() 算法逆向 — ChaCha20 识别 核心算法函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void process_core (PackedByteArray input) { pthread_mutex_lock(&global_lock); if (!global_buf) global_buf = malloc (0x100000 ); memset (global_buf, 0 , 0x100000 ); ctx = malloc (0x88 ); memset (ctx, 0 , 0x88 ); chacha20_init(ctx, key = "Th1s ls n0t a rea1 key!!@sec2026" , nonce = "012345678901" , mode = 0 ); uint8_t *data = get_packed_array_ptr(input); size_t size = get_packed_array_size(input); chacha20_encrypt(ctx, data, global_buf, size); String result = "" ; for (int i = 0 ; i < size; i++) { char hex[3 ]; sprintf (hex, "%02X" , global_buf[i]); result += hex; } return result; }
ChaCha20 密钥调度 函数将 32 字节 key 和 12 字节 nonce 以 4 字节为单位加载到 ctx 结构体,同时将加密常量 "expand 32-byte k" 复制到 ctx 开头——这正是 ChaCha20 标准常量 。
ChaCha20 初始状态矩阵 1 2 3 4 5 Column 0 Column 1 Column 2 Column 3 Row 0: 0x61707866 0x3320646F 0x79622D31 0x6B206573 ← 常量 Row 1: 0x73316854 0x20736C20 0x2074306E 0x65722061 ← key[0:16] Row 2: 0x6B203161 0x21217965 0x63657340 0x36323032 ← key[16:32] Row 3: 0x00000000 0x33323130 0x37363534 0x31303938 ← counter + nonce
Quarter Round 识别 从 trace 完整还原了 quarter round 函数,12 步全部精确匹配,确认为标准 ChaCha20 Quarter Round。从 trace 中的函数调用模式确认执行了 10 次 double round (即 ChaCha20 的 20 轮)。
完整算法总结 Token → Flag 完整流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 输入: Token = "b670d101" (8 字符小写 hex) Step 1: xor_enc 预处理 "b670d101" → UTF-8 bytes → 链式 XOR → 8 字节 Step 2: ChaCha20 流密码加密 Key: "Th1s ls n0t a rea1 key!!@sec2026" (32 bytes) Nonce: "012345678901" (12 bytes) Counter: 0 生成 64 字节 keystream,取前 8 字节与输入 XOR Step 3: Hex 格式化 加密后 8 字节 → 大写 hex 字符串 (16 字符) Step 4: 拼接 Flag "flag{sec2026_PART1_" + hex + "}" 输出: flag{sec2026_PART1_2E1E541D26372219}
Flag → Token 逆向流程 ChaCha20 是流密码,加密和解密操作完全相同(都是 XOR keystream)。逆向只需对 flag hex 做一次相同的 ChaCha20 运算,再逆向 xor_enc 即可还原 Token。
C 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 #include <stdio.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #define ROTL32(v, n) (((v) << (n)) | ((v) > > (32 - (n)))) static void quarter_round (uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d) { *a += *b; *d ^= *a; *d = ROTL32(*d, 16 ); *c += *d; *b ^= *c; *b = ROTL32(*b, 12 ); *a += *b; *d ^= *a; *d = ROTL32(*d, 8 ); *c += *d; *b ^= *c; *b = ROTL32(*b, 7 ); } static void chacha20_block (const uint32_t state[16 ], uint8_t out[64 ]) { uint32_t s[16 ]; memcpy (s, state, 64 ); for (int i = 0 ; i < 10 ; i++) { quarter_round(&s[0 ], &s[4 ], &s[8 ], &s[12 ]); quarter_round(&s[1 ], &s[5 ], &s[9 ], &s[13 ]); quarter_round(&s[2 ], &s[6 ], &s[10 ], &s[14 ]); quarter_round(&s[3 ], &s[7 ], &s[11 ], &s[15 ]); quarter_round(&s[0 ], &s[5 ], &s[10 ], &s[15 ]); quarter_round(&s[1 ], &s[6 ], &s[11 ], &s[12 ]); quarter_round(&s[2 ], &s[7 ], &s[8 ], &s[13 ]); quarter_round(&s[3 ], &s[4 ], &s[9 ], &s[14 ]); } for (int i = 0 ; i < 16 ; i++) s[i] += state[i]; memcpy (out, s, 64 ); } static void chacha20_crypt (const uint8_t key[32 ], const uint8_t nonce[12 ], uint32_t counter, uint8_t *data, size_t len) { uint32_t state[16 ]; state[0 ] = 0x61707866 ; state[1 ] = 0x3320646F ; state[2 ] = 0x79622D31 ; state[3 ] = 0x6B206573 ; memcpy (&state[4 ], key, 32 ); state[12 ] = counter; memcpy (&state[13 ], nonce, 12 ); uint8_t keystream[64 ]; for (size_t off = 0 ; off < len; off += 64 ) { chacha20_block(state, keystream); size_t chunk = (len - off < 64 ) ? len - off : 64 ; for (size_t i = 0 ; i < chunk; i++) data[off + i] ^= keystream[i]; state[12 ]++; } } static void xor_enc (const char *token, uint8_t out[8 ]) { for (int i = 0 ; i < 8 ; i++) out[i] = (uint8_t )token[i]; for (int i = 0 ; i < 7 ; i++) out[i] ^= out[i + 1 ]; out[7 ] ^= out[0 ]; } static void xor_dec (const uint8_t enc[8 ], char out[9 ]) { uint8_t t[8 ]; memcpy (t, enc, 8 ); t[7 ] ^= t[0 ]; for (int i = 6 ; i >= 0 ; i--) t[i] ^= t[i + 1 ]; for (int i = 0 ; i < 8 ; i++) out[i] = (char )t[i]; out[8 ] = '\0' ; } static const uint8_t KEY[32 ] = "Th1s ls n0t a rea1 key!!@sec2026" ;static const uint8_t NONCE[12 ] = "012345678901" ;static void token_to_flag (const char *token, char *flag_hex) { uint8_t buf[8 ]; xor_enc(token, buf); chacha20_crypt(KEY, NONCE, 0 , buf, 8 ); for (int i = 0 ; i < 8 ; i++) sprintf (flag_hex + i * 2 , "%02X" , buf[i]); } static void flag_to_token (const char *flag_hex, char *token) { uint8_t buf[8 ]; for (int i = 0 ; i < 8 ; i++) sscanf (flag_hex + i * 2 , "%02X" , (unsigned *)&buf[i]); chacha20_crypt(KEY, NONCE, 0 , buf, 8 ); xor_dec(buf, token); } int main (int argc, char **argv) { if (argc == 2 && strcmp (argv[1 ], "test" ) == 0 ) { char hex[17 ] = {0 }, token[9 ] = {0 }; token_to_flag("b670d101" , hex); printf ("enc b670d101 -> %s (expect 2E1E541D26372219)\n" , hex); flag_to_token("2E1E541D26372219" , token); printf ("dec 2E1E541D26372219 -> %s (expect b670d101)\n" , token); return 0 ; } if (argc < 3 ) { printf ("Usage:\n" ); printf (" %s enc <token> Token(8 hex) -> Flag\n" , argv[0 ]); printf (" %s dec <flag_hex> Flag(16 hex) -> Token\n" , argv[0 ]); return 1 ; } if (strcmp (argv[1 ], "enc" ) == 0 ) { char hex[17 ] = {0 }; token_to_flag(argv[2 ], hex); printf ("flag{sec2026_PART1_%s}\n" , hex); } else if (strcmp (argv[1 ], "dec" ) == 0 ) { char token[9 ] = {0 }; flag_to_token(argv[2 ], token); printf ("Token: %s\n" , token); } return 0 ; }
编译与测试 1 2 3 4 5 6 7 8 9 10 gcc -o solve solve.c -O2 ./solve test ./solve enc b670d101 ./solve dec 2E1E541D26372219
总结 frida脚本
脚本
用途
dump_decrypted_pck.js
Hook FileAccessEncrypted 析构函数 dump 解密后的 PCK 数据
dump_pck_key.js
Hook mbedtls_aes_setkey 捕获 PCK 加密密钥
find_extension.js
搜索假 key 字符串自动定位 Process() 地址
teleport_trigger2.js
自动找到 Trigger2 并提供坐标修改功能
hook_process_call.js
批量 hook extension 函数入口定位 Process()
find_process_func.js
搜索 “Process” 字符串定位并 hook Process()
多层壳保护 : libsec2026.so 使用 syscall 级别的壳加载器,解压 → memfd → mmap → 跳转执行
PCK 加密 : Godot 资源文件使用 AES-256-CFB 加密,密钥编译时嵌入引擎 SO
代码混淆 : extension 代码使用地址计算混淆、间接跳转、算术混淆等多种手法
无符号信息 : 脱壳后的代码没有任何符号,所有函数通过间接跳转调用
动态地址 : ASLR 导致每次运行地址不同,需要运行时定位
解题流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 APK 解包 → jadx 分析 Java 层 (排除干扰) ↓ 识别 Godot 引擎 + GDExtension 机制 ↓ Frida hook AES key → 获取 PCK 加密密钥 ↓ dump + 反编译 GDScript → 还原调用链 (xor_enc → Process) ↓ IDA 分析 libsec2026.so → 识别壳保护 ↓ Frida dump 脱壳 payload → IDA 分析 extension 代码 ↓ Frida 传送 Trigger2 + 定位 Process 地址 ↓ IDA 动调 Process() → instruction trace ↓ 分析 trace → 识别 ChaCha20 (quarter round + 常量 "expand 32-byte k") ↓ 提取 Key/Nonce → 实现正向/逆向算法 → 验证通过
算法本质 GameExtension.Process() 的核心算法是 标准 ChaCha20 流密码 ,使用固定的密钥和 nonce:
算法 : ChaCha20 (RFC 7539)
密钥 : "Th1s ls n0t a rea1 key!!@sec2026" (32 字节,伪装成假 key 彩蛋)
Nonce : "012345678901" (12 字节)
Counter : 0
特性 : 流密码,加密 = 解密 (XOR keystream)
参考资料 Protect Your Godot Game - Melaton’s Blog
godot 引擎逆向初探 | in1t’s blog
获取源代码 — Godot Engine (4.5) 简体中文文档
IDA Tracing - yring
GDRETools/gdsdecomp: Godot reverse engineering tools
godotengine/godot: Godot Engine – Multi-platform 2D and 3D game engine