Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 1)(译文)

tang2019 2022-03-23 11:49:00

简介

浏览器的漏洞利用技术,已经困扰我相当一段时间了。不久前,我写了一篇文章,介绍了Internet Explorer旧版本中一个非常微不足道的UAF漏洞。这让我渴望了解更多,因为在这个案例中ASLR是不成问题的。此外,随着MEMGC的出现,DOM中的UAF漏洞实际上得到了缓解。其他的缓解措施,如控制流保护(CFG),在这个案例中还没有提供。

为了了解更现代的浏览器的漏洞利用(特别是基于Windows的漏洞利用),我开始通过互联网查找相关资源。我一直想就这个话题学点东西,但始终没有下笔——因为我觉得自己还没有“弄明白”。这是由于多种因素造成的,包括浏览器漏洞利用本身是一个非常复杂的问题,同时,该主题的研究资料也不是很丰富。虽然我自己对系统内核已经比较熟悉,但浏览器对我来说还是一个相对陌生的领域。

此外,我几乎没有找到关于更“现代”的漏洞利用资源,比如专门攻击Windows系统上的实时(JIT)编译器。不仅如此,几乎所有在线可用的资源都以Linux操作系统为目标。从浏览器原语的角度来看,这很好。然而,当涉及到诸如CFG之类的安全组件时,以及实际的漏洞利用原语之类的东西时,这可能高度依赖于操作系统。对于一个只专注于Windows系统的人来说,的确让人很头痛。

我最近偶然发现了两个资源:第一个是Google Project Zero报告的一个安全漏洞(我们将在本文中利用该漏洞),即CVE-2019-0567。此外,我在Perception Point网站上发现了一篇关于CVE-2019-0539的"姊妹"漏洞(该漏洞也是由Project Zero报告的)的精彩文章。

虽然Perception Point网站上面的那篇文章的确很棒,但我觉得它更适合已经对浏览器中的漏洞原语相当熟悉的人阅读——如果您以前做过任何类型的浏览器漏洞研究,我将强烈推荐您阅读那篇文章。然而,对于像我这样从未接触过浏览器领域的JIT编译器漏洞研究的人来说,我必须先恶补相关知识:如何实现读/写原语;至于如何代码执行,我们将在后续文章中介绍。

此外,我们还需要其他方面的前提知识,比如为什么JIT编译一上来就会出现攻击面?JavaScript对象在内存中是如何布局的?由于JavaScript的值通常是32位的,如何利用它来实现64位的漏洞利用?在存在DEP、ASLR、CFG、ACG、无子进程以及Edge中的其他缓解措施的情况下,我们如何在获得读/写原语后实现代码执行?这些都是我需要答案的问题。为了分享我是如何解决这些问题的,也为了那些也想进入浏览器漏洞利用领域的读者,我将通过三篇文章专门介绍浏览器的漏洞利用。

第一篇文章(也就是本文)包含下列主题:

  • 浏览器的漏洞利用环境的配置和搭建
  • 理解JavaScript对象及其在内存中的布局(Chakracore/Chakra)
  • CVE-2019-0567漏洞的根因分析,并揭开JIT编译器中类型混淆漏洞的神秘面纱

第二篇文章将包括下列主题:

  • 通过ChakraCore实现从系统崩溃到exploit(并在此过程中处理ASLR、DEP和CFG等防御机制的绕过方法)
  • 实现代码执行
  • 第三篇文章将包括下列主题:

  • 将exploit移植到Microsoft Edge(基于Chakra引擎的Edge浏览器)

  • 使用现已修补的CVE漏洞绕过ACG防御机制
  • 在Edge中实现代码执行

此外,我们还应该注意下列限制:

  • 在这个系列文章中,我们将不得不绕过ACG机制。我们利用的绕过漏洞已经在Windows10 RS4中得到缓解。
  • 此外,我们还需了解Intel Control-Flow Enhancial Technology(CET),这是一种现在已经面世的缓解措施(尽管它尚未得到广泛采用)。我们针对的Edge版本尚未采用CET防御机制。
  • 我们的初步分析将通过ch.exe应用程序来完成,这是一个ChakraCore shell。简单来说,它实际上就是一个命令行JavaScript引擎,可以直接执行JavaScript代码(就像浏览器一样)。我们可以将其视为浏览器的“渲染”组件,但它不支持图形的渲染。也就是说,在ch.exe中发生的任何事情都可能在Edge浏览器本身(基于Chakra引擎的Edge浏览器)中发生。正如我们将在第三篇文章中看到的那样,我们最终的漏洞利用代码将在Edge浏览器本身中“引爆”。同时,ch.exe还是一款非常强大和有用的调试工具。
  • 无论是Chakra引擎,还是其开源的孪生ChakraCore引擎,Microsoft Edge都已经不再推荐使用。Edge浏览器现在使用的是V8 JavaScript引擎,同时,基于Chrome的浏览器也在使用该引擎。

最后,从漏洞利用的角度来看,如果没有Bruno Keith之前关于Chakra引擎的开发原语、Project Zero或Perception Point提供的优秀报告,这篇文章就不可能与读者见面。

配置Chakra/ChakraCore环境

所谓Chakra,就是在使用V8引擎之前,Edge浏览器所使用的“Microsoft专有”JavaScript引擎的名称。该引擎的“开源”变体被称为Chakracore。本文中,我们将会使用ChakraCore引擎,因为其源代码是公开的。实际上,CVE-2019-0567漏洞能同时影响这两个“版本”,最后,我们会把exploit移植到真正的目标,即Chakra/Edge浏览器环境下面。

在本系列的前两篇文章中,我们将使用开源版本的Chakra引擎(ChakraCore JavaScript引擎 + ch.exe shell)进行漏洞分析。在第三篇文章中,我们将使用标准的Microsoft Edge浏览器和Chakra JavaScript引擎完成漏洞利用的演示。

所以,我们可以实现“一石二鸟”:对于我们的环境,首先需要包含一个未使用V8引擎的Edge版本,以及一个没有为CVE-2019-0567(类型混淆漏洞)或CVE-2017-8637(ACG绕过原语)漏洞打过补丁的Edge版本。通过查看Microsoft提供的CVE-2019-0567漏洞通告,我们可以发现该漏洞对应的补丁程序是KB4480961。关于CVE-2017-8637漏洞的安全通告,请参考https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2017-8637。本例中适用的补丁程序是KB4034674。

我们需要解决的第二只“鸟”是处理Chakracore。

Windows 10 1703是一个64位的Windows版本,它不仅可以支持ChakraCore,而且(默认情况下)还会安装提前打好补丁的Edge版本。因此,对于本文来说,我们需要做的第一件事,就是在虚拟机中安装Windows 10 1703(并且不要通过服务包打补丁)。同时,请禁用自动更新。至于如何获得这个版本的Windows,那就要读者自己发挥主观能动性了。

如果读者没有找到Windows 10 1703 版本,另一个选择就是无视Edge或Windows的版本:因为我们将使用ch.exe(即ChakraCore shell),以及ChakraCore引擎来分析和利用漏洞。在第二篇文章中,我们将通过ch.exe来开发漏洞利用代码。在第三篇文章中,我们的内容则是围绕Microsoft Edge展开的。对于对安装Edge不感兴趣的读者,请移步第二篇,那里有关于exploit开发过程的具体介绍。然而,请注意,Edge本身也提供了许多缓解措施,这为漏洞的利用带来了很大的障碍。正因为如此,我强烈建议读者按顺序阅读这三篇文章,以便跟上笔者的思路。然而,在ch.exe环境和Edge环境中,漏洞利用的基本原理是相同的。

在安装完Windows 10 1703虚拟机后(硬盘至少100GB),我们的下一步是安装ChakraCore引擎。首先,我们需要在Windows机器上安装git。实际上,最简单的方法是,通过PowerShell快速安装Scoop.sh,然后在PowerShell提示符中执行scoop install git命令。为此,首先需要以管理员身份运行PowerShell,然后执行以下命令:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser(然后输入a,表示“全部同意”。)
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
    scoop install git 

1.png

安装完git后,还需要下载微软的Visual Studio。通常来说,Visual Studio 2017就不错,其下载链接为https://aka.ms/vs/15/release/vs_Community.exe。下载后,只需简单配置Visual Studio,使其安装Desktop development with C++环境,并保留所有相应的默认值。

1.png

git和Visual Studio安装完毕后,我们可以继续安装ChakraCore。ChakraCore是一个成熟的JavaScript环境,提供了运行时等功能,所以,这个软件教大,在克隆存储库时可能需要几秒钟的时间。下载后,请打开cmd.exe窗口,并执行以下命令:

    cd C:\Wherever\you\want\to\install
    git clone https://github.com/Microsoft/ChakraCore.git
    cd ChakraCore
    git checkout 331aa3931ab69ca2bd64f7e020165e693b8030b5 (这是与该漏洞相应的提交的哈希值)

1.png

下载ChakraCore并“签出”易受攻击的提交后,我们需要将ChakraCore配置为编译时启用控制流防护(CFG)功能。为此,请转到ChakraCore文件夹,并打开Build目录。在那里,可以找到Visual Studio解决方案文件。然后,双击该文件,并选择“Visual Studio 2017”(这不是“必需”步骤,但我们希望添加 CFG作为最终必须绕过的缓解措施!)

1.png

请注意,当Visual Studio打开时,它会提示使用帐户登录。您可以通过告诉Visual Studio稍后执行此操作来绕过这一要求,这样的话,您将获得30天的不受限制的访问权限。

在Visual Studio窗口的顶部,请选择“x64”,并将“Debug”选项保持不变。

1.png

选择“x64”后,单击“Project > Properties”以配置ChakraCore属性。在这里,请选择“ C/C++ > All Options”选项,然后启用“Control Flow Guard”。然后,依次单击“Apply”和“Ok”按钮。

1.png

然后,从菜单栏单击“File > Save All”选项,以保存对解决方案的所有更改。

1.png

接下来,我们需要打开x64 Native Tools Command Prompt for VS 2017窗口。为此,请单击Windows键,并键入x64 Native Tools命令。

1.png

最后,我们需要通过执行以下命令来实际构建项目:msbuild/m/p:platform=x64/p:configuration=debug build\chakra.core.sln(请注意,如果不使用x64 Native Tools Command Prompt for VS 2017窗口的话,将无法使用msbuild命令)。

1.png

完成上述步骤后,就应该已在机器上安装好了ChakraCore。为了验证这一点,我们可以打开一个新的cmd.exe提示符窗口,并执行以下命令:

    cd C:\path\to\ChakraCore
    cd Build\VcBuild\bin\x64_debug\
    ch.exe --version

1.png

我们可以清楚地看到,ChakraCore shell正在运行,ChakraCore引擎(ChakraCore.dll)已经出现!现在,我们已经安装好了Edge和ChakraCore,接下来,我们开始考察JavaScript对象在Chakra/ChakraCore的内存中的布局,以便为后面的漏洞利用打好基础!

JavaScript对象:Chakra/Chakracore版

要想理解现代漏洞(如类型混淆漏洞),首先要搞清楚JavaScript对象在内存中的布局。我们知道,在像C这样的编程语言中,数据类型是显式指定的,比如int var和char* string,其中第一个变量是整数,第二个变量是字符数组或char类型。然而,在ChakraCore中,对象可以这样声明:var a={o:1,b:2}或a=“testing”。当没有显式指定数据类型时,JavaScript是怎么知道该如何处理/表示内存中的给定对象的呢?这实际上是ChakraCore引擎的工作:确定正在使用的对象的类型以及如何正确地进行更新和管理。

这里提供的所有关于JavaScript对象的信息都来自这个博客,它是由Chakra引擎的开发者撰写的,具体地址为http://abchatra.github.io/Type/。虽然这篇文章同时介绍了“静态”和“动态”对象,但我们关注的重点在于ChakraCore引擎是如何管理动态对象的,因为静态对象非常简单,这里就不做介绍了。

