Lua字节码解析

一、前言

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

二、生成字节码

首先,我们先编写一个简单的lua代码

print("hello world")

以Lua5.15版本为例,使用以下命令生成字节码文件,默认是存在debug信息的,可以加入-s参数去除debug信息

$ luac -o hello.luac hello.lua

也可以写一个lua脚本,读取hello.lua生成对应的字节码文件

local func, err = loadfile(arg[1])
if not func then
   error("编译错误: " .. err)
end
    
-- 转换为字节码
local bytecode = string.dump(func)
    
-- 保存字节码文件
local file = io.open(arg[2], "wb")
file:write(bytecode)
file:close()

使用lua调用luac文件,可以正常运行

三、文件结构

我们使用hexdump分别以16进制的格式打印luac字节码

          00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
00000000  1b 4c 75 61 51 00 01 04  08 04 08 00 0b 00 00 00  |.LuaQ...........|
00000010  00 00 00 00 40 68 65 6c  6c 6f 2e 6c 75 61 00 00  |....@hello.lua..|
00000020  00 00 00 00 00 00 00 00  00 02 02 04 00 00 00 05  |................|
00000030  00 00 00 41 40 00 00 1c  40 00 01 1e 00 80 00 02  |...A@...@.......|
00000040  00 00 00 04 06 00 00 00  00 00 00 00 70 72 69 6e  |............prin|
00000050  74 00 04 0c 00 00 00 00  00 00 00 68 65 6c 6c 6f  |t..........hello|
00000060  20 77 6f 72 6c 64 00 00  00 00 00 04 00 00 00 01  | world..........|
00000070  00 00 00 01 00 00 00 01  00 00 00 01 00 00 00 00  |................|
00000080  00 00 00 00 00 00 00                              |.......|

字段解析,参考源码 src/lundump.c的LoadHeader、LoadFunction函数

偏移大小字段名作用
0x00-0x034"\x1BLua"signature标志
0x0410x51version版本5.1
0x0510format文件的格式标识
0x0611endian1为小端序,0为大端序
0x0714size_intint类型所占的字节大小
0x0818size_size_tsize_t类型所占的字节大小
0x0914size_InstructionLuac字节码的代码块中,一条指令的大小
0x0A18size_lua_Number标识lua_Number类型的数据大小类型能否正常的工作
0x0B10lua_num_valid字段通常为0,用来确定lua_Number类型能否正常的工作
0x0C-0x1380x0Bsource_size源码名称大小
0x14-0x1E0x0B@hello.luasource_name源码名称
0x1F-0x2240linedefined行定义
0x23-0x2640lastlinedefined
0x2710nups
0x2810numparams函数有几个参数
0x2912is_vararg参数是否为可变参数列表
0x2A12maxstacksize当前函数的Lua栈大小
0x2B-0x2E44sizecode指令Code条数,有4条指令
0x2F-0x3240x05 0x00 0x00 0x00指令1:GETGLOBAL
0x33-0x3640x41 0x40 0x00 0x00指令2:LOADK
0x37-0x3A40x1C 0x40 0x00 0x01指令3:CALL
0x3B-0x3E40x1E 0x00 0x80 0x00指令4:RETURN
0x3F-0x4242sizek常量Constants条数
0x4310x04const_type0:空,1:布尔型,3:数字,4:字符串
0x44-0x4B80x06val这里是字符串长度
0x4C-0x516"print"字符串为"print"
0x5210x04const_type0:空,1:布尔型,3:数字,4:字符串
0x53-0x5A10x0C这里是字符串长度
0x5B-0x660x0C"hello world"字符串为"hello world"
0x67-0x6A40sizep子函数Protos条数,这里为空,如果有的话就继续解析
0x6B-0x6E44sizelineinfo行信息条数
0x6F-0x7241lineinfo行信息
0x73-0x7641行信息
0x77-0x7A41行信息
0x7B-0x7E41行信息
0x7F-0x8240sizelocvars局部变量信息LocVars条数,这里为空,如果有的话就继续解析
0x83-0x8640sizeupvalues局部变量信息UpValueNames条数,这里为空,如果有的话就继续解析

