简介
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文件包含两部分:文件头与函数体。文件头格式定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
结构体表示,它的声明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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
的头部分。它的定义如下:
1
2
3
4
5
6
7
|
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
表示参数是否为可变参数列表,例如这个函数声明:
1
2
3
|
function f1(a1, a2, ...)
......
end
|
这点与C语言类似,三个点“...”表示这是一个可变参数的函数。
f1()
在这里的
numparams
为2,并且
is_vararg
的值为1。
maxstacksize
字段指明当前函数的Lua栈大小。值为2的幂。
在
ProtoHeader
下面是函数的代码部分,这里使用
Code
表示。
Code
存放了一条条的Luac机器指令,每条指令是一个32位的整型大小。
Code
定义如下:
1
2
3
4
|
struct
Code {
uint32 sizecode;
uint32 inst[];
} code;
|
sizecode
字段标识了接下来的指令条数。
inst
则存放了当前函数所有的指令,在Lua中,指令采用
Instruction
表示,它的定义如下:
1
2
3
|
#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
,它存放了函数中所有的常量信息。定义如下:
1
2
3
4
|
typedef
struct
{
uint32 sizek;
Constant constant[];
} Constants;
|
sizek
字段标识了接下来
Constant
的个数。
constant
则是
Constant
常量列表,存放了一个个的常量信息。的定义如下:
1
2
3
4
|
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用于存放具体的数据内容。它的定义如下:
1
2
3
4
5
6
7
8
9
10
|
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
,它表示当前函数包含的子函数信息。定义如下:
1
2
3
4
|
typedef
struct
(string level) {
uint32 sizep;
Proto proto[];
} Protos
|
sizep
字段表示当前函数包含的子函数的数目。所谓子函数,指的是一个函数中包含的嵌套函数与闭包。如下面的代码:
1
2
3
4
5
6
7
8
9
|
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
的定义如下:
1
2
3
4
|
typedef
struct
{
uchar instack;
uchar idx;
} Upvaldesc;
|
instack
字段表示UpValue是否在栈上创建的,是的话取值为1,反之为0。
idx
字段表示UpValue在UpValue数据列表中的索引,取值从0开始。
UpValueNames
存放了当前函数中所有UpValue的名称信息,它的定义如下:
1
2
3
4
|
typedef
struct
{
uint32 size_upvalue_names;
UpValueName upvalue_name[];
} UpValueNames;
|
size_upvalue_names字段表示
UpValueName
条目的数目,每一条
UpValueName
存放了一个UpValue的名称,它的定义如下:
1
2
3
4
|
typedef
struct
{
uint64 name_size;
char
var_str[];
} UpValueName;
|
name_size
字段是符号串的长度,
var_str
为具体的字符串内容。
SourceName
存放了当前Luac编译前存放的完整文件名路径。它的定义如下:
1
2
3
4
|
typedef
struct
{
uint64 src_string_size;
char
str[];
} SourceName
|
SourceName
的定义与
UpValueName
一样,两个字段分别存放了字符串的长度与内容。
Lines
存放了所有的行号信息。它的定义如下:
1
2
3
4
|
typedef
struct
{
uint32 sizelineinfo;
uint32 line[];
} Lines;
|
sizelineinfo
字段表示当前函数所有的行总数目。
line
字段存放了具体的行号。
LocVars
存放了当前函数所有的局部变量信息,它的定义如下:
1
2
3
4
|
typedef
struct
{
uint32 sizelocvars;
LocVar local_var[];
} LocVars;
|
sizelocvars
字段表示局部变量的个数。local_var字段是一个个的局部变量,它的类型LocVar定义如下:
1
2
3
4
5
6
|
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模板文件。
点击
010 Editor
菜单Templates->New Template,新建一个模板,会自动生成如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
|
//------------------------------------------------
//--- 010 Editor v8.0 Binary Template
//
// File:
// Authors:
// Version:
// Purpose:
// Category:
// File Mask:
// ID Bytes:
// History:
//------------------------------------------------
|
File
是文件名,
010 Editor
使用.bt作为模柏树的后缀,这里取名为luac.bt即可。
Authors
是作者信息。
Version
是当前模板的版本,如果将最终的模板文件上传到
010 Editor
的官方模板仓库,
010 Editor
会以此字段来判断模板文件的版本信息。
Purpose
是编写本模板的意图,内容上可以留空。
Category
是模板的分类,
010 Editor
中自带了一些内置的分类,这里选择
Programming
分类。
File Mask
是文件扩展名掩码,表示当前模板支持处理哪种文件类型的数据,支持通配符,如果支持多种文件格式,可以将所有的文件扩展名写在一行,中间使用逗号分开,这里设置它的值为“*.luac, *.lua”。
ID Bytes
是文件开头的Magic Number,用来通过文件的开头来判断是否为支持处理的文件,这里的取值为“1B 4c 75 61”。
History
中可以留空,也可以编写模板的更新历史信息。
最终,Luac.bt的开头内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//------------------------------------------------
//--- 010 Editor v8.0 Binary Template
//
// File: luac.bt
// Authors: fei_cong([email protected])
// Version: 1.0
// Purpose:
// Category: Programming
// File Mask: *.luac, *.lua
// ID Bytes: 1B 4c 75 61
// History:
// 1.0 fei_cong: Initial version, support lua 5.2.
//
// License: This file is released into the public domain. People may
// use it for any purpose, commercial or otherwise.
//------------------------------------------------
|
010 Editor
模板与C语言一样,支持C语言的宏、数据类型、变量、函数、代码语句、控制流程等,还支持调用常见的C语言函数。
数据类型上,支持的非常丰富,官方列出BS的支持的数据类型如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
- 8-Bit Signed Integer - char, byte, CHAR, BYTE
- 8-Bit Unsigned Integer - uchar, ubyte, UCHAR, UBYTE
- 16-Bit Signed Integer - short, int16, SHORT, INT16
- 16-Bit Unsigned Integer - ushort, uint16, USHORT, UINT16, WORD
- 32-Bit Signed Integer - int, int32, long, INT, INT32, LONG
- 32-Bit Unsigned Integer - uint, uint32, ulong, UINT, UINT32, ULONG, DWORD
- 64-Bit Signed Integer - int64, quad, QUAD, INT64, __int64
- 64-Bit Unsigned Integer - uint64, uquad, UQUAD, UINT64, QWORD, __uint64
- 32-Bit Floating Point Number - float, FLOAT
- 64-Bit Floating Point Number - double, DOUBLE
- 16-Bit Floating Point Number - hfloat, HFLOAT
- Date Types - DOSDATE, DOSTIME, FILETIME, OLETIME, time_t (for more information on date types see Using the Inspector)
|
在编写模板时,同一数据类型中列出的类型,使用上是一样,如下面的代码片断:
1
2
3
|
local
int a;
local
int32 a;
local
long a;
|
表示的都是一个32位的整型变量,这三种声明方式表达的含义是相同的。声明变量时,需要在前面跟上local关键字,如果没有跟上local,则表明是在声明一个占位的数据字段。所谓占位的数据字段,指的
010 Editor
在解析模板中的变量时,会对占位的数据部分使用指定的数据类型进行解析,如下面的代码:
1
2
3
4
5
|
typedef
struct
{
GlobalHeader header;
Proto proto;
} Luac;
Luac luac;
|
010 Editor
在解析这段代码时,会按照Luac中所有的占位数据字段信息解析当前的二进制文件。GlobalHeader与Proto的声明也中如此,没有加上local的数据字段,都会被0
10 Editor
解析并显示。
除了支持基本的C语言格式结构体struct外,
010 Editor
模板语法还加入了一些特性,比如字段注释与格式、结构体压缩与处理函数。看如下的结构体信息:
1
2
3
4
5
6
|
typedef
struct
{
uint64 varname_size
;
char
varname[varname_size];
uint32 startpc
"first point where variable is active"
>;
uint32 endpc
"first point where variable is dead"
>;
} LocVar
false
>;
|
这是按照前面介绍的
LocVar
结构体信息,按照
010 Editor
模板语法处理过后的效果。为字段后添加format可以指定它的输出格式为十六进制hex,默认是10进制;为字段后添加comment可以指定它的注释信息,这两个字段可以同时存在,在中间加入一个逗号即可;可以为结构体指定read来指定它的类型读取函数,也可以指定write来指定它的类型写入函数,read与write有着自己的格式,如下所示:
1
2
3
|
string LocVarRead(LocVar &val) {
return
val.varname;
}
|
所有的read与write返回值必须为string,参数必须为要处理的结构体类型的引用。注意:
010 Editor
模板语法不支持指针,但支持引用类型,但引用类型不能作为变量与函数的返回值,只能作为参数进行传递,在编写模板代码时需要注意。
除了以上的基础类型外,
010 Editor
模板还支持字符串类型string,这在C语言中是不存在的!它与char[]代表的含义是相同的,而且它支持的操作比较多,如以下字符串相加等操作:
1
2
|
local
string str =
"world"
;
local
string str2 =
"hello "
+ str +
"!\n"
;
|
010 Editor
模板中的宏有限制,并不能解析那些需要展开后替换符号的宏,只支持那些能够直接计算的宏。如下面的BITRK与ISK宏:
1
2
3
|
#define SIZE_B9
#define BITRK(1 <
#define ISK(x)((x) & BITRK)
|
前者可以直接解析并计算出来,
010 Editor
模板就支持它,而对于ISK宏,并不能在展开时计算出它的值,因此,
010 Editor
模板并不支持它。
010 Editor
模板支持enum枚举,与C语言中的枚举的差别是,在定义枚举时可以指定它的数据类型,这样的好处是可以在
010 Editor
模板中声明占位的枚举数据。如下所示是Luac.bt中用到的
LUA_DATATYPE
类型:
1
2
3
4
5
6
7
8
9
10
11
12
|
enum
LUA_DATATYPE {
LUA_TNIL= 0,
LUA_TBOOLEAN= 1,
LUA_TLIGHTUSERDATA = 2,
LUA_TNUMBER= 3,
LUA_TSTRING= 4,
LUA_TTABLE= 5,
LUA_TFUNCTION= 6,
LUA_TUSERDATA= 7,
LUA_TTHREAD= 8,
LUA_NUMTAGS = 9,
};
|
010 Editor
模板中支持调用常见的C语言库函数,如
strlen
()、
strcat
()、
print
()、
sprintf
()、
strstr
(),不同的是,函数名上有些差别,这些可调用的函数在
010 Editor
模板中首字母是大写的,因此,在调用时,它们分别是
Strlen
()、
Strcat
()、
Print
()、
Sprintf
()、
Strstr
()。更多支持的字符串操作的函数可以查看
010 Editor
的帮助文档“String Functions”小节,除了“String Functions”外,还有“I/O Functions”、“Math Functions”、“Tool Functions”、“Interface Functions”等函数可供模板代码使用。
接下来看下代码结构部分,
010 Editor
模板支持C语言中的for/while/dowhile等循环语句,这些语句可以用来组成到
010 Editor
模板的函数与代码块中。一点细微的差别是
010 Editor
模板的返回类型只能是上面介绍过的基础类型,不支持自定义类型与数组结构,这就给实际编写代码带来了一些麻烦,遇到这种函数场景时,就需要考虑更改代码的结构了。
编写luac.bt文件格式模板
了解了
010 Editor
模板语法后,就可以开始编写Luac.bt模板文件了。编写模板前,需要找好一个Luac文件,然后边写边测试,生成一个Luac文件很简单,可以编写好hello.lua后,执行下面的命令生成hello.luac:
1
|
$ luac -o .
/hello
.luac .
/hello
.lua
|
生成好Luac文件后,就是编写一个个结构体进行测试,这是纯体力活了。
luadec
提供了一个ChunkSpy52.lua,可以使用它打印Luac的文件格式内容,可以参考它的输出进行Luac.bt的编写工作,实际上我也是这么做的。
首先是
GlobalHeader
,它的定义可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
typedef
struct
{
uint32 signature
;
//".lua"
uchar version
;
uchar format
"format (0=official)"
>;
uchar endian
"1 == LittleEndian; 0 == BigEndian"
>;
uchar size_int
"sizeof(int)"
>;
uchar size_size_t
"sizeof(size_t)"
>;
uchar size_Instruction
"sizeof(Instruction)"
>;
uchar size_lua_Number
"sizeof(lua_Number)"
>;
uchar lua_num_valid
"Determine lua_Number whether it works or not, It's usually 0"
>;
if
(version == 0x52) {
uchar luac_tail[0x6]
"data to catch conversion errors"
>;
}
} GlobalHeader;
|
这种定义的方式与前面介绍的
LocVar
一样,具体就不展开讨论了。下面主要讨论编写过程中遇到的问题与难点。
首先是输出与ChunkSpy52.lua一样的function level,也就是函数的嵌套级别,定义结构体时可以传递参数,这一点是C语言不具备的,但这个功能非常实用,可以用来传递定义结构时的信息,如这里的function level就用到了该特性。这是Protos的定义:
1
2
3
4
5
6
7
8
9
10
|
typedef struct(string level) {
uint32 sizep
;
local uint32 sz = sizep;
local uint32 i =
0
;
local string s_level;
while
(sz-- >
0
) {
SPrintf(s_level,
"%s_%d"
, level, i++);
Proto proto(s_level);
};
} Protos
false
>;
|
为结构体加上一个
string
类型的
level
参数,初始时传值“0”,然后往下传递时,为传递的值累加一,这样就做到了function level的输出。
然后是
Constant
常量信息的获取,由于TValue支持多种数据的类型,因此在处理上需要分别进行处理,这里参考了
luadec
的实现,不过在细节上还是比较麻烦。
luadec
使用
DecompileConstant
()方法实现,它的代码片断如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
···
char
* DecompileConstant(
const
Proto* f,
int
i) {
const
TValue* o = &f->k[i];
switch
(ttype(o)) {
|