关于lua的一切。

lualualua

 最近geekpwn做到了一道lua的题目,正好之前从未做过,因此整理学习一下相关资料咯。

0x00 Background knowledge

What is lua

 Lua是一个脚本语言,代码一共2W余行。很多应用程序、游戏使用Lua作为自己的嵌入式脚本语言。类似于 Java、Python,Lua是运行在虚拟机上的,而虚拟机则屏蔽了底层不同的硬件,从而使得Lua程序可以跨平台执行。

 Lua 的代码可以被编译,它可将源程序编译成为字节码,然后交由虚拟机解释执行。在 Lua 中,每个函数编译器都将创建一个原型(prototype),它由一组指令及其使用到的常量组成。最初的 Lua 虚拟机是基于栈的,到 1993 年,Lua5.0 版本,采用了基于寄存器的虚拟机,使得 Lua 的解释效率得到提升。

 Lua是一种动态类型(定义的变量中不包含类型信息,只有相应的值才包含类型信息,类似于python)的语言。

VM class

 根据指令获取操作数的方式不同,可以把虚拟机分为基于栈的虚拟机基于寄存器的虚拟机

 JVM与python使用基于栈的虚拟机,该虚拟机是在当前栈中获取和保存操作数。例如:a=b+c,其相应指令为:

1
2
3
4
push b
push c
add
pop a

 其实现起来比较简单,每条指令占用的存储空间也小。但是,对于运算而言(例如加法),这需要4条指令才能完成,这会对效率有很大影响。

 Lua目前采用基于寄存器的虚拟机,例如:a=b+c,其相应指令为:

1
add a b c

 提高了效率。但是,每条指令占用的存储空间也增加了,在编译器设计上也增加了复杂度(例如需要用图着色算法对寄存器进行分配,详见VMPROTECT逆向与还原浅析)。

0x01 Details about lua

Lua语言本身

类型定义

 Lua有8种类型:nil、boolean、number、string、function、userdata、thread 和 table,他们都在src/lua.h中定义。例如,string对应LUA_TSTRING,function对应LUA_TFUNCTIOIN。而userdata比较特殊,它对应LUA_TLIGHTUSERDATALUA_TUSERDATALUA_TLIGHTUSERDATA是由 Lua 外部的使用者来完成,LUA_TUSERDATA则是通过 Lua 内部来完成,也就是说,LUA_TLIGHTUSERDATA是通过用户来维护其生命周期。

 Lua有垃圾回收机制,string、function、userdata、thread 和 table都需要gc。需要 gc 的数据类型,都会有一个 CommonHeader 成员。

 Lua中类型的值是由TValue结构体表示(这里简略了):

1
2
3
4
5
6
7
8
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} TValue;

 其中,gc 表示需要垃圾回收的一些值,如 string、table 等;p 表示 light userdata,不会被 gc。

Table

 Table是Lua中唯一表示数据结构的类型,他是一个混合数据结构。其中的函数环境 (env)、元表 (metatable)、模块 (module) 和注册表 (registery) 等都是通过 table 表示。Lua-5.0 后,table 包含一个哈希表部分和一个数组部分,哈希部分发生冲突就用链表解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct Table {
...
unsigned int sizearray;
TValue *array;
lu_byte lsizenode;
Node *node;
...
} Table;

typedef struct Node {
TValue i_val;
TKey i_key;
} Node;

typedef union TKey {
struct {
TValuefields;
int next;
} nk;
TValue tvk;
} TKey;

 array、sizearray 用于表示数组部分及其大小,node、lsizenode 表示哈希部分与大小。

 当查找table中的数据时,其逻辑如下:

(1)对于字符串类型,通过 luaH_getstr() 先获得相应字符串在哈希表中的链表,然后遍历这个链表,采用内存地址比较字符串,若找到则返回相应的值,否则 nil

(2)如果是整型,则调用 luaH_getint() 查找,如果 key 的值小于等于数组大小,则直接返回相应的值,否则去哈希表中去查找。

(3)对应其他类型,统一调用 getgeneric(),也就是计算 hash 值并在链表中查找,通过 luaV_equalobj() 对各种类型进行比较。

lua解释器体系结构