那么,什么是动态对象?动态对象就是任何不能用“静态”对象表示的对象(静态对象由数字、字符串和布尔值等数据类型组成)。例如,以下对象将在ChakraCore中表示为动态对象:

let dynamicObject = {a: 1, b:2};
dynamicObject.a = 2;            // Updating property a to the value of 2 (previously it was 1)
dynamicObject.c = "string";     // Adding a property called c, which is a string

print(dynamicObject.a);         // Print property a (to print, ChakraCore needs to retrieve this property from the object)
print(dynamicObject.c);         // Print property c (to print, ChakraCore needs to retrieve this property from the object)

您可能已经明白为什么它们被视为动态对象,而不是静态对象了。它们不仅涉及到两种数据类型(属性a是数字,属性c是字符串),而且它们被存储为对象中的属性(请回想一下C语言中的结构体)。由于无法解释属性和数据类型的所有可能组合,因此,ChakraCore提供了一种“动态”处理这些情况的方法(类似于“动态对象”)。

ChakraCore引擎必须对这些对象进行区别对待,比如说,let a=1,它就是一个静态对象。这种动态对象在内存中的“处理”和表示正是我们当前关注的重点。说了这么多,对象的内存布局到底是什么样子呢?下面通过几个例子来一探究竟。

下面是些JavaScript代码,我们将通过调试器来查看它们在内存中的布局:

print("DEBUG");
let a = {b: 1, c: 2};

现在,我们将上述代码保存到一个名为test.js的脚本中,并在ch.exe中的函数ch!WScriptJsrt::EchoCallback上设置一个断点。由于EchoCallback函数负责print()操作,这就相当于在ch.exe中设置一个断点,在每次print()被调用时中断运行(是的,我们就是用这个print语句来帮助调试)。设置断点后,我们可以恢复执行,并在EchoCallback处中断。

1.png
1.png

现在,我们已经命中该断点,我们知道在这一点之后发生的任何事情都应该涉及到test.js中print()语句之后的JavaScript代码。我们这样做的原因是,我们要检查的下一个函数在后台不断被调用,我们希望确保我们只是检查与我们的对象创建相对应的特定函数调用(接下来),以便在内存中检查它。

现在我们已经到达了EchoCallback断点,现在需要在chakracore!Js::DynamicTypeHandler::SetSlotUnchecked上设置一个断点。注意,chakracore.dll不会在ch.exe执行时被加载到进程空间,而只会在上次执行时被加载。

1.png

一旦命中chakracore!Js::DynamicTypeHandler::SetSlotUnchecked处的断点,就可以检查我们的对象了。由于我们也在本地构建了ChakraCore,所以,我们可以访问源代码。对于WinDbg和WinDbg Preview来说,都能在执行这个函数时提供相应的源代码。

1.png

这段代码可能看起来有点令人困惑。不过,这并不是问题!实际上,我们只要知道这个函数是负责为动态对象填充所需的属性值即可(在本例中,就是在test.js中通过a.b和a.c提供的值)。

现在,我们当前处理的对象位于RCX寄存器中(根据__fastcall,我们知道RCX是源代码中的参数DynamicObject * instance)。这一点可以在下图中看到。由于函数还没有执行,RCX中的这个值目前只是一个空白的“骨架”a:一个等待被填充的对象。

由于我们要在对象a中设置两个值,所以,需要执行这个函数两次。为此,让我们先在调试器中保存RCX,然后在WinDbg中执行一次g,这将设置第一个值,然后我们再执行一次该函数,但这次要在函数返回前通过pt命令中断执行,这样我们就可以检查对象的内容了。

1.png

太好了。在执行了两次函数之后,且在函数返回之前,让我们检查一下之前在RCX中保存的内容(我们的对象a)。

1.png

我们首先要注意的是,这似乎是某种类型的“结构体”,最前面的0x8字节包含一个指向DynamicObject虚函数表(vftable)的指针。接下来的0x8字节似乎是我们当前正在其中执行的同一地址空间中的某个指针。在此之后,可以看到值1和2位于前面提到的指针之后的0x8和0x10字节(以及从我们的“结构体”的实际开始处的0x10/0x18字节)。我们的值中的1看上去好像是随机的,这一点稍后详细介绍。

回想一下,对象a有两个属性:b(设置为1)和c(设置为2)。它们是“内联”声明和初始化的,这意味着这些属性在实际被实例化的对象的同一行中被赋值(例如a={B:1,C:2})。具有内联属性的动态对象(就像我们的例子一样)具体如下所示:

1.png

请注意,属性值以0x10为偏移量写入动态对象。

如果我们将这个原型与WinDbg中的值进行比较,我们可以确认这是一个具有内联属性的动态对象!这意味着vftable后面的前一个看似“随机”的指针实际上是数据结构的地址,在Chakracore中它被称为type。从漏洞利用的角度来看,type对我们来说并不太重要,只是我们应该知道这个地址包含关于对象的数据,例如知道属性存储在哪里、TypeId(供ChakraCore用来确定该对象是否是字符串、数字等)、JavaScript库的指针和其他信息。当然,所有这些信息都可以在ChakraCore代码库中找到,其地址为https://github.com/chakra-core/ChakraCore/blob/master/lib/Runtime/Types/Type.h#L26。

现在,让我们回头讨论一下为什么我们的属性值在高位32位(001000000000001)中有一个随机的1。上面32位中的1用于“标记”一个值,以便在Chakracore中将其标记为整数。在Chakracore中,任何以00100000为前缀的值都是整数。这怎么可能呢?这是因为ChakraCore和大多数JavaScript引擎只允许使用32位值,不包括指针(想想整数、浮点等)。然而,通过指针表示的对象的一个例子是字符串,就像在C语言中那样,字符串是由指针表示的字符数组。另一个例子是声明ArrayBuffer或其他JavaScript对象的时候,这些对象也可以用指针表示。

