作者:Danny_Wei@腾讯玄武实验室

背景

笔者在2016年11月发现并报告了 HP Support Assistant (HPSA) 的权限提升漏洞,HP Product Security Response Team (HP PSRT) 响应迅速,但却以此漏洞可以通过软件的自动更新功能自动修复为由拒绝为其发布安全公告和 CVE。4月份想起这件事后,笔者又分析了一遍修补后的 HPSA,发现 HP 的开发人员在修补中犯了更为低级的错误,导致补丁可以被绕过重新实现权限提升。在随后与 HP PSRT 的沟通与合作中,再一次利用其它技巧绕过了其后续修补,最终笔者协助 HP PSRT 完成了漏洞的修补。

本文将分析此漏洞的成因及多次补丁绕过,希望能以此为案例提高开发人员对安全的认识和理解,以减少由于对所用技术理解不到位和安全编程意识匮乏而导致的安全漏洞。

问题描述

HPSA 是惠普推出的系统管理软件,被默认安装在惠普的所有 PC 中。其用于维护系统及打印机,并提供自动更新等功能。HPSA 使用.Net开发,其系统服务 HPSupportSolutionsFrameworkService 使用 WCF 与客户端通信,完成系统更新、管理等高权限敏感操作。虽然 HPSA 使用了较新的分布式处理技术WCF,然而在 Server 与 Client 通信过程中,却采用了不正确的认证方式。导致攻击者可以绕过认证,最终利用其敏感服务接口的缺陷,实现 everyone 到 system 的权限提升。

本文将从 WCF 技术背景、漏洞发现、漏洞利用、补丁实现和两次绕过几个方面进行分析。

WCF技术背景

WCF(Windows Communication Foundation) 是用于面向服务应用程序的编程框架,基于WCF的服务可以有两种形式:1). 通过 IIS 寄宿的方式将服务寄宿于IIS中; 2). 通过自我寄宿(Self-Hosting)的方式将服务寄宿于普通应用程序、windows 服务之中。

WCF 使用 Endpoint 的概念,在服务 Endpoint 和客户 Endpoint 之间传输异步消息。 Endpoint 用来描述消息发往什么地方,如何被发送等行为。一个服务端 Endpoint 主要由三部分构成:

1). Addrsss

唯一标识endpoint,是描述服务接口的URI,可以是相对地址(相对于ServiceHost(Type, Uri[])的URI),也可以是绝对地址。

2). Binding

指定绑定在endpoint上的接口类型,描述endpoint间通信时使用的协议、消息编码方式、安全设置等。 WCF支持:HttpBindingBase, MsmqBindingBase, NetNamedPipeBinding, NetPeerTcpBinding, NetTcpBinding, UdpBinding, WebHttpBinding, WSDualHttpBinding, WSHttpBindingBase, CustomBinding多种绑定类型。

3). Contract

契约指定并设置绑定到当前 endpoint 上的服务接口,即哪些方法被导出给客户端,方法的授权情况、消息格式等。

漏洞成因

HPSA 的系统服务 HPSupportSolutionsFrameworkService 具有 SYSTEM 权限,并开启了多个允许 everyone 账户读写的 NamePipe。这一敏感行为引起了笔者的注意,因此dump下安装包进一步分析。

反混淆反编译后进行代码审计,发现HPSA的系统服务使用WCF与Client进行交互。它创建了一个绑定在NetNamedPipeBinding(URI:”net.pipe://localhost/HPSupportSolutionsFramework/HPSA”)上的Endpoint,并允许Client调用多个绑定在此Endpoint上的服务接口:HP.SupportFramework.ServiceManager.Interfaces::IServiceInterface

HPSA 在连接建立时对 Client 进行了认证,以阻止敏感接口被恶意程序调用。Server 与 Client 的交互过程如下表所示:

在 Server 与 Client 的交互过程中,HPSupportSolutionsFrameworkService 使用了多种途径来确保安全:验证 Client 是否为 HP 签名、使用 SecureString 存储 GUID、使用 RNGCryptoServiceProvider 生成随机数、调用敏感接口时验证 Client 的 Token。

千里之堤毁于蚁穴,在看似缜密的认证逻辑中却存在安全漏洞:HPSupportSolutionsFrameworkService 使用Process.MainModule.FileName获取 Client 的文件路径,随后验证其文件签名。然而,在C#中Process.MainModule.FileName是通过调用GetModuleFileName()索引进程的 PEB (Process Environment Block)来获取模块路径的。PEB 位于进程的用户空间中,因此可以被攻击者修改替换。攻击者只需在连接 Server 的 Endpoint 前修改 PEB,使模块路径指向一个有效的 HP 签名文件即可绕过签名检测,最终通过认证。

漏洞利用

绕过 HPSA Server 的认证后,就可以调用绑定在此 Endpoint 上的服务接口函数了。接下来的工作就是从可用的服务接口函数中寻找可以利用的方法,实现权限提升。HPSupportSolutionsFrameworkService 的服务接口函数实现在HP.SupportFramework.ServiceManager.ServiceTasks::ServiceTask中,大致浏览一遍接口函数发现UncompressCabFile服务接口可以用于任意文件写,DeleteFile 服务接口可以用于任意文件删除。

UncompressCabFile 的实现逻辑如下:

public bool UncompressCabFile(string cabFilePath, string destDirectory, string token)
{
    if (!\u0004.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage("signature validation failure for UncompressCabFile", DebugLog.IndentType.None);
        }
        return false;
    }

    if (!File.Exists(cabFilePath))
    {
        return false;
    }

    if (!Validation.VerifyHPSignature(cabFilePath))
    {
        File.Delete(cabFilePath);
        return false;
    }

    string text = "\"" + cabFilePath + "\"";
    string text2 = "\"" + destDirectory + "\"";
    ProcessStartInfo processStartInfo = new ProcessStartInfo();
    processStartInfo.set_WindowStyle(1);
    processStartInfo.set_Arguments("-qq " + text + " -d " + text2);
    processStartInfo.set_FileName(SupportAssistantCommon.FrameworkPath + "Modules\\unzip.exe");
    Process process = new Process();
    process.set_StartInfo(processStartInfo);
    process.Start();
    process.WaitForExit();

    if (File.Exists(cabFilePath))
    {
        File.Delete(cabFilePath);
    }
    return true;
}