image-20230802112721783

 其中,stack 成员用于指向栈底,而 base 指向当前正在执行的函数的第一个参数,而 top 指向栈顶。寄存器实际上是栈上元素的别名。pc 来指向下一条要执行的指令。

Lua字节码

 Lua 的指令使用 32bit 的无符号整型表示,可以通过luac编译成字节码。

 例如,查看lua5.1编译后的字节码文件,文件头部12字节为:1b4c 7561 5100 0104 0804 0800。其中,1b4c 7561\033Lua51表示Lua版本为5.1;00为保留位;01表示字节序为小端;04表示int大小为4字节;08表示size_t的大小为8字节;04表示内部指令的大小为4字节;08代表lua中数字的大小为8字节。

执行流程

 Lua虚拟机最后会调用luaV_execute()函数,其主要逻辑就是取指令、递增PC、根据指令操作码进行switch...case...

其他

(1)Lua 语言本身是支持闭包(closure)的(把几个值和函数绑定在一起),在 Lua 中,这些值被称为 upvalues;而且,每个函数和一个函数环境(env)绑定。

(2)Lua编译系统的工作就是将符合语法规则的代码转换成可运行的闭包,闭包对象是 Lua 运行中一个函数的实例对象。

(3)每个闭包都对应着自己的 proto,而在运行期间,一个 proto 可以产生多个闭包来代表这个函数实例。

Lua中重要的API

常用API函数

API name Description
void lua_pushcclosure (lua_State, lua_CFunction, int) 注册C函数,fn为要注册的函数指针
#define luaL_dofile (luaL_loadfile(L, filename) or lua_pcall(L, 0, LUA_MULTRET, 0)) 加载并运行指定的文件
int luaL_loadfilex (lua_State, const char, const char) 把文件加载为 Lua 代码,代码块的名字为name
int lua_load (lua_State,lua_Reader,void,const char,const char) 加载一段 Lua 代码块,但不运行它。 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息参数 reader 。 chunkname是一个字符串,标识了正在加载的块名。mode是一个字符串,指定如何编译数据块。可能取值为:(1)”b”:该块是预编译的二进制块。(2)”t”:预编译的文本块。
const char *lua_pushfstring (lua_State, const char, …) 把一个格式化的字符串压栈,然后返回这个字符串的指针,类似于sprintf
const char *lua_tolstring (lua_State, int, size_t) 将给定索引处的 Lua 值转换为一个 C 字符串,它还把字符串长度赋值到 *len中。

LuaU_undump函数

 此函数用于lua文件头的检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
LoadState S;
...
S.L = L;
S.Z = Z;
checkHeader(&S);
...
}

#define LUA_SIGNATURE "\x1bLua"
#define LUAC_FORMAT 0
// LUA_VERSION_MAJOR为主版本号,LUA_VERSION_MINOR为子版本号
// 例如Lua版本为5.3,则 LUAC_VERSION=83=0x53
#define MYINT(s) (s[0]-'0')
#define LUAC_VERSION (MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR))


static void checkHeader (LoadState *S) {
checkliteral(S, LUA_SIGNATURE + 1, "not a"); // 检查头部签名
if (LoadByte(S) != LUAC_VERSION) // 检查luac版本号
error(S, "version mismatch in");
if (LoadByte(S) != LUAC_FORMAT) // 检查格式号,一般为0
error(S, "format mismatch in");
checkliteral(S, LUAC_DATA, "corrupted"); // 检查头部的6-11字节
checksize(S, int); // 检查头部的12-16字节
checksize(S, size_t);
checksize(S, Instruction);
checksize(S, lua_Integer);
checksize(S, lua_Number);
if (LoadInteger(S) != LUAC_INT)
error(S, "endianness mismatch in");
if (LoadNumber(S) != LUAC_NUM)
error(S, "float format mismatch in");
}

Lua文件读取&解析的调用链示例

image-20230802135816577

 其中,luaX_next主要用于语法TOKEN的分割,而statlist主要根据分割出来的TOKEN,组装成语法块语句,最后将语句组装成语法树。

