Berd's Playground (Deprecated)

Won't receive any further updates.

09/21
21:17
迷の代码

某游戏 xLua 逆向笔记

0x00 前言

最近好像某款游戏挺火的,虽然我没怎么关注,不过群友在拆着玩,所以我也来凑个热闹

因为我压根没装游戏,搞逆向调试啥的基本不可能,群友就给我扔了一堆 luac 来拆着玩,之前我也没拆过这种 lua 游戏,正好学习一下,于是就有了这篇博客

0x01 基本分析

拿到一堆 luac 后,第一件事当然是扔到 unluac 这个万能工具里面去跑一下看看能不能直接解开啦:

意料之内的报错了,错误提示 “The input chunk reports a non-standard lua format: 1”

让我们挑一个小一点的 luac 来研究一下:

用 HEX 编辑器打开后,首先要看 lua 版本,有的游戏可能会修改前面的 .Lua 头,但只要确定了是 lua bytecode 而不是 luajit 编译出来的东西,基本上都可以在文件开头找到版本号。这里是 Lua 5.3,结合 xlua.dll 中的 lua 版本字符串可以确定使用的引擎是 lua 5.3.5

随后我们来分析到底哪些地方是“自定义”的,其实这个“自定义格式”是 xLua 搞出来的而不是游戏公司搞出来的,但我一开始以为这个是游戏公司搞的自定义格式,分析了半天走了不少弯路

这里就走捷径直接到 xLua 的仓库中 查看更改,发现这么一条 commit:

往下追一追,发现启用这个 “字节码兼容” 会设置字节码格式为 1 (非官方)

并且在 luac 的头部少了一个 size_t 的尺寸,这个 size_t 被换成了 uint32_t,也就是说尺寸固定为 4

了解这些信息后,我们就可以 Patch 掉这个 luac 的头部再丢给 unluac 看看了,如图将 01 修改为 00,并增加 size_t 的尺寸 04

顺便一提下面大块绿色的部分就是一堆 Instruction,前面的紫色区域就是这一堆 Instruction 的数量,在 lua 5.3 中指令长度是 4 个字节,所以我在 010 Editor 里建书签的时候直接建了 signed int [指令数量] 这么一个大数组就可以准确的分出代码区域

注意: 这种暴力 Patch 是可能出现问题的 (虽然现在没碰到),推荐的做法是修改 unluac 的源码来兼容数据而不是修改数据头来适配 unluac

假如游戏没有修改 OPCode,那么这篇文章到此就结束了,不过,本例中使用的游戏修改了 OPCode 映射表,因此 unluac 依然无法正常解析

另外,使用 unluac 时看到这个 IllegalStateException 基本就可以肯定 OPCode 被改过了,如果看到的是 Underflow 这种异常,则应该去检查 lua 头中的各个数据类型的尺寸是否有误

0x02 寻找 OPCode

接下来我们要做的事情就是寻找 OPCode 的对应关系。对于 5.3.15 版本的 lua而言,总共有 47 个 OP,定义于 src\lopcodes.h

假如游戏开发者非常认真的按照注释中的说明去改了,我们可以直接去找位于 src\lopcodes.cluaP_opnames 这个数组,通过 IDA 提取直接得出对应关系

不过开发者明显偷懒了,只是确保游戏能跑就行,根本没动过这个映射表

由于这些 OP 是通过 enum 定义的,编译后肯定全部是常量,去硬分析反汇编肯定是不现实的

这里就只能用另外一种思路: 让这个 lua 引擎加载一个用到大部分 OP 的 lua 文件,dump 出生成的字节码,然后再通过原版的 lua 引擎加载这个文件并 dump,最后我们通过一个进行比对来得出映射关系

于是我就写了 xLuaDumper 这个简单的小程序,加载 xlua.dll 并进行上面说的 dump 流程,使用方法非常简单,只需要执行 xLuaDumper.exe <DLL> 即可:

然后在研究怎么写一个脚本用上 lua 大部分 OP 的时候我找到了这个帖子 : https://bbs.pediy.com/thread-250618.htm

不得不承认,这个帖子中说到的思路可能比我的更简单,不过我都到这步了,只好坚持按之前的思路去做

参考这个帖子中的 test.lua、部分 OP 的编译逻辑以及 Lua 5.3 Bytecode Reference,最后我写出了这么一个覆盖了 42 个 OP 的脚本,完整的代码可以在 GitHub 上找到

剩下的几个 OP (LOADKX, GETUPVAL, SETUPVAL, TESTSET, EXTRAARG) 似乎并不能通过直接加载的方式取得,但 GETUPVAL, SETUPVAL 这两个 OP 还是挺常用的,这部分请自己想办法

通过 xLuaDumper 导出两份字节码,然后将其进行比对就可以找到 OP 的映射关系了

0x03 生成映射关系

让我们再回过头看一下两个 luac,图中绿色的部分为指令数量,两个 luac 的指令数量应该完全一样,否则肯定会出错,黄色的部分为具体的指令部分,可以很清晰的看到红色的区域就是有差别的地方

当然,这里说红色的部分就是 OP 是不准确的,在 Lua 源代码中我们可以看到只有最低 6 位是 OP,所以我们解析的时候还需要 & 0b111111 才能取到准确的 OP

手动比对 OPCode 然后去写映射关系并非不可行,但效率实在是太低了,并且我们还有着修改 unluac 中一大堆映射的需求,因此这里我写了 diff.php 脚本来自动化这个过程

这个脚本会自动调用两次 Dumper 来生成字节码,随后进行映射解析并输出可以直接粘贴到 unluac 源码中的格式及统计 (即底部的几个 Unknown OP),完整代码依然可以在 GitHub 上找到

特别要说明一下的是下图中红框部分,因为我并不想去写复杂的 Header 解析,所以直接写死了两个 Offset 来切掉 Header

由于调用 lua_dump 的时候会设置 strip = true,因此拿到的 luac 头部长度应该都是一样的,这么写问题不大

如果你使用的 xlua.dll 没有开启兼容模式,应该把上面的 45 也改成 46

拿到这一堆代码后,我们就可以直接粘贴到 unluac 的 decompile\OpcodeMap.java 中覆盖掉原来的 OP 定义了,如图所示

除此之外,别忘了修改 parse\LHeaderType.java 来适配我们的 luac 格式

这样我们就获得了一份可以基本正确反汇编这个游戏的 unluac 了

0x04 批量操作

只是拿到一个可以用的 unluac 当然远远不够,别忘了我们的每个 luac 都需要手动 Patch 一下再拿去反编译,对游戏里大量的 luac 手动做这件事明显不现实

因此我还写了一个 batch.php 来实现自动化操作,同样的完整源码在 GitHub 上可以找到

注意红框中的部分 由于不推荐进行这种 Patch,这部分代码已被删除,请参照上文修改 unluac 来适配数据格式

同样的,当检测到 luac 中 FORMAT = 1 时,这个脚本会尝试自动 Patch 掉这个标志位并在后面加上 size_t 的长度字节,确保 unluac 能正确读取

由于我手上的所有 luac 都没有调试信息,Header 长度都是一样的,这个脚本仍然采用了写死长度和数据的形式

最后,运行 batch.php <unluac> <Input> <Output>,我们就拿到完整的lua 代码了

当然,由于调试信息被抹掉了,我这里拿到的代码可读性非常的差,后面如果闲下来可能会考虑再写 Parser 对其进行优化,总之这篇博客到这里就结束啦

某游戏 xLua 逆向笔记