news 2026/9/28 4:45:31

House of Roman

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
House of Roman

House of Roman:无泄漏堆利用的艺术

House of Roman是一种极其巧妙的堆利用(Heap Exploitation)技术。用一句话来概括:它是一种在“没有任何内存泄漏(Info Leak)”的情况下,通过“部分覆盖(Partial Overwrite)”和一点点运气(爆破),最终控制程序执行流的攻击方法。

一、 核心障碍与破局思路

1. 核心障碍:ASLR(地址随机化)
在现代操作系统中,有一个安全机制叫 ASLR。它会在每次程序运行时,把堆(Heap)、栈(Stack)和系统库(Libc)加载到内存中完全随机的位置。
常规的黑客做法是先找一个漏洞,把内存中某个真实的地址“泄漏”出来,算出这一次运行时的“基址”,再去精确定位目标。而 House of Roman 的魔法在于:就算一点地址都泄漏不出来,它也能打通。

2. 核心武器:部分覆盖(Partial Overwrite)
ASLR 并不是把地址的每一位都随机化了。因为内存是按“页”(Page,通常是 0x1000 字节)对齐的,所以任何地址的最后 12 位(也就是十六进制的最后 3 个字符)永远是固定不变的。
部分覆盖的原理就是:在我们不知道完整地址前半部分的情况下,只要目标位置和当前位置在同一个区域(比如都在 Libc 里,或者都在堆里),我们只需要覆盖这个指针的最后 1 到 2 个字节。这样既保留了随机化产生的前缀,又把指针精确地“拨”到了想要的偏移位置。

二、 攻击的终极目标:劫持__malloc_hook

在 glibc 中,有一个叫__malloc_hook的钩子函数指针。每当程序调用malloc分配内存时,都会先检查这个指针。

我们可以用“快递拦截”来比喻它:
正常情况下,你寄快递(调用malloc),快递员直接走标准流程分配内存。但系统为了方便程序员调试内存泄漏等 Bug,设置了一个“安检专员”(__malloc_hook)。如果这个专员在场(指针不为空),所有的包裹必须先交给他处理——程序会直接跳过去执行钩子里存着的地址代码。

程序员眼里的“调试工具”,在黑客眼里就是官方合法的“执行流劫持后门”。
House of Roman 的目标就是:把__malloc_hook里的值,神不知鬼不觉地改成one_gadget(Libc 中可以直接获取 Shell 的神奇代码片段),然后在下一次分配内存时,直接拿下系统权限。

三、 关键底层机制补充

在实战开始前,必须了解 glibc 中两种不同的“垃圾回收站”(Bin)的排队机制:

  1. Fastbin(拿不到 Libc 地址的阵营):
    像“叠盘子”一样,是单向链表。释放的块里存的都是堆内存的地址,或者 0。这里绝对找不到 Libc 的地址。
  2. Unsorted Bin(必定留下 Libc 地址的阵营):
    像“手拉手围成一圈”的双向循环链表。这个圈的起始点(头部),是位于 Libc 内部的全局变量main_arena(系统大总管)。只要你释放一个较大的块进入 Unsorted Bin,为了保持圆圈的完整,这个块就必须伸出fd(右手)和bk(左手)去拉住main_arena。
    结论:只要堆块进入了 Unsorted Bin,它的肚子里就一定会留下真实的 Libc 地址。

四、 实战步骤拆解(结合内存布局)

假设我们有一个存在UAF (Use-After-Free,释放后重用)漏洞的程序。我们可以修改已经被释放的堆块内部的数据。

// 假设程序里有这样的功能: char *chunk_A = malloc(0x60); // 大小属于 Fastbin 范围 char *chunk_B = malloc(0x80); // 大小属于 Unsorted bin 范围 free(chunk_A); free(chunk_B); // 此时 A 进入 Fastbin,B 进入 Unsorted bin,但由于 UAF,我们仍能修改它们

第一步:通过 Fastbin 伪造目标地址(堆上的两级部分覆盖)

