StorSvc writeup and introduction about my analysis script
Author: k0shl of Qihoo 360 Vulcan Team
今天,我想分享两个我在2019年报告的最喜欢的逻辑权限提升漏洞——CVE-2019-0983和CVE-2019-0998,并简单介绍一下我的RPC静态分析脚本。我将这些PoC和脚本发布在我的GitHub上。实际上,所有这些都是通过逆向工程发现的,我不知道如何通过正常用户交互来触发它,并用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 ................
|
此结构的内容始终为零。这很糟糕,所以我试图找出失败的原因以及如何设置这个值。
经过一些代码审查,我注意到这个内容可以通过挂载扩展卷来设置。
现在我知道为什么这个值总是为零了,我在虚拟机中测试过,虚拟机中只有一个原始卷C:\。经过一些研究,有一个简单的方法可以使其工作,我可以在虚拟机中添加一个新磁盘,例如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通过交叉引用来解析代码路径,我还发现在分析Python脚本时可能存在路径爆炸问题,因此我将递归深度设置为10和7,如果函数调用次数大于递归深度,它将返回不同的结果,当然你可以更改它。现在我收集了这个脚本所需的所有内容。
在我的脚本中(关于脚本配置,请查看我的GitHub):
- 我遍历
C:\Windows\System32下的所有exe和dll文件(实际上,这并不包括所有的RPC服务器,在其他目录中或后缀不同于“dll”或“exe”的地方还有一些其他的RPC服务器,例如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