UncompressCabFile 利用 unzip.exe 将压缩文件 cabFilePath 解压至 destDirectory,在解压前首先验证了 cab 文件的签名。由于在签名验证和解压缩之间存在时间差,因此这里存在 TOCTTOU(Time of Check To Time of Use)问题,可以利用条件竞争绕过签名检测将文件写入任意目录,最终可以实现权限提升。

DeleteFile 的实现逻辑如下:

public void DeleteFile(string filePath, string token)
{
    if (\u0007.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        try
        {
            File.Delete(filePath);
            return;
        }
        catch (Exception ex)
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("exception in DeleteFile: " + ex.Message, DebugLog.IndentType.None);
            }
            return;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("token not valid in DeleteFile", DebugLog.IndentType.None);
    }
}

因此利用过程如下所述:

  1. 修改PEB,将进程路径指向合法的HP签名程序

  2. 通过反射机制获取HP.SupportFramework.ServiceManager.Interfaces命名空间中 ServiceInterface 类的get_Instance()方法

  3. 实例化 ServiceInterface

  4. 调用ServiceInterface::UncompressCabFile服务接口,结合条件竞争实现权限提升

补丁实现和绕过1

漏洞报告后 HP PSRT 快速响应,并在半个月内通过邮件告知已经发布了新版来解决此安全漏洞。4月初,再次分析后发现新版本的 HPSA 依旧在使用 everyone 可写的 NamePipe,笔者决定针对 HP 的修复再次分析。

通过短暂的逆向分析,定位了补丁修复位置。补丁在HP.SupportFramework.ServiceManager.Interfaces::ServiceInterface::get_Instance()中添加了如下逻辑:

StackFrame stackFrame = new StackFrame(1);
MethodBase method = stackFrame.GetMethod();
Type declaringType = method.get_DeclaringType();
string name = method.get_Name();

if (name.ToLowerInvariant().Contains("invoke"))
{
    string text2 = new \u0007().\u0001(Process.GetCurrentProcess());
    text2 = Uri.UnescapeDataString(Path.GetFullPath(text2));
    string text3 = Assembly.GetEntryAssembly().get_Location();
    text3 = Uri.UnescapeDataString(Path.GetFullPath(text3));
    if (text3.ToLowerInvariant() != text2.ToLowerInvariant())
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage(string.Concat(new string[]
            {
                "Illegal operation. Calling process (",
                text3,
                ") is not the same as process invoking method  (",
                text2,
                ")"
            }), DebugLog.IndentType.None);
        }
        throw new Exception("Invoking methods is not allowed.");
    }
}

namespace \u0007
{
    // Token: 0x02000081 RID: 129
    internal sealed class \u0007
    {
        internal string \u0001(Process \u0002)
        {
            try
            {
                string result = \u0002.get_MainModule().get_FileName();
                return result;
            }
            …
        }
        …
    }
}

以上代码在实例化时,首先通过Assembly.GetEntryAssembly().get_Location()获取 Client 的文件路径,并与通过Process.MainModule.FileName方法获取的 Client 模块路径进行对比,如果不一致则抛出异常。

.Net的运行时环境规定,拥有同样标识的.Net程序集只能被加载一次。由于HP.SupportFramework.ServiceManager.dll已经被 HPSupportSolutionsFrameworkService 加载,所以 HP 的开发人员认为此举可以有效阻止攻击者通过修改 PEB,并利用反射机制创建 ServiceInterface 来绕过认证。

