打ctf的时候遇到了lua逆向,浅记一下遇到的知识点和逆向思路
luac命令可以把.lua代码预编译为字节码文件
因为lua程序执行的核心是在JIT虚拟机上运行字节码,所以luac的作用就是把所有字节码打包成一个二进制文件
=>
https://sourceforge.net/projects/unluac/
处理lua5.0 - lua5.4
https://github.com/viruscamp/luadec
主要针对lua5.1,对lua5.2和lua5.3是实验性的 , 依赖lua源码
参数介绍
API文档
https://www.runoob.com/manual/lua53doc/manual.html
https://pgl.yoyo.org/luai/i/lua_pushcclosure
https://www.bookstack.cn/read/lua-5.3/spilt.23.spilt.1.5.md
API积累
(这里以读入text类型的代码块为例
其中lua_load 函数通过 luaD_protectedparser 保护方式来进行文件读取和语法树解析
luaD_protectedparser 函数内部调用 luaD_pcall 函数
而 luaD_pcall 函数 回调了 f_parser 函数
f_parser 函数中真正做语法树解析的是luaY_parser 函数(如果是text数据的话)
该函数最后执行mainfunc方法,用于执行语法树的解析工作。
mainfunc函数中,有两个函数比较关键。luaX_next:主要用于语法TOKEN的分割,是语法分割器;statlist:主要根据luaX_next分割器分割出来的TOKEN,组装成语法块语句statement,最后将语句逐个组装成语法树
而luaX_next函数内部真正执行Token分割的函数是llex...
...
参考:
https://zhuanlan.zhihu.com/p/429597744
https://blog.csdn.net/initphp/article/details/104428729
正常情况下,luaU_undump 函数调用 checkHeader 函数进行一系列的检查(不只是检查头部魔数)
checkHeader 函数源码如下:
我们从上往下逐个分析
checkliteral(S, LUA_SIGNATURE + 1, "not a");
LUA_SIGNATURE 的宏定义为:
刚好对应luac文件的开头四字节
if (LoadByte(S) != LUAC_VERSION)
LUAC_VERSION 的宏定义如下:
MYINT(s) 宏定义如下
LUA_VERSION_MAJOR 代表了主版本号,LUA_VERSION_MINOR 代表了子版本号
比如,我们的lua版本号为5.3
那么 LUAC_VERSION 就等于5*16+3=83 <=> 0x53
那么luac文件开头的第4个字节应该为 0x53
if (LoadByte(S) != LUAC_FORMAT)
lua5.3.3源码给出的LUAC_FORMAT的宏定义为
这个值应该一般都为0
检查地址在开头的第5字节
checkliteral(S, LUAC_DATA, "corrupted");
lua5.3.3源码给出的LUAC_DATA的宏定义为
这个值,不同版本应该也不怎么会变
检查地址在开头的第6 ~ 11 字节,检查长度为6字节
checksize(S, int);
checksize(S, size_t);
checksize(S, Instruction);
checksize(S, lua_Integer);
checksize(S, lua_Number);
程序用一个巧妙的宏定义完成了检测,#define后面加上一个#号表示将参数字符串化,将其转换成一个字符串常量
if (LoadInteger(S) != LUAC_INT)
if (LoadNumber(S) != LUAC_NUM)
LUAC_INT :
LUAC_NUM:
题目中给了一个lua5.33打包的elf文件和一个被魔改的 luac文件
分析elf文件,其读入的代码块类型为binary,我们直接沿着Lua的文件读取过程寻找哪里被魔改了
luaU_undump
查看 luaU_undump 源码
对比程序的checksize的函数和源码:
可以发现,程序魔改了LoadByte函数,添加了这样一句判断
经过一番研究,发现程序还魔改了 LoadInterger、LoadNumber、LoadInt等函数,既然有load,肯定有dump
对照源码,不难发现程序同样魔改了DumpByte、DumpInt、DumpNumber、DumpInteger函数
下面,我们有一种通用的思路来处理这道题目--魔改lua源码
如下图:
之后重新编译luadec,再进行反编译即可
这里我想介绍的是第二种方法--修复 picstore.bin 代码块
唯一的难题就是: 怎样知道哪些字节被修改了
已知:
(1)、程序load代码块的核心函数有且只有 一个函数 -- LoadBlock
(2)、程序dump代码块的核心函数有且只有 一个函数 --DumpBlock
(3)、这两个函数就像堆栈一样,Load代表 push ,Dump代表 pop
我们只需要记录 LoadBlock 和 DumpBlock 函数的执行次数和函数参数,就可以知道到哪些字节被改变了
我使用gdb_python 脚本进行了记录
修复 picstore.bin
反编译
剩下的问题就比较常规了,z3求解即可
exp:
flag即为
flag{U_90t_th3_p1c5t0re_fl49!}
新人第一次发帖,若有不足,欢迎提出
-
o : 把所有字节码打包成一个文件,
-
o 后跟打包成的文件名
-
l : 把所有字节码打包成一个文件,默认名为luac.out,并打印出所有字节码文件的信息,如下图
-
o : 把所有字节码打包成一个文件,
-
o 后跟打包成的文件名
-
l : 把所有字节码打包成一个文件,默认名为luac.out,并打印出所有字节码文件的信息,如下图
java
-
jar unluac.jar luac.out >
3.lua
java
-
jar unluac.jar
-
-
rawstring luac.out >
3.lua
java
-
jar unluac.jar luac.out >
3.lua
java
-
jar unluac.jar
-
-
rawstring luac.out >
3.lua
git clone https:
/
/
github.com
/
viruscamp
/
luadec
cd luadec
git submodule update
-
-
init lua
-
5.3
cd lua
-
5.3
make linux
cd ..
/
luadec
make LUAVER
=
5.3
git clone https:
/
/
github.com
/
viruscamp
/
luadec
cd luadec
git submodule update
-
-
init lua
-
5.3
cd lua
-
5.3
make linux
cd ..
/
luadec
make LUAVER
=
5.3
-
pn : 打印函数嵌套结构
-
dis : 反汇编luac.out或lua源码
luadec abc.lua 或 luadec luac.out : 反编译lua源码或luac二进制文件
-
pn : 打印函数嵌套结构
-
dis : 反汇编luac.out或lua源码
luadec abc.lua 或 luadec luac.out : 反编译lua源码或luac二进制文件
1
、lua_pushcclosure
lua_pushcclosure()函数是Lua C API提供注册C函数最基础的。其他注册方式都是在该函数上面拓展的
void lua_pushcclosure (lua_State
*
L, lua_CFunction fn,
int
n);
fn为要注册的函数指针
2
、luaL_dofile
加载并运行指定的文件。 它是用下列宏定义出来
(luaL_loadfile(L, filename) || lua_pcall(L,
0
, LUA_MULTRET,
0
))
3
、luaL_loadfilex
int
luaL_loadfilex (lua_State
*
L, const char
*
filename, const char
*
mode);
把一个文件加载为 Lua 代码块,代码块的名字被命名为 filename , 如果 filename 为 NULL,它从标准输入加载
4
、lua_load
int
lua_load (lua_State
*
L,lua_Reader reader,void
*
data,const char
*
chunkname,const char
*
mode);
加载一段 Lua 代码块,但不运行它。 如果没有错误, lua_load 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息
参数 reader , 用来读取数据,比如 luaL_loadfilex 内部调用 lua_load 函数,reader 就是getF函数,其通过fread函数读取文件
参数 data , 是指向可选数据结构的指针,可以传递给reader函数。
参数 chunkname是一个字符串,标识了正在加载的块了名字
参数 mode是一个字符串,指定如何编译数据块。可能取值为:
"b"
(二进制):该块是预编译的二进制块,加载速度比源块快。
"t"
(text): chunk是一个文本块,在执行前被编译成字节码。
"bt"
(both):数据块可以是二进制数据块也可以是文本数据块,函数首先尝试将其作为二进制数据块加载,如果加载失败,则尝试将其作为文本数据块加载。
5
、lua_pushfstring
const char
*
lua_pushfstring (lua_State
*
L, const char
*
fmt, ...);
把一个格式化过的字符串压栈,然后返回这个字符串的指针。 它和 C 函数 sprintf 比较像
6
、lua_pushstring
const char
*
lua_pushstring (lua_State
*
L, const char
*
s);
将指针 s 指向的零结尾的字符串压栈。
7
、lua_tolstring
const char
*
lua_tolstring (lua_State
*
L,
int
index, size_t
*
len
);
把给定索引处的 Lua 值转换为一个 C 字符串。 如果
len
不为 NULL , 它还把字符串长度设到
*
len
中。 这个 Lua 值必须是一个字符串或是一个数字; 否则返回返回 NULL 。 如果值是一个数字, lua_tolstring 还会 把堆栈中的那个值的实际类型转换为一个字符串。
1
、lua_pushcclosure
lua_pushcclosure()函数是Lua C API提供注册C函数最基础的。其他注册方式都是在该函数上面拓展的
void lua_pushcclosure (lua_State
*
L, lua_CFunction fn,
int
n);
fn为要注册的函数指针
2
、luaL_dofile
加载并运行指定的文件。 它是用下列宏定义出来
(luaL_loadfile(L, filename) || lua_pcall(L,
0
, LUA_MULTRET,
0
))
3
、luaL_loadfilex
int
luaL_loadfilex (lua_State
*
L, const char
*
filename, const char
*
mode);
把一个文件加载为 Lua 代码块,代码块的名字被命名为 filename , 如果 filename 为 NULL,它从标准输入加载
4
、lua_load
int
lua_load (lua_State
*
L,lua_Reader reader,void
*
data,const char
*
chunkname,const char
*
mode);
加载一段 Lua 代码块,但不运行它。 如果没有错误, lua_load 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息
参数 reader , 用来读取数据,比如 luaL_loadfilex 内部调用 lua_load 函数,reader 就是getF函数,其通过fread函数读取文件
参数 data , 是指向可选数据结构的指针,可以传递给reader函数。
参数 chunkname是一个字符串,标识了正在加载的块了名字
参数 mode是一个字符串,指定如何编译数据块。可能取值为:
"b"
(二进制):该块是预编译的二进制块,加载速度比源块快。
"t"
(text): chunk是一个文本块,在执行前被编译成字节码。
"bt"
(both):数据块可以是二进制数据块也可以是文本数据块,函数首先尝试将其作为二进制数据块加载,如果加载失败,则尝试将其作为文本数据块加载。
5
、lua_pushfstring
const char
*
lua_pushfstring (lua_State
*
L, const char
*
fmt, ...);
把一个格式化过的字符串压栈,然后返回这个字符串的指针。 它和 C 函数 sprintf 比较像
6
、lua_pushstring
const char
*
lua_pushstring (lua_State
*
L, const char
*
s);
将指针 s 指向的零结尾的字符串压栈。
7
、lua_tolstring
const char
*
lua_tolstring (lua_State
*
L,
int
index, size_t
*
len
);
把给定索引处的 Lua 值转换为一个 C 字符串。 如果
len
不为 NULL , 它还把字符串长度设到
*
len
中。 这个 Lua 值必须是一个字符串或是一个数字; 否则返回返回 NULL 。 如果值是一个数字, lua_tolstring 还会 把堆栈中的那个值的实际类型转换为一个字符串。
/
*
*
*
文件解析函数(保护方式调用)
*
调用:luaD_pcall方法
*
/
int
luaD_protectedparser (lua_State
*
L, ZIO
*
z, const char
*
name, const char
*
mode) {
status
=
luaD_pcall(L, f_parser, &p, savestack(L, L
-
>top), L
-
>errfunc);
}
/
*
*
*
文件解析函数(保护方式调用)
*
调用:luaD_pcall方法
*
/
int
luaD_protectedparser (lua_State
*
L, ZIO
*
z, const char
*
name, const char
*
mode) {
status
=
luaD_pcall(L, f_parser, &p, savestack(L, L
-
>top), L
-
>errfunc);
}
static void f_parser (lua_State
*
L, void
*
ud) {
/
/
通过数据头的signature来判断读取的数据是binary还是text的,如果是binary的数据,
/
/
则调用luaU_undump来读取预编译好的lua chunks,如果是text数据,则调用luaY_parser来parse lua代码
/
/
也就是说,读取text源码文件要多了一步paser工作
if
(c
=
=
LUA_SIGNATURE[
0
]) {
checkmode(L, p
-
>mode,
"binary"
);
cl
=
luaU_undump(L, p
-
>z, &p
-
>buff, p
-
>name);
}
else
{
checkmode(L, p
-
>mode,
"text"
);
cl
=
luaY_parser(L, p
-
>z, &p
-
>buff, &p
-
>dyd, p
-
>name, c);
}
}
static void f_parser (lua_State
*
L, void
*
ud) {
/
/
通过数据头的signature来判断读取的数据是binary还是text的,如果是binary的数据,
/
/
则调用luaU_undump来读取预编译好的lua chunks,如果是text数据,则调用luaY_parser来parse lua代码
/
/
也就是说,读取text源码文件要多了一步paser工作
if
(c
=
=
LUA_SIGNATURE[
0
]) {
checkmode(L, p
-
>mode,
"binary"
);
cl
=
luaU_undump(L, p
-
>z, &p
-
>buff, p
-
>name);
}
else
{
checkmode(L, p
-
>mode,
"text"
);
cl
=
luaY_parser(L, p
-
>z, &p
-
>buff, &p
-
>dyd, p
-
>name, c);
}
}
LClosure
*
luaY_parser (lua_State
*
L, ZIO
*
z, Mbuffer
*
buff, Dyndata
*
dyd, const char
*
name,
int
firstchar) {
LexState lexstate;
FuncState funcstate;
mainfunc(&lexstate, &funcstate);
}
LClosure
*
luaY_parser (lua_State
*
L, ZIO
*
z, Mbuffer
*
buff, Dyndata
*
dyd, const char
*
name,
int
firstchar) {
LexState lexstate;
FuncState funcstate;
mainfunc(&lexstate, &funcstate);
}
static void mainfunc (LexState
*
ls, FuncState
*
fs) {
luaX_next(ls);
/
*
读取第一个token read first token
*
/
statlist(ls);
/
*
语法树遍历解析 parse main body
*
/
}
static void mainfunc (LexState
*
ls, FuncState
*
fs) {
luaX_next(ls);
/
*
读取第一个token read first token
*
/
statlist(ls);
/
*
语法树遍历解析 parse main body
*
/
}
LClosure
*
luaU_undump(lua_State
*
L, ZIO
*
Z, const char
*
name) {
LoadState S;
LClosure
*
cl;
if
(
*
name
=
=
'@'
||
*
name
=
=
'='
)
/
/
加载的 chunkname
S.name
=
name
+
1
;
else
if
(
*
name
=
=
LUA_SIGNATURE[
0
])
S.name
=
"binary string"
;
else
S.name
=
name;
S.L
=
L;
S.Z
=
Z;
checkHeader(&S);
cl
=
luaF_newLclosure(L, LoadByte(&S));
setclLvalue(L, L
-
>top, cl);
luaD_inctop(L);
cl
-
>p
=
luaF_newproto(L);
LoadFunction(&S, cl
-
>p, NULL);
lua_assert(cl
-
>nupvalues
=
=
cl
-
>p
-
>sizeupvalues);
luai_verifycode(L, buff, cl
-
>p);
return
cl;
}
LClosure
*
luaU_undump(lua_State
*
L, ZIO
*
Z, const char
*
name) {
LoadState S;
LClosure
*
cl;
if
(
*
name
=
=
'@'
||
*
name
=
=
'='
)
/
/
加载的 chunkname
S.name
=
name
+
1
;
else
if
(
*
name
=
=
LUA_SIGNATURE[
0
])
S.name
=
"binary string"
;
else
S.name
=
name;
S.L
=
L;
S.Z
=
Z;
checkHeader(&S);
cl
=
luaF_newLclosure(L, LoadByte(&S));
setclLvalue(L, L
-
>top, cl);
luaD_inctop(L);
cl
-
>p
=
luaF_newproto(L);
LoadFunction(&S, cl
-
>p, NULL);
lua_assert(cl
-
>nupvalues
=
=
cl
-
>p
-
>sizeupvalues);
luai_verifycode(L, buff, cl
-
>p);
return
cl;
}
static void checkHeader (LoadState
*
S) {
checkliteral(S, LUA_SIGNATURE
+
1
,
"not a"
);
/
*
1st
char already checked
*
/
if
(LoadByte(S) !
=
LUAC_VERSION)
error(S,
"version mismatch in"
);
if
(LoadByte(S) !
=
LUAC_FORMAT)
error(S,
"format mismatch in"
);
checkliteral(S, LUAC_DATA,
"corrupted"
);
checksize(S,
int
);
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"
);
}
static void checkHeader (LoadState
*
S) {
checkliteral(S, LUA_SIGNATURE
+
1
,
"not a"
);
/
*
1st
char already checked
*
/
if
(LoadByte(S) !
=
LUAC_VERSION)
error(S,
"version mismatch in"
);
if
(LoadByte(S) !
=
LUAC_FORMAT)
error(S,
"format mismatch in"
);
checkliteral(S, LUAC_DATA,
"corrupted"
);
checksize(S,
int
);
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"
);
}
static void fchecksize (LoadState
*
S, size_t size, const char
*
tname) {
if
(LoadByte(S) !
=
size)
error(S, luaO_pushfstring(S
-
>L,
"%s size mismatch in"
, tname));
}
static void fchecksize (LoadState
*
S, size_t size, const char
*
tname) {
if
(LoadByte(S) !
=
size)
error(S, luaO_pushfstring(S
-
>L,
"%s size mismatch in"
, tname));
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-12-23 15:03
被tlsn编辑
,原因: