StorSvc漏洞分析与我的分析脚本介绍
作者:Qihoo 360 Vulcan Team的k0shl
今天,我想分享我在2019年报告的两个我最喜欢的逻辑提权漏洞——CVE-2019-0983和CVE-2019-0998,并简要介绍一下我的RPC静态分析脚本。我已经在我的GitHub上公开了这两个PoC和我的脚本。这些漏洞都是通过逆向工程发现的,实际上,我当时并不知道如何通过普通用户交互来触发它们,也无法用Procmon这样的监视器进行监控。我将在本文中分享更多关于我是如何发现它们的细节。
那么,让我们开始吧。
StorSvc概述
StorSvc是Windows存储服务,为存储设置和外部扩展存储提供服务。历史上曾出现过两个关于Storage Service的有趣漏洞,一个是James Forshaw报告的CVE-2018-0983,另一个是SandboxEscaper的一篇博客(SandboxEscaper后来删除了那篇文章)。因此我决定深入研究这个服务。
根据James Forshaw和SandboxEscaper的发现,他们都关注一个RPC接口storsvc!SvcMoveFileInheritSecurity,而在打补丁后,微软似乎用一种“简单的方式”修复了这个逻辑漏洞。
打补丁后
1
2
3
4
|
signed __int64 SvcMoveFileInheritSecurity()
{
return 0x80004001;
}
|
但这并不是该服务中唯一的RPC接口。在逆向了StorSvc.dll之后,我发现了两个有趣的点。
StorSvc卷结构
在介绍我的CVE之前,我想先谈谈在逆向过程中发现的一个有趣结构。
几乎每个RPC接口都引用并检查这个结构。
例如:
1
2
3
4
5
6
7
8
9
|
v6 = 0x450 * v5;
v7 = *(_DWORD *)(0x450 * v5 + g_StorageService[v3 + 5] + 564);
if ( !(v7 & 1)
....
LODWORD(v4) = StringCchCopyW(&FileName, 0x104ui64, (const wchar_t *)(v6 + g_StorageService[v3 + 5] + 4));
if ( (signed int)v4 >= 0 )
{
.....
}
|
如代码所示,变量v5看起来像是一个索引,并且存在一个大小为0x450的结构体。g_StorageService是一个全局变量,它像一个结构体表一样存储这些结构。当我进入这些RPC接口时,服务在检查这个结构时总是失败。
1
2
|
0:002> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820 00000000 00000000 00000000 00000000 ................
|
这个结构体的内容总是零。这很糟糕,所以我试图找出它失败的原因以及如何设置这个值。
经过一些代码审查,我注意到这个内容可以通过挂载一个扩展卷来设置。
现在我知道为什么这个值总是零了。我在虚拟机中进行了测试,虚拟机里只有一个原始卷C:\。经过简单研究,我发现有一个简单的方法可以让它工作:我可以在虚拟机中添加一个新磁盘,比如E:。然后,结构体的内容就被设置了,我可以获得结构中一些变量的含义,例如,该结构中的偏移量0x4指向卷名,偏移量0x234指向卷状态。
1
2
|
0:001> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820 00000000 003a0045 0000005c 00000000 ....E.:.\.......
|
现在让我介绍CVE-2019-0983和CVE-2019-0998。
(在这两个CVE中我使用了硬链接,因为当时微软还没有发布硬链接缓解措施)
CVE-2019-0983
该漏洞是由StorageService::ProvisionStorageCardForUser中的一个逻辑错误引起的,错误代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
__int64 __fastcall StorageService::ProvisionStorageCardForUser(__int64 a1, int a2, unsigned int a3, wchar_t *a4)
{
v22 = StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini");
v9 = 0;
if ( v22 >= 0 )
{
v23 = StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v21);
v9 = 0;
if ( v23 >= 0 )
{
CopyFileW(&ExistingFileName, &NewFileName, 0);
v9 = 0;
}
}
}
|
CVE-2019-0983很容易理解。ExistingFileName是"C:\User\k0shl\Video\desktop.ini",而NewFileName是"E:\User\k0shl\Video\desktop.ini",这两个文件都可以由普通用户控制。因此我可以创建一个指向高权限文件的硬链接。最终,该文件将被我控制的文件占用。
打补丁后:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
v15 = RpcImpersonateClient(0i64);
if ( v15 < 0 )
goto LABEL_44;
v23 = (void **)&v39;
if ( v41 >= 8 )
v23 = v39;
v24 = &v35;
if ( (unsigned __int64)Dst >= 8 )
v24 = (struct _SECURITY_ATTRIBUTES **)v35;
if ( StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini", v24) >= 0
&& StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v23) >= 0 )
{
CopyFileW(&ExistingFileName, &NewFileName, 0);
}
RpcRevertToSelf();
|
它在CopyFileW()之前调用了RpcImpersonateClient()。
CVE-2019-0998
该漏洞是由StorSvc!SvcSetStorageSettings中的逻辑错误引起的,错误代码在函数StorageService::SetWriteAccess中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
v14 = GetUserFolder(&pObjectName);
...
_wsplitpath_s(&pObjectName, 0i64, 0i64, 0i64, 0i64, &Filename, 0x104ui64, 0i64, 0i64);
LODWORD(phkResult) = StringCchCopyW(
&PathName,
0x104ui64,
(const wchar_t *)(*(_QWORD *)(v7 + 8i64 * (_QWORD)v6 + 40) + 1104 * v11 + 4));
if ( (signed int)phkResult >= 0 )
{
LODWORD(phkResult) = PathCchAppend(&PathName, 260i64, &Filename);
if ( !CreateDirectoryW(&PathName, &SecurityAttributes) )
{
v17 = GetLastError();
if ( v17 == 183 )
{
v18 = SetNamedSecurityInfoW(
&PathName,
SE_FILE_OBJECT,
4u,
0i64,
0i64,
*(PACL *)&SecurityAttributes.nLength,
0i64);
|
首先,服务调用GetUserFolder()来获取完整的文件夹路径,并调用_wsplitpath_()将完整路径拆分出其最终名称。例如,GetUserFolder()返回完整路径"C:\User\k0shl",经过_wsplitpath_()后,我得到文件名"k0shl"。
最终PathName将被设置为"E:\k0shl"并调用CreateDirectory。服务试图在另一个卷上创建一个用户文件夹,如果创建目录失败,它将获取最后的错误值。如果值是0xb7,则表示文件已存在。服务将调用SetNamedSecurityInfoW来设置其DACL,但它没有检查PathName是文件还是目录。如果"E:\k0shl"是一个文件而不是目录呢?如果我在卷中创建一个文件而不是目录,并创建一个指向高权限文件的符号链接,那么最终将修改高权限文件的DACL。
打补丁后:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
if ( CreateDirectoryW(&PathName, &SecurityAttributes) )
goto LABEL_107;
v17 = GetLastError();
if ( v17 == 183 )
{
if ( !(GetFileAttributesW(&PathName) & 0x10) )
{
LODWORD(phkResult) = -2147024891;
goto LABEL_92;
}
v18 = SetNamedSecurityInfoW(
&PathName,
SE_FILE_OBJECT,
4u,
0i64,
0i64,
(PACL)SecurityAttributes.lpSecurityDescriptor,
0i64);
|
打补丁后,它检查文件的属性以确认它是一个目录。实际上,我认为仍然存在一个TOCTOU问题,但在我测试后,时间窗口太小,我无法在GetFileAttribute和SetNamedSecurityInfoW之间删除目录并创建符号链接。当然,我也不能使用oplock,因为GetFileAttribute()只是查询文件对象信息。
我的分析脚本介绍
在我报告了这两个逻辑漏洞之后,我思考了我是如何发现这些漏洞的。首先,我找到了一些敏感函数,比如SetNamedSecurityInfo或CopyFile,然后我从RPC接口获得了一条代码路径。
正如我在另一篇博客中所说,我最终决定写一个脚本来帮助我分析所有的RPC服务器。
我为这个脚本构思了一个简单的框架。
步骤1:我需要获取所有RPC服务器。
步骤2:我需要获取所有RPC接口。
步骤3:我需要在IDA中解析RPC dll或exe。
步骤4:我需要找到从RPC接口到敏感函数的代码路径。
实际上,这些都很容易完成。我使用了James Forshaw的出色工具NtApiDotNet,我可以使用这个工具来帮助我解析RPC服务器。在NtApiDotNet中有一个名为Win32的类,以及一个有趣的方法名为ParsePeFile。
这个函数可以像RPCView一样解析RPC服务器并导出RPC接口,我只需要RPC接口名称。
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
|
public static IEnumerable<RpcServer> ParsePeFile(string file, string dbghelp_path, string symbol_path, bool parse_clients, bool ignore_symbols)
{
List<RpcServer> servers = new List<RpcServer>();
using (var result = SafeLoadLibraryHandle.LoadLibrary(file, LoadLibraryFlags.DontResolveDllReferences, false))
{
if (!result.IsSuccess)
{
return servers.AsReadOnly();
}
var lib = result.Result;
var sections = lib.GetImageSections();
var offsets = sections.SelectMany(s => FindRpcServerInterfaces(s, parse_clients));
if (offsets.Any())
{
using (var sym_resolver = !ignore_symbols ? SymbolResolver.Create(NtProcess.Current,
dbghelp_path, symbol_path) : null)
{
foreach (var offset in offsets)
{
IMemoryReader reader = new CurrentProcessMemoryReader(sections.Select(s => Tuple.Create(s.Data.DangerousGetHandle().ToInt64(), (int)s.Data.ByteLength)));
NdrParser parser = new NdrParser(reader, NtProcess.Current,
sym_resolver, NdrParserFlags.IgnoreUserMarshal);
IntPtr ifspec = lib.DangerousGetHandle() + (int)offset.Offset;
var rpc = parser.ReadFromRpcServerInterface(ifspec);
servers.Add(new RpcServer(rpc, parser.ComplexTypes, file, offset.Offset, offset.Client));
}
}
}
}
return servers.AsReadOnly();
}
|
在IDA中,我可以使用IDAPython通过交叉引用来解析代码路径,我还发现在分析Python脚本中可能存在路径爆炸,所以我将递归深度设置为10和7。如果函数调用次数超过递归深度,它将返回不同的结果,当然你可以更改它。现在我已经收集了这个脚本所需的所有内容。
在我的脚本中(关于脚本配置请查看我的GitHub):
- 我遍历C:\Windows\System32下的所有exe和dll文件(实际上,这并没有包含所有的RPC服务器,在其他目录中还有一些其他的RPC服务器,或者后缀不是"dll"或"exe",例如Windows Defender或unimdm.tsp,你可以在我的脚本中配置搜索路径)。
- 我使用Win32.RPC.ParsePeFile来解析每个文件,如果它是一个RPC服务器,它将返回类似IDL的代码。
- 我创建一个文件来存储敏感函数,并使用一个IDAPython脚本来解析RPC dll或exe。
- 我获取所有到敏感函数的代码路径,如果它起始于通过Win32.RPC.ParseFile获取结果中的RPC接口,我就将其存储在SpecialFinal.txt中。
结果如下所示:
1
2
3
|
SvcSetStorageSettings[////////__imp_SetNamedSecurityInfoW<--?CreateStorageCardDirectory@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEBGKPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@H@Z<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]
SvcSetStorageSettings[////////__imp_CopyFileW<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]
|
时间线
- 2019年2月:漏洞报告
- 2019年2月:微软复现
- 2019年5月:补丁发布
- 2019年5月:获得赏金
参考
2020-07-27
阅读次数
37320
评论
Serge Sciberras 回复
2024-06-18 17:05:51
亲爱的K0shl,我正在试图了解storsvc在WPSettings.dat文件中写入什么值,因为我正在尝试用FPGA系统制作我自己的ExFAT格式。你能帮助我吗?谢谢。Serge Sciberras - serge.sciberras@logic-design-solutions.com www.logic-design-solutions.com
Kabar4d 回复
2025-04-30 18:38:05
kabar4d