你可以把内存想象成一片巨大的未开发的土地,而House Of Force就是一种通过“篡改土地面积”,让你能够瞬间把房子盖到操作系统最核心区域的黑客魔法。
第一步:认识“Top Chunk”(荒野)
在 C 语言的malloc机制中,当你向系统申请内存时,系统会先给你划出一大块连续的内存区域(这就是堆 Heap)。
每次你调用malloc,系统就像切蛋糕一样,从这块区域里切下一小块给你。切剩下来的、最后面那块最大最完整的内存,被称为Top Chunk(也叫 Wilderness,荒野)。
Top Chunk 有一个非常重要的属性:它记录了自己还剩多大(Size)。
当你要申请的内存大于 Top Chunk 的剩余大小时,系统就会发现“哎呀,内存不够了”,然后去向操作系统申请扩容。
第二步:核心漏洞——“指鹿为马”
House Of Force 的起点,是你要有一个漏洞(比如堆溢出),能够让你修改 Top Chunk 的 Size。
试想一下,如果 Top Chunk 原本只剩0x1000大小,但你通过溢出漏洞,把它的 Size 强行改成了-1。
在计算机的无符号整数(unsigned)世界里,-1就是最大的数字(在 64 位系统下是0xFFFFFFFFFFFFFFFF)。
这时候,系统就被你骗了!
系统一看 Top Chunk 的 Size:“哇!原来我还有无限大的内存空间!”
从此以后,无论你向系统malloc申请多大的内存,它都不会去向操作系统要新的空间,而是直接从现在的 Top Chunk 往下切给你。
第三步:空间跳跃魔法
既然系统认为 Top Chunk 无限大,我们就可以利用它进行“空间跳跃”。
假设当前的 Top Chunk 在地址0x10000,而你想去改写系统的核心函数指针(假设在目标地址0x80000)。你要怎么过去呢?
系统切分配内存的逻辑非常简单粗暴:新的 Top Chunk 地址 = 现在的 Top Chunk 地址 + 你申请的内存大小
所以,你只需要做一道简单的减法题:
偏移量 = 目标地址 - 当前 Top Chunk 地址 - 头部大小(0x10)
你直接调用malloc(偏移量)。
系统一看,你要这么大的内存?没问题,我的 Top Chunk 是“无限大”的!于是它大笔一挥,把 Top Chunk 的指针直接向前推移了这么多距离。
此时,Top Chunk 的位置,已经被你精准地移动到了目标地址(0x80000)的前面!
(注:如果目标地址在 Top Chunk 的“上面”即低地址区,这个减法算出来会是一个负数。但没关系,计算机里的负数相加会产生“整数溢出”,照样能精准绕回到低地址!)
第四步:完成绝杀
现在,Top Chunk 就在你梦寐以求的目标地址门口了。
你只需要再执行一次普通的malloc,比如malloc(0x20)。
系统就会把目标地址(比如0x80000)分配给你。
此时,这个目标地址可能存放着非常重要的东西(比如__malloc_hook,或者 GOT 表中的puts函数指针)。你往这个新申请的内存里写入你的恶意代码地址(比如system("/bin/sh"))。
下次程序再调用相应的函数时,就会直接执行你的恶意代码,拿下服务器的控制权。
总结:House Of Force 的攻击三部曲
- 破门 (Overwrite Top Chunk Size):利用溢出漏洞,把 Top Chunk 的 size 改成
-1(最大值)。 - 铺路 (Bridge the Gap):计算目标地址和 Top Chunk 的距离,申请一个巨大(或极小)的内存块,把 Top Chunk 的指针推到目标地址附近。
- 夺权 (Arbitrary Write):再次申请内存,拿到目标地址的控制权,写入恶意数据。
发动这个魔法必须具备的三个条件:
- 能够覆盖 Top Chunk 的 size(通常需要一个 Heap Overflow 漏洞)。
- 能够自由控制
malloc申请的尺寸(需要传入我们计算出的偏移量)。 - 知道地址(需要有信息泄露漏洞,知道当前 Top Chunk 在哪,以及你想打的目标地址在哪)。
目前的现状:
这个方法非常经典且暴力,但在较新的 glibc 版本(2.29 及以后)中,加入了对 Top Chunk size 的合法性检查,直接把 size 改成-1会导致程序崩溃。但这依然是理解堆利用(Heap Exploitation)和内存布局最棒的入门级手法。
有没有哪个步骤的逻辑你觉得跳跃了?我们可以针对那一部分再细聊。
例子
我们用一个具体的数字推演,结合几行简单的代码,来看看这场“空间跳跃”是如何在内存中真实发生的。
假设我们有一个存在漏洞的 C 语言程序,并且我们已经通过某种方式(信息泄露漏洞)知道了内存中的两个关键位置:
- 当前 Top Chunk 的地址:
0x602020 - 我们的攻击目标地址:
0x804010(比如这里存放着一个只有管理员才能调用的函数指针,我们想把它改成我们的恶意代码地址)
第一步:破门(篡改 Top Chunk Size)
假设程序允许我们申请一段内存,并且存在越界写入(Heap Overflow)的漏洞。
// 1. 申请一段 0x10 大小的内存 char *p = malloc(0x10); // 此时 p 的地址是 0x602000 // 因为这块内存本身需要 0x10 的头部(存放元数据),加上你申请的 0x10,总共占了 0x20。 // 所以,剩下的 Top Chunk 就在它紧挨着的地方:0x602020。 // 2. 利用溢出漏洞,把 Top Chunk 的 Size 强行覆盖为 -1 (在 64 位下是 0xFFFFFFFFFFFFFFFF) // 正常情况下你只能写 p[0] 到 p[15],但漏洞让你能写得更远 write_overflow(p, data_with_0xFFFFFFFFFFFFFFFF);经过这一步,系统再去检查地址0x602020(Top Chunk)时,会认为:“我这里还有无限大的内存,随便你要多少!”
第二步:铺路(计算偏移并跨越空间)
现在我们要算一笔账:从现在的 Top Chunk (0x602020),跳到目标地址 (0x804010),中间差了多远?
因为我们下一次malloc申请拿到的内存,其实是从目标地址往后算的。为了让我们最终能够往0x804010里面写东西,我们要让新的 Top Chunk刚好落在目标地址的“头顶上”(也就是目标地址减去 0x10 的块头部大小)。
核心计算公式:跳跃距离 = 目标地址 - 0x10 (预留头部) - 当前 Top Chunk 地址
代入我们的数字:跳跃距离 = 0x804010 - 0x10 - 0x602020 = 0x201FE0
接下来,我们在攻击脚本(比如 Python)里向程序发送请求,要求申请这么大的内存:
# 3. 申请巨大内存,完成跳跃 malloc(0x201FE0)系统一看,你要0x201FE0这么大?没问题,Top Chunk 的 Size 是无限大,够切!
于是系统把 Top Chunk 的指针加上了0x201FE0 + 0x10。
此时,Top Chunk 的新地址已经变成了0x804000。
第三步:夺权(拿到目标并写入)
这就到了激动人心的最后一步。Top Chunk 已经在家门口了。
# 4. 再次申请一个小块内存 malloc(0x10)系统会从当前的 Top Chunk(0x804000)切下一块。系统会保留 0x10 作为块头部,然后把供你使用的用户区域指针返回给你。
0x804000 + 0x10 = 0x804010
你拿到的返回指针,精准地落在了0x804010!也就是我们最初的目标地址。
# 5. 向刚刚拿到的内存里写入恶意数据的地址(比如 system("/bin/sh")) write_to_new_malloc( "恶意代码地址" )就这样,目标地址原本合法的函数指针,被你替换成了打开服务器 shell 的钥匙。下次程序尝试调用那个合法函数时,就会直接执行你的命令。
如果目标地址比 Top Chunk 还低怎么办?
在真实的漏洞挖掘中,目标地址(比如 glibc 的__malloc_hook)往往比堆的地址要低。这时候目标地址 - 当前 Top Chunk算出来会是一个负数。
但由于底层是无符号运算,这个负数会变成一个极其巨大的正数。当你malloc这个巨大正数时,内存地址会一直向上加,加到突破内存上限(Integer Overflow),最终“绕了一圈”回到底部,精准落在低地址的目标上。逻辑和上面的加法完全一致!