四、代码还原

可以使用luac解析luac字节码,得到了类似汇编的代码

如果还要还原为伪代码,可以使用unluac、luadec反编译器,但是如果python解释器加以修改字节码就可能无法解析了,需要知道修改了什么,然后对应修改一下反编译的内容。

如果遇到了opcode顺序修改的程序,需要找到liblua.so文件里面的正确的opcode顺序,首先使用反编译工具分析liblua.so,找到“MOVE”等字符串所在的地方,如果找不到,可以用十六进制工具搜索之后对应上程序的地址。

交叉引用,找到完整的opcode列表顺序

当然还要对一下实际的处理代码opcode是否是这样的,搜索lua_call,找到里面的函数luaV_execute(有可能没有函数名称),找里面的switch(如果显示不出来,可以试着换一个反编译工具),对应下每一个opcode的代码是否对应上。

修改lua源码lua-5.1/src/lopcodes.c

const char *const luaP_opnames[NUM_OPCODES+1] = {
"GETTABLE","GETGLOBAL","SETGLOBAL","SETUPVAL","SETTABLE","NEWTABLE","SELF","LOADNIL","LOADK","LOADBOOL","GETUPVAL","LT","LE","EQ","DIV","MUL","SUB","ADD","MOD","POW","UNM","NOT","LEN","CONCAT","JMP","TEST","TESTSET","MOVE","FORLOOP","FORPREP","TFORLOOP","SETLIST","CLOSE","CLOSURE","CALL","RETURN","TAILCALL","VARARG",NULL};

const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       T  A    B       C     mode		   opcode	*/
opmode(0, 1, OpArgR, OpArgK, iABC)		/* OP_GETTABLE */
,opmode(0, 1, OpArgK, OpArgN, iABx)		/* OP_GETGLOBAL */
,opmode(0, 0, OpArgK, OpArgN, iABx)		/* OP_SETGLOBAL */
,opmode(0, 0, OpArgU, OpArgN, iABC)		/* OP_SETUPVAL */
,opmode(0, 0, OpArgK, OpArgK, iABC)		/* OP_SETTABLE */
,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_NEWTABLE */
,opmode(0, 1, OpArgR, OpArgK, iABC)		/* OP_SELF */
,opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_LOADNIL */
,opmode(0, 1, OpArgK, OpArgN, iABx)		/* OP_LOADK */
,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_LOADBOOL */
,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_GETUPVAL */
,opmode(1, 0, OpArgK, OpArgK, iABC)		/* OP_LT */
,opmode(1, 0, OpArgK, OpArgK, iABC)		/* OP_LE */
,opmode(1, 0, OpArgK, OpArgK, iABC)		/* OP_EQ */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_DIV */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_MUL */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_SUB */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_ADD */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_MOD */
,opmode(0, 1, OpArgK, OpArgK, iABC)		/* OP_POW */
,opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_UNM */
,opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_NOT */
,opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_LEN */
,opmode(0, 1, OpArgR, OpArgR, iABC)		/* OP_CONCAT */
,opmode(0, 0, OpArgR, OpArgN, iAsBx)		/* OP_JMP */
,opmode(1, 1, OpArgR, OpArgU, iABC)		/* OP_TEST */
,opmode(1, 1, OpArgR, OpArgU, iABC)		/* OP_TESTSET */
,opmode(0, 1, OpArgR, OpArgN, iABC) 		/* OP_MOVE */
,opmode(0, 1, OpArgR, OpArgN, iAsBx)		/* OP_FORLOOP */
,opmode(0, 1, OpArgR, OpArgN, iAsBx)		/* OP_FORPREP */
,opmode(1, 0, OpArgN, OpArgU, iABC)		/* OP_TFORLOOP */
,opmode(0, 0, OpArgU, OpArgU, iABC)		/* OP_SETLIST */
,opmode(0, 0, OpArgN, OpArgN, iABC)		/* OP_CLOSE */
,opmode(0, 1, OpArgU, OpArgN, iABx)		/* OP_CLOSURE */
,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_CALL */
,opmode(0, 0, OpArgU, OpArgN, iABC)		/* OP_RETURN */
,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_TAILCALL */
,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_VARARG */
};