由于只使用64位值的低32位(因为我们是在64位计算机上),所以高32位(更具体地说,实际上只使用高17位)可以用于其他方面,例如这个“标记”过程。不要想太多,即使现在没有搞明白,那也没什么。现在,我们只需知道JavaScript(在ChakraCore中)使用高17位来保存关于对象的数据类型(在本例中是动态对象的proerty)的信息,但是不包括前面所说的由指针表示的类型。这个过程实际上被称为“nan-boxing”,这意味着64位值的高17位(请记住我们是在64位系统上)被保留用于提供关于给定值的类型信息。任何没有在高17位中存储信息的64位值都可以被视为指针。

现在,让我们更新test.js,看看不使用内联属性时对象长啥样:

print("DEBUG");
let a = {};
a.b = 1;
a.c = 2;
a.d = 3;
a.e = 4;

我们在这里要做的是在WinDbg中重启应用程序,清除第二个断点(即设在chakracore!Js::DynamicTypeHandler::SetSlotUnchecked上的断点),然后让print()函数再次发生中断。

1.png
1.png

在命中print()断点后,我们将重新在chakracore!Js::DynamicTypeHandler::SetSlotUnchecked上设置断点,恢复执行以击中该断点,检查RCX(如果我们记得上次调试的对象,我们的动态对象应该在这里),并执行SetSlotUnchecked函数以检查属性值是否得到更新。

1.png

现在,根据我们上次的调试情况来看,这应该就是我们的对象保存在RCX中的地址。然而,看一下本例中的vftable,我们可以看到它指向GlobalObject vftable,而不是DynamicObject vftable。这说明断点被命中了,但这并不是我们创建的对象。我们可以简单地在调试器中再次点击g,看看下一个调用是否会作用于我们的对象。要弄清楚这一点,只需在RCX中查看vftable是否来自DynamicObject,这只是一个不断试错的过程。另一种识别这是否是我们的对象的好方法,是看对象中除了vftable和type之外的其他内容是否都被设置为0。这可能表明这是新分配的内存,并没有作为一个“完整的”动态对象来设置属性值。

然后,再次按下g键,我们可以看到现在我们已经找到了我们的对象。现在,我们会看到vftable和type之外的所有内存都已经初始化为0,因为我们的属性值还没有被设置。

1.png

这里我们可以看到一个稍微不同的布局。在我们的第一个“内联”属性中,上次其值为1,现在我们却在与type相同的地址空间中看到了另一个指针。检查这个指针,我们可以看到其值是0。

1.png

让我们在WinDbg中再次按g键,以再次调用chakracore!Js::DynamicTypeHandler::SetSlotUnchecked,看看我们的第一个值(1)被写入对象后,这个对象看起来长啥样:

1.png

太有意思了! 这个指针,位于type之后(之前存放我们的“内联”动态对象的值的地方),似乎存放的是我们的第一个值a.b=1!

让我们再执行两次g命令,看看我们的值是否总是写入这个指针。

1.png

我们可以清楚地看到,这次我们的值并没有直接存储到这个对象中,而是存储在type下面的一个指针中。这个指针实际上是一个数组的地址,在ChakraCore中被称为auxSlots。auxSlots是一个数组,用来保存对象的属性值,从auxSlots[0]开始保存第一个属性值,auxSlots[1]保存第二个,以此类推。下面是它在内存中的布局:

1.png

这与我们之前的“内联”动态对象的主要区别是,现在的属性是通过一个数组来引用的,而不是直接在对象“主体”中引用。然而,请注意,无论动态对象使用的是auxSlots数组还是内联属性,两者在动态对象中的偏移量都是0x10(第一个内联属性值从dynamic_object+0x10处开始,而auxSlots也是从偏移量0x10处开始)。

实际上,ChakraCore代码库的DynamicObject.h头文件的注释中提供了包含这些信息的图表。

1.png

然而,我们没有谈到上图中的“第2种情况”。在第2种情况下,动态对象不仅具有保存属性值的auxSlots数组,而且还具有直接在对象中设置的内联属性。在这里,虽然我们不会通过它来发动攻击,但如果一个对象开始时只有几个内联属性,后来又增加了其他的值,那么,这时就可以用来发动攻击,例如:

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

因为我们以内联方式声明了一些属性,并且在次之后还声明了一个属性值,所以,将会有内联存储的属性值组合,也会存储在auxSlots数组中。再次声明,我们不会将这种内存布局用于我们的目的,指出这一点是为了保持连续性,并表明它是可能的。

CVE-2019-0567: 一个基于浏览器的类型混淆漏洞

我们已经为读者介绍了JavaScript对象及其在内存中的布局,并配置了相应的漏洞开发环境,现在让我们把这些理论付诸实践。

让我们先在ch.exe中执行以下JavaScript代码。为此,请将以下JavaScript代码保存在一个名为poc.js的文件中,并运行以下命令:ch.exe C:\Path\to\poc.js。请注意,下面的概念验证代码来自Google Project Zero,读者可以在这里找到:https://bugs.chromium.org/p/project-zero/issues/detail?id=1702。请注意,这里有两个PoC,我们使用的是后一个。

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x1234);

    print(o.a);
}

main();

1.png

从上图中我们可以看到,当我们的JavaScript代码执行时,发生了访问冲突!这可能是由于访问了无效的内存。让我们再次执行这个脚本,但这次附加到 WinDbg。

1.png

执行脚本时,我们可以看到与访问冲突有关的违规指令。

1.png

由于ChakraCore是开源的,我们也可以看到相应的源代码。

1.png

接着,让我们看一下与崩溃相关的反汇编代码。

1.png

我们可以清楚地看到一个无效的内存地址(在本例中为0x1234)正在被访问。显然,我们可以作为攻击者控制这个值,因为它是由PoC提供的。

我们还可以看到通过[rax+rcx*0x8]引用了一个数组。这一点不难理解,因为我们可以在源代码中看到一个auxSlots数组(我们知道这是一个管理动态JavaScript对象的属性值的数组)正在被索引。即使我们没有源代码,这个汇编过程也表示一个数组索引。在这种情况下,RCX将包含数组的基址,其中RAX是数组的索引。将值乘以64位地址的大小(因为我们在 64 位机器上),就能通过索引得到对应的地址,而不仅仅是索引base_address+1、base_address+2等。

