现在的漏洞绝大部分都出在了堆这一部分,所以了解一些现在常用的漏洞利用技巧是非常必要的。
堆的概念
堆是一个用于动态分配的内存池。使用malloc()函数可以从堆中申请内存。而使用free()函数可以释放由malloc函数申请的内存。下面来看看一个程序运行起来后它的一个内存布局是怎样的。
下面给出一个通过动态分配使用堆内存的例子。
int main(){
char * buffer = NULL;
/* allocate a 0x100 byte buffer */
buffer = malloc(0x100);
/* read input and print it */
fgets(stdin, buffer, 0x100);
printf(“Hello %s!\n”, buffer);
/* destroy our dynamically allocated buffer */
free(buffer);
return 0;
}
下面来看看堆和栈的区别。
从内存分配的时期来看,堆内存的分配在程序运行时进行,而栈内存的分配会在编译时进行。
从存储的东西来看,堆中一般会存储所需内存比较大的缓冲区,结构体,类对象等等,而栈中一般存储局部变量、返回地址、函数的参数....
从分配的方式来看,堆内存一般由程序员自己分配,通常使用malloc/calloc/recalloc/分配,free释放,或者 new/delete来管理堆内存,而栈内存的使用是由编译器自动完成的。
堆有很多种实现,其中比较主流的有dlmalloc ptmalloc jemalloc,还有些程序自己实现了堆的管理机制.再继续往下讲前,来做个小测验。猜一猜下面的malloc调用计算机(32位)实际上会分配多少字节的数据。
malloc(32);
malloc(4);
malloc(20);
malloc(0);
来看看答案,看你能答对几个
malloc(32); – 40 bytes
malloc(4); – 16 bytes
malloc(20); – 24 bytes
malloc(0); – 16 bytes
下面使用程序来验证下,其输出就是它实际分配到的内存地址和大小
堆的内存分配是以chunk为单位进行分配的,下面以一张图来形象的看看每个堆chunk的结构。
下面看看源代码中对这个chunk结构的描述。做了注释。
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; 如果前一个堆块是被释放的,这个值为前一个堆快的大小,否则为0
INTERNAL_SIZE_T size; 最后一位表示前一个堆块是否在使用, 为 1 表示在使用
struct malloc_chunk* fd; 前向指针,指向前一个堆快的起始位置
struct malloc_chunk* bk; 后向指针,指向后一个堆快的起始位置
struct malloc_chunk* fd_nextsize; /* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* bk_nextsize; /* Only used for large blocks: pointer to next larger size. */
};
如果想继续深入了解堆分配相关细节的话可以去 https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/
几种常见堆漏洞的利用技巧
一、堆缓冲区溢出
堆缓冲区溢出和栈上的缓冲区溢出非常类似,他们主要区别就是缓冲区所处的位置不同。我们先来看看如果我们不断的向堆申请内存,那么堆中的那些chunk的分布情况。
发生溢出后的情况是
如果溢出覆盖的内存只是一些很简单的数据类型,其实并没有什么用,但是在堆上分布着很多复杂的数据结构的比如,结构体、对象。而在在这些数据中可能会存储着一些有趣的东西,比如一个函数指针、虚表指针等,一旦我们通过溢出重写了这些特殊的字段我们就有可能控制程序的流程,实现代码执行。
这里来一个Demo,假设存在这样一个结构体。
struct toystr {
void (* message)(char *);
char buffer[20];};
触发堆溢出的代码为
coolguy = malloc(sizeof(struct toystr));lameguy = malloc(sizeof(struct toystr));
coolguy->message = &print_cool;
lameguy->message = &print_meh;
printf("Input coolguy's name: ");
fgets(coolguy->buffer, 200, stdin); // oopz...
coolguy->buffer[strcspn(coolguy->buffer, "\n")] = 0;
printf("Input lameguy's name: ");
fgets(lameguy->buffer, 20, stdin);
lameguy->buffer[strcspn(lameguy->buffer, "\n")] = 0;
coolguy->message(coolguy->buffer);
lameguy->message(lameguy->buffer);
coolguy = malloc(sizeof(struct toystr));
lameguy = malloc(sizeof(struct toystr));
coolguy->message = &print_cool;
lameguy->message = &print_meh;
printf("Input coolguy's name: ");
fgets(coolguy->buffer, 200, stdin); // 这里有一个很明显的堆溢出
coolguy->buffer[strcspn(coolguy->buffer, "\n")] = 0;
printf("Input lameguy's name: ");
fgets(lameguy->buffer, 20, stdin);
lameguy->buffer[strcspn(lameguy->buffer, "\n")] = 0;
coolguy->message(coolguy->buffer);
lameguy->message(lameguy->buffer);
堆溢出之后
通过溢出可以将lameguy的message函数指针设置为0x41414141(AAAA),后来当lameguy调用message函数时就可以劫持程序流程到0x41414141,覆盖对象的虚表指针也能实现类似的效果。