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-0x03 | 4 | "\x1BLua" | signature | 标志 |
| 0x04 | 1 | 0x51 | version | 版本5.1 |
| 0x05 | 1 | 0 | format | 文件的格式标识 |
| 0x06 | 1 | 1 | endian | 1为小端序,0为大端序 |
| 0x07 | 1 | 4 | size_int | int类型所占的字节大小 |
| 0x08 | 1 | 8 | size_size_t | size_t类型所占的字节大小 |
| 0x09 | 1 | 4 | size_Instruction | Luac字节码的代码块中,一条指令的大小 |
| 0x0A | 1 | 8 | size_lua_Number | 标识lua_Number类型的数据大小类型能否正常的工作 |
| 0x0B | 1 | 0 | lua_num_valid | 字段通常为0,用来确定lua_Number类型能否正常的工作 |
| 0x0C-0x13 | 8 | 0x0B | source_size | 源码名称大小 |
| 0x14-0x1E | 0x0B | @hello.lua | source_name | 源码名称 |
| 0x1F-0x22 | 4 | 0 | linedefined | 行定义 |
| 0x23-0x26 | 4 | 0 | lastlinedefined | |
| 0x27 | 1 | 0 | nups | |
| 0x28 | 1 | 0 | numparams | 函数有几个参数 |
| 0x29 | 1 | 2 | is_vararg | 参数是否为可变参数列表 |
| 0x2A | 1 | 2 | maxstacksize | 当前函数的Lua栈大小 |
| 0x2B-0x2E | 4 | 4 | sizecode | 指令Code条数,有4条指令 |
| 0x2F-0x32 | 4 | 0x05 0x00 0x00 0x00 | 指令1:GETGLOBAL | |
| 0x33-0x36 | 4 | 0x41 0x40 0x00 0x00 | 指令2:LOADK | |
| 0x37-0x3A | 4 | 0x1C 0x40 0x00 0x01 | 指令3:CALL | |
| 0x3B-0x3E | 4 | 0x1E 0x00 0x80 0x00 | 指令4:RETURN | |
| 0x3F-0x42 | 4 | 2 | sizek | 常量Constants条数 |
| 0x43 | 1 | 0x04 | const_type | 0:空,1:布尔型,3:数字,4:字符串 |
| 0x44-0x4B | 8 | 0x06 | val | 这里是字符串长度 |
| 0x4C-0x51 | 6 | "print" | 字符串为"print" | |
| 0x52 | 1 | 0x04 | const_type | 0:空,1:布尔型,3:数字,4:字符串 |
| 0x53-0x5A | 1 | 0x0C | 这里是字符串长度 | |
| 0x5B-0x66 | 0x0C | "hello world" | 字符串为"hello world" | |
| 0x67-0x6A | 4 | 0 | sizep | 子函数Protos条数,这里为空,如果有的话就继续解析 |
| 0x6B-0x6E | 4 | 4 | sizelineinfo | 行信息条数 |
| 0x6F-0x72 | 4 | 1 | lineinfo | 行信息 |
| 0x73-0x76 | 4 | 1 | 行信息 | |
| 0x77-0x7A | 4 | 1 | 行信息 | |
| 0x7B-0x7E | 4 | 1 | 行信息 | |
| 0x7F-0x82 | 4 | 0 | sizelocvars | 局部变量信息LocVars条数,这里为空,如果有的话就继续解析 |
| 0x83-0x86 | 4 | 0 | sizeupvalues | 局部变量信息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就能反编译了,如果还有问题,就调试查看是哪里修改了。