about-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 | push b |
其实现起来比较简单,每条指令占用的存储空间也小。但是,对于运算而言(例如加法),这需要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_TLIGHTUSERDATA
和 LUA_TUSERDATA
。LUA_TLIGHTUSERDATA
是由 Lua 外部的使用者来完成,LUA_TUSERDATA
则是通过 Lua 内部来完成,也就是说,LUA_TLIGHTUSERDATA
是通过用户来维护其生命周期。
Lua有垃圾回收机制,string、function、userdata、thread 和 table都需要gc。需要 gc 的数据类型,都会有一个 CommonHeader 成员。
Lua中类型的值是由TValue结构体表示(这里简略了):
1 | typedef union Value { |
其中,gc 表示需要垃圾回收的一些值,如 string、table 等;p 表示 light userdata,不会被 gc。
Table
Table是Lua中唯一表示数据结构的类型,他是一个混合数据结构。其中的函数环境 (env)、元表 (metatable)、模块 (module) 和注册表 (registery) 等都是通过 table 表示。Lua-5.0 后,table 包含一个哈希表部分和一个数组部分,哈希部分发生冲突就用链表解决。
1 | typedef struct Table { |
array、sizearray 用于表示数组部分及其大小,node、lsizenode 表示哈希部分与大小。
当查找table中的数据时,其逻辑如下:
(1)对于字符串类型,通过 luaH_getstr()
先获得相应字符串在哈希表中的链表,然后遍历这个链表,采用内存地址比较字符串,若找到则返回相应的值,否则 nil
。
(2)如果是整型,则调用 luaH_getint()
查找,如果 key 的值小于等于数组大小,则直接返回相应的值,否则去哈希表中去查找。
(3)对应其他类型,统一调用 getgeneric()
,也就是计算 hash 值并在链表中查找,通过 luaV_equalobj()
对各种类型进行比较。
lua解释器体系结构
其中,stack 成员用于指向栈底,而 base 指向当前正在执行的函数的第一个参数,而 top 指向栈顶。寄存器实际上是栈上元素的别名。pc 来指向下一条要执行的指令。
Lua字节码
Lua 的指令使用 32bit 的无符号整型表示,可以通过luac
编译成字节码。
例如,查看lua5.1
编译后的字节码文件,文件头部12字节为:1b4c 7561 5100 0104 0804 0800
。其中,1b4c 7561
为\033Lua
;51
表示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 | LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) { |
Lua文件读取&解析的调用链示例
其中,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
反编译的对比:
(2)找到魔改的地方。可以看到,反编译结果将checkHeader
函数融合到了lua_undump
函数中。再来对比checksize
函数:
可以清楚地发现,加入了红框所示的代码。其中,v2
代表size的大小。若v2<0xFE
,则返回取反的值。在loadInteger
与loadNumber
也发现了类似的情况:
不难猜到,在undump
(也就是load)上做的这些改动,在dump
相关的函数上同样会发生。但是,由于我们要将二进制文件(lua字节码文件)加载并反编译,因此,只需改动undump
相关函数即可。
Step2:对luac源码进行修改并进行反编译。
需要说明的是,Step1中关于luac的源码我摘抄自lua5.4.6
,是有问题的。最终是在lua5.3.3
上进行的修改。如下所示:
1 | git clone https://github.com/viruscamp/luadec |
但是,我没有复现成功,一直显示:format mismatch in precompiled chunk
。
思路2
Step1:查找哪些字节码文件中哪些字节被修改了。
直接抛出结论:程序load代码块的核心函数有且只有一个函数(LoadBlock
)。程序dump代码块的核心函数有且只有一个函数(DumpBlock)。那么,只需要记录 LoadBlock 和 DumpBlock 函数的执行次数和函数参数,就可以知道到哪些字节被改变了。
wp使用了gdb自动化脚本进行调试,脚本如下(文中说frida hook不到dumpBlock函数,但我没有尝试):
1 | """ |
输出结果log.log
如下:
其中,0x10=>8
表示为地址0x10之后8字节都取反。
Step2:修复并得到正确的字节码文件。
根据log.log
,可以修复题目中给的字节码文件,脚本如下:
1 | import struct |
上述脚本有一个难理解的地方,即,脚本中逻辑为:若某字节不为0或0xff,就取反,否则不取反。这与:
1 | if v[i]-1 <= 0xfd: |
是等价的。最终得到修复后的fix_picStore.bin
字节码文件。
Step3:字节码反编译成lua。
一般的lua字节码文件,可以使用unluac
、LuaDec
等工具进行反编译。在此使用unluac
,运行java -jar ./unluac.jar ./fix_picStore.bin > ./oplua.lua
。
Step4:z3约束求解。
1 | from z3 import * |
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 | typedef struct { |
上述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 | typedef struct { |
上述ProtoHeader
解释如下:
(1)size表示proto结构体的大小。
(2)flags是ProtoHeader
的标志位,有以下几部分组成:
(a)FLAG_HAS_CHILD
标识当前proto是一个子函数,也就是闭包(closure)。举个例子来理解一下:
1 | function Create(n) |
上述代码中,最外层的Create()
向内,每个function都包含一个Closure
。在Luac文件格式中,每个Proto
都有一个Protos
字段,它用来描述Proto
与Closure
之间的层次信息,Proto
采用从外向内的递归方式进行存储。而Luajit
则采用线性的从内向外的同级结构进行存储,Proto
与Closure
之前的层级关系使用flags
字段的FLAG_HAS_CHILD
标志位进行标识,当flags
字段的FLAG_HAS_CHILD
标志位被置位,则表示当前层的Proto
是上一层Proto
的Closure
。上述代码在Luajit的文件结构如下:
1 | struct Luajit lj; |
从存局中可以看出,最内层的foo3()
位于Proto
的最外层,它与Luac
的布局是相反的,而proto[4]
表示了整个Lua文件,它是Proto
的最上层。最后的proto[5]
,它在读取其ProtoHeader
的size
字段时,由于其值为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
是否包含了ILOOP
与JLOOP
等指令,编译器好进行优化。
(3)arguments_count
表示当前Proto
有几个参数。
(4)framesize
标识了Proto
使用的栈大小。
(5)upvalues_count
、complex_constants_count
、numeric_constants_count
、instructions_count
分别表示UpValue个数、复合常数个数、数值常数个数、指令条数等信息。
UpValue:当一个函数引用了一个外部函数的局部变量时,这个局部变量就成为了 Upvalue。Upvalue 会在堆上被创建并持续存活,直到没有任何函数引用它为止。当一个函数被垃圾回收时,它所引用的 Upvalue 也会被垃圾回收。
(6)如果包含调试信息,那么会有debuginfo_size
、first_line_number
、lines_count
,分别表示DebugInfo
结构体占用的字节大小、当前Proto
在源文件中的起始行、当前Proto
在源文件中所占的行数。
下面就到了proto的主体部分:
(1)指令Instruction
数组,每条指令长度与Luac
一样,占用32位,但使用的指令格式完全不同。
(2)常量信息,主要包含3个数组,分别是upvalues
、complex_constants
、numeric_constants
数组。complex_constants
可以保存字符串、整型、浮点型、TAB表等信息。
(3)debuginfo
调试信息。分为LineInfo
与VarInfos
两部分,前者是存储的一条条的行信息,后者是局部变量信息,包括变量类型、名称、以及它的作用域起始地址与结束地址。
其他
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 | V variable slot。变量槽。 |
前缀有:
1 | T table。表。 |
那么,USETS代表为UpValue设置字符串值,TGETV代表获取一个表结构中指定索引的数据。
0x05 例题2-ezlua
ezlua这道题与picStore感觉差不多,具体看geekpwn2023。
0x06 例题3-super_flagio
其实第七期分享会的内容是0x00-0x05,0x06是后来加的。这道题是XCTF final的一道逆向,看上去很有复现意义(也好难呜呜…)。
所以0x06就是对这道题的复现啦。它的hint有:
1 | [flagio] “I can see the flag!” |
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/
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!