我们需要在__malloc_hook附近伪造一个 Fastbin 堆块。但由于 Fastbin 里只有堆地址,而__malloc_hook在 Libc 里,我们如何跨越区域?这里需要一个极其漂亮的“两级跳”:

  1. 第一跳(用堆找堆):利用 UAF 修改chunk_A(在 Fastbin 中)的fd指针的最后 1 个字节,使其精确指向处于 Unsorted Bin 中的chunk_B。此时系统被骗,误以为chunk_B也是 Fastbin 的一部分。
  2. 第二跳(用 Libc 找 Libc):chunk_B的fd里原本装的是main_arena的 Libc 地址。我们再次利用部分覆盖,修改chunk_B肚子里这个 Libc 地址的最后 2 个字节。因为main_arena和__malloc_hook都在 Libc 里(前缀相同),我们成功将其拨到了__malloc_hook附近。

错位伪造的魔法(为什么是指向“附近”?):
glibc 对 Fastbin 有严格的安全检查(检查size字段)。黑客发现,在__malloc_hook地址往前数 0x23 个字节的地方,内存数据恰好存在序列0x7f。如果把指针精确指向__malloc_hook - 0x23,系统会把这个0x7f误认为是一个合法的size(0x70大小)。骗过系统后,我们申请出这块内存,就能直接覆盖其后方的__malloc_hook。

# 代码演示:修改 chunk_A 的 fd 指向 __malloc_hook 附近伪造的 chunk edit(chunk_A, p16(0xED))

第二步:Unsorted Bin Attack(借刀杀人写 Libc 地址)

现在我们控制了__malloc_hook的周边,但里面是空的。我们需要往里面写入一个庞大的 Libc 真实地址。这要用到chunk_B。

当程序执行malloc把chunk_B从 Unsorted Bin(双向链表)中摘除时,系统为了把断开的链表重新接上,会执行解链操作:Chunk_B->bk->fd = Chunk_B->fd;。

黑客的篡改:
黑客悄悄把chunk_B的左手(bk指针)硬生生掰开,塞到了__malloc_hook - 0x10的位置。

# 修改 bk 指向 __malloc_hook - 0x10 edit(chunk_B, "伪造的fd不变", "修改bk指向__malloc_hook-0x10") malloc(0x80) # 触发解链

为什么是减去 0x10?(严密的数学逻辑)
在 C 语言结构体中,堆块的fd指针永远在基地址+0x10的偏移处。系统在执行Chunk_B->bk->fd时,实际的物理动作是往(Chunk_B->bk) + 0x10的地址里写数据。
黑客给出提前量(-0x10),系统自作聪明地加上0x10走过去时,一加一减完美抵消。最终,系统会亲自把chunk_B右手里握着的大总管地址(main_arena),分毫不差地写入了__malloc_hook里面!整个过程没有使用任何输出函数泄露地址。

第三步:把 Libc 地址变成 One Gadget(Libc 上的部分覆盖)

此时,__malloc_hook里面已经有了一个合法的 Libc 地址(main_arena+88)。
main_arena和直接弹 Shell 的one_gadget只有最后几个字节不一样。
我们通过第一步布局好的 Fastbin,把__malloc_hook申请出来,并再次使用部分覆盖,只修改最后 2 个字节。

# 假设 one_gadget 相对基址的偏移尾数是 0xA456 edit(malloc_hook_chunk, p16(0xA456))

第四步:命运的 1/16 爆破触发

内存按页对齐,地址最后的 12 位(3 个十六进制字符,如456)是绝对固定的。但倒数第 4 个字符(如上面的A,占 4 个 bit)是受 ASLR 影响的,每次运行都在 0-F 的 16 种可能中随机变化。

这意味着,我们写入的0xA456中,456绝对正确,但那个A是我们蒙的。

# 最后一步:随便 malloc 一个东西,触发 __malloc_hook try: malloc(0x10) # 如果蒙对了,__malloc_hook 完美指向 one_gadget,拿到 Shell! # 如果蒙错了程序崩溃。在 CTF 或本地提权中,重新运行脚本再试即可。 except: pass

总结

  1. 布局 Fastbin(造伪指针):利用漏洞,修改一个已经被释放的 Fastbin 堆块的fd指针(部分覆盖),让它指向__malloc_hook附近,为你后续能申请到这块内存铺路。
  2. Unsorted Bin 盲写(借刀杀人):修改一个已经被释放的 Unsorted Bin 堆块的bk指针。然后触发一次内存分配(申请走这个 bin),利用系统底层的解链机制,直接把main_arena(Libc 地址)盲写到__malloc_hook里面。
  3. 篡改 Hook(偷天换日):顺着第 1 步铺好的路,把__malloc_hook所在的内存申请出来。此时里面已经有了第 2 步写进去的 Libc 地址。你再次利用部分覆盖,把这个地址的最后 2 个字节改成one_gadget的末尾字节,完成 1/16 概率的绝杀。

