news 2026/10/17 9:49:30

house of force:从top chunk劫持到任意地址分配的堆利用详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
house of force:从top chunk劫持到任意地址分配的堆利用详解

1. 从一道2016年的老题说起:house of force到底值不值得重新调一遍

这几年CTF赛题越来越依赖复杂利用链,动不动就是tcache double free、large bin attack、io_file利用,很多人反而把一类经典堆利用方式给忽略了。bcloud这道题,出题年份是2016年,考察的就是house of force。我第一次接触这个题的时候,对top chunk的理解只停留在"分配内存时会自动往前切"这种程度,结果调试了三四个小时才把一个-1的size字段和一次大malloc串成完整利用链。说实话,把这道题手动调通之后,再去读glibc malloc源码里关于top chunk的路径,顺滑得像换了个脑子。

bcloud是BCTF 2016的一道pwn题,漏洞点很典型:程序在初始化时用gets读取name和org,这两个输入会覆盖到堆上后续的chunk头。利用方式是典型的house of force:先改掉top chunk的size,再利用malloc的分配逻辑把top指针推到任意目标地址附近,最后通过一次普通malloc拿到指向__free_hook的chunk,把free_hook改成system,实现任意命令执行。整个过程不涉及复杂的UAF链,也没有tcache,靠的是对malloc分配器内部计算的精确控制。

为什么到现在还有人在翻这道题?因为house of force虽然在libc 2.29以后被加了校验卡得死死的,但它的思维模型是理解现代堆利用的地基。你能解释清楚为什么改掉top chunk的size之后,一次巨大的malloc就能让后续malloc返回任意地址吗?你能在不知道目标地址时,仅凭一次unsorted bin泄漏就算出top chunk的新落点吗?如果能,说明malloc底层逻辑你已经吃透了;如果不能,bcloud就是最好的补课题。

2. top chunk与malloc的"算账"逻辑

2.1 top chunk到底是什么

从操作系统的角度看,进程通过brk或mmap向内核申请内存。glibc malloc维护的arena中,有一段被映射进来但还没有分割给用户的内存,这段内存就是top chunk。top chunk相当于malloc手里的一整块"待切割的原料",每次用户malloc一小块,malloc就从top chunk顶部切一小块下来,把剩下的继续当作新的top chunk。

正常情况下的切割流程是这样的:假设top chunk从地址T开始,size字段为S,用户请求大小为R(内部会加上chunk头的16字节并对齐,记作nb)。malloc检查后发现nb小于等于S,就从T处切出nb字节,新的top chunk地址变成T + nb,size字段变成S - nb。整个公式简单说就是:新top地址 = 原top地址 + 请求块的真实大小。

这里有个容易被忽略的细节:top chunk的size字段可以直接读写吗?正常情况下不能,因为堆布局里top chunk紧跟最后一个已分配chunk,而前一个chunk的data区域通常由用户的输入控制。只要存在堆溢出,尤其是有gets这种无边界输入,就能越过当前用户块的数据区,改写下一块chunk的header。一旦改写的是top chunk的size字段,上面那个切割公式就会出现完全不同的走向。

2.2 改掉size后,malloc会怎么算

house of force的核心就是把top chunk的size字段直接改成一个巨大的值,比如0xffffffffffffffff。这样无论用户请求多大的nb,检查nb小于等于S都会通过,因为-1在无符号比较里是最大的数。

此时,如果我们故意调用一次malloc申请一个极大值,让nb等于"某个目标地址与控制top chunk地址的差值",malloc就会忠实地执行:

新top地址 = 原top地址 + nb

结果就是,一次大malloc之后,新top chunk的地址被我们精确挪到了目标地址附近的某个位置。接下来再调用一次malloc申请一个小请求,返回的chunk地址就会落在目标地址上,于是我们就拿到了一个指向任意地址的读写窗口。

这里可能有人会问:目标地址要是在当前top chunk的下方怎么办?比如top chunk在堆里,目标地址在libc的BSS段,而且libc加载地址高于堆。其实glibc的size计算全部用的是无符号数,我们传入的"目标地址 - 原top地址 - 0x10"得到负数时,会被当作一个巨大的无符号数。这个巨大值在malloc看来依然是合法的nb,切割公式照样执行,最后top指针同样能落到目标地址附近。不要被有符号/无符号的直觉骗了,这也是house of force最反直觉的地方。

2.3 为什么libc 2.29之后这招不好使了

house of force之所以在2016到2018年称霸,是因为当年的glibc很少对top chunk做校验。但在较新版本的malloc源码中,分配路径里加入了一个检查条件,大概逻辑是:如果top chunk的size大于系统映射的总内存大小,就认为top chunk被破坏了,从而改用sysmalloc重新申请内存。这个检查直接堵死了改-1这条路。

所以做bcloud时,一定要选择正确的环境。推荐使用Ubuntu 16.04自带的glibc 2.23,或者手动切换到一个2.23的libc,配合对应版本的ld。用新版系统直接跑老题,大部分情况连house of force的第一步都会崩,因为系统分配器变了,堆布局和校验逻辑完全不同。

3. bcloud的漏洞点定位与堆布局构造

3.1 入口的两个gets是整道题的钥匙

bcloud的main函数一开始会先读取name和org,这两个读入用的都是gets。gets的终止符是换行或者EOF,完全不限制长度,这就为堆溢出提供了入口。

初始化时的分配顺序是name先malloc一个块,org再malloc一个块,两者在堆上是相邻的。之后用户每次创建note,程序会再malloc一个新块。如果我们把org填满,再继续输入超过org块长度的内容,就能直接覆盖到下一个chunk的header。当这个chunk恰好是top chunk时,我们写入的size字段就会覆盖掉真正的top size。

这个漏洞点设计得其实很巧妙:它不需要你先泄漏堆地址,也不需要你精确计算偏移,只需要把org输入长度控制在一个合理的范围,覆盖到top chunk的size字段即可。真正需要花心思的是,如何在该改的地方改,不该碰的地方不要碰。

3.2 怎么摆布局,才够得着top chunk的size字段

直接上来就在初始化阶段覆盖top chunk size是可行的,但更稳妥的做法是先分配几个note,然后把其中一两个free掉,制造出空闲chunk,再在适当时候触发溢出。

我个人习惯的布局是:

  • 先分配一个0x40的note,让它紧挨着org块。
  • 再分配一个0x40的note,让它作为填充。
  • 通过org的gets溢出,伪造top chunk的size为0xffffffffffffffff。
  • 后续create一个新的note,在house of force路径中作为"推动top指针"的请求。

注意,覆盖top chunk size的偏移要拿本地调试器确认。不要在没开调试的情况下凭空猜偏移,不同libc版本、不同malloc策略下,相邻chunk的padding和复用情况都有差异。

3.3 泄漏libc地址的稳定线路

bcloud的print功能会把note内容区的数据原样打印出来,而这个题在某些操作路径下会让note的内容区残留unsorted bin的fd指针。具体做法是:先构造一个note,把它释放掉,让这个空闲chunk进入unsorted bin。由于unsorted bin的fd、bk指针会被写入chunk的user data区域,而释放后这个数据区域并不会被清空,再次调用print就能把这些指针读出来。

读到的fd指针指向main_arena+88的位置。拿到这个地址后,用libc基址 = 泄漏地址 - offset就能算出libc的加载基址。offset怎么确定?最简单的方法是用本地的libc.so.6开一个gdb,在main_arena附近看一眼偏移;或者直接使用libc-database。bcloud这道题在Ubuntu 16.04默认环境下的偏移是0x3c4b78,但我建议还是自己算一遍,不要死记这个数,因为换libc版本就废了。

这里涉及一个关键的技巧:house of force的目标地址往往需要精确到__free_hook的偏移,而__free_hook的偏移只能通过libc基址加符号偏移得到。没有泄漏这一步,house of force顶多算一个"能把堆打飞"的堆风水题,有了libc地址,它才真正升级为可以任意地址写的漏洞利用链。

4. 从理论到shell:bcloud的完整利用链与调试记录

4.1 环境准备和工具

做这道题我建议的环境组合是Ubuntu 16.04虚拟机(glibc 2.23),配合pwntools和gdb插件。

调试之前先把ASLR的情况确认好。本地调试时可以关闭ASLR省心一点,但最终exp不能依赖ASLR关闭,因为我们的利用链里有unsorted bin泄漏,拿到了实际libc基址,所以开着ASLR也能稳定打通。

工具方面,pwndbg对堆块查看比较友好,输入heap、bins、top三个命令就能把当前堆布局、bin链表和top chunk的size全部看清楚。遇到段错误时,千万不要急着改exp,先用x/50gx 目标地址把目标内存区域的内容拉出来看,很多问题一眼就能定位。

4.2 第一步:把top chunk的size改成-1

初始化时,我们通过org输入写一个足够长的字符串,把top chunk的size位置覆盖成0xffffffffffffffff。