在前面的反汇编代码中,我们可以看到RCX中的值(应该是数组的基址)来自值RSP+0x58。

1.png

让我们仔细检查这个地址。

1.png
1.png

这个“结构体原型”看着眼熟吗?在这里,我们不仅可以看到DynamicObject的虚函数表,还有一个type指针,此外,还可以看到我们在poc.js脚本中提供的一个属性的值,即0x1234!下面,让我们将看到的内容与脚本实际执行的内容进行对照。

首先,它创建了一个循环,执行opt()函数2000次。此外,它还创建了一个名为o的对象,并设置了属性a和b(分别设置为1和2)。这个对象与{}的两个空值一起传递给opt()函数,对应代码为opt(o, {}, {})。

    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

其次,函数opt()实际上被执行了2000次,即opt(o, {}, {})。opt()函数的内部代码如下所示:

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

让我们先看看opt()函数内部发生了什么。

当opt(o, {}, {})被执行时,在opt()函数的第一行,会将第一个参数,即对象o(在每个函数调用之前被创建为let o = {a: 1, b: 2};)的属性b设置为1(o.b = 1;)。在这之后,tmp(本例中是一个函数)的原型被设置为由proto提供的值。

在JavaScript中,原型是可以分配给函数的内置属性。它的目的是为JavaScript提供一种方法,以便在后期为一个函数添加新的属性,这些属性将在该函数的所有实例中共享。如果这听起来很让人困惑,请不要担心,我们只需要知道原型是一个内置的属性,可以归属于一个函数即可。本例中的函数被命名为tmp。

    重要的是执行tmp = {__proto__: proto}; 与执行tmp.prototype = proto.prototype的效果是一样的 

当opt(o, {}, {})被执行时,我们为该函数提供了两个NULL值。由于调用方提供的proto被设置为NULL值,所以tmp函数的prototype属性被设置为0。当这种情况发生在JavaScript中时,将在没有原型的情况下创建相应的函数(本例中为tmp)。本质上,opt函数所做的全部工作如下:

  • 设置对象o的(由调用方提供)a和b属性
  • 属性b被设置为1(在通过let o = {a: 1, b: 2}创建o对象时,它最初的值是2)
  • 一个名为tmp的函数被创建,它的prototype属性被设置为0,这意味着在没有原型的情况下创建了函数tmp
  • o.a被设置为调用方通过value参数提供的值。由于我们是以opt(o, {}, {})的形式执行该函数,所以o.a属性也将是0

上面的代码被执行了2000次。这样做的目的是让JavaScript引擎知道opt()已经成为一个所谓的“热”函数。所谓“热”函数,就是一个被JavaScript识别为不断被执行的函数(在本例中是2000次)。这就指示ChakraCore让这个函数经历一个称为即时编译(JIT)的过程,在这个过程中,上述JavaScript被从解释代码(基本上是字节代码)转换为实际编译后的机器代码,例如C.exe二进制文件。这样做是为了提高性能,这个函数就不必在每次执行时都要经过解释过程(这已经超出了本文的范围)。我们过一会儿再来讨论这个问题。

在opt()被调用2000次后(这也意味着opt在以后的函数调用中继续被优化),会发生以下情况。

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);

出于连续性的目的,让我们再次显示opt函数:

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

看一下第二段代码(不是上面的opt()函数,而是上面将opt()调用为opt(o, o, 0x1234)的那段代码)。我们可以看到,它一上来就再次声明了一个对象o。请注意,对象o是以内联属性的方式声明的。我们知道,它将在内存中被表示为一个动态对象。

在对象o被实例化为具有内联属性的动态对象后,它被传递给对象o和参数proto中的protoopt()函数。此外,还提供了一个0x1234的值。

当函数调用opt(o, o, 0x1234)发生时,o.b属性被设置为1,就像上次一样。但是,这次我们没有提供一个空白的prototype属性,而是提供了动态对象o(带有内联属性)作为函数tmp的原型。这实质上就是令tmp.prototype = o;,让JavaScript知道tmp函数的原型现在是动态对象o。此外,o.a属性(之前在o对象实例化时是1)被设置为value,我们在这里提供的是0x1234。下面,让我们来仔细聊聊这到底意味着什么。

我们知道,动态对象o是通过内联属性来声明的。我们还知道,这些类型的动态对象在内存中的布局如下图所示:

1.png

现在跳过prototype,我们可以看到o.a被设置了。o.a是一个在声明对象时就存在的属性,并且直接在对象中被表示出来,因为它是内联声明。所以从本质上讲,这就是在内存中应该有的样子。

当对象被实例化的时候(让o = {a: 1, b: 2})。

1.png

