在PLAYSTATION 4中利用WebKit 0-DAY

 

简介

基于webkit的浏览器通常是全链攻击的入口点:从浏览器利用到内核利用。但是,由于浏览器引擎加固技术加上完全没有调试功能,使得成功利用最新PS4固件中的bug非常困难。在这篇文章中,我们将介绍这个bug的根本原因。bug提供了有限的利用原语。但是,由于我们在ASLR机制中发现了一个缺陷,因此我们能够利用这个bug。我们将重点讨论我们采用的利用策略,以及我们如何将一个Use-After-Free转换为一个R/W原语,从而导致代码的执行。

 

攻击 PS4

浏览器可能是攻击PS4最常见的入口点。该浏览器基于WebKit,并在沙箱中运行。与iOS设备不同,没有现代缓解机制,例如Gigacage或StructureID随机化,并且未启用JIT。

典型的漏洞利用链以WebKit漏洞开始,以在渲染器进程中执行代码,然后绕过沙箱来运行内核exploit。

过去有几个WebKit漏洞被成功利用。@Fire30_创建的bad-hoistexploit是PS4上最后一个已知公开的exploit。它利用@lokihardt发现的漏洞CVE-2018-4386,并提供读/写原语。该exploit可在高达6.72的固件上使用。@lokihardt(CVE-2018-4441)发现的另一个漏洞也被@SpecterDev在PS4上利用。该exploit还提供了读/写原语,并在固件6.20上工作。

关于内核漏洞利用,@theflow0发布了最新的exploit。该exploit在内核中提供了读/写原语,可以从WebKit沙箱中访问。这个bug出现在7.02的固件上,最近又与bad-hoist的exploit一起出现在6.xx固件中。

最后,@qwertyoruiopz发现并利用了BPF上的一些漏洞。我们强烈建议读者看看@SpecterDevwrite-up@CTurt关于攻击PS4的系列文章

 

WebKit堆

WebKit有许多堆分配器:

  • FastMalloc是标准分配器。它被许多WebKit组件使用;
  • DOM引擎使用的是IsoHeap。其目的是使用类型来排序每个分配以缓解UAF漏洞;
  • JavaScript引擎使用垃圾回收器分配JavaScript对象;
  • JavaScript引擎也使用IsoSubspace。它的目的与IsoHeap相同,但用于一些对象;
  • Gigacage实现了缓解机制,以防止对特定对象进行超出范围的读/写。如上所述,它在PS4上被禁用。

主分配器

主堆由分成smallpages(4 KB)的块组成。small page为存储在smalllines(256字节)中的相同大小的对象提供分配。

主分配器本质上是一个bump分配器。使用fastMalloc原语分配对象时,该原语在使用快路径提供分配服务时仅执行以下操作。

--m_remaining;
char* result = m_ptr;
m_ptr += m_size;
return result;

当bump分配器的可用空闲对象slots用完时,将触发fastMalloc的慢路径以重新填充bump分配器。分配器从缓存(fast path)—即bumpRangeCache—或从新分配的page(慢路径)重新填充。从缓存中获取page——即lineCache,或者从为每个块维护的空闲页面列表中获取页面。如果页面有碎片(例如,从lineCache分配的页),则连续的空闲lines用于重新填充分配器。其余的空闲lines用于填充bumpRangeCache。

释放对象后,它不会立即用于后续分配。它被插入到一个专用的vector m_objectLog中,在Deallocator::processObjectLog中处理,当它达到最大容量(512)或通过慢路径填充bump分配器时将对其进行处理。processObjectLog的作用是在refcount达到0时释放一个smallLine(例如,没有使用smallLine中的所有slot对象)。当smallLine被释放时,它的相关页面推入cacheLine。请注意,smallPages和chunks也被计数,因此当它们各自的计数达到0时被释放。

 

bug

该bug已由我们的内部fuzzer触发。它存在于WebKit DOM引擎中。更准确地说,该bug存在于WebCore::ValidationMessage::buildBubbleTree方法中:

void ValidationMessage::buildBubbleTree()
{
    /* ... */
    auto weakElement = makeWeakPtr(*m_element); // [1] flawed weak pointer

    document.updateLayout();                    // [2] call user registered JS events

    if (!weakElement || !m_element->renderer())
        return;

    adjustBubblePosition(m_element->renderer()->absoluteBoundingBoxRect(), m_bubble.get());

    /* ... */
}

方法buildBubbleTree调用更新布局,在此期间执行所有用户注册的JS处理程序。如果ValidationMessage在一个JS回调中被销毁,当我们返回到buildBubbleTree代码时,这可能会导致后无使用的情况。WebKit开发人员已经发现,在更新样式或布局时可能会出现问题。然而,由于在生成弱指针时产生了额外的解引用,他们未能修复代码。也就是说,我们仍然可以在布局更新期间销毁ValidationMessage实例。

以下是漏洞报告后的修复程序:

void ValidationMessage::buildBubbleTree()
{
    /* ... */
-
-   auto weakElement = makeWeakPtr(*m_element);
-
-   document.updateLayout();
-
-   if (!weakElement || !m_element->renderer())
-       return;
-
-   adjustBubblePosition(m_element->renderer()->absoluteBoundingBoxRect(), m_bubble.get());

    /* ... */


+   if (!document.view())
+       return;
+   document.view()->queuePostLayoutCallback([weakThis = makeWeakPtr(*this)] {
+       if (!weakThis)
+           return;
+       weakThis->adjustBubblePosition();
+   });
}

触发bug

下图总结了易受攻击的路径。我们可以通过调用一些HTML输入字段上的reportValidity实例化ValidationMessage对象。我们在那个输入字段上注册一个JS处理程序。例如,我们可以定义一个JS处理程序,只要在输入字段上设置了焦点,它就会执行。

reportValidity方法触发一个计时器来调用buildBubbleTree函数。如果我们在定时器到期之前将焦点设置在HTML输入字段上,buildBubbleTree将在布局更新期间执行我们的JS处理程序。如果ValidationMessage在JS回调中被销毁,这将在返回buildBubbleTree代码时导致UAF。

现在,如果我们设法以某种方式生存到由于对ValidationMessage字段的无效访问而导致的早期崩溃,我们最终会调用deleteBubbleTree,这将再次破坏ValidationMessage实例。

我们第一次尝试触发这个bug失败了。reportValidity将焦点设置在过早触发JS回调的输入字段上。作为一种解决方案,我们使用了两个输入字段:input1和input2。首先,我们在焦点事件的第一个输入上注册一个JS处理程序。然后,我们reportVality在上调用input1。这将触发我们的JS处理程序执行,该处理程序将简单地将焦点设置在其他位置(例如input2)。最后,在buildBubbleTree执行之前,我们在input1上定义了一个新的处理程序,该处理程序将销毁 ValidationMessage实例。

调试bug

触发PS4上的bug会导致浏览器崩溃,没有任何调试信息。为了克服此限制,我们有两种选择:

  • 设置一个调试环境,尽可能接近PlayStation环境。也就是说,安装一个FreeBSD box,然后从从doc.dl.playstation.net下载的源代码构建WebKit。这是很有帮助的,但有时在我们的环境中有效的exploit并不意味着在PS4上也能有效。
  • 使用1-day漏洞调试0-day漏洞。为此,我们可以使用bad-hoist exploit,因为它提供了有用的原语,比如读/写原语,还提供了经典的addrof/fakeobj原语。然而,这个exploit是不稳定的,并且只能在固件6.xx上使用。此外,在我们之前运行这个exploit会给堆带来一些干扰。

漏洞解析

ValidationMessage对象是由reportValidity创建的,主要由buildBubbleTree访问。该对象是fastmalloc’ed的,由以下字段组成。

在布局更新之后,黄色的字段被实例化(m_messageBody, m_messageHeading)或者被重新实例化(m_timer)。绿色的字段(m_element和m_bubble)每个指向一个HTMLElement实例,在布局更新后被访问。

ValidationMessage的销毁是通过deleteBubbleTree方法完成的:

void ValidationMessage::deleteBubbleTree()
{
    if (m_bubble) {
        m_messageHeading = nullptr;
        m_messageBody = nullptr;
        m_element->userAgentShadowRoot()->removeChild(*m_bubble);
        m_bubble = nullptr;
    }
    m_message = String();
}

