Lua 程序逆向之 Luac 文件格式分析(上)

阅读数:48 2019 年 11 月 29 日 15:03

Lua程序逆向之Luac文件格式分析(上)

简介

Lua 语言对于游戏开发与相关逆向分析的人来说并不陌生。Lua 语言凭借其高效、简洁与跨平台等多种特性,一直稳立于游戏、移动 APP 等特定的开发领域中。

目前 Lua 主要有 5.1、5.2、5.3 共三个版本。5.1 版本的 Lua 之所以目前仍然被广泛使用的原因之一,是由于另一个流行的项目 LuaJit 采用了该版本 Lua 的内核。单纯使用 Lua 来实现的项目中,5.2 与 5.3 版本的 Lua 则更加流行。这里主要以 Lua 版本 5.2 为例,通过分析它生成的 Luac 字节码文件,完成 Lua 程序的初步分析,为以后更深入的反汇编、字节码置换与重组等技能打下基础。

Lua 与 Luac

Lua 与 Python 一样,可以被定义为脚本型的语言,与 Python 生成 pyc 字节码一样,Lua 程序也有自己的字节码格式 luac。Lua 程序在加载到内存中后,Lua 虚拟机环境会将其编译为 Luac(下面文中 Luac 与 luac 含义相同)字节码,因此,加载本地的 Luac 字节码与 Lua 源程序一样,在内存中都是编译好的二进制结构。

为了探究 Luac 的内幕,我们需要找到合适的资料与工具来辅助分析 Luac 文件。最好的资料莫过于 Lua 的源码,它包含了 Lua 相关知识的方方面面,阅读并理解 Luac 的构造与 Lua 虚拟机加载字节码的过程,便可以通透的了解 Luac 的格式。但这里并不打算这么做,而采取阅读第三方 Lua 反编译工具的代码。主要原因是:这类工具的代码往往更具有针对性,代码量也会少很多,分析与还原理解 Luac 字节码文件格式可以省掉不少的时间与精力。

luadec 与 unlua 是最流行的 Luac 反汇编与反编译工具,前者使用 C++ 语言开发,后者使用 Java 语言,这两个工具都能很好的还原与解释 Luac 文件,但考虑到 Lua 本身采用 C 语言开发,并且接下来打算编写 010 Editor 编辑器的 Luac.bt 文件格式模板,010 Editor 的模板语法类似于 C 语言,为了在编码时更加顺利,这里分析时主要针对 luadec。

Luac 文件格式

一个 Luac 文件包含两部分:文件头与函数体。文件头格式定义如下:

复制代码
typedef struct {
char signature[4]; //".lua"
uchar version;
uchar format;
uchar endian;
uchar size_int;
uchar size_size_t;
uchar size_Instruction;
uchar size_lua_Number;
uchar lua_num_valid;
uchar luac_tail[0x6];
} GlobalHeader;

第一个字段 signature 在 lua.h 头文件中有定义,它是 LUA_SIGNATURE,取值为“\033Lua",其中,\033 表示按键。LUA_SIGNATURE 作为 Luac 文件开头的 4 字节,它是 Luac 的 Magic Number,用来标识它为 Luac 字节码文件。Magic Number 在各种二进制文件格式中比较常见,通过是特定文件的前几个字节,用来表示一种特定的文件格式。

version 字段表示 Luac 文件的格式版本,它的值对应于 Lua 编译的版本,对于 5.2 版本的 Lua 生成的 Luac 文件,它的值为 0x52。

format 字段是文件的格式标识,取值 0 代表 official,表示它是官方定义的文件格式。这个字段的值不为 0,表示这是一份经过修改的 Luac 文件格式,可能无法被官方的 Lua 虚拟机正常加载。

endian 表示 Luac 使用的字节序。现在主流的计算机的字节序主要有小端序 LittleEndian 与大端序 BigEndian。这个字段的取值为 1 的话表示为 LittleEndian,为 0 则表示使用 BigEndian。

size_int 字段表示 int 类型所占的字节大小。size_size_t 字段表示 size_t 类型所占的字节大小。这两个字段的存在,是为了兼容各种 PC 机与移动设备的处理器,以及它们的 32 位与 64 位版本,因为在特定的处理器上,这两个数据类型所占的字节大小是不同的。

size_Instruction 字段表示 Luac 字节码的代码块中,一条指令的大小。目前,指令 Instruction 所占用的大小为固定的 4 字节,也就表示 Luac 使用等长的指令格式,这显然为存储与反编译 Luac 指令带来了便利。

