StorSvc权限提升漏洞分析及自动化RPC脚本技术分享

本文深入分析了Windows存储服务(StorSvc)中的两个逻辑权限提升漏洞CVE-2019-0983和CVE-2019-0998,详细介绍了漏洞原理、利用条件及微软补丁修复方式,并分享了一套用于自动化分析RPC接口敏感调用路径的脚本工具。

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

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计