本文说得稍微详细点,主要给没接触过pwn的童鞋看,大神请绕道。另外我也是第一次接触堆的漏洞利用,有错误请指出,感激不尽。
这是一题heap漏洞利用的题,程序有一个保存5个chunk地址及其有效性的结构数组,并提供申请并写入、删除、修改chnuk的功能。
先简单看下程序功能,主函数及菜单函数就不看了。 chunk申请函数中,能定义chunk大小,其过程是:先检查现有的chunk数量不能超过5个及申请大小不超过4kb,申请内存并写入内容,大小小于112字节的chunk的输入内容将在栈上中转下,最后将申请的chunk地址、大小写入全局变量,并使能可写标记。 程序在接受数字类的选择参数时使用了read_to_i
,开始还以为能通过这个在申请chunk时形成栈溢出呢,结果发现要么绕不过条件要么参数有问题。就作罢了。
内容修改也对操作的chunk序号进行了检查,功能主要是检查了有效性标志,大小控制使用对应保存的尺寸数据。
chunk释放功能中,将chunk free掉,有效性并没有检查前面说的有效性标志(所以我说那只是可写有效性标志),还有一个问题就chunk释放后并没有清指针,形成悬空指针。这种情况一般会出现的漏洞利用方式有UAF(use after free)、double free等。我觉得本题的预期做法应该是double free。
另外,程序为了功能实现,采用了一个结构体数组保存chunk的数据指针和有效标志。结构体数组如下:
程序没有明显的地址泄露的地方,也没有其它可利用的漏洞,目前只有可利用的double free漏洞。检查下保护,开了Partial RELRO
和NX
,GOT表可写。
所以大概思路是:通过double free,在保存chunk地址的数据结构中伪造chunk地址,再使用程序修改chunk内容的功能,改写目标内存内容,这样就能改写GOT表,将free
函数替换成system
,这样free(addr)
就变成了system(addr)
。
在此之前还要泄露libc的物理基址,所以要先将free
改成puts
,然后输出导入函数的地址,根据提供的Libc,查找函数偏移,再计算出Libc的基址。此处实际上是用puts
的.plt
地址覆盖。
思路很清晰,实现很残酷。其实我开始以为double free能直接将任意地址写入保存chunk地址的结构数组中,然而不是。我看了一天多的资料,终于对double free有一点点的理解。
先大概说下基本知识。 不管是在用的还是释放的chunk,其数据结构是差不多一样的,差别在于prev_size
、'fd'和'bk',prev_size
只有前一个chunk是free状态才会放置其大小,后两个只有当前chunk是free状态才会有,不然这三个位置只会存放数据。
由于堆的分配大小是8字节对齐的,所以pre_size
和size
后3bit都为0,为了节省空间,glibc把size
的后三位用于3个标志位,分别表示:
PREV_INUSE (P) –标示前一个chunk的分配在用状态。 IS_MMAPPED (M) – 标示通过mmap分配。 NON_MAIN_ARENA (N) – 标示此chunk属于线程arena。
关于堆的分配,还需要说明下,堆管理中的chunk指针是指向chunk头部,大小也是包括头部的,而用户申请的大小只是数据空间的大小,返回的指针也是指向数据空间 。
堆的double free利用主要是根据堆分配的原理及规律、堆悬空指针的存在及unlink机制实现的。
堆的分配一般是从低地址到高地址连续分配,这就会发生新申请的chunk直接释放,再申请的新chunk其堆指针是一样的。而其回收释放是通过bins完成的,释放的chunk根据其大小不同将其加入bins的单身或双向链表。关于chunk的数据结构及类型和bins的类型及特性,请查阅Understanding glibc malloc 。此文章讲解得特别细,建议不了解堆的分配管理细节的童鞋精读下。
堆的释放过程大概是这样的:检查相邻前后chunk是否释放,如果释放,就会进行向前或向后合并(也有些地方说是融合),当前chunk指针变为指向前一个(后一个chunk)的指针,并将free状态的相邻chunk从bins中unlink,再合并后的chunk添加到双向链表(非fast chunk)中。
unlink的主要宏代码如下:
当前的libc堆管理为了防止double free,释放chunk前,检查FD->bk=BK->fd=P
, P为当前需要free的chunk指针,BK的前一个chunk的指针,FD为后一个chunk的指针。如果有一个堆指针可控,并在一个chunk的数据段内,再如果有个可控的地址是指向P的,记为*X=P。那么我们就在此chunk上构造两个chunk,第一个chunk在pre_size
的标志位P设为1,大小到P结束,第二个chunk的pre_size
的标志位P设为0,针对64位系统,第一个chunk的fd设为(X-0x18),bk设为(X-0x10),即P->fd=(X-0x18),P->fd=(X-0x10),又因为* X=P,所以(X-0x18)->bk=P,(X-0x10)->fd=P,通过unlink的检查,按照unlink的宏代码,unlink过程中X的内容前后被写为(X-0x10)、(X-0x18)
,最终X的内容被我们改写。
这些条件我们有符合。具体做法是:先申请两个small chunk:
两个chunk大小都为0x100,序号分别为2和1。堆空间内容应该是
申请完了立即free掉,再申请一个大小为0x210的chunk,序号3。此时堆空间是这样的,虚线表示并不存在,为了和上图比较。chunk 3的堆指针和chunk 2是一样的,其数据空间直到chunk 1的数据空间结束。为保证double free利用万无一失,最好后申请的大chunk的空间与之前两个chunk完全重叠。此时数据指针X和堆指针P的指向均落在chunk3的数据空间里。
接下来伪造两个chunk,记为chunk4和chunk5,对应的payload为:
p64(0x0)+p64(0x101)+p64(X-0x18)+p64(X-0x10)+'A'*(0x100-0x20)+p64(0x100)+p64(0x110)
当前payload在申请chunk的时候就提交的,所以实际上现在的堆的情况是这样的
多出了两个伪造的chunk。分析下payload。 p64(0x0)+p64(0x101)表示前一个chunk在使用中,当前chunk尺寸为0x100;p64(X-0x18)+p64(X-0x10)表示chunk4的fd和bk指向地址;'A'*(0x100-0x20)为chunk4的数据填充;p64(0x100)+p64(0x110)表示chunk5前一个chunk即chunk4未使用,大小为0x100,chunk5的尺寸为0x110。
然后free(1),我们传递的是数据指针libc会转换成chunk指针,过程就和上面说的一样,P的前一个chunk4处于free状态,要向后合并,P=*X
,而*X
就是已经释放的chunk2的数据指针,申请chunk2时系统返回,free之后并示释放指针而成的悬空指针。然后执行unlink操作,hp[2].addr = (&hp[0]+8)
。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)