这一步最容易把size字段前面的padding和prev_size也一起改掉,不过对house of force来说问题不大,因为top chunk是堆末尾的特殊chunk,它前面的物理相邻chunk会被我们在布局阶段释放掉,malloc不会对它做严格的consolidate检查。下面是核心代码的骨架:

from pwn import * context.arch = 'amd64' context.log_level = 'debug' def create(size, data): p.sendlineafter('choice:', '1') p.sendlineafter('size:', str(size)) p.sendafter('content:', data) def edit(idx, data): p.sendlineafter('choice:', '3') p.sendlineafter('index:', str(idx)) p.sendafter('content:', data) def delete(idx): p.sendlineafter('choice:', '2') p.sendlineafter('index:', str(idx)) def show(idx): p.sendlineafter('choice:', '4') p.sendlineafter('index:', str(idx)) p.recvuntil(b'content:') return p.recv(0x30)

在调试器里改完size之后,记得看一次top命令。如果显示的size是一个巨大的数,说明覆盖成功。如果显示的是正常的0x210之类的数,说明输入长度不够或者偏移算错了,需要从偏移量开始排查。

4.3 第二步:越界分配,拿到__free_hook附近的chunk

这一步是整个利用链的核心,也是最容易算错的地方。

假设当前top chunk地址为top_addr,我们先请求一个大小为adjust_size的块,之后再请求一个0x20的小块。目标是小块返回的地址正好对齐到__free_hook - 0x10的位置,这样我们写入的数据就从chunk头后面的user data开始,正好覆盖__free_hook。

计算公式是:

adjust_size = (target_addr - 0x10) - top_addr - 0x10

也就是要分配的块,真实占用为adjust_size + 0x10,切割后新的top chunk地址指向target_addr - 0x10。这样下一次malloc(0x20)返回user data的地址恰好是target_addr。

为什么这里要减掉0x10?因为malloc返回的指针永远指向chunk头的后面16字节,而chunk头要占16字节。目标地址是__free_hook本身,我们希望chunk的user data起点正好是__free_hook,那么chunk头就得从__free_hook - 0x10开始。

# 假设已经通过show泄漏得到libc_base libc.address = libc_base free_hook = libc.sym['__free_hook'] system_addr = libc.sym['system'] top_addr = 0x602000 + 0x... # 需要实际泄漏或由堆布局推算 adjust_size = (free_hook - 0x10 - top_addr - 0x10) & 0xffffffffffffffff create(adjust_size, b'a') create(0x20, p64(system_addr)) # 此时返回的就是free_hook的地址

注意,adjust_size可能是一个很大的值,甚至超过2的31次方,程序里读取size如果用了int类型,最好直接传负数的字符串形式,因为无符号转换后等价。比如在Python里计算完再转成有符号负值传给程序,或者直接在create里传str(adjust_size)让底层atoi把它解析成无符号大数,常见实现都没问题。

实际调试中发现,很多人卡在这一步不是因为公式错,而是因为top_addr没有更新。第一次大malloc之后,当前top chunk已经变成目标地址了,很多人还用原来的top_addr计算下一个请求的size,结果malloc直接申请失败。

4.4 第三步:写__free_hook,拿shell

当第二次create返回的chunk指向__free_hook时,我们就有了一个对__free_hook的任意写窗口。直接把system的地址写进去,然后找一个内容为"/bin/sh"的note,调用delete,free就会执行system("/bin/sh")。

这里有一个细节:为了确保触发时传入的字符串正好是"/bin/sh",可以创建一个小note,内容写b'/bin/sh\x00',并且保证这个note的指针不会因为后续house of force的布局而被覆盖。最好在完成house of force布局之前就创建好这个note,留到最后一步释放。

# 在修改free_hook之前就创建好 create(0x20, b'/bin/sh\x00') # 修改free_hook create(0x20, p64(system_addr)) # 触发 delete(1) # 假设笔记1的内容区是/bin/sh p.interactive()

如果在写free_hook之后还有别的chunk操作,容易导致malloc内部状态出错,特别是新top chunk落在libc地址区域,后续malloc会被系统判定为异常。所以我的建议是:完成free_hook覆盖后,立刻触发,不要做任何多余操作。

4.5 实战调试中容易翻车的三处细节

第一,gets读入时如果本地测试环境和远程环境libc不同,堆分配的chunk大小可能差好几个字节,导致org溢出覆盖不到top size。遇到这情况不要硬调偏移,直接把远程libc和ld的patchelf环境搭好再调试。

第二,house of force里那个巨大的malloc请求,有些题目在create函数内部会对size做二次对齐或者加一个固定偏移。写exp之前先看反汇编,确认用户传入的size和malloc真正的参数之间的关系。如果程序内部写死了chunk大小,那就不能直接传adjust_size,得用text的预留字段做转换。

