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提取, 云资产清单, 反编译, 无后门, 游戏开发, 解密解包, 逆向工具, 逆向工程