一、前言
在2020年3月,我发现了Chrome WebAudio模块中存在释放后使用(UAF)漏洞。这是在非垃圾回收对象上存在UAF漏洞,该对象由PartitionAlloc
内存分配器进行分配。在blink中(WebAudio是其中的组成部分),根据堆对象的类型为堆对象分配了不同的内存分配器。例如,大多数垃圾回收对象是由Oilpan
分配的,而非垃圾回收对象是由PartitionAlloc
分配的。ArrayBuffer
和String
的后备存储例外,即使对象本身属于垃圾回收对象,它们也是在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分配在Buffer
和ArrayBuffer
中,而普通对象是在Fast
区域中分配。首先,这种分隔让我们很难使用ArrayBuffer
或Buffer
区域中的内存损坏来劫持控制流。其次,这导致在Fast
区域中很难利用内存损坏来在ArrayBuffer
或Buffer
区域中创建数据可控的伪造对象,因为其中的写入原语非常有限。在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
包含两个子类,也就是ConstantSourceNode
和OscillatorNode
。当调用AudioScheduleSourceNode
的start
方法时,其AudioHandler
将添加到active_source_handlers_
。例如,我们可以看一下Ticket 1057593的PoC,其中有两行。
let src = audioCtx.createConstantSource();
src.start();
将src
的AudioHandler
添加到active_source_handlers_
。此时,node src
和active_source_handlers_
都负责保证AudioHandler
处于活动状态。在src
上调用stop
事件时,会立即安排src
的stop
事件。而这个事件会由HandleStoppableSourceNode
函数处理,并将其添加到finished_source_handlers_
中。在这里,我们可以暂停audio,并通过处理promise运行JavaScript:
audioCtx.suspend((3 * 128)/3072.0).then(()=>{
gc();
audioCtx.resume();
});
由于现在停止了创建的constantSource
(ConstantSourceNode
的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
。创建AudioWorkletNode
和PannerNode
的主要原因是控制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
的子类,即ConstantSourceHandler
或OscillatorHandler
。这些对象在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
来减少BreakConnectionWithLock
,biquad_
是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,并且与我设法移动的AudioArray
的allocate_
字段位于同一容器中。理想情况下,我希望将HRTFPanner
分配给损坏的location_
指针的位置,让这个HRTFPanner
的开头部分与占用前一个chunk的另一个对象的最后部分重合,如下图所示。
然后,当我分配HRTFPanner
时,其vtable
和共享指针字段database_loader_
将映射到对象的末尾,占用前一个chunk,因此,如果我能够找到一个可以轻松在JavaScript中读取其字段的对象,就可以实现这一点。但是,在查看了不同大小的多个对象后,并没有找到一个很适合的目标。
并且,如前所述,PartitionAlloc
区分了数据容器和普通对象的分配。像HRTFPanner
这些对象是在Fast
区域中分配的,而数据容器是在缓冲区区域或数组缓冲区区域中分配的,因此我们不能仅分配动态大小的对象(例如JavaScript中的ArrayBuffer),使其和HRTFPanner
重叠,并从条目中读取vtable
等内容。
回顾之前,我首先使用AudioArray
的allocation_
字段破坏了空闲列表,该列表是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
来覆盖HRTFPanner
的vtable
,然后使用虚析构函数来运行代码。这正是我的目标。这样一来,我甚至不需要利用原始的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的版本进行了测试。