当o.b和o.a通过opt()函数被更新时(opt(o, o, 0x1234)。

1.png

我们可以看到,JavaScript只是直接作用于已经内联的1和2的值,并直接用opt()提供的值覆盖它们来更新o对象。这意味着,当ChakraCore更新相同类型的对象(例如,具有内联属性的动态对象)时,它不需要改变内存中的类型,而只是直接作用于对象中的属性值。

在继续之前,让我们快速回顾一下JavaScript动态对象分析部分的一个代码片段:

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

这里的a是用许多内联属性创建的,这意味着1、2、3和4都直接存储在对象a中。但是,当在对象a的实例化之后添加新属性a.f时,JavaScript将通过auxSlots数组将该对象转换为引用数据,因为该对象的布局显然随着引入一个未内联声明的新属性而发生了变化。我们可以回想一下下面的情况。

1.png
1.png
1.png

这个过程被称为类型转换,其中Chakracore/Chakra将基于诸如具有内联属性的Dynamcic对象添加未在事后内联声明的新属性等因素来更新动态对象在内存中的布局。

既然我们已经介绍了类型转换,现在让我们回到以下代码(在调用2000次opt函数并创建o对象之后,继续调用opt函数)

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

我们知道,在opt()函数中,o.a和o.b被更新为o.a = 0x1234和o.b = 1;。我们还知道,这些属性更新后在内存中将变成下面的样子:

1.png

然而,我们没有讨论let tmp = {__proto__: proto};这行代码。

之前,我们将proto的值提供给了tmp.prototype。在这种情况下,这将执行以下操作:

tmp.prototype = o

这初看起来没什么问题,但这实际上是我们的漏洞就出现在这里。当一个函数设置了它的原型(例如tmp.prototype = o),将成为原型的对象(在这种情况下,就是我们的对象o,因为它被分配给tmp的原型属性)必须首先经历一个类型转换。这意味着o在内存中不再用内联值表示,而是更新为使用auxSlots来访问对象的属性。

o发生类型转换之前(o.b = 1发生在类型转换之前,所以它仍然被内联更新):

1.png

o发生类型转换之后:

1.png

然而,由于opt()已经经过了JIT处理,它已经变成了机器代码。在访问一个给定的属性之前,JavaScript解释器通常会进行各种类型检查,这些被称为护栏。然而,由于opt()被标记为“热”函数,它现在在内存中被表示为机器代码,就像任何其他C/C++二进制代码一样。这时,用于类型检查的护栏现在已经消失了。它们消失的原因是一个被称为推测性JIT的原因,因为这个函数被执行了很多次(在这个例子中是2000次),JavaScript引擎就会假定这个函数调用只会使用到目前为止已经看到的对象类型来调用。就本例来说,由于opt()到目前为止只见过2000次形如 opt(o, {}, {})的调用,所以,它假设未来的调用也只会以这种方式进行。然而,在第2001次调用时,函数opt()不仅被编译成机器码,并且“护栏”也随之消失,同时,这次函数调用变成了opt(o, o, 0x1234)。

函数opt()所做的推测是,o在内存中总是被表示为一个只有内联属性的对象。然而,由于tmp函数现在有一个实际的原型属性(而不是一个空白的{},它实际上被JavaScript忽略了,让引擎以为tmp没有原型),我们知道这个过程会对被指定为相应函数原型的对象进行类型转换(例如,tmp的原型现在是o,o现在必须进行类型转换)。

由于o现在已经历了类型转换,而opt()并不知道这一点,所以“类型混淆”成为可能,而且确实发生了。在对象o经历了类型转换后,o.a属性被更新为0x1234。但是对于opt()函数来说,它还认为遇到对象o时,应该把属性当作内联的(例如,直接在对象中设置它们,就在type指针之后)。因此,由于我们在opt()函数中把o.a设置为0x1234,在它被“JIT”后,opt()会屁颠屁颠地把值0x1234写入第一个内联属性(因为o.a是创建的第一个属性,它被直接保存在type指针的后面)。然而,这会带来毁灭性的影响,因为对象o在内存中实际上是作为auxSlots指针保存的。

1.png

因此,当o.a属性被更新时(函数opt()认为内存中的布局是| vftable | type | o.a | o.b,,而实际上是 | vftable | type | auxSlots |),函数opt()并不知道o现在是通过auxSlots(存储在动态对象的偏移量0x10处)来存储属性,它把0x1234写到它认为应该去的地方,那就是第一个内联属性(也是存储在动态对象的偏移量0x10处)!

函数opt()认为它正在更新o(因为JIT推测执行会告诉该函数:对象o应该总是有内联属性)。

1.png

然而,由于对象o在内存中被布置成一个带有auxSlots指针的动态对象,因此实际上会发生以下情况。

1.png
1.png

类型混淆的结果是,auxSlots指针被覆盖为0x1234。这是因为一个动态对象的第一个内联属性与另一个使用auxSlots数组的对象在动态对象中存储在同一偏移量处。由于“没有人”告诉opt()函数,对象o在内存中已经被布置成一个使用auxSlots数组的对象,所以,该函数仍然认为o.a是以内联形式存储的。正因为如此,它将向dynamic_object+0x10处(也就是o.a曾经的存储位置)写入数据。然而,由于o.a现在存储在auxSlots数组中,这就导致用值0x1234覆盖了auxSlots数组的地址。

尽管这是漏洞发生的地方,但实际的访问违规发生在print(o.a)语句中,如下图所示:

opt(o, o, 0x1234);  // Overwrite auxSlots with the value 0x1234

print(o.a);         // Try to access o.a

在通过tmp.prototype进行类型转换之后,对象o在内部知道它现在被表示为一个使用auxSlots数组来保存其属性的动态对象。因此,当对象o访问属性o.a时(因为print语句需要用到它),将通过“auxslots”指针来完成访问。但是,由于auxSlots指针被0x1234覆盖,所以,ChakraCore试图解除对内存地址0x1234的引用(因为这是auxSlots指针应该在的位置),来访问o.a(因为我们要求ChakraCore检索该值以供print函数使用)。

1.png

由于ChakraCore也是开源的,我们可以访问对应的源代码。实际上,WinDbg会自动提供相应的源代码(我们前面已经看到了)。参考源代码,我们可以看到,事实上,ChakraCore正在访问(或试图访问)的是一个auxSlots数组。

1.png

我们也知道auxSlots是一个动态对象的成员。考察发生访问违规的函数的第一个参数(DynamicTypeHandler::GetSlot),我们可以看到一个名为instance的变量被传递进来,其类型为DynamicObject。这个实例实际上是我们的o对象的地址,它也是动态对象。此外,还有一个index值也被传入,它是我们想从auxSlots数组中获取的值的索引。由于o.a是o的第一个属性,即auxSlots[0]。因此,这个GetSlots函数是一个能够检索通过auxSlots存储属性的对象的特定属性的函数。

尽管我们现在知道了漏洞的成因,但仍然值得设置一些断点来看看auxSlots被破坏的确切时刻。下面,让我们用print()调试语句来更新我们的poc.js脚本。

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Adding a debug print statement
    print("DEBUG");

    opt(o, o, 0x1234);

    print(o.a);
}

main();