这个方法清除了大多数ValidationMessage字段(包括m_bubble),如果它在布局更新期间被调用,会导致浏览器崩溃。更准确地说,渲染器进程在试图解引用m_bubble.get()时在函数adjustBubblePosition中崩溃。

为了利用此漏洞,我们需要内存泄漏或绕过ASLR。事实证明,我们可以通过堆喷射在可预测的位置分配一些对象。但是,利用此漏洞需要对早期的内存映射有一定了解。由于bad-hoist的exploit,我们已经将其用于固件6.xx。理论上,预测的地址在7.xx固件上是可以强制执行的(稍后会详细介绍)。

以下是避免过早崩溃的方法:

  • 喷射HTMLElement对象(如HTMLTextAreaElement)→分配在固定位置的对象。
  • 混淆ValidationMessage和受控对象(例如ArrayBuffer的内容)。
  • 修复m_bubble和m_element值,以便它们指向我们预测的地址。

 

Exploitation

重用目标对象

为了重用我们的目标对象,我们如下图所示进行操作:

  • 我们在分配目标对象之前和之后向堆中喷射对象O——其大小与ValidationMessage相同(48字节)。
  • 我们删除ValidationMessage实例以及周围的O对象,从而使包含这些对象的smallLine被释放,相关的smallPage被缓存。

我们再次向堆中喷射与目标对象大小相同的对象。在我们的exploit中,我们喷洒了ArrayBuffer(48),以便用ArrayBuffer的内容覆盖ValidationMessage内容。

初始化内存泄漏

如前所述,一些ValidationMessage在布局更新之后被实例化,因此它们的内容可能会泄漏。更准确地说,通过简单地dump ArrayBuffer的内容,我们可以泄漏m_messageBody、m_messageHeading和m_timer的值。m_timer特别有趣,因为它是fast的,因此允许我们推断分配在同一smallPage上的对象的地址。

任意递减原语

现在,如果我们正确地使用目标对象,我们最终会调用deleteBubbleTree方法,该方法将大多数ValidationMessage字段设置为空指针值。需要注意的是,在refcount类上重载空指针赋值会导致refcount递减。这意味着我们在多个受控制的ValidationMessage的字段上有一个refcount递减:m_messageBody, m_messageHeading和m_bubble。

我们可以通过破坏例如m_messageHeading指针并将其refcount字段与某些对象的length字段混淆来利用refcount递减。通过将对象指定为具有length和data字段的StringImpl对象,在length字段上未对齐的写入将使我们能够读取超出数据限制的内容。

下面,我们将两次利用任意递减原语:

  • 1.第一次运行的目的是建立一个相对读取的原语,以泄漏a的地址JSArrayBufferView。
  • 2.第二次运行的目标是通过破坏先前泄漏的JSArrayBufferView地址的length字段来建立相对的读/写原语。

下图总结了工作流程:

相对读原语

这一部分的目标是泄漏JSArrayBufferView对象的地址。从我们的角度来看,这个对象很有趣,因为它有一个length字段,并且它允许读/写数据缓冲区中的任意数据。如果我们可以控制length字段,那么我们可以读/写超出数据缓冲区的限制。此对象是使用垃圾回收器分配的。由于我们没有这个堆的任何泄漏,所以无法使用任意的递减原语到达它。

这是我们覆盖一个StringImpl对象的length的方法:

  • 在m_timer实例化之前和之后喷射多个StringImpl对象。我们分配StringImpl,这样他们有相同的大小作为一个定时器对象,以确保m_timer和StringImpl驻留在相同的smallPage。
  • 利用喷过的StringImpl length字段上的任意递减原语。
  • 遍历StringImpl对象,并检查哪个对象的length已损坏(例如,length较大的对象是正确的)。

一旦我们通过损坏的StringImpl的length字段获得了相对原语,让我们看看如何在FastMalloc堆上获得JSArrayBufferView引用。

defineproperties是一个内置的JavaScript,允许定义JavaScript对象属性。