第三,在2.23的malloc里,大malloc请求有可能因为"nb >= MINSIZE"之外的其他限制被拒绝,比如MMAP_THRESHOLD。这里的house of force之所以能成功,是因为nb在无符号数上虽然很大,但在有符号比较时可能会走mmap路径。如果你的exp在create(adjust_size, ...)这一步触发mmap,而不是推进top指针,说明数值解析出了问题,可以尝试用负数形式传入,或者确认程序的read_line会不会对负数做额外处理。

5. house of force在今天还能怎么用

5.1 场景一:改hook函数指针

__free_hook和__malloc_hook是这类利用的首选目标,因为它们在libc的数据段,地址固定,计算方便。只要泄漏一次libc基址,就能算出精确落点。bcloud用__free_hook改system是教科书打法,代码简单,稳定性高。

但要注意,新版glibc已经将__free_hook等符号从动态符号表里弱化或者改用其他机制,如果你在自己的环境里用pwnlib的libc.sym查不到对应符号,就先更换libc版本,不要在旧题上强行适配新环境。

5.2 场景二:配合堆地址泄漏做精准落点

如果目标不是libc的hook,而是某个堆上的结构体,比如note的指针数组,那house of force同样可以把top指针推到note数组地址附近。这种方式在2016年的题目里也出现过,它的好处是不需要libc泄漏,只要一个堆地址就能完成任意写。虽然bcloud不强制走这条路,但理解了这种变体,你在遇到其他house of force题目时会灵活很多。

5.3 新版本下的绕过思路与练习建议

libc 2.29和2.31分别在malloc的top chunk路径里加了system_mem校验,house of force的传统玩法基本失效。如今还能见到的变体包括:利用large bin里的堆地址残留做unsorted bin attack,或者通过House of Spirit将空闲chunk放在可控位置,配合tcache poisoning让后续malloc返回任意地址。这些利用方式本质都是在回答同一个问题:你能不能伪造一个让malloc相信的chunk头。

练习顺序上,我建议先做bcloud,再找几道house of force变种题练手,然后回头读glibc 2.23的malloc.c源码,重点看_int_malloc、sysmalloc和_consolidate三个函数。把bcloud手动调通一次之后,你会发现后续学large bin attack、tcache stashing unlink attack时会轻松很多,因为这些技术都是在同一个分配器内部计算模型上做文章。

从个人经验来说,bcloud这道题真正有价值的地方不在house of force本身,而在于它逼着你把malloc的切割逻辑拆开揉碎。调通它之后,再看到"new_top = old_top + nb"这行代码,你脑子里浮现的不是公式,而是整个堆从分配到释放的生命周期。这就是经典题目的意义。

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

大厂CV算法实习面试复盘:基础八股、手撕代码与业务场景全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 9:47:39

AI代码安全上线指南:从代码评审到回滚的工程化路径

1. 为什么这个话题突然值得聊"AI 生成的代码,到底能不能直接扔到生产环境?"这句话我最近被问了不下二十次,问的人里有技术合伙人、有传统企业的 IT 总监,也有刚带团队的一线主管。大家的心态很一致:一边被 A…

作者头像 李华
网站建设 2026/10/17 9:47:32

用 DESIGN.md 约束 AI 生成 UI,消除“AI 味”的工程实践

1. AI 做 UI 的“AI 味”到底从哪来1.1 先搞清楚“AI 味”是什么你肯定见过那种界面:紫色渐变背景、圆角卡片、Inter 字体、一堆无意义的留白、图标风格不统一、按钮颜色像是从调色盘里随机抓的。这就是典型的“AI 味”。我用 Claude Code 和各类 AI 编程助手做过不…

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

机器学习课件代码包复现指南:从环境配置到调参避坑

简介:《机器学习全方位解析:从基础到实践》配套的邹博机器学习全套课件及代码,面向零基础或有一定编程经验的AI入门者,可解决从算法原理到代码落地的全流程学习问题。包内涵盖线性回归、逻辑回归、决策树、随机森林、SVM、KNN、神…

作者头像 李华
网站建设 2026/10/17 9:46:47

Linux下Qt Creator安装与Kit配置深度指南

1. 项目概述:为什么一个“安装 Qt Creator”会值得花一整篇来写?Qt Creator 不是那种点几下就能装好的普通图形软件。它表面看是个 IDE,背后却牵扯到 Linux 系统底层的依赖管理、C 工具链协同、GUI 库版本兼容、甚至用户权限模型的微妙博弈。…

作者头像 李华