StorSvc writeup 和我的分析脚本介绍
作者:k0shl,360 Vulcan团队成员
今天我想分享我在2019年报告的两个最喜欢的逻辑权限提升漏洞——CVE-2019-0983和CVE-2019-0998,以及关于我的RPC静态分析脚本的简单介绍。我已经在我的github上公开了这两个PoC和我的脚本。这些都是通过逆向工程发现的,实际上,我不知道如何通过普通用户交互来触发它,并使用procmon等监视器进行监控。我将在本文中分享更多关于如何发现这些漏洞的细节。
那么让我们开始我们的旅程。
StorSvc概述
StorSvc是Windows存储服务,为存储设置和外部扩展存储提供服务。历史上有两个关于存储服务的有趣漏洞,分别是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,表示文件已存在。服务将调用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通过xref解析代码路径,我还发现在分析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
阅读次数:37120
2条评论
评论
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