vshbnj/slua-decode

GitHub: vshbnj/slua-decode

一款用于解包并去混淆隐藏在 Unity AssetBundle 中的加密 SLua 游戏脚本、还原可读 Lua 源码的专用工具。

Stars: 0 | Forks: 0

# slua-decode **解包并去混淆隐藏在 Unity AssetBundles 中的 SLua (Lua 5.3) 游戏脚本。** 一些 Unity (IL2CPP) 手游通过 SLua 绑定在 **Lua** 中运行其整个游戏逻辑,并费尽心机将其隐藏: 脚本被编译为 bytecode,经过**混淆**、压缩、**加密**,打包成自定义归档,隐藏在**伪装成纹理的 TextAssets** 中,并以**损坏头部**的 AssetBundles 形式发布,导致 UABEA / AssetStudio 等标准工具崩溃。 该工具能剥开每一层伪装,还原出**可读的 `.lua` 源码**。 ## 功能说明 ``` AssetBundle (.abd/.ast) │ ① strip 32-byte decoy "UnityFS" header ← this is what crashes UABEA ▼ UnityFS bundle (standard LZ4HC) │ ② UnityPy → TextAsset.m_Script ▼ flat archive: [i32 nameLen][name][i32 dataLen][data] … │ ③ split into named blobs ▼ luaLS_pack blob: [i32 origSize][ zlib | brotli ] + LCG stream cipher │ ④ LCG-decrypt → decompress ▼ obfuscated Lua 5.3 bytecode (masked lundump + opcode rotation + opcode permutation) │ ⑤ de-obfuscate → stock Lua 5.3 bytecode ▼ unluac → readable .lua ✓ ``` ## 环境要求 - **Python 3.8+** - `pip install UnityPy` - 仅在需要打开 AssetBundles 时需要(步骤 ②) - `pip install brotli` - 可选,仅当数据块经过 brotli 压缩时需要(本版本使用 zlib) - **Java 8+** 和 **`unluac.jar`** - 用于最终反编译为 `.lua` ([下载地址](https://sourceforge.net/projects/unluac/files/),支持 Lua 5.0–5.4) ``` pip install -r requirements.txt # 将 unluac.jar 放在 ./tools/ 中(或传递 --unluac /path/to/unluac.jar) ``` ## 用法 只需一条命令,AssetBundle → `.lua`: ``` python slua_decode.py bundle path/to/data/**/*.abd -o out --unluac tools/unluac.jar ``` 其他入口点: ``` # 在反混淆的标准 bytecode (.luac) 处停止,跳过 decompile 步骤 python slua_decode.py bundle game.abd -o out # 你已经提取了原始的 TextAsset payload(即 .m_Script bytes) python slua_decode.py textasset sctexture_d.bin -o out --unluac tools/unluac.jar # 单个 luaLS_pack blob(archive 的一个条目) python slua_decode.py blob one_script.bin -o out --unluac tools/unluac.jar # 同时保留 bytecode 和 source python slua_decode.py bundle game.abd -o out --unluac tools/unluac.jar --keep-bytecode ``` 输出结果会镜像还原归档中的脚本路径,例如 `out/Data/MonsterData.lua`、`out/Network/ENETConnect.lua`、`out/Actor.lua`。 ### AssetBundle 在哪里? 它们作为可下载的 AssetBundle 集发布,并按内容哈希缓存在设备上: - `…/Android/data/<2-hex>/.abd` - 缓存的 bundles(以 MD5 命名) - `…/Android/_AssetBundleSet.txt` - 清单映射 `逻辑名称 → MD5 → 大小` - Lua 脚本具体存在于名为 `sctexture_c/d/m/s/u.ast` 的 bundle 中(伪装成“纹理”)。 基础 APK 随附空的 StreamingAssets 存根;真实内容在首次运行时下载。 使用清单可将逻辑 bundle 名称映射到磁盘上的 `.abd` 文件。 ## 原理说明(格式规范) ### ① 诱饵 bundle 头部 每个 `.abd`/`.ast` 都以一个**模拟 `UnityFS` 头部起始的 32 字节片段**开头,紧接着才是_真正的_ `UnityFS` bundle。简单的解析器会读取诱饵,计算出几 TB 的“bundle 大小”和虚假的块计数,然后崩溃。修复方法:跳转到**第二个** `UnityFS` 出现的位置。其余部分是一个完全标准的 LZ4HC UnityFS bundle - **不需要 AES 密钥**。 ### ② + ③ TextAsset 归档 每个 Lua “pack” bundle 都包含一个单一的 `TextAsset`,其 `m_Script` 字节是一个扁平化归档: ``` repeat until EOF: int32 nameLen (little-endian) bytes name (e.g. "Data/MonsterData.lua") int32 dataLen bytes data (one luaLS_pack blob) ``` ### ④ luaLS_pack(压缩 + 加密) 每个数据块的格式为 `[int32 origSize][compressed]`,然后**整个数据块**会经过 LCG 密钥流密码处理。**`origSize` 的符号决定了所使用的编解码器**:`>0` 为 zlib,`<0` 为 brotli,`0` 为直接存储。 解密(来自 `libslua.so` 中的 `unpack`): ``` state = 42815 for each cipher byte c: plain = c ^ ((state >> 8) & 0xFF) state = (26829 * ((state + int8(c)) & 0xFFFFFFFF) + 48215) & 0xFFFFFFFF # 然后:size = int32_le(plain[:4]);对 plain[4:] 进行 zlib/brotli decompress ``` (`int8(c)` = 作为带符号 char 的字节。该工具会同时尝试带符号/无符号,并保留能够成功解压的那一种。) ### ⑤ 混淆的 Lua 5.3 bytecode → 标准 bytecode 解压后的结果是 **Lua 5.3 bytecode** - 其头部 100% 标准(`\x1bLua\x53\x00`, `LUAC_DATA`, `LUAC_INT=0x5678`, `LUAC_NUM=370.5`),但**代码块主体使用了自定义的 `lundump`**(位于 `libslua.so` 中的 `luaU_undump`/`LoadFunction`)。如果不还原这一步,标准的 unluac 会报错 _"unmapped type code"_: - **动态 XOR 掩码。** 掩码从 0 开始(因此头部和主函数的起始字段是明文)。每个函数在其代码_之后_,会读取 `sizek`,接着是 **1 个垃圾字节**,然后是一个 **8 字节的种子**,并从中推导出: `mask_byte = seed & 0xFF`, `mask_int = (seed_s >> 3) + 13`, `mask_integer = (seed_s >> 2) + 23` (`seed_s` = 作为**带符号** int32 的种子;位移为算术位移)。掩码是有状态的,并由嵌套函数继承(DFS 顺序)。每个 int/byte 字段都经过 `value ^ mask` 处理。 - **偏移大小:** `sizek = raw − 5`, `sizep = raw − 7`, `sizeupvalues = raw >> 1`。 - **常量标签被掩盖**(`type ^ mask_byte`);int 常量被掩盖;**字符串的_长度_被掩盖,但字符串的_字节内容_保持原样**(这就是为什么函数名和字段名能以明文形式保留下来的原因)。 - **4 字节的垃圾字**位于调试信息 `lineinfo` 之前;`locvars` 的存储顺序为 `startpc, endpc, varname`(与标准的 `varname, startpc, endpc` 相反)。 - **Opcode 混淆,分为两层:** 1. 仅**每第 3 条指令**(`i % 3 == 0`)的 6 位 opcode 字段会被**循环移位** `seed_s % 47`(C 语言带符号取模); 2. **整个 opcode 集合被置换** - VM 使用了自定义的 opcode 编号。游戏→标准的映射表是通过 `luaP_opmodes` + `luaV_execute` 调度表恢复出来的(算术 opcode 通过其 `TM_*` 元方法事件识别,其余通过特征调用如 `luaH_new`, `luaD_poscall`, `luaV_lessthan`, … 识别)。该映射表位于 `slua_decode.py:OPMAP` 中。 该工具会重新输出一个字节级精确的**标准** Lua 5.3 代码块,`unluac` 可以将其完美反编译。 ## 文件说明 | 文件 | 职责 | | ------------------ | ---------------------------------------------------------------- | | `slua_decode.py` | **核心工具** - 独立运行,包含全部 5 个解密解包层,命令行工具 | | `tools/unluac.jar` | 外部 Lua 5.0–5.4 反编译器(未包含在此处;请参见“环境要求”) | ## 适配到其他游戏 如果你正在研究_不同的_ SLua 版本,其结构大概率是相同的,但魔术数字可能会有所不同。请从该游戏的 `libslua.so`(以及 `libunity.so`)中重新推导它们: - **LCG 密码** - 读取 `unpack` / `luaLS_pack`:种子(`42815`)、乘数(`26829`)、增量(`48215`)。 - **lundump 掩码和大小偏移** - 读取 `LoadFunction` / `luaU_undump`。 - **Opcode 映射表** - 读取 `luaP_opmodes` 和 `luaV_execute` 调度表;重建 `OPMAP`。 - **Bundle 上的诱饵/加密** - 如果 UnityPy 报错 _"BundleFile is encrypted"_,这可能是真正的 UnityCN 加密(在 `libunity.so` 中寻找密钥,例如通过 `UnityPy.set_assetbundle_decrypt_key` / `brute_force_key`),而不是可以直接剥离的诱饵头。 ## 常见问题 - **UABEA / AssetStudio 在处理 bundle 时崩溃** → 这是诱饵头部造成的;本工具已经处理了此问题。 - **`luaLS unpack failed`** → 你的游戏版本中的 LCG 常量或编解码器有所不同(请参见上文)。 - **unluac:"unmapped type code N"** → 你向其输入了_已混淆_的代码块;请先使用本工具处理(`OPMAP` / 掩码逻辑就是专门用于修复此问题的)。 - **`.lua` 输出为空** → 该模块的编译代码块只是一个隐式的 `return`(一个空源文件);这很正常,并非处理失败。 ## 致谢 - [UnityPy](https://github.com/K0lb3/UnityPy) - AssetBundle 解析 - [unluac](https://sourceforge.net/projects/unluac/) - Lua 5.0–5.4 反编译器 ## 免责声明 仅用于互操作性、安全研究和资料存档。请仅对您拥有或获得授权进行分析的软件使用此工具,并尊重原作者的权利及您所在地的法律法规。
标签:JS文件枚举, Lua, Python, rizin, Unity, URL提取, 云资产清单, 反编译, 无后门, 游戏开发, 解密解包, 逆向工具, 逆向工程