0x02 例题1-picStore

 题目与题解来自RCTF,参考链接为:picStore

 题目中给出了一个ELF文件与一个bin文件,经过运行发现ELF文件加载了bin文件并作处理。使用IDA打开,发现是lua。

 这道题的思路很重要,其实主要逻辑就是:ELF文件是一个魔改的luac程序,而bin文件是lua源代码使用魔改luac编译后的bytecode。那就有两种做法:(1)分析题目给出的ELF相对于luac哪里做了修改,对luac源码做同样的修改,之后重新编译luac,最后将bin文件反编译。(2)分析bin文件的哪些部分与正常luac编译出来的bytecode不同,对bin文件进行修复,最后将bin文件进行反编译。

思路1

Step1:分析题目给出的ELF相对于luac哪里做了修改。

 由于是读入bin文件并转成可运行的lua字节码,因此很容易想到luaU_undump,此函数会读入luac字节码并转为可执行的lua函数。

  (1)定位luaU_undump。根据binary string字符串定位此函数。下图是luaU_dump源代码与ida反编译的对比:

image-20230802151720262

  (2)找到魔改的地方。可以看到,反编译结果将checkHeader函数融合到了lua_undump函数中。再来对比checksize函数:

image-20230802153656313

 可以清楚地发现,加入了红框所示的代码。其中,v2代表size的大小。若v2<0xFE,则返回取反的值。在loadIntegerloadNumber也发现了类似的情况:

image-20230802153623538

 不难猜到,在undump(也就是load)上做的这些改动,在dump相关的函数上同样会发生。但是,由于我们要将二进制文件(lua字节码文件)加载并反编译,因此,只需改动undump相关函数即可。

Step2:对luac源码进行修改并进行反编译。

 需要说明的是,Step1中关于luac的源码我摘抄自lua5.4.6,是有问题的。最终是在lua5.3.3上进行的修改。如下所示:

image-20230802163352568

1
2
3
4
5
6
7
8
git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.3
cd lua-5.3
# 更改lua-5.3的源码
make linux
cd ../luadec
make LUAVER=5.3

 但是,我没有复现成功,一直显示:format mismatch in precompiled chunk

思路2

