House of Orange glibc-2.23
什么是 House of Orange?
House of Orange 是针对于 libc-2.23 的一种攻击方法,是将 libc_heap 攻击和 __IO_FILE 攻击组合起来的利用手段

House of Orange Poc
1 | /* |
解释一下这一段 POC 吧:
内存布局与地址泄露
1 | ptr = malloc(0x100); |
- 操作:申请一个较大的 chunk(0x100),它在被 free 后会进入 Unsorted Bin(而不是 Fastbin)。紧接着申请 0x18 大小的 chunk,防止 0x100 被释放后直接与顶部的 Top Chunk 合并。
- 泄露原理:当 Unsorted Bin 中只有一个空闲 chunk 时,它的 fd 和 bk 指针都会指向 Unsorted Bin 的链表头。而这个链表头位于 libc 内部的 main_arena 结构体中,偏移量正好是 88(0x58)。因此,通过读取 fd 减去 88,就能精准计算出 main_arena 的基地址。
伪造 Chunk 元数据
1 | this->size = 0x61; // 对应 _chain 偏移 |
- size = 0x61:这是整个攻击链中最巧妙的一环。将原本大小为 0x100 的 chunk 大小改写为 0x61(即 0x60 大小 + PREV_INUSE 标志位)。这是为了在后续分配时,让 malloc 将这个 chunk 整理(sort)进大小为 0x60 的 Small Bin 中。
- memcpy:将字符串 “/bin/sh” 写在 chunk 的最开头。在后续被当作 __IO_FILE 结构体时,这里对应文件流的起始部分。由于虚函数调用时会将文件结构体的指针作为第一个参数传递,这相当于把 “/bin/sh” 作为参数传给了 system。
Unsorted Bin Attack 劫持链表头
劫持 UnSortedbin 的 bk 值,从而触发 UnSortedbin Attack:
1 | this->bk = (struct malloc_chunk *)(main_arena_addr + 0xa00 - 0x10); // __GI__IO_list_all |
自己可以去看一下程序中 main_arena 和 _IO_list_all 的距离:距离为 0xa00==>
1 | pwndbg> p &(main_arena) |
0x64c0 - 0x5ac0 = 0xa00
- 目标:_IO_list_all 是 libc 中管理所有文件流(如 stdin, stdout, stderr)的单向链表头指针。
- 原理:Unsorted Bin Attack 的核心在于利用 malloc 过程中的 unlink 操作:bck->fd = fwd
- 结果:这段代码将伪造 chunk 的 bk 指向了
_IO_list_all - 0x10。当下一次触发 malloc 时,libc 会将这个 chunk 从 Unsorted Bin 卸下,执行bck -> fd = fwd,这会导致 _IO_list_all 的值被覆盖为main_arena + 0x58= main_arena_addr(即 Unsorted Bin 的链表头)。
FSOP 结构体重叠
这一步是系统自动发生的,也是代码精妙所在:
- 现在
_IO_list_all指向了main_arena + 0x58。libc 把main_arena + 0x58当作了一个_IO_FILE结构体。 - libc 如果遇到严重错误(比如后面的 malloc 发现堆被破坏)或者程序正常退出,会调用
_IO_flush_all_lockp来刷新所有的文件流。 - 系统会顺着
_IO_list_all往下找。它通过读取_IO_FILE结构体中偏移为0x68的_chain字段来寻找下一个文件流。 - 巧合发生:
(main_arena + 0x58) + 0x68 = main_arena + 0xc0。而main_arena + 0xc0恰好是smallbins[4]的地址,也就是专门存放大小为0x60的 chunk 的双向链表头! - 因为我们前面把 chunk 的
size改成了0x61,在下一次malloc遍历 Unsorted Bin 时,它会被系统主动放入这个smallbins[4]中。 - 结论:libc 顺着 _chain 往下找,刚好找到了我们完全可控的 this->chunk,并把它当作第二个
_IO_FILE结构体进行处理。
这里对 main_arena 和 bins 的偏移再进行解释:
main_arena:分配区结构 (malloc_state)
在 glibc 源码中,分配区是由 struct malloc_state 定义的。对于主线程,系统会创建一个全局静态变量 main_arena 来管理主线程的堆。
glibc 规定了 128 个常规 Bin。因为是双向链表,每个 Bin 需要两个指针:一个当 fd(前向指针),一个当 bk(后向指针)。其中 bin 0 不使用,所以数组大小是 128 × 2 − 2 = 254 个指针元素。
这 126 个实际使用的 Bin 被严格划分为三个梯队:
- Bin 1(1 个):Unsorted Bin。刚刚被释放的大块,非 fastbin 大小会先扔到这里,相当于一个缓存区。
- Bin 2 ~ Bin 63(62 个):Small Bins。管理固定大小的 chunk(如 0x20, 0x30, …, 0x3F0 等)。每个 bin 只装一种大小的 chunk。
- Bin 64 ~ Bin 126(63 个):Large Bins。管理范围大小的 chunk(例如 0x400 ~ 0x430 放一个 bin 里)。按大小排序。
在 64 位 glibc-2.23 中,bins 数组位于 main_arena + 0x68。数组里的元素依次交替为 fd 和 bk。
我们来做个推演:
bins[0](offset0x68): Unsorted Bin 的fd(指向链表中第一个 chunk)bins[1](offset0x70): Unsorted Bin 的bk(指向链表中最后一个 chunk)bins[2](offset0x78): Small Bin (0x20 大小) 的fdbins[3](offset0x80): Small Bin (0x20 大小) 的bkbins[4](offset0x88): Small Bin (0x30 大小) 的fdbins[5](offset0x90): Small Bin (0x30 大小) 的bk
依次类推,规律是:对于目标大小 size(包含 chunk header),它的双向链表表头在数组中的索引可以通过公式计算。 对于 0x60 大小的 Small Bin:
- 它是第 5 个 Small Bin(0x20, 0x30, 0x40, 0x50, 0x60)。
- 它的
fd就是bins[10],地址 =0x68 + 10 * 8 = 0xb8。 - 它的
bk就是bins[11],地址 =0x68 + 11 * 8 = 0xc0。
这完美印证了我们前面讨论的 House of Orange main_arena + 0xc0 确确实实就是用来存放 0x60 大小 chunk 链表尾部指针的地方。
伪造 _IO_FILE 绕过检查与劫持执行流
设置 _IO_write_base < _IO_write_ptr 来触发刷新
1 | // 伪造 _IO_FILE 结构体字段绕过检查 |
- 绕过检查:
_IO_flush_all_lockp函数在调用虚函数前,会检查流的状态(判断_IO_write_ptr > _IO_write_base等)。我们利用fd_nextsize和bk_nextsize(它们在内存中的位置刚好对应 _IO_write_base 和 _IO_write_ptr)赋上 2 和 3,满足 3 > 2,成功骗过系统。
此时,系统已经把我们的这个 chunk(this)强行当成了一个 _IO_FILE 结构体来读取。我们需要让伪造的结构体满足上述条件。
而 malloc_chunk 的字段和 _IO_FILE 的字段在内存偏移上已经形成了完美的对应:
1 | 0x00 prev_size _flags "/bin/sh\x00" |
- vtable 劫持:
- 在 _IO_FILE 结构体的末尾(64位系统下偏移为 0xd8 的位置),存放着一个指向 _IO_jump_t 结构体的指针,这就是 vtable(虚函数表)指针。代码将其指向了我们全局控制的数组 buf。
- 填入 System:buf[3] 对应虚函数表中的 __overflow 函数。将其赋值为 system。
引爆 (The Trigger)
1 | malloc(0x18); |
当执行最后的 malloc(0x18) 时,发生了以下连环反应:
- 触发 Unsorted Bin Attack,改写了 _IO_list_all。
- 将伪造的 chunk 放入了 Small Bin (0x60)。
- 因为此时堆结构已经被破坏(比如 Unsorted bin 链表断裂),malloc 会触发 malloc_printerr 报错。
- 报错机制会调用 abort,进而触发 _IO_flush_all_lockp。
- libc 顺着被篡改的 _IO_list_all 和 _chain 找到了我们的 fake chunk。
- 检查通过,调用 vtable 中的 __overflow。
- 实际执行 system(this),由于 this 开头是 “/bin/sh”,最终成功获得 Shell。
更新: 2026-04-26 12:04:29
原文: https://www.yuque.com/idcm/wnemg9/bnmzebk3hfmig539