House of Foce 是堆溢出在特定场景下的一种简单利用方式,通过一个例子来学习下。
首先看下运行环境, Ubuntu 16.04.1 LTS x64 中 Ubuntu GLIBC 2.23-0ubuntu10 版本的 GLIBC。
再看下源码,有四次 malloc() 调用,中间夹着一次模拟溢出的内存改写。
上 gdb,调试走起,我们一句一句的看。
现在是在调用第一个 malloc(16) 之前的状态。
可以看到,堆 heap 还没有分配出来(main_arena的top字段等于0,vmmap 还没有 heap 的内存段)。
按 ni 执行 **malloc(16)**之后,返回值是 0x602010。
此时 main_arena 的 top 指针指向 0x602020,而vmmap 的 heap 段起始于 0x602000。
看看 0x602000 起始的这段堆内存的情况。
0x602000 到 0x60201F 的这 32 字节内存,就是 malloc(16) 所占用的堆内存。其中,前 16 个字节(0x602000 ~ 0x60200F)是 GLIBC 管理的堆头,后 16 个字节(0x602010 ~ 0x60201F)是返回给程序使用的空间,所以 malloc(16) 的返回值就是 0x602010。而 main_arena 的 top 指针指向空闲堆块的起始地址 0x602020。示意图如下:
模拟溢出的内存改写
接下来源码的 10 和 11 两行,是模拟用溢出的方式修改空闲内存块的 size 大小为 全F。
修改成功。
为什么要修改空闲内存块的大小为 全F?是为了下一步申请超大内存时,避免因为空闲内存块大小不够而返回失败。继续看源码的第 13 行,第二个 malloc(),申请负数大小的内存。
第二个 malloc:-4128
从汇编可以看出,由于 malloc 的入参格式是正整数,因此程序运行时会将负数 -4128 转换成超大整数 0xFFFFFFFFFFFFEFE0.
我们计算一下,这一次堆块分配,从空闲堆块起始位置 0x602020 开始,加上 16 字节的堆头,再减去 4128 之后,应该是 0x601010。
看看执行 malloc(-4128) 之后,main_arena 的 top 指针,果然指向了 0x601010。
而 0x601010 所在的区域,就是程序的 GOT 表。其中 0x601018 是 libc_start_main() 函数的 GOT 表项地址,0x601020 是 malloc() 函数的 GOT 表项地址。
也就意味着,堆块的内存分配已经被程序劫持到了 GOT 表中。此时堆块的示意图如下:
第三个 malloc:16
第三个 malloc() 分为两步,首先是分配 16个字节,然后再向分配的内存中写入 main() 函数地址。
分配 16 个字节之后,main_arena的top指针是 0x602030,返回给程序的地址是 0x601020。
注意到 0x601020 其实是 malloc() 的 GOT 表项地址,现在被 malloc() 输出到了程序里。当源码中用 *(long *)p = (long)main; 来修改分配的内存时,我们其实是覆盖了 malloc() 函数的 GOT 表项值,也就是说,malloc() 函数被劫持成了 main() 函数!
示意图如下
第四个 malloc:16
第四个 malloc() 仍然是分配 16 个字节。
但此时,malloc() 的 GOT 表项值已经被劫持成了 main() 函数地址。我们按 si 单步调试 step into,会发现 rip 走进了 main() 函数的空间。
程序的流程被成功劫持!
回顾一下,这个程序是怎么做到劫持运行流程导致重入了 main() 函数?其实只做了两件事情:
修改了空闲堆块的 size 字段,从而避免下一步空间不够
控制了 malloc() 申请的字节数,从而分配了超大空间
这就是 House of Force 的堆溢出利用技术。通常,这种利用方式需要满足两个条件:
需要存在溢出漏洞,攻击者可以控制空闲堆块的 size 字段
攻击者可以控制 malloc 的字节数和 malloc 的调用次数
只要满足这些条件,就可以利用例子中的方法抬高或者压低空闲堆块的起始地址,从而获得任意地址写的机会。
当然,不同版本 GLIBC 的堆块分配和处理方法都略有差异,真实利用时还需要在对应版本的 GLIBC 上仔细分析。
[来源说明] 题图来自 https://www.pexels.com/photo/hungarian-horntail-dragon-at-universal-studios-3359734/,摄影师 Craig Adderley