文章目录
堆溢出
1.介绍
堆溢出指的是程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(堆块本身可使用的字节数>用户申请的字节数),因此导致了溢出,并覆盖到物理相邻的高地址的下一个堆块。
原理和栈溢出基本一样,不过堆溢出很难像栈溢出一样直接控制程序指向流程。一般堆溢出的策略为:
-
覆盖与其物理相邻的下一个chunk内容。
- prev_size
- size
- NON_MAIN_ARENA
- IS_MAPPED
- PREV_INUSE
- the True chunk size
- chunk content,从而改变程序固有的执行流。
-
利用堆的机制(如unlink)来实现任意地址写入或者控制堆块中的内容等效果,从而控制程序的执行流
2.示例
给出一个示例代码:
#include <stdio.h>
int main(void) {
char* chunk;
chunk = malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}
在malloc完后,gdb断点查看,可以看到0x555555559290是malloc_chunk,0x5555555592b0是top_chunk
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x5555555592b0
Size: 0x20d51
pwndbg> x/32g 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x0000000000000000 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000020d51
0x5555555592c0: 0x0000000000000000 0x0000000000000000
0x5555555592d0: 0x0000000000000000 0x0000000000000000
在gets处,输入cyclic 200
的结果,再次gdb看一下:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x21
Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x5555555592b0
Size: 0x6161616861616167
pwndbg> x/32g 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x6161616261616161 0x6161616461616163
0x5555555592b0: 0x6161616661616165 0x6161616861616167
0x5555555592c0: 0x6161616a61616169 0x6161616c6161616b
0x5555555592d0: 0x6161616e6161616d 0x616161706161616f
0x5555555592e0: 0x6161617261616171 0x6161617461616173
0x5555555592f0: 0x6161617661616175 0x6161617861616177
0x555555559300: 0x6261617a61616179 0x6261616362616162
发现0x5555555592b0处的top_chunk被覆盖了。
3.小结
3.1 寻找堆分配函数
3.1.1 malloc
一般情况下都是malloc
void *ptr = malloc(0x20);
3.1.2 calloc
calloc是malloc的加强版,等价于malloc + memset,即将malloc得到的内存用0进行清空
void *ptr = calloc(0x20)
3.1.3 relloc
realloc 函数可以身兼 malloc 和 free 两个函数的功能。
#include <stdio.h>
int main(void) {
char *chunk, *chunk1;
chunk = malloc(16);
chunk1 = realloc(chunk, 32);
return 0;
}
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 掉后一部分
- 如果申请 size > 原来 size
- 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
- 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作
3.2 寻找危险函数函数
常见的危险函数如下,这个和栈溢出基本一样:
- 输入
- gets,直接读取一行,忽略
'\x00'
- scanf
- vscanf
- gets,直接读取一行,忽略
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到
'\x00'
停止 - strcat,字符串拼接,遇到
'\x00'
停止 - bcopy
- strcpy,字符串复制,遇到
3.3 确定填充长度
计算我们开始写入的地址与我们所要覆盖的地址之间的距离。
有几个需要注意的点:
-
malloc得到的内存不是自己申请的大小,而是>自己申请的大小。因为,申请的内存首先需要对齐,这个长度一般是字长的2被,比如32位是8字节的倍数,64位是16字节的倍数。对于不大于2倍字长的请求,malloc会直接返回2倍字长的块,也就是最小的chunk,比如malloc(0),会返回16字节的块作为数据区。同时,堆还需要加上prev_size和size段,这两段加起来也是2倍字长。
-
用户申请的内存大小可能会被修改,有可能会使用与其物理相邻的下一个chunk的prev_size字段存储内容。比如,用户申请的内存数据块大小为24字节,但是可能只会分配16字节,剩下的8字节是占用了下一个堆块的prev_size段。