size_lua_Number 字段标识 lua_Number 类型的数据大小。lua_Number 表示 Lua 中的 Number 类型,它可以存放整型与浮点型。在 Lua 代码中,它使用 LUA_NUMBER 表示,它的大小取值大小取决于 Lua 中使用的浮点数据类型与大小,对于单精度浮点来说,LUA_NUMBER 被定义为 float,即 32 位大小,对于双精度浮点来说,它被定义为 double,表示 64 位长度。目前,在 macOS 系统上编译的 Lua,它的大小为 64 位长度。

lua_num_valid 字段通常为 0,用来确定 lua_Number 类型能否正常的工作。

luac_tail 字段用来捕捉转换错误的数据。在 Lua 中它使用 LUAC_TAIL 表示,这是一段固定的字符串内容:"\x19\x93\r\n\x1a\n"。

在文件头后面,紧接着的是函数体部分。一个 Luac 文件中,位于最上面的是一个顶层的函数体,函数体中可以包含多个子函数,子函数可以是嵌套函数、也可以是闭包,它们由常量、代码指令、Upvalue、行号、局部变量等信息组成。

在 Lua 中,函数体使用 Proto 结构体表示,它的声明如下:

复制代码
typedef struct {
//header
ProtoHeader header;
//code
Code code;
// constants
Constants constants;
// functions
Protos protos;
// upvalues
Upvaldescs upvaldescs;
// string
SourceName src_name;
// lines
Lines lines;
// locals
LocVars loc_vars;
// upvalue names
UpValueNames names;
} Proto;

ProtoHeader 是 Proto 的头部分。它的定义如下:

复制代码
typedef struct {
uint32 linedefined;
uint32 lastlinedefined;
uchar numparams;
uchar is_vararg;
uchar maxstacksize;
} ProtoHeader;

ProtoHeader 在 Lua 中使用 lua_Debug 表示,lua_Debug 的作用是调试时提供函数的行号,函数与变量名等信息,只是它部分字段的信息在生成 Luac 字节码时,最终没有写入 Luac 文件中。linedefined 与 lastlinedefined 是定义的两个行信息。numparams 表示函数有几个参数。is_vararg 表示参数是否为可变参数列表,例如这个函数声明:

复制代码
function f1(a1, a2, ...)
......
end

这点与 C 语言类似,三个点“…”表示这是一个可变参数的函数。f1() 在这里的 numparams 为 2,并且 is_vararg 的值为 1。

maxstacksize 字段指明当前函数的 Lua 栈大小。值为 2 的幂。

在 ProtoHeader 下面是函数的代码部分,这里使用 Code 表示。Code 存放了一条条的 Luac 机器指令,每条指令是一个 32 位的整型大小。Code 定义如下:

复制代码
struct Code {
uint32 sizecode;
uint32 inst[];
} code;

sizecode 字段标识了接下来的指令条数。inst 则存放了当前函数所有的指令,在 Lua 中,指令采用 Instruction 表示,它的定义如下:

复制代码
#define LUAI_UINT32unsigned int
typedef LUAI_UINT32 lu_int32;
typedef lu_int32 Instruction;

当 LUAI_BITSINT 定义的长度大于等于 32 时,LUAI_UINT32 被定义为 unsigned int,否则定义为 unsigned long,本质上,也就是要求 lu_int32 的长度为 32 位。

接下来是 Constants,它存放了函数中所有的常量信息。定义如下:

复制代码
typedef struct {
uint32 sizek;
Constant constant[];
} Constants;

sizek 字段标识了接下来 Constant 的个数。constant 则是 Constant 常量列表,存放了一个个的常量信息。的定义如下:

复制代码
typedef struct {
LUA_DATATYPE const_type;
TValue val;
} Constant;

LUA_DATATYPE 是 Lua 支持的各种数据类型结构。如 LUA_TBOOLEAN 表示 bool 类型,使用 lua_Val 表示;LUA_TNUMBER 表示数值型,它可以是整型,使用 lua_Integer 表示,也可以是浮点型,使用 lua_Number 表示;LUA_TSTRING 表示字符串。这些所有的类型信息使用 const_type 字段表示,大小为 1 字节。

TValue 用于存放具体的数据内容。它的定义如下:

复制代码
typedef struct {
union Value {
//GCObject *gc; /* collectable objects */
//void *p; /* light userdata */
lua_Val val; /* booleans */
//lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} value_;
} TValue;

对于 LUA_TBOOLEAN,它存放的值可以通过 Lua 中提供的宏 bvalue 来计算它的值。

对于 LUA_TNUMBER,它存放的可能是整型,也可能是浮点型,可以直接通过 nvalue 宏自动进行类型判断,然后获取它格式化后的字符串值。对于 Lua 的 5.3 版本,对 nvalue 宏进行了改进,可以使用 ivalue 宏获取它的整型值,使用 fltvalue 宏来获取它的浮点值。

对于 LUA_TSTRING,它存放的是字符串信息。可以使用 rawtsvalue 宏获取它的字符串信息。而写入 Luac 之后,这里的信息实则是 64 位的值存放了字符串的大小,并且紧跟着后面是字符串的内容。

接下来是 Protos,它表示当前函数包含的子函数信息。定义如下:

复制代码
typedef struct(string level) {
uint32 sizep;
Proto proto[];
} Protos

sizep 字段表示当前函数包含的子函数的数目。所谓子函数,指的是一个函数中包含的嵌套函数与闭包。如下面的代码:

复制代码
function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end

Create() 函数包含了 foo1() 与 foo2() 两个子函数,因此,这里 sizep 的值为 2。proto 表示子函数信息,它与父函数使用一样的结构体信息。因此,可见 Lua 的函数部分使用了一种树式的数据结构进行数据存储。

Upvaldescs 与 UpValueNames 共同描述了 Lua 中的 UpValue 信息。当函数中包含子函数或团包,并且访问了函数的参数或局部变量时,就会产生 UpValue。如上面的 Create() 函数,foo1() 与 foo2() 两个子函数都访问了参数 n,因此,这里会产生一个 UpValue,它的名称为“n”。

Upvaldesc 的定义如下:

复制代码
typedef struct {
uchar instack;
uchar idx;
} Upvaldesc;

instack 字段表示 UpValue 是否在栈上创建的,是的话取值为 1,反之为 0。idx 字段表示 UpValue 在 UpValue 数据列表中的索引,取值从 0 开始。

UpValueNames 存放了当前函数中所有 UpValue 的名称信息,它的定义如下:

复制代码
typedef struct {
uint32 size_upvalue_names;
UpValueName upvalue_name[];
} UpValueNames;

size_upvalue_names 字段表示 UpValueName 条目的数目,每一条 UpValueName 存放了一个 UpValue 的名称,它的定义如下:

复制代码
typedef struct {
uint64 name_size;
char var_str[];
} UpValueName;

name_size 字段是符号串的长度,var_str 为具体的字符串内容。

SourceName 存放了当前 Luac 编译前存放的完整文件名路径。它的定义如下:

复制代码
typedef struct {
uint64 src_string_size;
char str[];
} SourceName

SourceName 的定义与 UpValueName 一样,两个字段分别存放了字符串的长度与内容。

Lines 存放了所有的行号信息。它的定义如下:

复制代码
typedef struct {
uint32 sizelineinfo;
uint32 line[];
} Lines;

sizelineinfo 字段表示当前函数所有的行总数目。line 字段存放了具体的行号。

LocVars 存放了当前函数所有的局部变量信息,它的定义如下:

复制代码
typedef struct {
uint32 sizelocvars;
LocVar local_var[];
} LocVars;

sizelocvars 字段表示局部变量的个数。local_var 字段是一个个的局部变量,它的类型 LocVar 定义如下:

复制代码
typedef struct {
uint64 varname_size;
char varname[];
uint32 startpc;
uint32 endpc;
} LocVar;

varname_size 字段是变量的名称长度大小。varname 字段存放了变量的名称字符串内容。startpc 与 endpc 是两个指针指,存储了局部变量的作用域信息,即它的起始与结束的地方。

到此,一个 Luac 的文件格式就讲完了。

010 Editor 模板语法

为了方便分析与修改 Luac 二进制文件,有时候使用 010 Editor 编辑器配合它的文件模板,可以达到很直观的查看与修改效果,但 010 Editor 官方并没有提供 Luac 的格式模板,因此,决定自己动手编写一个模板文件。

010 Editor 支持模板与脚本功能,两者使用的语法与 C 语言几乎一样,只是有着细微的差别与限制,我们看看如何编写 010 Editor 模板文件。

评论

发布