从一个补了三次的漏洞看WCF的安全编程

背景

笔者在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的交互过程如下表所示:

Timeline Client Server
0 创建 Endpoint
1 实例化HP.SupportFramework.ServiceManager.Interfaces命名空间中ServiceInterface类, 创建一个随机GUID, 将其作为参数调用绑定在 Endpoint 上的 StartClientSession(string guid); StartClientSession(string guid)接收Client传递过来的随机GUID, 创建命名管道HPSA_guid, 等待Client连接
2 连接Server创建的命名管道HPSA_guid, 进行身份“认证”
3 通过GetNamedPipeClientProcessId()获取Client的PID, 通过Process.get_MainModule().get_FileName();获取进程路径, 对Client的签名进行验证
4 若签名验证通过, 生成一个ClientId(随机数)和一个Token(GUID), 并将ClientId、Token、CallerName保存到内部维护的“Client对象链表”中, 最后将随机数ClientId发送给Client。关闭命名管道
5 接收到Server返回的ClientId(随机数)后, 将其保存在ServiceInterface.ClientInfo.ClientId中。关闭命名管道
6 调用绑定在 Endpoint上的 GetClientToken(ServiceInterface.ClientInfo.ClientId), 通过ClientId从Server处获取Token, 转换成SecureString, 保存在ServiceInterface.ClientInfo.Token中, Token将作为临时身份令牌使用 GetClientToken(int clientId) 接收Client传过来的ClientId, 从“Client对象链表”中索引其对应的Token(secureString形式的GUID)并返回
7 随后Client即可使用随机数Token作为参数, 调用绑定在StartClientSession上的其它敏感服务接口 绑定在Endpoint上的敏感操作, 例如DeleteFile(string filePath, string token), 在收到Client发送来的Token时, 通过遍历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

  • 11/30/2016    Provide vulnerability details and PoC to HP Inc. via hp-security-alert@hp.com
  • 12/02/2016    HP Inc. responded that they had validated and opened case PSR-2016-0118 for the issuse
  • 12/13/2016    HP Inc. released a fix for the reported issue
  • 01/04/2017    HP Inc. responded that the vulnerability was fixed
  • 01/05/2017    Ask for more infomation
  • 01/14/2017    HP Inc. responded that they are still investigating
  • 02/03/2017    HP Inc. responded that this issue can be automatically resolved, thus they don’t issue security bulletin and CVE numbers
  • 04/20/2017    Report the patch can be bypass. Provide vulnerability details and PoC to HP Inc.
  • 04/20/2017    HP Inc. responded that they had validated and opened case PSR-2017-0056 for the issuse
  • 05/29/2017    HP Inc. responded that the fixed version will be released in mid-June 2017
  • 06/07/2017    HP Inc. published a new patch and asked me confirm the vulnerability doesn’t exist
  • 06/07/2017    Report the patch can be bypass again. Provide vulnerability details and PoC to HP Inc. Also provide some repair advices.
  • 06/15/2017    HP Inc. published a new patch and asked me confirm the vulnerability doesn’t exist
  • 06/15/2017    Confirm the patch is valid. And recommend HP Inc. make sure there no other vulnerable functions can be exploited now, nor will be in the future.
  • 08/31/2017    HP Inc. published a security bulletin (https://support.hp.com/sk-en/document/c05648974) and issued a CVE (CVE-2017-2744).

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