Step1:查找哪些字节码文件中哪些字节被修改了。

 直接抛出结论:程序load代码块的核心函数有且只有一个函数(LoadBlock)。程序dump代码块的核心函数有且只有一个函数(DumpBlock)。那么,只需要记录 LoadBlock 和 DumpBlock 函数的执行次数和函数参数,就可以知道到哪些字节被改变了。

 wp使用了gdb自动化脚本进行调试,脚本如下(文中说frida hook不到dumpBlock函数,但我没有尝试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""
gdb ./picStore
source wp.py
"""
import gdb
import time

startt = time.time()
fp = open(r"./log.log","w")
strr = ""
def pt(p):
global strr
strr += p + "\n"

Esp = 0
gdb.execute('b *0x55555559C078') # LoadBlock 断点
gdb.execute('b *0x55555559C184') # LoadByte
gdb.execute('b *0x55555559C1D1') # LoadInt
gdb.execute('b *0x55555559C111') # LoadNumber
gdb.execute('b *0x55555559C0A1') # LoadInteger

gdb.execute('r')
while 1:
frame = gdb.selected_frame()
rip = frame.read_register("rip")
# 因为不只有LoadByte、LoadInt、LoadNumber、LoadInteger函数调用LoadBlock
if rip == 0x55555559C078 :
# rdx为LoadBlock的size参数
rdx = frame.read_register("rdx")
Esp += rdx
# LoadByte
elif rip == 0x55555559C184:
pt(f"{hex(Esp).ljust(10,' ')} => 1")
# LoadInt
elif rip == 0x55555559C1D1:
pt(f"{hex(Esp).ljust(10,' ')} => 4")
# LoadNumber
elif rip == 0x55555559C111:
pt(f"{hex(Esp).ljust(10,' ')} => 8")
# LoadInteger
elif rip == 0x55555559C0A1:
pt(f"{hex(Esp).ljust(10,' ')} => 8")
# 0x19f0是lua字节码文件的大小
if Esp == 0x19f0:
fp.write(strr)
fp.close()
print("finish!!!")
enddd = time.time()
print(enddd - startt)
break
gdb.execute('c')

 输出结果log.log如下:

image-20230802194030503

 其中,0x10=>8表示为地址0x10之后8字节都取反。

Step2:修复并得到正确的字节码文件。

 根据log.log,可以修复题目中给的字节码文件,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import struct
fp = open(r"C:\\Users\\23957\\Desktop\\pic\\log.log","r")
fps = open(r"C:\\Users\\23957\\Desktop\\pic\\picStore.bin","rb")
fix_bin = open(r"C:\\Users\\23957\\Desktop\\pic\\fix_picStore.bin","wb")
# data为要修改的地方,data_bin为原始文件
data = fp.readlines()
data_bin = fps.read()
fix_data = b""

ans = 0
for i in range(len(data)):
op_str = data[i]
# loa为addr,step为字节数
loa = int(op_str[2:6],16) + 1
step = int(op_str[14:15],16)
# 将要修改的addr之前的内容加入到fix_data里,这部分并没有被修改
for j in range(ans, loa):
fix_data += struct.pack("B",data_bin[j])

for j in range(loa, loa + step):
if data_bin[j] != 0 and data_bin[j] != 0xff:
# 取反
t = data_bin[j] ^ 0xff
fix_data += struct.pack("B", t)
else:
# 若为0或0xff就不去反,为什么?
fix_data += struct.pack("B", data_bin[j])
ans = loa + step
fix_bin.write(fix_data)
fix_bin.close()

 上述脚本有一个难理解的地方,即,脚本中逻辑为:若某字节不为0或0xff,就取反,否则不取反。这与:

1
2
if v[i]-1 <= 0xfd:
v[i] = ~v[i]

 是等价的。最终得到修复后的fix_picStore.bin字节码文件。

Step3:字节码反编译成lua。

 一般的lua字节码文件,可以使用unluacLuaDec等工具进行反编译。在此使用unluac,运行java -jar ./unluac.jar ./fix_picStore.bin > ./oplua.lua

Step4:z3约束求解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from z3 import *
solver = Solver()

# chk_23函数
a1 = [Int(f'c{i}') for i in range(30)]
v1 = a1[0]
v2 = a1[1]
v3 = a1[2]
v4 = a1[3]
v5 = a1[4]
v6 = a1[5]
v7 = a1[6]
v8 = a1[7]
v10 = a1[8]
v24 = a1[9]
v25 = a1[10]
v26 = a1[11]
v27 = a1[12]
v28 = a1[13]
v29 = a1[14]
v30 = a1[15]
v31 = a1[16]
v32 = a1[17]
v33 = a1[18]
v34 = a1[19]
v35 = a1[20]
v36 = a1[21]
v37 = a1[22]
v38 = a1[23]
v39 = a1[24]
v40 = a1[25]
v20 = a1[26]
v41 = a1[27]
v22 = a1[28]
solver.add(255036*v7+-90989*v3+-201344*v4+122006*v5+-140538*v6+109859*v2-109457*v1-9396023 == 0)
solver.add(277432*v6+110191*v3+-186022*v4+175123*v2-75564*v5-252340*v1-12226612 == 0)
solver.add(127326*v4+260948*v2+-102835*v1+225038*v5-129683*v3-45564209 == 0)
solver.add(-170345*v2+217412*v3-26668*v1+38500*v4-27440782 == 0)
solver.add(25295*v2+69369*v3+191287*v1-24434293 == 0)
solver.add(72265*v1-2384745 == 0)
solver.add(264694*v1-190137*v2+19025100 == 0)
solver.add(101752*v24+67154*v8+-20311*v1+-30496*v6+-263329*v7+-99420*v10+255348*v3+169511*v4-121471*v2+231370*v5-33888892 == 0)
solver.add(17253*v8+-134891*v7+144501*v4+220594*v2+263746*v3+122495*v6+74297*v10+205480*v1-32973*v5-115484799 == 0)
solver.add(251337*v3+-198187*v6+-217900*v2+-62192*v8+-138306*v7+-165151*v4-118227*v1-22431*v5+72699617 == 0)
solver.add(243012*v27+-233931*v4+66595*v7+-273948*v5+-266708*v24+75344*v8-108115*v3-17090*v25+240281*v10+202327*v1-253495*v2+233118*v26+154680*v6+25687761 == 0)
solver.add(41011*v8+-198187*v1+-117171*v7+-178912*v3+9797*v24+118730*v10-193364*v5-36072*v6+10586*v25-110560*v4+173438*v2-176575*v26+54358815 == 0)
solver.add(-250878*v24+108430*v1+-136296*v5+11092*v8+154243*v7+-136624*v3+179711*v4+-128439*v6+22681*v25-42472*v10-80061*v2+34267161 == 0)
solver.add(65716*v30+-18037*v26+-42923*v7+-33361*v4+161566*v6+194069*v25+-154262*v2+173240*v3-31821*v27-80881*v5+217299*v8-28162*v10+192716*v1+165565*v24+106863*v29-127658*v28-75839517 == 0)
solver.add(-236487*v24+-45384*v1+46984*v26+148196*v7+15692*v8+-193664*v6+6957*v10+103351*v29-217098*v28+78149*v4-237596*v5-236117*v3-142713*v25+24413*v27+232544*v2+78860648 == 0)
solver.add(-69129*v10+-161882*v3+-39324*v26+106850*v1+136394*v5+129891*v2+15216*v27+213245*v24-73770*v28+24056*v25-123372*v8-38733*v7-199547*v4-10681*v6+57424065 == 0)
solver.add(-268870*v30+103546*v24+-124986*v27+42015*v7+80222*v2+-77247*v10+-8838*v25+-273842*v4+-240751*v28-187146*v26-150301*v6-167844*v3+92327*v8+270212*v5-87705*v33-216624*v1+35317*v31+231278*v32-213030*v29+114317949 == 0)
solver.add(-207225*v1+-202035*v3+81860*v27+-114137*v5+265497*v30+-216722*v8+276415*v28+-201420*v10-266588*v32+174412*v6+249222*v24-191870*v4+100486*v2+37951*v25+67406*v26+55224*v31+101345*v7-76961*v29+33370551 == 0)
solver.add(175180*v29+25590*v4+-35354*v30+-173039*v31+145220*v25+6521*v7+99204*v24+72076*v27+207349*v2+123988*v5-64247*v8+169099*v6-54799*v3+53935*v1-223317*v26+215925*v10-119961*v28-83559622 == 0)
solver.add(43170*v3+-145060*v2+199653*v6+14728*v30+139827*v24+59597*v29+2862*v10+-171413*v31+-15355*v25-71692*v7-16706*v26+264615*v1-149167*v33+75391*v27-2927*v4-187387*v5-190782*v8-150865*v28+44238*v32-276353*v34+82818982 == 0)
solver.add(-3256*v27+-232013*v25+-261919*v29+-151844*v26+11405*v4+159913*v32+209002*v7+91932*v34+270180*v10+-195866*v3-135274*v33-261245*v1+24783*v35+262729*v8-81293*v24-156714*v2-93376*v28-163223*v31-144746*v5+167939*v6-120753*v30-13188886 == 0)
solver.add(-240655*v35+103437*v30+236610*v27+100948*v8+82212*v6+-60676*v5+-71032*v3+259181*v7+100184*v10+7797*v29+143350*v24+76697*v2-172373*v25-110023*v37-13673*v4+129100*v31+86759*v1-101103*v33-142195*v36+28466*v32-27211*v26-269662*v34+9103*v28-96428951 == 0)
solver.add(-92750*v28+-151740*v27+15816*v35+186592*v24+-156340*v29+-193697*v2+-108622*v8+-163956*v5+78044*v4+-280132*v36-73939*v33-216186*v3+168898*v30+81148*v34-200942*v32+1920*v1+131017*v26-229175*v10-247717*v31+232852*v25+25882*v7+144500*v6+175681562 == 0)
solver.add(234452*v34+-23111*v29+-40957*v2+-147076*v8+16151*v32+-250947*v35+-111913*v30+-233475*v24+-2485*v28+207006*v26+71474*v3+78521*v1-37235*v36+203147*v5+159297*v7-227257*v38+141894*v25-238939*v10-207324*v37-168960*v33+212325*v6+152097*v31-94775*v27+197514*v4+62343322 == 0)
solver.add(-142909*v34+-111865*v31+258666*v36+-66780*v2+-13109*v35+-72310*v25+-278193*v26+-219709*v24+40855*v8+-270578*v38+96496*v5+-4530*v1+63129*v28-4681*v7-272799*v30-225257*v10+128712*v37-201687*v39+273784*v3+141128*v29+93283*v32+128210*v33+47550*v6-84027*v4+52764*v40-140487*v27+105279220 == 0)
solver.add(216020*v38+-248561*v29+-86516*v33+237852*v26+-132193*v31+-101471*v3+87552*v25+-122710*v8+234681*v5+-24880*v7+-245370*v1+-17836*v36-225714*v34-256029*v4+171199*v35+266838*v10-32125*v24-43141*v32-87051*v30-68893*v39-242483*v28-12823*v2-159262*v27+123816*v37-180694*v6+152819799 == 0)
solver.add(-116890*v3+67983*v27+-131934*v4+256114*v40+128119*v24+48593*v33+-41706*v2+-217503*v26+49328*v6+223466*v7+-31184*v5+-208422*v36+261920*v1+83055*v20+115813*v37+174499*v29-188513*v35+18957*v25+15794*v10-2906*v28-25315*v8+232180*v32-102442*v39-116930*v34-192552*v38-179822*v31+265749*v30-54143007 == 0)
solver.add(-215996*v4+-100890*v40+-177349*v7+-159264*v6+-227328*v27+-91901*v24+-28939*v10+206392*v41+6473*v25+-22051*v20+-112044*v34+-119414*v30+-225267*v35+223380*v3+275172*v5+95718*v39-115127*v29+85928*v26+169057*v38-204729*v1+178788*v36-85503*v31-121684*v2-18727*v32+109947*v33-138204*v8-245035*v28+134266*v37+110228962 == 0)
solver.add(-165644*v32+4586*v39+138195*v25+155259*v35+-185091*v3+-63869*v31+-23462*v30+150939*v41+-217079*v8+-122286*v6+5460*v38+-235719*v7+270987*v26+157806*v34+262004*v29-2963*v28-159217*v10+266021*v33-190702*v24-38473*v20+122617*v2+202211*v36-143491*v27-251332*v4+196932*v5-155172*v22+209759*v40-146511*v1+62542*v37+185928391 == 0)
solver.add(57177*v24+242367*v39+226332*v31+15582*v26+159461*v34+-260455*v22+-179161*v37+-251786*v32+-66932*v41+134581*v1+-65235*v29+-110258*v28+188353*v38+-108556*v6+178750*v40+-20482*v25+127145*v8+-203851*v5+-263419*v10+245204*v33+-62740*v20+103075*v2-229292*v36+142850*v30-1027*v27+264120*v3+264348*v4-41667*v35+130195*v7+127279*a1[29]-51967523 == 0)
# z3约束器求解
assert solver.check() == sat
flag2 = []
mol = solver.model()
for i in a1:
flag2.append(mol[i].as_long())

mappp = [105, 244, 63, 10, 24, 169, 248, 107, 129, 138, 25, 182, 96, 176, 14, 89, 56, 229, 206, 19, 23, 21, 22, 198, 179, 167, 152, 66, 28, 201, 213, 80, 162, 151, 102, 36, 91, 37, 50, 17, 170, 41, 3, 84, 85, 226, 131, 38, 71, 32, 18, 142, 70, 39, 112, 220, 16, 219, 159, 222, 11, 119, 99, 203, 47, 148, 185, 55, 93, 48, 153, 113, 1, 237, 35, 75, 67, 155, 161, 74, 108, 76, 181, 233, 186, 44, 125, 232, 88, 8, 95, 163, 200, 249, 120, 243, 174, 212, 252, 234, 58, 101, 228, 86, 109, 144, 104, 121, 117, 87, 15, 132, 12, 20, 165, 115, 136, 135, 118, 69, 68, 2, 82, 123, 250, 251, 53, 255, 51, 221, 211, 195, 145, 140, 254, 0, 116, 43, 29, 217, 197, 183, 168, 188, 34, 218, 146, 147, 98, 149, 246, 180, 103, 33, 40, 207, 208, 192, 143, 26, 154, 225, 100, 141, 175, 124, 230, 62, 177, 205, 110, 202, 253, 173, 46, 52, 114, 164, 166, 137, 158, 122, 13, 83, 178, 133, 189, 187, 7, 184, 77, 245, 216, 190, 194, 72, 157, 172, 171, 199, 160, 45, 49, 27, 204, 81, 6, 92, 59, 209, 239, 130, 97, 61, 214, 215, 73, 90, 126, 42, 30, 240, 79, 224, 78, 223, 111, 60, 4, 5, 196, 231, 106, 64, 139, 235, 150, 227, 238, 191, 127, 31, 156, 54, 241, 242, 134, 247, 128, 65, 94, 57, 210, 236, 9, 193]
flag = ""
for i in range(len(flag2)):
ttt = mappp.index(flag2[i])
ttt &= 0xFF
flag += chr(ttt ^ 0xFF ^ i)

print(flag)
# flag{U_90t_th3_p1c5t0re_fl49!}

0x03 Luajit details

Luajit将原生Lua进行了扩展,使它支持JIT方式编译运行,Luajit有如下特点:(1)运行时编译。(2)兼容AOT编译。(3)引入了中间表示IR。

Luajit文件格式

 Luajit官方并没有直接给出Luajit字节码文件的格式文档,但可以通过阅读Luajit源码中加载与生成Luajit字节码文件的函数,来单步跟踪分析出它的文件格式,这两个函数分别是lj_bcread()lj_bcwrite()

 从这两个函数调用的bcread_header()bcread_proto()bcwrite_header()bcwrite_proto()等子函数,因此,可以了解到:Luajit字节码文件与Luac一样,将文件格式分为头部分信息Header与函数信息Proto两部分。

 Luajit字节码文件的header可以定义为:

1
2
3
4
5
6
7
8
9
typedef struct {
char signature[3];
uchar version;
GlobalHeaderFlags flags;
if (!is_stripped) {
uleb128 length;
char chunkname[uleb128_value(length)];
}
} GlobalHeader;

 上述header解释如下:

(1)luajit字节码文件的头3个字节必须为0x1b4c4a,这是它的Magic Number(signature)。

(2)version是luajit字节码文件的版本号,占1个字节。

(3)flags是文件的标志位,采用uleb128编码(可变长)。其中包含3个字段:

  (a)FLAG_IS_BIG_ENDIAN表示大端序还是小端序。

  (b)FLAG_IS_STRIPPED表示是否去除调试信息。如果包含调试信息,即FLAG_IS_STRIPPED没有被置位,那么会多出两个字段:length(字符串长度),chunkname(Luajit文件的源文件名字符串)。

  (c)FLAG_HAS_FFI表示是否有外部函数接口。

 Luajit字节码文件的Proto中有ProtoHeader字段,它描述了Proto的头部信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

 上述ProtoHeader解释如下:

(1)size表示proto结构体的大小。

(2)flags是ProtoHeader的标志位,有以下几部分组成:

  (a)FLAG_HAS_CHILD标识当前proto是一个子函数,也就是闭包(closure)。举个例子来理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

   上述代码中,最外层的Create()向内,每个function都包含一个Closure。在Luac文件格式中,每个Proto都有一个Protos字段,它用来描述ProtoClosure之间的层次信息,Proto采用从外向内的递归方式进行存储。Luajit则采用线性的从内向外的同级结构进行存储,ProtoClosure之前的层级关系使用flags字段的FLAG_HAS_CHILD标志位进行标识,当flags字段的FLAG_HAS_CHILD标志位被置位,则表示当前层的Proto是上一层ProtoClosure上述代码在Luajit的文件结构如下:

1
2
3
4
5
6
7
8
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],它在读取其ProtoHeadersize字段时,由于其值为0,而中止了整个文件的解析。即它的内容为空。

  (b)FLAG_IS_VARIADIC标识了当前Proto是否返回多个值,上面的代码中,只有Create()flags字段会对该标志置位(因为只有它有返回值)。

  (c)FLAG_HAS_FFI同上。

  (d)FLAG_JIT_DISABLED标识当前Proto是否禁用JIT,对于包含了具体代码的Proto,它的值通常没有没有被置位,表示有JIT代码。

  (e)FLAG_HAS_ILOOP标识了当前Proto是否包含了ILOOPJLOOP等指令,编译器好进行优化。

(3)arguments_count表示当前Proto有几个参数。

(4)framesize标识了Proto使用的栈大小。

(5)upvalues_countcomplex_constants_countnumeric_constants_countinstructions_count分别表示UpValue个数、复合常数个数、数值常数个数、指令条数等信息。

 UpValue:当一个函数引用了一个外部函数的局部变量时,这个局部变量就成为了 Upvalue。Upvalue 会在堆上被创建并持续存活,直到没有任何函数引用它为止。当一个函数被垃圾回收时,它所引用的 Upvalue 也会被垃圾回收。

(6)如果包含调试信息,那么会有debuginfo_sizefirst_line_numberlines_count,分别表示DebugInfo结构体占用的字节大小、当前Proto在源文件中的起始行、当前Proto在源文件中所占的行数。

 下面就到了proto的主体部分:

(1)指令Instruction数组,每条指令长度与Luac一样,占用32位,但使用的指令格式完全不同。

(2)常量信息,主要包含3个数组,分别是upvaluescomplex_constantsnumeric_constants数组。complex_constants可以保存字符串、整型、浮点型、TAB表等信息。

(3)debuginfo调试信息。分为LineInfoVarInfos两部分,前者是存储的一条条的行信息,后者是局部变量信息,包括变量类型、名称、以及它的作用域起始地址与结束地址。

其他

Luajit的线性结构解析起来比Luac简单,只需要按序解析Proto,直接读取到字节0结束即可。

0x04 Luajit字节码

 Luajit的字节码设计与原生Lua有很多不同,最终到的效果是:字节码的编码实现更加简单,执行效率也比原生Luac指令更加高效。

 Lua指令的参考文档为:参考文档

 每条指令都为32位,指令分为opcode与操作数两部分。Lua原生指令是不对齐的,即不同的域(A、B、C等)不一定为8位或16位,而Luajit的每个域都为8位或16位。

 Luajit的指令由5部分组成,分别为:指令名称name、3个操作数域ma/mb/mc、指令类型mt。

 指令名称例如:ISLT、ADDVV、USETS、TGETV。它们有些有前后缀,后缀有:

1
2
3
4
5
6
V variable slot。变量槽。
S string constant。字符串常量。
N number constant。数值常量。
P primitive type。原始类型。
B unsigned byte literal。无符号字节字面量。
M multiple arguments/results。多参数与返回值。

 前缀有:

1
2
3
4
5
T table。表。
F function。函数。
U UpValue。上值。
K constant。常量。
G global。全局。

 那么,USETS代表为UpValue设置字符串值,TGETV代表获取一个表结构中指定索引的数据。

0x05 例题2-ezlua

 ezlua这道题与picStore感觉差不多,具体看geekpwn2023。

0x06 例题3-super_flagio

 其实第七期分享会的内容是0x00-0x05,0x06是后来加的。这道题是XCTF final的一道逆向,看上去很有复现意义(也好难呜呜…)。

 所以0x06就是对这道题的复现啦。它的hint有:

1
2
3
4
[flagio] “I can see the flag!”
<system> A wild Goomca has appeared!
[Goomca] “Try your best to beat me! Ahhhh!!!”
[flagio] “I have to find the mushroom to gain more power. But how can I solve this puzzle?”

0x07 Reference

[1] https://dun.163.com/news/p/d937c7174124424ca80bcb888a604ac8

[2] https://gohalo.me/post/lua-sourcecode.html

[3] https://www.cnblogs.com/dmeng2009/p/11329406.html

[4] https://www.52pojie.cn/forum.php?mod=viewthread&tid=664517

[5] https://bbs.kanxue.com/thread-275620.htm

[6] https://xz.aliyun.com/t/9004

[7] https://github.com/feicong/lua_re/blob/master/lua/lua_re3.md

[8] https://in1t.top/2023/04/02/7th-xctf-final-super-flagio/

留言

2023-08-02

© 2024 wd-z711

⬆︎TOP