例子

为了让你能直观地感受到 House of Roman 的精妙之处,我们来构建一个极简的漏洞场景,并用Python 攻击脚本(伪代码)结合内存布局,一步一步演示它是如何完成这套“盲打”连招的。

1. 假设的漏洞场景 (C语言伪代码)

假设我们有一个典型的菜单题(CTF 中常见的形式),程序存在UAF (Use-After-Free)漏洞:释放了内存,但指针没清空,我们还能继续往里面写数据。

// 假设程序里有这样的功能: char *chunk_A = malloc(0x60); // 大小属于 fastbin 范围 char *chunk_B = malloc(0x80); // 大小属于 unsorted bin 范围 char *chunk_C = malloc(0x60); free(chunk_A); free(chunk_B); // 此时 A 进入 fastbin,B 进入 unsorted bin。 // 漏洞:即使 free 了,我们依然可以使用 edit 功能修改 A 和 B 里面的数据 (UAF)

2. 攻击过程拆解 (结合内存布局)

在 ASLR(地址随机化)开启的情况下,我们不知道堆和 Libc 的真实基址。我们只知道它们在内存中的十六进制地址大概长这样:

  • 堆地址 (Heap):0x55xx_xxxx_xYYY(YYY 是相对固定的偏移)
  • Libc 地址:0x7fxx_xxxx_xZZZ(ZZZ 是相对固定的偏移)

接下来开始我们的“部分覆盖”表演:

第一步:修改 Fastbin 的fd指针 (堆基址 1 字节覆盖)

目前chunk_A在 fastbin 里,它的fd指针指向 0(因为它是链表最后一个)。
我们知道__malloc_hook附近可以伪造一个 chunk。我们要让chunk_A的fd指向那里。

# 假设 __malloc_hook 伪造 chunk 的相对偏移最后两个字节是 0xED # 我们利用 UAF,只覆盖 chunk_A 的 fd 指针的最低 1 个或 2 个字节 edit(chunk_A, p16(0xED))

内存视角:

  • 修改前:chunk_A -> fd = 0x00000000
  • 修改后:chunk_A -> fd = 0x55xx_xxxx_xED(成功利用堆块相对偏移,指向了__malloc_hook伪造的 chunk)

此时,如果我们再分配两次0x60的大小,系统就会把__malloc_hook所在的内存当成堆块分配给我们。

第二步:Unsorted Bin Attack (将 Libc 地址写入 hook)

我们要往__malloc_hook里写点东西。现在轮到chunk_B(在 unsorted bin 中) 发挥作用了。
Unsorted bin 是一个双向链表,被释放的块的fd和bk都指向 Libc 内部的main_arena。

# 我们利用 UAF,修改 chunk_B 的 bk 指针,部分覆盖让它指向 __malloc_hook 的地址。 edit(chunk_B, "伪造的fd不变", "修改bk指向__malloc_hook-0x10") # 接着,我们触发一次 malloc,把 chunk_B 分配走 malloc(0x80)

发生的事情:
当 ptmalloc 把chunk_B从 unsorted bin 中卸下时,会执行类似bk->fd = main_arena的操作。
因为我们将bk伪造成了__malloc_hook - 0x10,所以这个操作等同于:把main_arena+88的真实 Libc 地址,写入到了__malloc_hook里面!

此时__malloc_hook里面已经有了一个合法的 Libc 地址(例如0x7fxx_xxxx_x123)。

第三步:将 main_arena 地址改为 One Gadget (Libc 2字节覆盖)

main_arena和直接弹 Shell 的one_gadget都存在于 Libc 模块中,它们只有最后几个字节不一样。
现在,我们通过第一步布局好的 Fastbin,把__malloc_hook申请出来,并修改里面的值。

# 假设 one_gadget 相对 libc 基址的偏移尾数是 0xA456 # 我们向 __malloc_hook 写入时,只覆盖最后 2 个字节 edit(malloc_hook_chunk, p16(0xA456))

