堆溢出的原理

堆溢出,本质上和栈溢出我感觉差不多。【缓冲区溢出的一种】

主要就是写入的字节的数目,超过了声请的空间。倒置覆盖掉物理相邻的高地址的下一个堆块。

前提

  • 要有数据的写入(gets())
  • 写入的数据没有良好的被控制

对于攻击者来说:堆溢出可以程序崩溃也可以控制程序的执行

但是堆溢出无法控制EIP所以

利用的方法有

  1. 覆盖与之相邻的下一个chunk的内容
    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the True chunk size
    • chunk content,从而改变程序固有的执行流。

​ 2.利用堆中的机制(如unlink)来实现任意地址写入(Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流

例子

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(void)
{
char *chunk;
chunk=malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}

这个程序的主要目的是调用malloc()函数分配一块堆上的内容,然后向这个堆块中写入一个字符串,如果输入的字符串过长就会导致溢出chunk的区域并覆盖后面的top chunk之中(实际puts内部会调用malloc 分配堆内存,覆盖的可能并不是top chunk)

image-20230529213957962

步骤:

1.寻找堆分配函数

  • glibc中的malloc()函数
  • calloc():这个函数和malloc的区别是:在分配后会自动进行清空,这个对于信息泄露的利用来说是很牛的
1
2
3
4
calloc(0x20);
//等同于
ptr = malloc(0x20);
memset(ptr,0,0x20);
  • realloc():这个函数身兼malloc和free两个函数的功能
1
2
3
4
5
6
#include <stdio.h>
int main(){
char *chunk,*chunk1;
chunk = malloc(16);
chunk1 = realloc(chunk,32);
}

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
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到 ‘\x00’ 停止
    • strcat,字符串拼接,遇到 ‘\x00’ 停止
    • bcopy

3.确定需要填充的长度

主要就是计算:我们开始写入的地址和我们所需要覆盖的掉的地址之间的距离。

有一个误区:malloc的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为16 字节的块。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(void)
{
char *chunk;
chunk=malloc(0);
puts("Get input:");
gets(chunk);
return 0;
}
//根据系统的位数,malloc会分配8或16字节的用户空间

注意用户区域的大小不等于 chunk_head.size,chunk_head.size = 用户区域大小 + 2 * 字长

还有一点是用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的prev_size 字段储存内容。回头再来看下之前的示例代码

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(void)
{
char *chunk;
chunk=malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}

观察如上代码,我们申请的 chunk 大小是 24 个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个。

16 个字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的 pre_size 域。我们可来看一下用户申请的内存大小与 glibc 中实际分配的内存大小之间的转换。

1
2
3
4
5
6
/* pad request bytes into a usable size -- internal version */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

当 req=24 时,request2size(24)=32。而除去 chunk 头部的 16 个字节。实际上用户可用 chunk 的字节数为 16。而根据我们前面学到的知识可以知道 chunk 的 pre_size 仅当它的前一块处于释放状态时才起作用。所以用户这时候其实还可以使用下一个 chunk 的 prev_size 字段,正好 24 个字节。

实际上ptmalloc分配内存是以双字为基本单位,以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16字节对齐的。