深入分析Chrome浏览器textbook UAF漏洞

 

一、前言

在2020年3月,我发现了Chrome WebAudio模块中存在释放后使用(UAF)漏洞。这是在非垃圾回收对象上存在UAF漏洞,该对象由PartitionAlloc内存分配器进行分配。在blink中(WebAudio是其中的组成部分),根据堆对象的类型为堆对象分配了不同的内存分配器。例如,大多数垃圾回收对象是由Oilpan分配的,而非垃圾回收对象是由PartitionAlloc分配的。ArrayBufferString的后备存储例外,即使对象本身属于垃圾回收对象,它们也是在PartitionAlloc中被分配。

在2019年,Chrome有两个值得关注的在野利用漏洞,都与blink中的PartitionAlloc对象有关。一个是Google威胁分析团队Clement Lecigne发现的CVE-2019-5786,另一个是Kaspersky Labs的Anton Ivanov和Alexey Kulaev发现的CVE-2019-13720(WizardOpium)。

要利用这类漏洞,其中的一个难点就在于PartitionAlloc,它将原始bin(字符串、向量、ArrayBuffers等)与普通的“可执行”对象区分开。原始bin分配在BufferArrayBuffer中,而普通对象是在Fast区域中分配。首先,这种分隔让我们很难使用ArrayBufferBuffer区域中的内存损坏来劫持控制流。其次,这导致在Fast区域中很难利用内存损坏来在ArrayBufferBuffer区域中创建数据可控的伪造对象,因为其中的写入原语非常有限。在19年发现的这两个漏洞中,UAF都位于ArrayBuffer区域中,因此研究人员面临的一个挑战就是突破ArrayBuffer区域。这两个漏洞的利用方法此前已经有非常详尽的文章来做介绍,CVE-2019-5786可以参考这篇文章,CVE-2019-13720可以参考这篇文章。在这里,可以找到关于如何利用PartitionAlloc的一篇技术文章

在这篇文章中,我们要进行一个与此相反的挑战,利用Fast区域中存在的UAF漏洞来实现RCE。我们将以CVE-2020-6449漏洞为例,来进行具体说明,这种技术其实更为通用,也适用于Fast区域中对象存在UAF的其他场景。

 

二、漏洞分析

有关该漏洞的详细信息和根本原因分析,可以参考上面的链接和Chrome官方的漏洞说明。在这篇文章中,我们仅对漏洞的详细信息进行概述。此外,我还假设大家已经事先了解过WebAudio的工作原理。

该漏洞位于DeferredTaskHandler::BreakConnections函数中:

void DeferredTaskHandler::BreakConnections() {
  ...
  wtf_size_t size = finished_source_handlers_.size();
  if (size > 0) {
    for (auto* finished : finished_source_handlers_) {
      // Break connection first and then remove from the list because that can
      // cause the handler to be deleted.
      finished->BreakConnectionWithLock();
      active_source_handlers_.erase(finished);
    }
    finished_source_handlers_.clear();
  }
}

通常,active_source_handlers_负责保证finished_source_handlers_中的原始指针有效。finished仅在使用完毕后,才会从active_source_handlers_中清除。在正常情况下,这是没问题的。但是,如果我们以某种方式清除了active_source_handlers_,但与此同时没有清除finished_source_handlers_,那么上述函数中的finished可能已经释放,这样就会导致UAF。

 

三、触发漏洞

要了解如何触发该漏洞,我们可以首先看看另一个与CVE-2020-6449密切相关的漏洞,这个也是我之前提交的。

void DeferredTaskHandler::BreakConnections() {
  ...
  wtf_size_t size = finished_source_handlers_.size();
  if (size > 0) {
    for (auto* finished : finished_source_handlers_) {
      active_source_handlers_.erase(finished);          //<-- finished is now free'd
      finished->BreakConnectionWithLock();              //<-- UaF
    }
    finished_source_handlers_.clear();
  }
}

正如我们在上述代码中看到的,由于finished在使用前已经从active_source_handlers_中清除,因此要触发此漏洞,我们只需要确保active_source_handlers_是唯一可以使用finished_source_handlers_保持活跃状态的句柄,即可触发CVE-2020-6449,当然我们还需要提前清除active_source_handlers_
根据官方漏洞描述,active_source_handlers_finished_source_handlers_是与AudioScheduleSourceNode相关的,AudioScheduleSourceNode包含两个子类,也就是ConstantSourceNodeOscillatorNode。当调用AudioScheduleSourceNodestart方法时,其AudioHandler将添加到active_source_handlers_。例如,我们可以看一下Ticket 1057593PoC,其中有两行。

  let src = audioCtx.createConstantSource();
  src.start();