内存视角:

  • 修改前__malloc_hook内容:0x7fxx_xxY1_2123(main_arena+88 的地址)
  • 修改后__malloc_hook内容:0x7fxx_xxYA_4456(变成了 one_gadget 的地址)

第四步:命运的 1/16 爆破触发

仔细看第三步的地址,这里就是 1/16 爆破的由来。

因为内存按页(0x1000)对齐,只有最后 12 位(即 3 个十六进制字符,如上面的456)是完全固定不变的。
倒数第 4 个字符(上面的A和1)是受 ASLR 影响,每次运行都在变化的(取值范围是0-F,共 16 种可能)。

所以我们强行写入0xA456时:

  • 456绝对是正确的。
  • 那个A是我们蒙的(假设本次运行它恰好被随机成了 A)。
# 最后一步:随便 malloc 一个东西,触发 __malloc_hook try: malloc(0x10) # 如果前面蒙对了 'A',__malloc_hook 完美指向 one_gadget,拿到 Shell! # 如果蒙错了(比如其实本次运行是 'C'),程序崩溃。 except: pass # 重新运行脚本,再抽一次卡

总结整个过程

  1. Fastbin 局部写:拿到__malloc_hook附近内存的控制权。
  2. Unsorted Bin 攻击:借助 glibc 自己内部的链表解链机制,往__malloc_hook填入一个 Libc 地址。
  3. Libc 局部写:把这个 Libc 地址的尾巴“拨”到one_gadget。
  4. 拼人品触发:1/16 概率拿到 Shell。

没有任何一个地方使用printf或puts去泄漏真实地址,完全是在黑盒状态下,通过精准的相对偏移覆盖完成了内存结构的劫持。这也是 glibc ptmalloc 利用中最有艺术感的打法之一。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:44:38

PLC断电重启后无法写入指令,核心原因是通讯链路未正确重建或PLC内部状态在断电时发生了异常

PLC断电重启后无法写入指令,核心原因是通讯链路未正确重建或PLC内部状态在断电时发生了异常。建议按以下顺序排查处理: 1. 确认物理层与通讯连接状态 观察指示灯:检查PLC的电源灯、运行灯(RUN)以及通讯口指示灯(如COM、LINK、PRPHL)。如果通讯灯不闪烁,说明物理连接或…

作者头像 李华
网站建设 2026/9/28 4:44:04

枚举进阶:90%代码错误的终结者

这里介绍的是进阶技巧, 这些技巧可以让你提交的代码里面所包含的低级错误数量大幅度减少, 降低的幅度能够达到百分之九十。在开发的这个过程里头, 存在一种特性的样子, 这种特别的程度非常微小, 但是它却能够产生出那种特别巨大的能量, 如果能把这个特性给使用得适当得当的话, …

作者头像 李华
网站建设 2026/9/28 4:44:00

小白程序员必看:Agent开发与大模型开发的区别及入行指南

本文详细对比了大模型算法工程师与agent开发工程师的薪资待遇、工作内容、学习门槛及发展前景。大模型算法岗薪资高但门槛极高,而agent开发岗入门相对简单,更适合小白和程序员。随着AI技术的发展,agent开发将成为未来的趋势,具备落…

作者头像 李华
网站建设 2026/9/28 4:43:36

GemeOpen 86型零火智能开关-三开-WiFi版 (型号:GSCW3B) - 开发者指令

86型零火智能开关-三开-WiFi版 (型号:GSCW3B) - 开发者指令 产品基本描述 产品主要特点 WiFi网络,三路控制 AC110v-250v宽电压输入 单路最大负载800W 开发者文档 https://www.smart-bird.cn/doc/product/GSCWB/device/GSCW3B/command 产品电气参数…

作者头像 李华
网站建设 2026/9/28 4:43:17

人体行为数据集 跌倒检测数据集 摔倒识别 AI图像识别

室内跌倒目标检测数据集 数据集概述室内跌倒目标检测数据集,通过室内监控摄像头采集,聚焦室内场景下的人员跌倒行为检测,共约 7000 张图像,全部完成人工标注,适用于老人看护、居家安全监测、异常行为识别等视觉任务&am…

作者头像 李华