static JSValue defineProperties(ExecState* exec, JSObject* object, JSObject* properties)
{
    Vector<PropertyDescriptor> descriptors;
    MarkedArgumentBuffer markBuffer;

    /* ... */
    JSValue prop = properties->get(exec, propertyNames[i]);
    /* ... */
    PropertyDescriptor descriptor;
    toPropertyDescriptor(exec, prop, descriptor);
    /* ... */
    descriptors.append(descriptor);        // [1] store JSValue reference on fastMalloc
    /* ... */
    markBuffer.append(descriptor.value()); // [2] store one more JSValue reference on fastMalloc
}

在其实现中,此方法使用两个对象:

  • Vector<PropertyDescriptor>
  • MarkedArgumentBuffer

这两个对象都有一个使用fastMalloc分配的backing缓冲区,defineProperties使用这两个对象来存储JSValue引用。JavaScript引擎使用JSValue来表示许多JavaScript值。我们对这个对象很感兴趣,因为它可以是一个JSObject引用(例如JSArrayBufferView)。从而允许我们将JSObject引用推入FastMalloc堆。使用我们的FastMalloc相对读取,我们可以找到这些引用。

这是@qwertyoruiopz用来在FastMalloc堆上获取JSObject引用的技术:

  • 1.分配多个JSArrayBufferView对象。
  • 2.使用Object.defineProperties在FastMalloc堆上推入引用。当离开Object.defineProperties时,引用被释放,但不会被清除。我们必须小心不要重用这些分配,以避免清除JSValue引用。
  • 3.使用相对读取来扫描FastMalloc堆并搜索喷射出来的JSArrayBufferView引用。目标引用必须能够从相对读操作中访问到。

相对读/写原语

在JSArrayBufferView泄漏的情况下,我们可以通过以下步骤轻松获取相关读/写操作:

  • 1.再次运行exploit程序以重新使用任意递减原语。
  • 2.使用任意递减来覆盖JSArrayBufferView的length字段。可以通过检查每个喷射过的对象的length来找到覆盖的JSArrayBufferView。大的是正确的。

这个引用可以用于读/写超出fastMalloc支持的缓冲区的限制。

任意读写原语

JSArrayBufferView对象有一个到它的数据缓冲区的引用字段。破坏这个引用允许任意读/写原语。

我们仍然有一个相对读,相对读数据缓冲区的内存地址,我们知道泄漏的JSArrayBufferView可以从相对读数据缓冲区中访问。由于有了相对读,我们可以很容易地找到相对读/写backing缓冲区的内存位置。

现在,我们可以使用backing缓冲区引用来访问使用相对读/写操作喷出来的JSArrayBufferView,并破坏另一个JSArrayBufferView对象的backing缓冲区引用。这允许使用第二个JSArrayBufferView引用在任意地址读取和写入任意数据,如下图所示:

代码执行

PS4浏览器不允许分配RWX内存页面。但用任意的R/W,我们可以控制RIP寄存器。例如,我们可以覆盖先前喷射的HTMLTextAreaElement的虚表指针,使其指向我们控制的数据。在HTMLTextAreaElement上调用一个JavaScript方法将导致调用我们控制的一个地址。然后,我们可以进行代码重用(如ROP或JOP)来实现下一阶段。

这个exploit可以在Synacktiv的Github中找到。

在7.XX固件上移植这个exploit

由于ASLR机制方面的缺陷使我们能够预测HTML对象的地址,因此我们成功地咋6.xx固件上的利用了这个漏洞。可预测的地址在exploit程序中进行了硬编码,并且由于bad-hoist的exploit程序已被识别出来。但是,在不知道早期内存映射状况的情况下,确定已喷射的HTMLElement对象的地址的唯一方法是强制使用该地址。

由于浏览器需要用户交互才能重新启动,因此PS4上的暴力破解非常乏味。我们的想法是插入充当PS4键盘的Raspberry Pi。其主要目标是在崩溃后定期(5s)按下Enter键以重新启动浏览器。强制地址在每次尝试时更新并存储在一个cookie中。

不幸的是,到目前为止我们没有得到任何结果。我们可能没有足够长的时间来覆盖整个地址空间。

(完)