堆溢出原理

一、概述

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

堆溢出发生的条件

  • 程序向堆上写入数据。
  • 写入的数据大小没有被良好地控制。

堆溢出与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略有以下两种

  • 覆盖与其物理相邻的下一个 chunk 的内容。我们可以控制的是size的三个标志位、 prev_size, 以及堆块的大小,堆的数据区。
  • 利用堆中的机制比如unlink来实现任意地址的写入或者控制堆块中的内容从而间接控制程序流程。

二、示例程序

#include
int main(void)
{
char *chunk;
chunk=malloc(24);
puts(“Get input:”);
gets(chunk);
return 0;
}

接下来使用GDB进行调试

首先输入较少的内容,其中0x20fe1处指向了top chunk。可以看到数据并没有溢出。

然后输入较多的数据(大于24),可以看到数据以及覆盖到了top chunk。

三、堆溢出步骤

1.寻找堆溢出函数

  • malloc
  • calloc

calloc(0x20);
//等同于
ptr=malloc(0x20);
memset(ptr,0,0×20);

  • realloc

当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时
如果申请 size > 原来 size
如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
如果申请 size < 原来 size 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持 不变 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr) 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

2.寻找危险函数

  • gets,直接读取一行,忽略 ‘\x00’
  • scanf
  • vscanf
  • sprint
  • strcpy,字符串复制,遇到 ‘\x00’ 停止
  • strcat,字符串拼接,遇到 ‘\x00’ 停止
  • bcopy

3.确定覆盖长度

这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。一个常见的误区是 malloc 的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。

另外值得注意的一点是假设申请的 chunk 大小是 24 个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个。原因是借用了下一个块的 pre_size 域。