然而,HP 的.Net开发人员显然是忽视了进程空间的安全边界。此处所做的检测仍然位于 Client 进程空间,如同修改 PEB 那样, Client 依旧拥有权限修改进程空间内的数据和代码。Client 可以采取多种方案绕过检测:

  1. 在实例化前,定位并修改HP.SupportFramework.ServiceManager.dll中的检测逻辑;

  2. 自己实现与 Server 的交互,认证,服务接口调用等;

  3. 静态 Patch 检测逻辑,并修改程序集HP.SupportFramework.ServiceManager.dll的标识,使修改后的文件可以被加载进 Client 进程空间。

其中方案3最为简洁,这里可以直接利用工具修改其判断逻辑为 if (text3.ToLowerInvariant() == text2.ToLowerInvariant()),并修改程序集的版本号(微软官方文档中描述了影响.Net可执行程序标识的属性包括:AssemblyCultureAttribute, AssemblyFlagsAttribute, AssemblyVersionAttribute [3])。最终实现对补丁的绕过,重新实现权限提升。

补丁实现和绕过2

又一次,将漏洞和修补方案报告给 HP PSRT 后,HP 的开发人员从两个方面做了修补:

  1. 对 Client 的认证方式做调整,Server不再使用Process.MainModule.FileName获取Client的文件路径,而是通过GetProcessImageFileName()来获取,避免从PEB获取到被篡改的Client文件路径。

  2. 在 UncompressCabFile 和 DeleteFile 中,检查了参数里的文件/目录路径是否合法。

查看 UncompressCabFile 和 DeleteFile 里的文件/目录路径检测逻辑,发现其仅仅使用了字符串比较来检测路径是否合法,而不是对规范化后的路径进行检测。代码如下:

internal static bool \u0001(string \u0002)
{
    string[] array = new string[]
    {
        "AppData\\Local\\Hewlett-Packard\\HP Support Framework",
        Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\Hewlett-Packard\\HP Support Framework",
        SupportAssistantCommon.MainAppPath,
        SupportAssistantCommon.FrameworkPath
    };
    string[] array2 = array;

    for (int i = 0; i < array2.Length; i++)
    {
        string text = array2[i];
        if (\u0002.ToLowerInvariant().Contains(text.ToLowerInvariant()))
        {
            return true;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
    }
    return false;
}

因此这里使用目录穿越即可绕过路径检查。对 Client 的认证也很容易绕过,使用 Hewlett-Packard 安装目录里任意一个拥有有效签名的程序,将漏洞利用代码注入其中即可绕过对 Client 的认证检测。

最终,HP PSRT 修正了路径检测的逻辑,增加了对目录穿越行为的检测,相关代码如下所示:

    internal static bool \u0002(string \u0002)
    {
        if (!Path.IsPathRooted(\u0002) || \u0002.StartsWith("\\") || \u0002.Contains("..") || \u0002.Contains(".\\"))
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
            }
            return false;
        }
        return true;
    }

笔者在漏洞细节中建议 HP PSRT 彻查所有服务接口的安全性,对其参数进行正确的检测,以免再次被攻击者利用。

总结

安全漏洞会在软件生命周期(需求分析、设计、实现、维护等过程)内的各个阶段被引入,研发人员除了需要在设计和实现阶段避免安全漏洞外,还需要在出现漏洞后运用合理的修补方案。这里 HPSA 出现的问题就是在设计、实现、维护阶段共同引入的。

1). 设计阶段

也许是为了保证未签名程序也可以调用服务端的非敏感接口(例如 DecryptFile, DeleteTempSession 等未验证 Client 身份的服务接口),又或许是为了让 Guest 用户也可以对系统进行更新等操作。最终导致 HPSA 没有利用系统提供的访问权限检查机制[2]来隔离权限边界,使得软件从设计之初就引入安全风险。

2). 实现阶段

HPSA 的开发人员未意识到通过Process.MainModule.FileName获取 Client 文件路径的不安全性,从而导致认证可以被绕过;也未意识到敏感服务接口的危险性,未对敏感服务接口的参数的合法性进行正确检测,从而导致可以被攻击者用于权限提升。事实上,任何试图通过进程对应的文件来检查进程安全性的做法都是存在安全隐患的。

3). 维护阶段

在对一个漏洞的三次修补过程中,HPSA的开发人员更是忽视了进程的安全边界,使用了多种错误的修补方案,导致补丁被多次绕过。

从这个漏洞的成因和多次修补可以看出,HP的开发人员存在对所用技术理解不到位,缺乏安全编程经验的问题。希望这篇文章能给研发人员带来安全编程的思考和经验的提升,不在设计、实现、维护阶段发生类似HPSA这样的一系列错误。

Timeline

Reference

  1. Windows Communication Foundation Security
    https://msdn.microsoft.com/en-us/library/ms732362(v=vs.110).aspx

  2. Authentication and Authorization in WCF Services – Part 1
    https://msdn.microsoft.com/en-us/library/ff405740.aspx

  3. Setting Assembly Attributes
    https://msdn.microsoft.com/en-us/library/4w8c1y2s(v=vs.110).aspx


源链接

Hacking more

...