在WinDbg中运行脚本时,首先在print语句上设置一个断点。这确保了任何作用于动态对象的函数都应该作用于我们的对象o。

1.png

实际上,我们可以参考Google Project Zero的漏洞报告(https://bugs.chromium.org/p/project-zero/issues/detail?id=1702)。漏洞描述如下:

“尽管NewScObjectNoCtor和InitProto操作码被视为没有副作用,但实际上它们可以通过类型处理程序的SetIsPrototype方法产生副作用,该方法可以导致向新类型的转换。这会导致JITed代码中的类型混淆。”

在这里,我们知道InitProto是一个将要执行的函数,这是由于我们设置了tmp函数的.prototype属性。如上面的代码所示,该函数在内部调用一个名为SetIsPrototype的方法(函数),该方法最终负责转换用作函数原型的对象的类型(在本例中,这意味着o将进行类型转换)。

知道了这一点后,我们想继续弄清楚这种类型转换发生的确切位置,以确认该漏洞的存在以及最终漏洞是如何产生的。为此,让我们在chakracore!Js::DynamicObject中的SetPrototype方法上设置一个断点(因为我们处理的是动态对象)。请注意,我们是在SetPrototype而不是在SetIsPrototype上设置了一个断点,因为SetIsPrototype最终会在SetPrototype的调用堆栈中调用。实际上,调用SetPrototype时,最终将调用SetisPrototype:

1.png
1.png

在点击chakracore!Js::DynamicObject::SetPrototype之后,我们可以看到对象o(尚未进行类型转换)当前位于RDX寄存器中:

1.png

我们知道当前正在一个函数中执行,在某一时刻,比如因为SetPrototype内部调用的结果所致,会将对象o从一个具有内联属性的对象转换为一个通过auxSlots表示其属性的对象。同时,我们知道auxSlots数组总是位于动态对象的偏移量0x10处。既然我们知道对象必须在某个时候进行类型转换,不妨让我们设置一个硬件断点,告诉WinDbg当o+0x10被写到8个字节(1个QWORD,或64位值)的边界时中断运行,看看转换到底发生在ChakraCore中的哪些位置。

1.png

我们可以看到,WinDbg在一个名为chakracore!Js::DynamicTypeHandler::AdjustSlots.的函数中发生了中断。我们可以在下面看到该函数的代码:

1.png

现在,让我们检查一下调用堆栈,看看到底是如何执行到这一点的。

1.png

太有意思了! 正如我们在上面看到的,InitProto函数(叫做OP_InitProto)在内部调用了一个叫做ChangePrototype的函数,这个函数最终调用了我们的SetPrototype函数。正如我们前面提到的,函数SetPrototype又会调用Google Project Zero漏洞报告中提到的SetIsPrototype函数。这个函数执行了一连串的函数调用,最终导致执行到我们目前的位置,即AdjustSlots函数。

我们也知道,我们可以接触到ChakraCore的源代码。所以,让我们检查一下我们在AdjustSlots的源代码中的位置,也就是我们的硬件断点所在的位置。

1.png

我们可以看到,object(估计是我们的动态对象o)现在有一个auxSlots成员。这个值是由newAuxSlots的值设置的。但是,newAuxSlots是从哪里来的呢?在前面的图片中再往上看一下,我们可以发现一个叫做oldInlineSlots的值,它是一个数组,被分配给newAuxSlots的值。

这非常有趣,因为我们从进行类型转换前的对象o中知道,这个对象是一个具有内联属性的对象!这个函数似乎在转换一个具有内联属性的对象。具体来说,这个函数似乎是将一个具有内联属性值的对象转换为通过auxSlots表示的对象。

接下来,让我们快速回顾一下AdjustSlots的反汇编代码。

1.png

通过上图,我们可以看到:当前执行的指令mov rax, qword ptr [rsp+0F0h]上面是指令mov qword [rax+10h], rcx。回想一下,指针auxSlots就是存储在动态对象中的0x10的偏移处。这条指令表明,我们的对象o就位于RAX中,并且其值位于偏移量0x10处(其中o.a,即第一个内联属性,就被存储在这里,因为第一个内联属性总是存储在以这种方式表示的对象内的dynamic_object+0x10处)。这个值被指定为RCX的当前值。下面,让我们通过调试器检查一下。

1.png

很完美! 我们可以通过RCX可以看到内联属性值o.a和o.b! 这些值被储存在一个指针中,即000001229cd38200,这就是RCX中的值。这实际上就是auxSlots数组的地址,作为类型转换的结果,它将被分配给我们的对象o! 我们还可以看到,RAX目前保存的是对象o,它现在已经被转换为一个动态对象的auxSlots变体!我们可以通过检查位于o+0x10处的auxSlots数组来确认这一点! 通过上图可以发现,我们的对象已经从一个具有内联属性的对象转换为一个属性被保存在auxSlots数组中的对象!

让我们再设置一个断点,通过观察内存中的值已经被更新,来百分之百地确认这一点。为此,可以在mov qword [rax+10h], rcx指令上设置一个断点,并删除所有其他断点(我们的print()调试断点除外)。我们可以通过删除断点和利用WinDbg中的.restart命令来重新执行ch.exe来轻松做到这一点(请注意,下面的图片分辨率较低。如果看不清楚的话,请右击它并在新的标签页中打开,以便于仔细查看)。

1.png
1.png

在命中print()上的断点后,我们可以通过执行g命令,继续执行到我们预定的断点。

1.png

我们可以看到,在WinDbg中,我们实际上是在预定断点前的几条指令上停下来的。这完全没问题,我们可以在要检查的mov qword [rax+10h], rcx指令上设置另一个断点。

1.png

然后,我们就可以在下一个断点上看到当到达mov qword [rax+10h], rcx指令时的执行流程状态。

1.png

然后,我们可以在执行上述指令的前后,分别检查对象o和寄存器RAX的状态,看看我们的对象是否从一个内联表示的动态对象更新为一个基于auxSlots数组的对象!

1.png

在检查auxSlots数组时,我们可以看到我们的a和b属性!

1.png

很完美! 我们现在可以确定:对象o已经在内存中被更新了,而且它的布局也已经变了。然而,函数opt()并没有意识到这个类型的变化,仍然会执行o.a = value(其中value为0x1234)指令,就像o没有进行过类型转换一样。这是因为,函数opt()仍然认为对象o在内存中被表示为一个具有内联属性的动态对象。由于我们知道内联属性也存储在dynamic_object+0x10处,而函数opt()还将执行o.a = value指令,就像我们的auxSlots数组并不存在一样(因为该函数不知道它的存在,因为JIT编译过程告诉opt()无需关心对象o到底是什么类型!)。这意味着,它将直接用值0x1234覆盖指针auxSlots!下面,让我们看看这个过程。

1.png

1.png

再次之前,我们首先需要清除所有的断点,为此,可以在WinDbg中启动一个全新的ch.exe实例,方法是利用.restart或直接关闭并再次打开WinDbg。之后,在我们的print()调试函数(ch!WScriptJsrt::EchoCallback)上设置一个断点。

1.png

现在,在对我们的对象进行类型转换的函数上设置一个断点,我们知道,这个函数就是chakracore!Js::DynamicTypeHandler::AdjustSlots。

1.png

好了,让我们再次检查调用栈。

1.png

注意在我们调用OP_InitProto之前的内存地址,我们之前已经介绍过了。下面的地址是发起对OP_InitProto调用的函数的地址,但是我们没有看到相应的符号。如果我们对这个内存地址执行!address命令,我们也会看到这个地址没有对应的映像名或用法。

1.png

我们所看到的这些情况,都是拜JIT所赐。这个内存地址是我们的opt()函数的地址。之所以这个函数没有对应的符号,是因为ChakraCore将这个函数优化为实际的机器代码。我们不再需要通过任何用于设置属性、更新属性等的ChakraCore函数/API。ChakraCore引擎会利用JIT将这个函数编译成可以直接作用于内存地址的机器代码,就像C语言在执行下面的操作时所做一样。

STRUCT_NAME a;

// Set a.Member1
a.Member1 = 0x1234;

在Microsoft Edge中,这一过程是通过一个称为进程外JIT编译的技术实现的。Edge的“JIT服务器”是与实际的“渲染器”进程或“内容”进程相互独立的进程,后者是用户与之交互的进程。当一个函数进行JIT编译时,会对其进行优化,然后将其从JIT服务器注入到内容进程中(我们将在第三篇文章中使用任意代码保护(ACG)绕过漏洞来滥用这一点。另外请注意,我们将使用的ACG绕过漏洞已经在Windows 10 RS4中得到了修复)。

现在让我们通过在其上设置一个断点来考察该函数(请注意,下面的图像分辨率较低。如果看不清楚的话,可以右键单击它,从而在新建选项卡上打开,以便进行查看)。

1.png

另外需要注意的是,这里看到了对OP_InitProto的调用,表明这是我们的opt函数。另外,如下图所示,这里没有JavaScript操作符或ChakraCore函数。我们看到的是纯粹的机器代码,因为这是JIT编译后得到的代码。

1.png

然而,更致命的是,我们可以看到R15寄存器即将参与其中,相应的偏移量为0x10。这表明R15中保存的是我们的对象o。这是因为O.A=value位于OP_InitProto调用之后,这意味着mov qword ptr[R15+10H],r13就是我们的O.A=value指令。我们还知道value是0x1234,所以,这应该就是R13中的值。

1.png
1.png

然而,这正是我们的漏洞之所在,因为opt函数并不知道对象o已经从使用内联属性改为使用auxSlots数组。同时,它也没有对o对象进行必要的安全检查,因为这是JIT的工作!所以,严格来说,这里的漏洞是因为JIT代码没有进行类型检查,所以才导致发生类型混淆的。

断点命中后,我们可以看到,函数opt()仍然把对象o当作一个具有内联存储属性的对象,它仍然会执行o.a=0x1234指令,从而用用户提供的0x1234值覆盖auxSlots指针,因为opt()函数仍然认为o.a位于o+0x10处,因为ChakraCore没有通知opt()函数所发生的变化,也没有在操作前对类型进行检查。这个类型混淆漏洞非常严重,因为攻击者可以用一个受控的值来覆盖auxSlots指针!如果我们清除所有的断点,就会发现,在这个过程中,我们可以看到,有很多代码都在使用这个指针。

1.png
1.png

如果我们清除所有断点,并在WinDbg中输入g,我们可以清楚地看到:ChakraCore会试图通过print(o.a)来访问o.a。当ChakraCore读取属性o.a时,由于类型已经发生了转换,所以,它实际上是通过auxSlots进行的。然而,由于函数opt()破坏了这个值,因此,ChakraCore会试图解除对内存中auxSlots点的引用,其中保存的值为0x1234。这显然是一个无效的内存地址,因为ChakraCore所期待的是内存中的合法指针,因此,发生了访问违规。

1.png

小结

正如我们在前面的分析中所看到的,JIT编译虽然能提高的性能,但它也暴露了一个非常大的攻击面,以至于微软为Edge提供了一个新的模式,叫做Super Duper Secure Mode,实际上就是禁用JIT,这样所有的缓解措施都可以启用。

到目前为止,我们已经看到了关于如何从POC->访问违规以及为什么会发生这种情况的全面分析,包括配置分析环境。在第二篇中,我们将把DOS概念验证转换为读/写原语,然后通过实现代码执行并绕过ch.exe中的CFG机制来发动攻击。在ch.exe中获得代码执行权限后,为了更便于展示代码执行是如何实现的,我们将把重点转移到如何构建易受攻击的Edge上面,在第三篇中我们还必须绕过ACG机制。让我们在第二篇中见!

原文地址:https://connormcgarr.github.io/type-confusion-part-1/

评论

T

tang2019

这个人很懒,没有留下任何介绍

随机分类

MongoDB安全 文章:3 篇
前端安全 文章:29 篇
memcache安全 文章:1 篇
Android 文章:89 篇
iOS安全 文章:36 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

目录