# Lua程序逆向之Luajit文件格式 `Luajit`将原生Lua进行了扩展,使它支持JIT方式编译运行,比起原生Lua程序,它有着如下特点: 1. JIT即时编译器让执行效率更高。 2. 它同时兼容传统的AOT编译。 3. 全新设计的Luajit字节码文件格式,更加高效与更强的调试支持。(这一点在后面会着重介绍) 4. 全新的Lua指令集。引入了中间表示IR,以及编译引擎支持不同平台的处理器指令即时编译,完全的符合现代化编译器设计,是编译理论学习的绝佳好资料。 `Luajit`在游戏软件中应用广泛,学习Lua程序逆向,就避免不了与`Luajit`打交道,下面,我们以最基本的`Luajit`文件格式开始,逐步深入的学习Lua程序的逆向基本知识。 ## 安装Luajit `Luajit`是开源的,它的项目地址是:`https://github.com/LuaDist/luajit`。任何人都可以从网络上下载编译并安装它。 目前,最新正式版本的Luajit为2.0.5版,Beta版本为2.1.0-beta3版,官方还在缓慢的更新中。正式版本的Luajit只只兼容Lua的5.1版本,5.2版本的Lua正在添加支持中。这里重点讨论Luajit2.0.5正式版本。 笔者研究Luajit使用的操作系统是macOS,通过`Homebrew`软件包管理工具,可以执行如下的命令进行快速的安装: ``` $ brew install luajit ``` 安装完成后,它的目录结构如下所示: ``` $ tree /usr/local/opt/luajit /usr/local/opt/luajit ├── COPYRIGHT ├── INSTALL_RECEIPT.json ├── README ├── bin │   ├── luajit -> luajit-2.0.5 │   └── luajit-2.0.5 ├── include │   └── luajit-2.0 │   ├── lauxlib.h │   ├── lua.h │   ├── lua.hpp │   ├── luaconf.h │   ├── luajit.h │   └── lualib.h ├── lib │   ├── libluajit-5.1.2.0.5.dylib │   ├── libluajit-5.1.2.dylib -> libluajit-5.1.2.0.5.dylib │   ├── libluajit-5.1.a │   ├── libluajit-5.1.dylib -> libluajit-5.1.2.0.5.dylib │   ├── libluajit.a -> libluajit-5.1.a │   ├── libluajit.dylib -> libluajit-5.1.dylib │   └── pkgconfig │   └── luajit.pc └── share ├── luajit-2.0.5 │   └── jit │   ├── bc.lua │   ├── bcsave.lua │   ├── dis_arm.lua │   ├── dis_mips.lua │   ├── dis_mipsel.lua │   ├── dis_ppc.lua │   ├── dis_x64.lua │   ├── dis_x86.lua │   ├── dump.lua │   ├── v.lua │   └── vmdef.lua └── man └── man1 └── luajit.1 10 directories, 30 files ``` 安装目录下的`luajit`程序是指向`luajit-2.0.5`程序的软链接,它是`Luajit`的主程序,与Lua官方的`lua`程序一样,它是Luajit程序的解释器,不同的是,它没有与`luac`编译器对应的`Luajitc`,`Luajit`同时负责了Lua文件编译为Luajit字节码文件的编译工作。include目录下存放的是`Luajit`的头文件,可以编译C/C++程序与Luajit进行交互。lib目录为链接C/C++程序用到的库文件。share/luajit-2.0.5/jit目录下的lua文件是`Luajit`提供的扩展模块,可以用来反汇编与Dump输出`Luajit`字节码文件的指令信息,在学习`Luajit`字节码指令格式时,这些工具非常有用。man目录下提供了`Luajit`的man帮助信息,即终端中执行`man luajit`显示的帮助内容。 ## 编译生成`Luajit`文件 编写hello.lua文件,内容如下: ``` function add(x, y) return x+y; end function showinfo() print("welcome to lua world ") end function showstr(str) print("The string you input is " .. str) end local i = 6; return 1; ``` 这段代码包含了三个函数、一个局部变量,一条返回语句。使用`luajit`的-b参数即可生成hello.luajit文件,命令如下所示: ``` $ luajit -b ./hello.lua ./hello.luajit ``` 上面命令生成的hello.luajit文件不包含调试信息,`luajit`默认编译参数中有一个-s参数,作用是去除luajit文件中的调试信息。调度信息中,包含了原Lua源文件中的行号与变量本等信息,如果想要保留这些信息,可以加上-g参数。执行如下命令,可以生成带调试信息的hello_debug.luajit文件: ``` $ luajit -bg ./hello.lua ./hello_debug.luajit ``` ## `Luajit`文件格式 Luajit官方并没有直接给出`Luajit`字节码文件的格式文档。但可以通过阅读Luajit源码中加载与生成`Luajit`字节码文件的函数,来单步跟踪分析出它的文件格式,这两个方法分别是`lj_bcread()`与`lj_bcwrite()`。 从这两个函数调用的`bcread_header()`、`bcread_proto()`、`bcwrite_header()`、`bcwrite_proto()`等子函数名可以初步了解到,`Luajit`字节码文件与`Luac`一样,将文件格式分为头部分信息Header与函数信息Proto两部分。具体的内容细节则需要使用`gdb`或`lldb`等工具调试分析得出。 `Luajit`字节码文件的Header部分为了与`Luac`命名上保持一致,这里将其描述为`GlobalHeader`,它的定义如下: ``` typedef struct { char signature[3]; uchar version; GlobalHeaderFlags flags; if (!is_stripped) { uleb128 length; char chunkname[uleb128_value(length)]; } } GlobalHeader; ``` 第一个`signature`字段是`Luajit`文件的Magic Number,它占用三个字节,定义分别如下: ``` /* Bytecode dump header. */ #define BCDUMP_HEAD1 0x1b #define BCDUMP_HEAD2 0x4c #define BCDUMP_HEAD3 0x4a ``` 即`Luajit`字节码文件的头三个字节必须为“\x1bLJ”。`version`字段为`Luajit`的版本号,目前它的值为1。第三个字段`flags`描述了该文件的一组标志位集合,它们的取值可以为这些值的组合: ``` typedef enum { FLAG_IS_BIG_ENDIAN = 0b00000001, FLAG_IS_STRIPPED = 0b00000010, FLAG_HAS_FFI = 0b00000100 } FLAG; ``` `FLAG_IS_BIG_ENDIAN`标识了该`Luajit`文件是采用大端字节序还是小端字节序、`FLAG_IS_STRIPPED`标识该`Luajit`文件是否去除了调试信息、`FLAG_HAS_FFI`标识是否包含FFI信息。`flags`字段使用的数据类型为uleb128,占用的字节码与数据的实际大小相关。 uleb128是一种常见的压缩形式的数据存储方式,如果了解Android DEX文件格式的话,对它应该不会陌生。它最长采用5个字节表示数据的大小,最少采用1个字节表示数据的大小,具体采用的位数,可以通过判断每字节的最高位是否为1,为1则使用下一字节的数据,如果使用`010 Editor`模板语法表示,则它的数据类型定义如下: ``` typedef struct { ubyte val ; if(val > 0x7f) { ubyte val ; if (val > 0x7f) { ubyte val ; if(val > 0x7f) { ubyte val ; if(val > 0x7f) { ubyte val ; } } } } } uleb128; ``` 读取uleb128表示的数据大小的方法如下: ``` uint uleb128_value(uleb128 &u) { local uint result; local ubyte cur; result = u.val[0]; if(result > 0x7f) { cur = u.val[1]; result = (result & 0x7f) | (uint)((cur & 0x7f) << 7); if(cur > 0x7f) { cur = u.val[2]; result |= (uint)(cur & 0x7f) << 14; if(cur > 0x7f) { cur = u.val[3]; result |= (uint)(cur & 0x7f) << 21; if(cur > 0x7f) { cur = u.val[4]; result |= (uint)cur << 28; } } } } return result; } ``` 接下来`GlobalHeader`中,如果判断`Luajit`文件中包含调试信息,即`flags`字段中的`FLAG_IS_STRIPPED`没有被置位,则会多出`length`与`chunkname`两个字段。`length`是uleb128表示的字段串长度,`chunkname`则是存放了`length`长度的字段串内容,它表示当前`Luajit`文件的源文件名。 在`GlobalHeader`之后,是`Proto`函数体内容。它的定义如下: ``` typedef struct() { ProtoHeader header; if (uleb128_value(header.size) > 0) { if (uleb128_value(header.instructions_count) > 0) Instruction inst[uleb128_value(header.instructions_count)]; Constants constants; if (header.debuginfo_size_ > 0) DebugInfo debuginfo; } } Proto; ``` 这里`Proto`的定义仍然采用与上面`GlobalHeader`一样的`010 Editor`模板语法方式,这种类似C语言的描述,更容易从定义上看出`Proto`结构体的字段信息。 `ProtoHeader`类型的`header`字段描述了`Proto`的头部信息,定义如下: ``` typedef struct { uleb128 size; if (uleb128_value(size) > 0) { ProtoFlags flags; uchar arguments_count; uchar framesize; uchar upvalues_count; uleb128 complex_constants_count; uleb128 numeric_constants_count; uleb128 instructions_count; if (!is_stripped) { uleb128 debuginfo_size; uleb128 first_line_number; uleb128 lines_count; } } } ProtoHeader; ``` `size`字段是标识了从当前字段开始,整个`Proto`结构体的大小,当该字段的取值大于0时,表示当前`Proto`不为空,即`Proto`的`header`字段后,接下来会包含`Instruction`指令与`Constants`常量等信息,并且`ProtoHeader`部分也会多出其他几个字段。首先是`flags`字段,`ProtoFlags`是一个uchar类型,这里单独使用一个结构体表示,是为了之后编写`010 Editor`模板时,更方便的为其编写read方法。`ProtoFlags`取值如下: ``` typedef enum { FLAG_HAS_CHILD = 0b00000001, FLAG_IS_VARIADIC = 0b00000010, FLAG_HAS_FFI = 0b00000100, FLAG_JIT_DISABLED = 0b00001000, FLAG_HAS_ILOOP = 0b00010000 } PROTO_FLAG; ``` `FLAG_HAS_CHILD`标识当前`Proto`是一个“子函数”,即闭包(`Closure`)。这个标志位非常重要,为了更好的理解它的用处,先看下如下代码: ``` function Create(n) local function foo1() print(n) local function foo2() n = n + 10 print(n) local function foo3() n = n + 100 print(n) end end end return foo1,foo2,foo3 end f1,f2,f3 = Create(1000) f1() ``` 这段Lua代码中,最外层的`Create()`向内,每个function都包含一个`Closure`。现在回忆一下`Luac`文件格式中,它们是如何存储的? **在`Luac`文件中,每个`Proto`都有一个`Protos`字段,它用来描述`Proto`与`Closure`之间的层次信息,`Proto`采用从外向内的递归方式进行存储。而`Luajit`则采用线性的从内向外的同级结构进行存储,`Proto`与`Closure`之前的层级关系使用`flags`字段的`FLAG_HAS_CHILD`标志位进行标识,当`flags`字段的`FLAG_HAS_CHILD`标志位被置位,则表示当前层的`Proto`是上一层`Proto`的`Closure`** 上面的代码片断在`Luajit`文件结构中的存局如下所示: ``` struct Luajit lj; struct GlobalHeader header; struct Proto proto[0]; //foo3() struct Proto proto[1]; //foo2() struct Proto proto[2]; //foo1() struct Proto proto[3]; //Create() struct Proto proto[4]; //Full file struct Proto proto[5]; //empty ``` 从存局中可以看出,最内层的`foo3()`位于`Proto`的最外层,它与`Luac`的布局恰恰是相反的,而`proto[4]`表示了整个Lua文件,它是`Proto`的最上层。最后的`proto[5]`,它在读取其`ProtoHeader`的`size`字段时,由于其值为0,而中止了整个文件的解析。即它的内容为空。 `FLAG_IS_VARIADIC`标识了当前`Proto`是否返回多个值,上面的代码中,只有`Create()`的`flags`字段会对该标志置位。`FLAG_HAS_FFI`标识当前`Proto`是否有通过FFI扩展调用系统的功能函数。`FLAG_JIT_DISABLED`标识当前`Proto`是否禁用JIT,对于包含了具体代码的`Proto`,它的值通常没有没有被置位,表示有JIT代码。`FLAG_HAS_ILOOP`标识了当前`Proto`是否包含了`ILOOP`与`JLOOP`等指令。 在`flags`字段后面,是`arguments_count`字段,表示当前`Proto`有几个参数。接着是`framesize`字段,标识了`Proto`使用的栈大小。接下来四个字段`upvalues_count`、`complex_constants_count`、`numeric_constants_count`、`instructions_count`,它们分别表示UpValue个数、复合常数、数值常数、指令条数等信息。 如果当前`Proto`包含调试信息,则接下来是3个uleb128类型的字段`debuginfo_size`、`first_line_number`、`lines_count`。其中`debuginfo_size`字段指明后面`DebugInfo`结构体占用的字节大小,`first_line_number`指明当前`Proto`在源文件中的起始行,`lines_count`字段指明当前`Proto`在源文件中所占的行数。 如果上面的`instructions_count`字段值不为0,接下来则存放的是指令`Instruction`数组,每条指令长度与`Luac`一样,占用32位,但使用的指令格式完全不同,此处不展开讨论它。 指令后面是常量信息,它的定义如下: ``` typedef struct(int32 upvalues_count, int32 complex_constants_count, int32 numeric_constants_count) { while (upvalues_count-- > 0) { uint16 upvalue; } while (complex_constants_count-- > 0) { ComplexConstant constant; } while (numeric_constants_count-- > 0) { NumericConstant numeric; } } Constants; ``` 可以看到,`Constants`中包含3个数组字段,每个字段的具体数目与前面指定的`upvalues_count`、`complex_constants_count`、`numeric_constants_count`相关。每个`UpValue`信息占用16位,`ComplexConstant`保存的常量信息比较丰富,它可以保存字符串、整型、浮点型、TAB表结构等信息。它的结构体开始处是一个uleb128类型的`tp`字段,描述了`ComplexConstant`保存的具体的数据。它的类型包括: ``` typedef enum { BCDUMP_KGC_CHILD = 0, BCDUMP_KGC_TAB = 1, BCDUMP_KGC_I64 = 2, BCDUMP_KGC_U64 = 3, BCDUMP_KGC_COMPLEX = 4, BCDUMP_KGC_STR = 5 } BCDUMP_KGC_TYPE; ``` 这里重点关注下`BCDUMP_KGC_TAB,它表示这是一个Table表结构,即类似如下代码片断生成的数据内容: ``` tab={key1="val1",key2="val2"}; ``` Table数据在`Luajit`中有专门的数据结构进行存储,它的定义如下: ``` typedef struct { uleb128 array_items_count; uleb128 hash_items_count; local int32 array_items_count_ = uleb128_value(array_items_count); local int32 hash_items_count_ = uleb128_value(hash_items_count); while (array_items_count_-- > 0) { ArrayItem array_item; } while (hash_items_count_-- > 0) { HashItem hash_item; } } Table; ``` 有基于数组的`ArrayItem`与基于Hash的`HashItem`两种Table类型结构,上面的`tab`即属于`HashItem`,它的定义如下: ``` typedef struct { TableItem key; TableItem value; } HashItem; ``` `TableItem`描述了Table的键`key`与值`value`的类型与具体的数据内容,它的开始处是一个uleb128类型的`tp`字段,具体的取值类型如下: ``` typedef enum { BCDUMP_KGC_CHILD = 0, BCDUMP_KGC_TAB = 1, BCDUMP_KGC_I64 = 2, BCDUMP_KGC_U64 = 3, BCDUMP_KGC_COMPLEX = 4, BCDUMP_KGC_STR = 5 } BCDUMP_KGC_TYPE; ``` 当取到`tp`的类型值后,判断它的具体类型,然后接下来存放的即是具体的数据,`TableItem`在`010 Editor`中的模板结构体表示如下: ``` typedef struct { uleb128 tp; local int32 data_type = uleb128_value(tp); if (data_type >= BCDUMP_KTAB_STR) { local int32 len = data_type - BCDUMP_KTAB_STR; char str[len]; } else if (data_type == BCDUMP_KTAB_INT) { uleb128 val; } else if (data_type == BCDUMP_KTAB_NUM) { TNumber num; } else if (data_type == BCDUMP_KTAB_TRUE) { } else if (data_type == BCDUMP_KTAB_FALSE) { } else if (data_type == BCDUMP_KTAB_NIL) { } else { Warning("TableItem need update\n"); } } TableItem; ``` 当取值大于5,即大于`BCDUMP_KTAB_STR`时,它的类型为字符串,需要减去5后计算出它的实际内容长度。另外,上面的`TNumber`是由两个uleb128组成的分为高与低各32位的数据类型。 `NumericConstant`存储数值型的常量,比如`local`语句中赋值的整型与浮点型数据。它的定义如下: ``` typedef struct { uleb128_33 lo; if (lo.val[0] & 0x1) uleb128 hi; } NumericConstant; ``` 数值常量分为`lo`低部分与`hi`高部分,注意`lo`的类型为`uleb128_33`,它是一个33位版本的uleb128,即判断第一个字节后面是否还包含后续数据时,首先判断第33位是否置1。它的定义如下: ``` typedef struct { ubyte val; if((val >> 1) > 0x3f) { ubyte val ; if (val > 0x7f) { ubyte val ; if(val > 0x7f) { ubyte val ; if(val > 0x7f) { ubyte val ; } } } } } uleb128_33; ``` 当读取到`lo`的最低为是1时,说明这是一个`TNumber`类型,还需要解析它的高32位部分。 在`Constants`常量结构体后面,如果`ProtoHeader`的`debuginfo_size`值大于0,那么接下来此处存放的是`Debuginfo`调试信息,它的定义如下: ``` typedef struct(int32 first_line_number, int32 lines_count, int32 instructions_count, int32 debuginfo_size, int32 upvalues_count) { if (debuginfo_size > 0) { LineInfo lineinfo(lines_count, instructions_count); if (upvalues_count > 0) UpValueNames upvalue_names(upvalues_count); VarInfos varinfos; } } DebugInfo ``` 分为`LineInfo`与`VarInfos`两部分,前者是存储的一条条的行信息,后者是局部变量信息。`VarInfos`中存储了变量的类型、名称、以及它的作用域起始地址与结束地址,它的定义如下: ``` typedef struct(uchar tp) { local uchar tp_ = tp; //Printf("tp:0x%x\n", tp); if (tp >= VARNAME__MAX) { string str; } else { VARNAME_TYPE vartype; } if (tp != VARNAME_END) { uleb128 start_addr; uleb128 end_addr; } } VarInfo; ``` 代码中的指令引用一个局部变量时,调试器可以通过其`slot`槽索引值到`VarInfos`中查找它的符号信息,这也是`Luajit`文件支持源码级调试的主要方法。 ## 编写`Luajit`文件的`010 Editor`文件模板 在掌握了`Luajit`的完整格式后,编写`010 Editor`文件模板应该没有难度与悬念了。 `Luajit`的线性结构解析起来比`Luac`简单,只需要按顺序解析`Proto`,直接读取到字节0结束。整体部分的代码片断如下: ``` typedef struct() { ProtoHeader header; if (uleb128_value(header.size) > 0) { if (uleb128_value(header.instructions_count) > 0) Instruction inst[uleb128_value(header.instructions_count)]; Constants constants(header.upvalues_count, uleb128_value(header.complex_constants_count), uleb128_value(header.numeric_constants_count)); if (header.debuginfo_size_ > 0) DebugInfo debuginfo(uleb128_value(header.first_line_number), uleb128_value(header.lines_count), uleb128_value(header.instructions_count), header.debuginfo_size_, header.upvalues_count); local int64 end = FTell(); //Printf("start:0x%lx, end:0x%lx, size:0x%lx\n", header.start, end, end - header.start); if (uleb128_value(header.size) != end - header.start) { Warning("Incorrectly read: from 0x%lx to 0x%lx (0x%lx) instead of 0x%lx\n", header.start, end, end - header.start, uleb128_value(header.size)); } } } Proto ; typedef struct { GlobalHeader header; while (!FEof()) Proto proto; } Luajit ; string LuajitRead(Luajit &lj) { return lj.header.name; } ``` `Proto`的`header`的`size`字段是当前`Proto`的大小,在解析的时候有必要对其合法性进行检查。 在编写模板时,只遇到过一个比较难解决的问题,那就是对`NumericConstant`中浮点数的解析。如下面的代码片断: ``` local dd = 3.1415926; ``` 编译生成`Luajit`文件后,它会以浮点数据存储进入`NumericConstant`结构体中,并且它对应的64位数据为0x400921FB4D12D84A。在解析该数据时,并不能像`Luac`中`TValue`那样直接进行解析,`Luac`中声明的结构体`TValue`可以直接解析其内容,但`Luajit`中0x400921FB4D12D84A值的`lo`与`hi`是通过uleb128_33与uleb128两种数据类型动态计算才能得到。 将0x400921FB4D12D84A解析为double,虽然在C语言中只需要如下代码: ``` uint64_t p = 0x400921FB4D12D84A; double *dd = (double *)&p; printf("%.14g\n", *dd); ``` 但`010 Editor`模板不支持指针数据类型,如果使用结构体UNION方式,C语言中如下方法即可转换: ``` union { long long i; double d; } value; value.i = l; char buf[17]; snprintf (buf, sizeof(buf),"%.14g",value.d); ``` `010 Editor`虽然支持结构体与UNION,但并不支持声明`local`类型的结构体变量。所以,浮点数据的解析工作一度陷入了困境!最后,在`010 Editor`的帮且文档中执行“double”关键字,查找是否有相应的解决方法,最后找到了一个`ConvertBytesToDouble()`方法,编写代码进行测试: ``` local uchar chs[8]; chs[0] = 0x4A; chs[1] = 0xD8; chs[2] = 0x12; chs[3] = 0x4D; chs[4] = 0xFB; chs[5] = 0x21; chs[6] = 0x09; chs[7] = 0x40; local double ddd = ConvertBytesToDouble(chs); Printf("%.14g\n", ddd); ``` 输出如下: ``` 3.141592502594 ``` 可见,不是直接进行的内存布局转换,而是进行了内部的计算转换,虽然与原来的3.1415926有少许的出入,但比起不能转换还是要强上许多,通过`ConvertBytesToDouble()`,可以为`NumericConstant`编写其read方法,代码如下: ``` string NumericConstantRead(NumericConstant &constant) { if (constant.lo.val[0] & 0x1) { local string str; local int i_lo = uleb128_33_value(constant.lo); local int i_hi = uleb128_value(constant.hi); local uchar bytes_lo[4]; local uchar bytes_hi[4]; local uchar bytes_double[8]; ConvertDataToBytes(i_lo, bytes_lo); ConvertDataToBytes(i_hi, bytes_hi); Memcpy(bytes_double, bytes_lo, 4); Memcpy(bytes_double, bytes_hi, 4, 4); local double n = ConvertBytesToDouble(bytes_double); SPrintf(str, "%.14g", ((uleb128_value(constant.hi) == (3 | (1 << 4))) ? i : n)); return str; } else { local string str; local int number = uleb128_33_value(constant.lo); if (number & 0x80000000) number = -0x100000000 + number; SPrintf(str, "0x%lx", number); return str; } } ``` 最后,编写完成后,效果如图所示: ![luajit](luajit.jpg) 完整的luajit.bt文件可以在这里找到: [https://github.com/feicong/lua_re](https://github.com/feicong/lua_re)。