StorSvc漏洞分析及RPC分析脚本介绍
作者:奇虎360 Vulcan Team - k0shl
今天我想分享我在2019年报告的两个最喜欢的逻辑权限提升漏洞——CVE-2019-0983和CVE-2019-0998,并简要介绍我的RPC静态分析脚本。这些漏洞的PoC和脚本已发布在我的GitHub上。实际上,我是通过逆向工程发现这些漏洞的,当时并不知道如何通过正常用户交互来触发它们,也无法使用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 ................
|
此结构的内容始终为零。这很糟糕,所以我试图找出失败的原因以及如何设置该值。
经过一些代码审查,我注意到可以通过挂载扩展卷来设置此内容。
现在我明白为什么这个值总是为零了。我在VM中进行了测试,VM中只有一个原始卷C:\。经过一点研究,有一个简单的方法可以使其工作:我可以在VM中添加一个新磁盘,例如E:。然后,结构的内容就被设置了,我可以获得结构中一些变量的含义。例如,此结构中的偏移量0x4指向VolumeName,偏移量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_()处理后,我得到FileName"k0shl"。
最后PathName将被设置为"E:\k0shl"并调用CreateDirectory。服务希望在另一个卷中创建一个用户文件夹,如果创建目录失败,它将获取最后一个错误值。如果值为0xb7(十进制183),则表示文件已存在。服务将调用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之间删除目录并创建符号链接。当然,我也不能使用oplocks,因为GetFileAttribute()只是查询文件对象信息。
RPC分析脚本介绍
在我报告了这两个逻辑漏洞后,我思考了如何发现这些漏洞。首先,我发现了一些敏感函数,如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
- 我获取到敏感函数的所有代码路径。如果它从RPC接口开始(该接口从Win32.RPC.ParseFile的结果中获得),我将其存储在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
阅读次数 37240
评论
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