11.3. 堆溢出¶
11.3.1. 第一部分¶
首先简要介绍一下堆chunk的结构 在malloc.c中找到关于堆chunk结构的代码
struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
/* Size of previous chunk (if free). */
INTERNAL_SIZE_T size;
/* Size in bytes, including overhead. */
struct malloc_chunk* fd;
/* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize;
/* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
这指明了一个heap chunk是如下的结构
+-----------+---------+------+------+-------------+
| | | | | |
| | | | | |
| prev_size |size&Flag| fd | bk | |
| | | | | |
| | | | | |
+-----------+---------+------+------+-------------+
如果本chunk前面的chunk是空闲的,那么第一部分prev_size会记录前面一个chunk的大小,第二部分是本chunk的size,因为它的大小需要8字节对齐,所以size的低三位一定会空闲出来,这时候这三个位置就用作三个Flag(最低位:指示前一个chunk是否正在使用;倒数第二位:指示这个chunk是否是通过mmap方式产生的;倒数第三位:这个chunk是否属于一个线程的arena)。之后的FD和BK部分在此chunk是空闲状态时会发挥作用。FD指向下一个空闲的chunk,BK指向前一个空闲的chunk,由此串联成为一个空闲chunk的双向链表。如果不是空闲的。那么从fd开始,就是用户数据了。(详细信息请参考glibc的malloc.c部分,在此不再多做解释。)
引用一位外国博主的漏洞示例程序解释
/*
Heap overflow vulnerable program.
*/
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
char * first, * second;
/*[1]*/
first = malloc( 666 );
/*[2]*/
second = malloc( 12 );
/*[3]*/
if(argc!=1)
strcpy( first, argv[1] );
/*[4]*/
free( first );
/*[5]*/
free( second );
/*[6]*/
return( 0 );
}
这个程序在[3]处有很明显的堆溢出漏洞,argv[1]中的内容若过长则会越界覆盖到second部分。
程序堆结构如下
+---------------------+ <--first chunk ptr
| prev_size |
+---------------------+
| size=0x201 |
+---------------------+ <--first
| |
| allocated |
| chunk |
+---------------------+ <--second chunk ptr
| prev_size |
+---------------------+
| size=0x11 |
+---------------------+ <--second
| Allocated |
| chunk |
+---------------------+ <-- top
| prev_size |
+---------------------+
| size=0x205d1 |
+---------------------+
| |
| |
| |
| TOP |
| |
| CHUNK |
| |
+---------------------+
此时利用方法为通过溢出构造,使得second chunk
prev_size=任意值
size=-4(因为最低位的flag没有设置,所以prev_size可以任意)
fd=free@got-12
bk=shellcode地址
在我们的payload将指定位置的数值改好后。下面介绍在[4][5]行代码执行时发生的详细情况。
第四行执行free(first)发生如下操作
1).检查是否可以向后合并
首先需要检查previous chunk是否是空闲的(通过当前chunk size部分中的flag最低位去判断),当然在这个例子中,前一个chunk是正在使用的,不满足向后合并的条件。
2).检查是否可以向前合并
在这里需要检查next chunk是否是空闲的(通过下下个chunk的flag的最低位去判断),在找下下个chunk(这里的下、包括下下都是相对于chunk first而言的)的过程中,首先当前chunk+当前size可以引导到下个chunk,然后从下个chunk的开头加上下个chunk的size就可以引导到下下个chunk。但是我们已经把下个chunk的size覆盖为了-4,那么它会认为下个chunk从prev_size开始就是下下个chunk了,既然已经找到了下下个chunk,那就就要去看看size的最低位以确定下个chunk是否在使用,当然这个size是-4,所以它指示下个chunk是空闲的。
在这个时候,就要发生向前合并了。即first chunk会和 first chunk的下个chunk(即second chunk)发生合并。在此时会触发unlink(second)宏,想将second从它所在的bin list中解引用。
但是执行unlink宏之后,再调用free其实就是调用shellcode,这时就可以执行任意命令了。
11.3.2. 第二部分¶
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
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;
}
}
}
}