资讯动态

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

发布时间:2026/10/17 9:49:39 来源:尧图企业网站定制
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 nbsize字段变成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_arena88的位置。拿到这个地址后用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. 从理论到shellbcloud的完整利用链与调试记录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(bcontent:) 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, ba) 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调用deletefree就会执行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这行代码你脑子里浮现的不是公式而是整个堆从分配到释放的生命周期。这就是经典题目的意义。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