srcAudioHandler添加到active_source_handlers_。此时,node srcactive_source_handlers_都负责保证AudioHandler处于活动状态。在src上调用stop事件时,会立即安排srcstop事件。而这个事件会由HandleStoppableSourceNode函数处理,并将其添加到finished_source_handlers_中。在这里,我们可以暂停audio,并通过处理promise运行JavaScript:

  audioCtx.suspend((3 * 128)/3072.0).then(()=>{
    gc();
    audioCtx.resume();
  });

由于现在停止了创建的constantSourceConstantSourceNode的JavaScript处理),因此没有任何方法能使其保持活跃状态,对垃圾回收(gc)的调用将会对其进行收集和销毁。随后,active_source_handlers_负责保持finished_source_handlers_的活跃。对audioCtx.resume的调用将到达BreakConnection并触发UAF。

现在,我们已经知道如何使用简单的方法来触发CVE-2020-6449漏洞,我们需要做的就是增加一个步骤,在到达BreakConnection前先清除active_source_handlers_。这里的主要区别在于,清除active_source_handlers_的唯一方法就是销毁JavaScript执行的上下文,这基本上意味着将所有内容放入iframe中,然后进行销毁。新的PoC中的main文件位于iframe中,并且还在audio graph中添加了许多新节点,例如onLoad方法现在会创建2000个PannerNode

function onLoad() {
  startStop().then((audioCtx) => {
    audioCtx.suspend((3 * 128)/3072.0).then(()=>{
      //======new======
      let dest = audioCtx.createConstantSource();
      dest.start();
      for (let i = 1; i < 2000; i++) {
        dest = dest.connect(audioCtx.createPanner());
      }
      dest.connect(audioCtx.destination);
      //=====new end======
      ....
    });
    audioCtx.startRendering();
  });
}

startStop方法还添加了AudioWorkletNode。创建AudioWorkletNodePannerNode的主要原因是控制audio线程(运行BreakConnection)和主线程(清除active_source_handlers_)之间的时序。这些会导致audio线程出现延迟,以便有足够的时间在触发漏洞之前清除掉active_source_handlers_

 

四、利用漏洞

为了利用这个漏洞,我首先需要使用一个大小相似的对象对其进行替换,并期望BreakConnectionWithLock在被替换对象的上下文中做一些“有用的事情”。总体来说,我希望能够具备以下条件:

1、在替换对象的上下文中调用BreakConnectionWithLock时,希望能够推断出堆指针的位置,以及函数或vtable等的地址。随后我们根据这些信息,也许可以找到一些已加载的库的地址,或者rop gadgets的位置等信息。例如,如果将指针地址通过BreakConnectionWithLock写入替换对象中的整数字段,那么我就可以通过读取整数字段来获取一些指针地址。

2、在知道了这些地址后,我希望能够创建一个对象,让我可以伪造vtable,并将其指向rop gadget的位置(从上一条步骤中得知)。然后,当我调用虚函数时,就会执行我选择的小工具。举例来说,我们可以使用ArrayBuffer或类似的数据结构来创建伪造对象,这样就可以使用数组条目伪造vtable

既然如此,我们来看一下BreakConnectionWithLock的实际功能:

void AudioHandler::BreakConnectionWithLock() {
  deferred_task_handler_->AssertGraphOwner();  //<---- No effect in release build
  connection_ref_count_--;

#if DEBUG_AUDIONODE_REFERENCES
  fprintf(stderr,
          "[%16p]: %16p: %2d: AudioHandler::BreakConnectionWitLock %3d [%3d] "
          "@%.15g\n",
          Context(), this, GetNodeType(), connection_ref_count_,
          node_count_[GetNodeType()], Context()->currentTime());
#endif

  if (!connection_ref_count_)
    DisableOutputsIfNecessary();  //<--- calls virtual function
}

非常幸运,第一行仅是对调试版本进行编译,因此可以避免了指针解引用,从而防止崩溃等异常现象的发生。
第二行也很不错,它减少了计数器,因此给我提供了有限的写入原语,并且不太会发生崩溃。之后,它将检查connection_ref_count_并有选择地调用DisableOutputsIfNecessary,最后会进行虚函数调用。目前,我们尽量避免调用虚函数的路径,因为在不了解堆布局的情况下,很可能会引发崩溃。

到目前为止,我们已经得到了:

1、一个UAF,在释放和使用之间的时间可以轻松控制;
2、有限的写入原语,将释放对象的特定偏移量处的字段减一;
3、进行虚函数调用的可能性。

稍后我们将看到,要实现漏洞利用,可能只需要上述条件中的1-2个。

4.1 替换对象

UAF攻击的第一步通常是将释放的对象替换为大小相似的其他对象,从而导致类型混乱,然后尝试从中获得信息泄露。之后,我们可以再次触发该漏洞,并用另一个伪造的对象替换释放的对象,在这个伪造的对象中,我们可以伪造一个vtable,并使其指向某些rop gadget。

如前所述,用于分配这些对象的内存分配器是PartitionAlloc。从漏洞利用的角度来看,PartitionAlloc的优势在于:

1、这是一个bucket分配器,用于维护每个bucket的已释放/已分配对象列表。在释放对象后,该对象将成为其bucket中空闲列表的头部,而空闲列表之前的头部会变为下一个空闲chunk。在同一个bucket中,下一个分配大小的对象将代替这个最近释放的对象。在bin中,所有chunk都是连续分配的。

2、其中包含4个不同的区域,将大多数的bin(ArrayBuffer、向量、字符串等后备存储与“普通对象”区分开。
现在,我们只需要关注第一点,我们会在后面再讨论第二点。

在这里,释放的对象是AudioScheduledSourceHandler的子类,即ConstantSourceHandlerOscillatorHandler。这些对象在Linux 80.0.3987.137发行版本中的大小分别是240和312,分别对应于bin大小(225-240)和(289-320)。使用CodeQL,可以轻松在这些bin中查找特定的类型:

from Type t
where (t.getSize() <= 240) and (t.getSize() > 225)
select t

在我们看过多种类型之后,发现BiquadDSPKernel最有希望。可以使用AudioContext::createBiquadFilter()函数和ConstantSourceHandler中的connection_ref_count_从JavaScript中创建,我们可以通过其字段biquad_biquad_.a1_.allocation来减少BreakConnectionWithLockbiquad_AudioDoubleArray中的指针字段。

但是,allocation_仅用于创建aligned_data_,此后不再使用。这意味着,如果我们要用BiquadDSPKernel替换释放的ConstantSourceHandler,就不会再使用到由BreakConnectionWithLock修改后的biquad_.a1_.allocation_的值。所以,这是一条死路吗?

4.2 损坏空闲列表

我们刚刚说过,在我们将其修改为connection_ref_count_之后,allocation_的值就没有用过,其实这并不完全正确,因为在销毁AudioArray时它会被释放。

这意味着,在释放AudioArray时,allocation_会成为空闲列表的头部。但是,当我们将其值减一时,这个指针可能与仍在使用的前一块内存重叠。尽管两个chunk之间存在1个字节的重叠可能没什么用,但如果我们重复触发漏洞,并使biquad_.a1_.allocation_位于同一个位置,就可以重复减少此指针,并让两个chunk之间产生足够大的重叠,这样就会引起另一种类型的错误。实际上,我们可以触发漏洞,每次都使用BiquadDSPKernel对其进行替换。由于PartitionAlloc会一次又一次地重复使用相同的chunk(这里的allocate_存放在大小为8 * 128 = 1024的bin中,这并不经常使用),我们可以保证每次都会修改相同的allocation_ pointer指针。因此,如果我想将allocation_ pointer的值减小n,那么只需要在这里对remove函数进行更改:

function remove() {
  let frame = document.getElementById("ifrm");
  frame.parentNode.removeChild(frame);

  if (counter < n) {
    //Trigger bug to move chunk backwards
    let biquad = audioCtx.createBiquadFilter();
    counter++;
    delete biquad;
    sleep(700);
    createIframe();
  }
}

这里的sleep是为了确保对象被垃圾回收。在实际的漏洞利用中,我们需要触发62次,这个过程可能要花费几分钟。

到目前为止,我已经破坏了大小为1024的bin中的空闲列表,从而导致两个对象之间存在重叠。现在,我们利用它来构造一个信息泄露,从而为我们提供libchrome的地址和堆指针的地址,进而能够在已知地址处创建一些受控数据。

4.3 构造信息泄露

为了构建信息泄露,我使用了HRTFPanner类,该类的大小为1152,并且与我设法移动的AudioArrayallocate_字段位于同一容器中。理想情况下,我希望将HRTFPanner分配给损坏的location_指针的位置,让这个HRTFPanner的开头部分与占用前一个chunk的另一个对象的最后部分重合,如下图所示。

然后,当我分配HRTFPanner时,其vtable和共享指针字段database_loader_将映射到对象的末尾,占用前一个chunk,因此,如果我能够找到一个可以轻松在JavaScript中读取其字段的对象,就可以实现这一点。但是,在查看了不同大小的多个对象后,并没有找到一个很适合的目标。

并且,如前所述,PartitionAlloc区分了数据容器和普通对象的分配。像HRTFPanner这些对象是在Fast区域中分配的,而数据容器是在缓冲区区域或数组缓冲区区域中分配的,因此我们不能仅分配动态大小的对象(例如JavaScript中的ArrayBuffer),使其和HRTFPanner重叠,并从条目中读取vtable等内容。

回顾之前,我首先使用AudioArrayallocation_字段破坏了空闲列表,该列表是Fast区域中的一个对象,大小易于控制,其内容也可以从JavaScript中读取。虽然这似乎是占据前一个chunk的不错选择,但存在一个问题。AudioArray仅在内部作为存储临时audio数据的缓冲区,并不会直接与JavaScript交互。更糟糕的是,在几乎所有用例中,它都作为临时缓冲区,在被JavaScript读取之前,数据将会被覆盖。因此,即使我可以创建AudioArray并用来自重叠HRTFPanner对象的vtable和堆指针覆盖器缓冲区,在我从中读取数据之前,这些数据也很可能会被覆盖。

AudioDelayDSPKernel::Process中有一个用例,其中AudioFloatArray字段buffer_在返回给用户之前可能没有被完全覆盖,这让我可以创建特制的DelayNode,一旦空闲列表损坏,其buffer_就会被HRTFPanner对象覆盖,然后便可以从audio输出中检索出来:

  //Create a DelayDSPKernel whose buffer_ has the right size, which will be used to leak data.
  delay_leak = audioCtx.createDelay(0.0908);
  //3/3072 = 1./1024, need to divide by power of 2 to avoid rounding error when converting to double
  delay_leak.delayTime.value =  3 * 0.0009765625;

然后,使用这个延迟节点渲染音频图,就让我可以读取输出中HRTFPanner对象的database_loader_字段的vtable和堆指针。

一旦获得这两条信息,接下来的操作就非常容易。我只需要在与ConstantSourceHandler相同的容器中使用另一个AudioArray创建一个伪造的对象,并使用vtable指向rop gadget,然后再次触发漏洞,使用任意参数调用任意函数。

另一种简单的方法是销毁delay_leak,并分配另一个AudioArray来覆盖HRTFPannervtable,然后使用虚析构函数来运行代码。这正是我的目标。这样一来,我甚至不需要利用原始的UAF漏洞来调用任何虚函数。

最后,我使用了以下gadget:

  //mov rax,QWORD PTR [rdi + 0x20]; <-- function call
  //mov rsi,QWORD PTR [rdi + 0x98]; <-- arg0
  //mov rdx,QWORD PTR [rdi + 0xa0]; <-- arg1
  //add rdi, 0x28 <--- arg2

大致在这个符号的地址(这只是带有三个参数的回调之一):

base::internal::Invoker<base::internal::BindState<void (*)(blink::KURL const&, base::WaitableEvent*, std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> >*), blink::KURL, WTF::CrossThreadUnretainedWrapper<base::WaitableEvent>, WTF::CrossThreadUnretainedWrapper<std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> > > >, void ()>::RunOnce(base::internal::BindStateBase*)

libchrome中,有很多这样的回调,它们基本上使得gadget可以使用任意参数来调用任意函数,尽管在其中要找到具有正确类型的函数可能会很漫长。

使用这个gadget调用OS::SetPermissions,可以让我将受控数据的页面权限覆盖到rwx,这样可以让我在渲染器中运行任意Shell代码。

 

五、总结

在这篇文章中,我详细介绍了CVE-2020-6449漏洞的利用细节,以及利用过程中涉及到的一些常见策略和技术。我们还看到了内存分配器中的缓解措施,借助这些缓解措施可以导致漏洞利用难度加大。最后,我们通过减少替换对象中的指针字段来实现漏洞利用,这是一个相当有限的原语。这表明,如果同时存在几个漏洞点,即使这几个漏洞很有限,组合起来也有可能会导致严重的后果。幸运的是,根据Chrome中的沙箱体系结构,要利用这个漏洞,还需要配合另一个沙箱逃逸漏洞。这也说明了从多个级别处理安全性的重要性,这会真正有助于提高Chrome的安全性。

可以在这里找到完整的漏洞利用。我再Ubuntu上针对80.0.3987.137的版本进行了测试。

(完)