avatar

Catalog
缓冲区溢出入门(下)

前言

堆溢出相对前两种缓冲区溢出方式更为复杂一些,因此这里单独开一篇对堆溢出进行简单介绍。

堆的生命周期

为了方便理解,这里先不讲堆的利用方式,而是先过一遍堆的分配与释放的流程。然后根据内存中堆的变化情况,在去查看源码去了解堆的结构,寻找堆的利用方式,再进行堆的利用。

首先,这里还是选择Protostar上的一道题进行分析,题目如下:

由题,程序首先将3个输入的参数复制到申请的堆上,然后再释放掉。下面来看程序的执行流程:

  1. 第一步,在每个函数调用结束后的地方下断,这样可以可以较为清晰的看到堆中内存的变化情况;然后运行一次程序,找到堆在内存中的位置,从而设置好hook工具,在程序断下时,能够自动查看堆部分的内存。操作如下图所示:

  2. 重新运行程序,分别在3次malloc调用后断下,观察堆中的内存分布,如下图所示。橙色表示第一块malloc出来的区域,蓝色表示第二块malloc出来的区域,紫色表示第三块malloc出来的区域。这些malloc出来的区域又称作chunk,0x29表示的正是chunk的大小,稍后会解释为什么在调用malloc时传入的参数是0x20,而这里chunk的大小又是0x29。红色方框内的数字,表示堆中剩余内存的大小。可以发现每调用一次malloc,堆中剩余的大小就会减少0x28。

  3. 接着执行3次strcpy,众所周知,这是一个不检查参数长度的函数,也是导致各类溢出的罪魁祸首之一。我们这次传入了长度大小适宜的参数作为演示,可以看到,参数值会被写入chunk大小后的位置。

  4. 最后是3次free,注意观察发生变化的位置:

    • 先是free掉最后一个chunk,这里将chunk中的字符串的前四字节清零了
    • 接着free掉第二个chunk,并让原先字符串的前四字节指向了第三个chunk的地址
    • 最后free掉第一个chunk,做法同free掉第二个chunk时一样

    这里就会产生疑问,为什么修改的是字符串的前4字节?为什么会指向下一个chunk,这些将在下一部分讲解

堆的利用方式

首先需要说明的是,本题的利用方式并不通用。这道题的环境,使用dlmalloc(Linux早期的堆分配与回收的实现,由Doug Lea编写)作为堆分配器,而现如今Linux的发行版使用的是glibc中的堆分配器:ptmalloc2。

接下来看利用方式,首先观察题目本身,需要重定向到函数winner,所以考虑将printf的GOT修改为winner的地址。但显然,没有很直接的方式去修改,因此下面需要了解一些堆的相关概念和函数的实现细节,从而寻找突破口。这部分源码参考此处(注意源码的版本,有些版本已经修复此漏洞)

堆的结构

前面提到malloc出来的区域其实是一个chunk,其结构如下:

下面以之前的运行结果为例,说明一下各个结构

  • 橙色方框:整个chunk
  • 绿色方框:前一个chunk的prev_size,不包括(prev_inuse)
  • 蓝色方框:当前chunk的size,其中最后一个bit值的含义为prev_inuse。若前一个块正在被使用,则该bit置1。第一个chunk之前的区域为代码段,所以会被认为是正在使用的区域,因此该值设置为1,因此这里的值为0x29而不是0x28。
  • 紫色方框:fd,指向前一个chunk
  • 红色方框:bk,指向后一个chunk。这里的fd和bk的使用都是有前提的,而本题在free时,仅用了fd,并且指向的不是前一个chunk,而是后一个chunk,形成的是一个单链表。

free的执行流程

这里为什么选择free呢?首先,strcpy是能够产生溢出的函数,但不是实现溢出利用的函数,因此想要实现堆溢出利用,需要往后找其它函数,通过之前的溢出练习,可以很自然的想到通过修改printf的GOT表实现重定向,而谁来帮我们做这件事呢?strcpy肯定不能,那么就只能把目光放在free上了。