再修改lua-5.1/src/lopcodes.h

typedef enum {
/*----------------------------------------------------------------------
name		args	description
------------------------------------------------------------------------*/
OP_GETTABLE,/*	A B C	R(A) := R(B)[RK(C)]				*/
OP_GETGLOBAL,/*	A Bx	R(A) := Gbl[Kst(Bx)]				*/
OP_SETGLOBAL,/*	A Bx	Gbl[Kst(Bx)] := R(A)				*/
OP_SETUPVAL,/*	A B	UpValue[B] := R(A)				*/
OP_SETTABLE,/*	A B C	R(A)[RK(B)] := RK(C)				*/
OP_NEWTABLE,/*	A B C	R(A) := {} (size = B,C)				*/
OP_SELF,/*	A B C	R(A+1) := R(B); R(A) := R(B)[RK(C)]		*/
OP_LOADNIL,/*	A B	R(A) := ... := R(B) := nil			*/
OP_LOADK,/*	A Bx	R(A) := Kst(Bx)					*/
OP_LOADBOOL,/*	A B C	R(A) := (Bool)B; if (C) pc++			*/
OP_GETUPVAL,/*	A B	R(A) := UpValue[B]				*/
OP_LT,/*	A B C	if ((RK(B) <  RK(C)) ~= A) then pc++  		*/
OP_LE,/*	A B C	if ((RK(B) <= RK(C)) ~= A) then pc++  		*/
OP_EQ,/*	A B C	if ((RK(B) == RK(C)) ~= A) then pc++		*/
OP_DIV,/*	A B C	R(A) := RK(B) / RK(C)				*/
OP_MUL,/*	A B C	R(A) := RK(B) * RK(C)				*/
OP_SUB,/*	A B C	R(A) := RK(B) - RK(C)				*/
OP_ADD,/*	A B C	R(A) := RK(B) + RK(C)				*/
OP_MOD,/*	A B C	R(A) := RK(B) % RK(C)				*/
OP_POW,/*	A B C	R(A) := RK(B) ^ RK(C)				*/
OP_UNM,/*	A B	R(A) := -R(B)					*/
OP_NOT,/*	A B	R(A) := not R(B)				*/
OP_LEN,/*	A B	R(A) := length of R(B)				*/
OP_CONCAT,/*	A B C	R(A) := R(B).. ... ..R(C)			*/
OP_JMP,/*	sBx	pc+=sBx					*/
OP_TEST,/*	A C	if not (R(A) <=> C) then pc++			*/ 
OP_TESTSET,/*	A B C	if (R(B) <=> C) then R(A) := R(B) else pc++	*/ 
OP_MOVE,/*	A B	R(A) := R(B)					*/
OP_FORLOOP,/*	A sBx	R(A)+=R(A+2);
			if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/*	A sBx	R(A)-=R(A+2); pc+=sBx				*/
OP_TFORLOOP,/*	A C	R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2)); 
                     if R(A+3) ~= nil then R(A+2)=R(A+3) else pc++	*/ 
OP_SETLIST,/*	A B C	R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B	*/
OP_CLOSE,/*	A 	close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/*	A Bx	R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))	*/
OP_CALL,/*	A B C	R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_RETURN,/*	A B	return R(A), ... ,R(A+B-2)	(see note)	*/
OP_TAILCALL,/*	A B C	return R(A)(R(A+1), ... ,R(A+B-1))		*/
OP_VARARG/*	A B	R(A), R(A+1), ..., R(A+B-1) = vararg		*/
} OpCode;

最后编译luadec就能反编译了,如果还有问题,就调试查看是哪里修改了。

五、参考文档

https://zhuanlan.zhihu.com/p/30094117

https://zhuanlan.zhihu.com/p/678462349

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注