堆溢出之Heap Unlinking

Shellcoder's handbook中关于heap unlinking的补充说明

加上原书写得非常简略晦涩,很多内容我是一知半解的,在这里就不继续这部分了

红黑联盟网站中shellcode之五:堆溢出一文关于shellcoder's handbook堆溢出部分评价

这块那本善良人手册写得实在是太晦涩了,我实在是没看懂

Oilbeater 的部落格上关于shellcoder‘s handbook堆溢出部分评价

好吧,某个夏夜里对着计算机发了半个小时的呆也没有看懂作者在干什么。本来在挖财宝书中听说在linux下现在想Unlinking几乎不可能了,确实经过实验我的机器会抛出=sigabrt=之类的异常强行终止程序。不过探寻和了解下先前的黑客们如何不按规矩做事是很有意思的事情。最后上网搜了搜算是弄明白些了。现记如下,希望对其他人有帮助。

很悲剧的是,接下来我要说的还是只关乎linux的事情,关于windows下的堆溢出搞得似乎很复杂,对windows一窍不通的我只能看着复杂的各种结构长叹一口气,默默隐去,深藏功与名。

已分配和可分配内存结构

先说堆溢出为什么会发生在linux下,在glibc中有一个内存分配的malloc实现,linux下系统分配内存则是通过malloc调用brk和mmap来实现的。好吧,具体怎么实现先不管它1,就当它一块一块的把内存分配出来,并将元数据和内存数据放在一起。

首先声明:下面所有的ascii图每行都是四个字节,然后地址由上向下递增。

内存的元数据和实际的数据保存在同一个内存空间中,已经分配的一块内存块以下结构存储,其它已经分配的内存块会接着它继续在内存空间中以相同方式分配。

+++++++++++++++
上一个块的大小   <-- 区域1
+++++++++++++++
  本块的大小     <-- 区域2
+++++++++++++++
    实际         <-- 内存会被显示的地址
    分配         <-- 区域3  
    内存
    ...
+++++++++++++++

这些内存分配时肯定是该死的8字节对齐,这样检索起来似乎更快效率更高。那么,这些表示块的尺寸一定是8的倍数,那么,区域1和2中所保存的尺寸信息一定是8的倍数,就是说,最低位(8 = 0b1000)2一定是0。为了充分利用每一位内存,伟大的设计者们把这一位就设定为一个特殊的标记位,为了标记是否上一块内存正在使用(已分配)。

下图是区域2详解:

+++++++++++++++
本块大小(最后一位是标记位)
+++++++++++++++

未被分配的内存块则是一种稍微不同的结构

+++++++++++++++++++++++++
       上一块大小
+++++++++++++++++++++++++
        本块大小
+++++++++++++++++++++++++
指向下一块自由块地址的指针  <-- fd
+++++++++++++++++++++++++
指向上一块自由块地址的指针  <-- bk
+++++++++++++++++++++++++
    未使用的区块部分
    ......
    ......
+++++++++++++++++++++++++

好的,现在我们知道内存块的结构了,现在看看free是如何释放内存的。

遍历内存

现在看看内存块如何被遍历。

malloc实现从当前内存地址加上当前内存块的大小,得到下一个块的地址。

于是我们可以通过覆盖关于尺寸的元数据可以误导malloc让它以为我们指定的地方是下一个内存块。

判断自由块

malloc通过检查下一个内存块的头部标志位,来判定当前内存块是否是自由块。

创造unlinking的条件

就是说,让malloc以为有两个连续自由块需要合并。我们可以通过操作内存块的尺寸部分来完成这一切。

可以这么理解malloc的行为,当准备free一块内存,比如对某个分配的内存buf,malloc会先将buf的地址加上buf-4的尺寸来得到下一块内存的地址,比如说是buf2,检查buf2-4位置的最后一个一个标志位,如果标志位为0,表示上一块也是自由块,那么malloc觉得就应该合并这两块即unlink就会发生。

那么,我们通过溢出操纵尺寸大小和标志位就可以操纵malloc的行为,通过覆盖自由区块中的fd和bk指针从而改写任意内存地址(我只能感慨有些人太天才了,把明明是用来分配内存的东西拿来改写任意内存地址= =)

改写任意内存

以下例子通过更改尺寸伪造了一个内存块,然后把buf2 unlink掉。

先来一个倒霉的可以堆溢出程序,比如我们已经有了一个24字节的shellcode。

int main(void)
{
char *buf1, *buf2;
buff1 = malloc(40);
buff2 = malloc(40);
gets(buf1);
free(buf1);
exit(0);
}

编译它。然后运行它。输入=\xeb\x08=(jmp short 10)=+ shellcode + 无用字符占位 + ‘0xfffffffc’=(-4)=X 2 + (GOT中exit地址-12) + buff1的地址=

以上所做的是更改GOT中exit的地址。你可以通过=objdump=查看

于是程序退出调用的exit函数就被替换成buff1的地址了。

最后得到的堆如下所示:

+++++++++++++++++++++++++
       上一块大小           <-- buf1-8
+++++++++++++++++++++++++
        本块大小
+++++++++++++++++++++++++
|\xeb |\x08 |占位 |占位 |   <-- buf1开始的位置, 伪造块头部1
|占位 |占位 |占位 |占位 |   <-- 伪造块头部2
|占位 |占位 |占位 |占位 |   <-- 即将被更改的伪造块fd(即buf2的bk2+8位置)
|                       |
|                       |
|       shellcode       |
|                       |
|                       |
|                       |
|占位 |占位 |占位 |占位 |   
+++++++++++++++++++++++++   
|\xff |\xff |\xff |\xfc |   
+++++++++++++++++++++++++
|\xff |\xff |\xff |\xfc |   
+++++++++++++++++++++++++
|exit函数在GOT中地址-12 |  <-- buf2的fd2
+++++++++++++++++++++++++  
|      buf1的地址       |  <-- buf2的bk2
    ......
    ......
+++++++++++++++++++++++++

于是,当buf1被free之时,malloc通过检查buf2接下来一块的标志位来查看下一块是否为自由块。因为buf2大小被改为-4,倒霉的malloc就开始检查buf2-8位置,发现buf2是自由块,于是准备unlink来合并buf1和buf2。结果fd2+12被更改为bk2,bk2+8会被更改为fd2来把buf2从一个自由块双向链表中unlink掉。

那么具体细节请参照:Heap Overflows

实践

现在的glibc(大概2.25以来,忘记了)中malloc的实现,在free之前都会有一个检查部分,如果发现堆溢出覆盖发生,那么就会

*** glibc detected *** ./heap: free(): invalid next size (fast): 0x0804b008 ***

我在gdb中看了看,似乎前一个块的地址也不再用了,听说有些其它的保护方法让负的地址也不起作用了。

总之,所以别指望在一台现代操作系统中3尝试成功。如果对如何绕过这些防范措施有兴趣可以看看Linux Heap Exploiting Revisited(西班牙语)

参考资料

你可以在上面的链接找到其它资料。

最后吐槽下,草泥马的天朝网络!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

FootNotes

Footnotes:

1

A Memory Allocator by Doug Lea

2

一块内存最小是16字节,反正最后一位为0

3

并不是说堆溢出没有用,也许不能执行任意代码,但还有其它可能。