用我找的这部分源码做图会比较麻烦,为了方便观看,就直接用视频里的截图了(虽然他这一期有些小错误,我后面会指出来),不过也建议自己阅读一遍这部分源码,很短,难度不大:

  1. dlmalloc的free的实现由_int_free完成,这里传入的参数mem,就是需要free掉的内存地址,也就是字符串的起始地址(chunk+8的位置)。开始会调用宏mem2check获取chunk的地址。

  2. 来到第一个if块,当chunk的size小于fastbin规定的最大size(80或0x50)时,就会执行下面的部分。这部分,也就是上面介绍堆的生命周期时,在执行free时看到的过程,会形成一个单链表。这部分显然不能构造出溢出,所以不关心。

  3. 来到else if这里,走到这里,也就是说要保证我们free掉的chunk大小必须大于80或0x50。这里还有一个判断条件,就是is_mmaped,由图,前面说到,chunk的size位于第二个字段,其中倒数第一个bit表示prev_inuse,而倒数第二个bit就表示is_mmaped(图中红框中也有显示),根据这里的条件,想要进入语句块,需要将is_mmaped位置0。

  4. 这里有一个很重要的点,就是unlink,当然了,视频里这里出错了,实际上不需要在这个unlink进行GOT的覆盖,后面还有一个unlink,但是,unlink是一个关键的函数

    来看unlink的定义,FD表示前一个chunk的地址,BK表示后一个chunk的地址。unlink的操作就是将当前chunk从(fd和bk维护的)双链表断掉,将前一个chunk和后一个chunk链接起来,其目的是为了使当前chunk和前一个chunk合并成一个chunk。这就是当chunk的size大于fastbin规定的最大size时可能会做的操作(说可能是因为,这里走的是prev_inuse为0的操作,下面还有一个unlink也可以用于利用)。

    这里需要特别注意的是,unlink操作时,会有一个将BK赋值给FD->bk的操作。换个思路,如果FD就是printf的GOT表,BK就是winner的地址,那么不就可以实现函数执行的重定向了?当然不会这么简单,因为紧接着,就会有一个FD赋值给BK->fd的操作,如果BK是winner的地址,那么在BK->fd处,也就是&winner+0x8的位置会被赋值,这样在执行winner时,就会出错了。不过这里可以换个思路,如果BK设置的不是winner的地址,而是一个位于堆上的跳转指令(通常小于8字节),这样在FD赋值到BK->fd时,就不会对winner函数本身造成影响了。有了这个思路,我们开始手动去去实现堆溢出的利用。

利用过程

根据上述对堆free执行流程以及堆结构本身的了解,下面来尝试实现本题对于堆的利用:

  1. 首先将程序断在第一个free执行前的位置,目前堆中是我们熟悉的情况。接下来,结合前面讲到的方法,修改此时堆中的内存,利用free执行时的漏洞实现对printf的GOT表的修改,从而完成执行时的重定位。

  2. 回顾一下,前面提到的free的利用过程,这里一共有两个unlink。我们选择从第二个unlink入手。想要执行这个unlink则需要实现以下操作:

    • chunksize > 80或0x50,这样才能走到执行到这里,不然free的执行流程就和前面讲堆生命周期时介绍的一样了
    • prev_inuse == 1,来保证不执行红色方框的unlink
    • nextchunk != av->top,这意思就是下一个chunk不是最后一个chunk,由于这里我们要free的本身就是最后一个chunk了,因此,我们之后还要再构造2个chunk
    • nextinuse == 0,这是判断下一个chunk是否在使用,这个值通过最后一个chunk的prev_inuse来判断(因为我们会多设置2个chunk)
  3. 根据第2步的要求,对堆进行如下设置。这里要free的块的大小选择了0x60,并设置了prev_inuse,从而不会去执行第一个unlink。绿色方框则是构造的2个chunk,大小都是0x10,其中第二个没有设置prev_inuse,从而可以执行第二个unlink。最后还构造了新的剩余堆的大小,以防崩溃。

  4. 下面是构造的很关键,由于我们选择的是第二个unlink,因此需要去构造free时的下一个chunk的fd和bk,这里橙色方框是将fd的值设置为printfGOT表的地址减去0xC的位置,这里减去0xC是因为调用的时候bk位于chunk+0xC的偏移处;绿色方框则是设置了一个堆中的地址,指向了第一个chunk;蓝色方框非常关键,这其实是jmp 0x83e58955(winner的地址)的机器指令,共5个字节,这个跳转的是根据偏移进行跳转,偏移的计算过程不在此列出,可自行查阅。

    构造完上述内容后,在执行free时,绿色方框所指向的堆的地址就会写入printf的GOT表中;而GOT表中偏移位为-0xC处的值(也就是橙色方框地址所指的值),也会写入图中灰色方框的位置,这样刚好不影响我们嵌入的跳转指令。最后,继续执行程序,发现可以成功执行winner函数中的内容,利用成功!

Shellcode编写

有了上述的理论,就可以编写Shellcode了。这里不作演示,不过有以下注意点:

  1. strcpy遇到\x00是会停止的,所以构造新的chunk时需要注意。可以利用32位机器上0xfffffffc == -4这种机制去设置chunk的大小
  2. 跳转指令的编写可以参考在线汇编器,不一定只使用jmp。这种语句的构造就和hook一样:
    • push addr + ret
    • mov eax, addr + call eax
    • jmp addr
  3. 还有当跳转指令过长,可能会被从GOT表写入到堆里面的东西覆盖。这里可以采用在堆中多设置一些字符的方法来避免,就和堆喷时设置很多nop在前面一样

参考资料

  1. LiveOverflow HomePage
  2. Prostar Exploit Education
  3. CSDN:什么是堆漏洞挖掘
  4. dlmalloc Version 2.7.1pre1 2001
  5. Online Assembler/Disassembler
Author: cataLoc
Link: http://cata1oc.github.io/2021/11/13/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E5%85%A5%E9%97%A8-%E4%B8%8B/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