点击蓝字 关注我们
日期: 2024-01-19
作者: Mr-hello
介绍: 艰难的堆学习之路。
0x00 前言
堆学习第二篇文章了,也是比较难的一部分,因为堆和栈不同,栈的结构较为简单,并且栈结构的溢出可以将返回地址覆盖,并轻易控制程序走向,但是堆块涉及到很多的结构体,以及各种函数等,堆上不会存在返回地址等可以让攻击者直接控制执行流程的数据,所以针对于堆溢出,我们一般不可能直接控制到 EIP
。针对于堆溢出的策略来说,我们一般是需要利用堆中的机制(如 unlink
等)来实现任意地址写入或者控制堆块中的内容等,这就需要我们深入了解 glibc
中的某些函数的源码,所以和栈溢出相比较,堆溢出难度比较大。
0x01 堆溢出
堆溢出指的是,向程序里的某个堆块中写入的字节数超过了堆块本身的可利用字节数,导致出现数据溢出问题,覆盖到物理相邻的高地址的下一个堆块。一般我们利用堆溢出的流程如下:
-
覆盖物理相邻的下个
chunk
内容 -
利用堆中的机制来实现任意地址写入或者控制堆块中的内容等效果,从而控制程序的执行
堆申请函数
最常见的是使用 malloc
函数进行申请堆块,有些情况下会使用 calloc
分配,其中的区别为,calloc
在分配之后会自动进行清空。而 malloc
函数并不会自动清空。
还有部分情况下,会使用 realloc
函数进行分配,该函数主要出现在,堆块已经存在,但需要重新对堆块大小进行调整时,也就是说,realloc
函数,会先 free
之前的堆块,然后对其再分配。
realloc
函数使用时,存在以下几种情况。
size
> 原本 size
-
如果
chunk
与top chunk
相邻,直接利用top chunk
拓展这个chunk
到新size
-
如果
chunk
与top chunk
不相邻,相当于free + malloc
size
< 原本 size
-
size
相差不足以容纳最小chunk
(64
位系统32
字节,32
位系统16
字节),保持不变 -
可以容纳最小
chunk
,对原堆块进行切割,free
掉后一部分
size
= 原本 size
,不进行任何操作4.申请 size
= 0,相当于 free
危险函数
1.输入
-
gets
-
scanf
-
vscanf
2.输出
-
sprintf
3.字符串
-
strcpy
-
strcat
-
bcopy
重点:其实堆溢出和栈溢出最重要的一个点,就是要确定填充长度,也就是输入多少字符可以实现溢出,在堆的数据结构处,已经提到,每个堆块在申请的时候,其实是和系统的机器字长有关系的,每次堆块的申请,并不是根据用户输入来决定的,而是由 ptmalloc
函数分配出来的大小决定的,该大小是对齐的,一般是机器字长的 2
倍。
用户区域大小和 chunk_head.size
不同,chunk_head.size = user_data_size + 2 * 字节长度
另外一点
之前文章讲过一次堆块空间复用的问题,也就是说,prev_size
只有在前面堆块空闲时,才会记录大小,但是当前面堆块不是空闲状态时,prev_size
为 0
,所以当一个堆块被申请时,可能会出现,用户申请24
字节的用户空间,但系统只给分配16
字节,因为系统会把下个堆块的 prev_size
分配给该堆块使用。
我这里申请了一个 24
字节的堆块,但是,系统只给分配了,16
字节,剩余的 8
个字节,可以复用下一个堆块的 prev_size
字段。
系统分配内存是以最小单元为基本单位,用于做内存对齐,32位系统最小分配空间的单元大小为 16字节
,即分配的空间肯定是 16字节
的整数倍,64位系统最小分配空间的单元大小为 32字节
,即分配的空间肯定是 32字节
的整数倍。
0x02 unlink
看完了堆溢出之后,本来想接着看 Off-By-One
,但是发现里面提到一个知识点,unlink
于是又转过头去研究了一下这方面内容,发现其基本原理就是双向循环链表的删除元素操作。只不过这个元素是一个 chunk
。并且涉及到堆块的分配、释放等。
前文在讲 bins
数据结构时提到,只有small bin、large bin
才会存在双向循环列表,small bin
中,每个下标头部之间的 chunk
不会再次形成循环双向列表,所以放入 small bin
中的空闲堆块,不会涉及到 fd_nextsize、bk_nextsize
的改变。在网上找到一张较为详细的图示。
在较早版本的 glibc
中,unlink
未对 FD->bk、BK->fd
进行限制,导致该两处指针可控,从而造成任意地址读写,在 glibc2.26
之前的版本都是存在 unlink
漏洞。
//较早期glibc中的 unlink函数
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{
FD = P->fd;
BK = P->bk;
//这里如果FD->bk和BK->fd不做限制的话,我们可以通过输入,溢出覆盖P->fd和P->bk,从而达到控制任意位置写入。
FD->bk = BK;
BK->fd = FD;
}
在较新版本的 glibc
中可以看到已经对 FD->bk
和 BK->fd
做了判断。
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}
0x03 结尾
先写到这里吧,堆知识越到后面越难,作为 CTF
大龄选手,只好慢慢啃这块硬骨头,实在太难理解了,后悔自己大学的时候,没好好学习数据结构,理解指针太费事了,现在想想,当时学数据结构的时候,已经是八九年前了。。。
往期回顾
文章丨『CTF』堆入门
点此亲启
原文始发于微信公众号(宸极实验室):『CTF』堆入门(二)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论