通过IndexedDB条件竞争实现Chrome沙箱逃逸(下)

 

接着上文,我们开始讲这个漏洞的利用。

 

Exploitation

一旦我们通过竞争条件触发了UAF(free-after-free)场景,则可以绕过时间的限制,我们可以完美控制使用已释放对象的时间。

在下面部分中,我们要分享漏洞利用的目标是在Android上运行的64位版本的Chrome。 但只需稍加修改,该漏洞即可在Linux或Windows上利用。

构造信息泄漏

首先我们要做的事是把bug转为信息泄漏漏洞,为此我们需要将Chrome的基地址泄漏到渲染器中。 有几种方法可以完成,但我们决定使用IndexedDB mojo接口及其回调来实现。

我们没有找到直接泄漏Chrome基地址的方法,因此我们需要触发bug两次。

泄漏堆指针

我们通过分别使用两次连接(版本0,2),实现两次Open调用从而触发bug,然后调用Close和AbortTransactionsForDatabase方法来触发竞争条件并最终释放IndexedDBDatabase对象。

然后,我们可以调用CreateObjectStore方法,使用相应的对象库创建的元数据的IDB keypath字符串,重新分配释放的IndexedDBDatabase对象。

我们可以完全控制字符串keypath的内容,并用于构造假的IndexedDBDatabase对象,同时将pending_requests_.bufferpending_requests_.capacity字段设置为0。

如果现在我们调用Open方法,获取指向已释放的IndexedDBDatabase对象的mojo接口指针,即可对伪对象做一些操作。 调用Open只会向pending_requests_ queue(队列)添加一个新的OpenRequest。 因为我们将buffer(缓冲区)和capacity(容量)设置为0,所以将重新分配缓冲区,并将伪对象的pending_requests_.buffer设置为新的堆指针,而且该指针实际上存储在keypath字符串中。

通过调用Commit(数据库事务)方法,可以将对象存储库的元数据泄漏到渲染器中。因此,我们可以轻松泄漏出指向渲染器的指针。

在这之前,我们继续在已释放的IndexedDBDatabase上调用Open方法,这可以为pending_requests_队列添加更多的OpenRequest指针,从而增加底层的后备缓冲区。 通过控制对Open的调用次数,我们还可以控制分配的后备缓冲区的大小。

最后我们可以通过调用Commit方法,从返回的元数据中提取出堆指针,最终泄漏出指向后备缓冲区的指针。

使用对象替换堆指针内存

一旦我们获取指向pending_requests_的后备缓冲区的指针,我们将在释放的IndexedDBDatabase对象上继续调用几次Open方法,之后将再次重新分配后备缓冲区并且增长。 这将导致我们泄漏的堆指针被释放。

为了防止其他代码占用已泄漏指针指向的内存,需要再次调用CreateObjectStore方法,以便使用新对象库的keypath重新分配释放的后备缓冲区。 通过这点我们可以控制何时再次释放内存。

现在,我们已将一个堆指针泄漏到渲染器中,指向对象存储库的元数据中已分配的keypath字符串。

泄漏Vtable指针

为了泄漏vtable指针,我们需要再次触发漏洞。 首先我们要再次使用正常的IndexedDBDatabase对象,重新分配之前释放的IndexedDBDatabase对象,以便在第二次触发时不引发程序崩溃。 因为调用AbortTransactionsForDatabase方法会遍历database_map_并接触所有引用的对象。

在第二次触发错误之后,我们要再次使用CreateObjectStore方法,通过新制作的假对象重新分配已释放的IndexedDBDatabase对象。

在精心设计的假对象中,需要将pending_requests_.buffer设置为先前泄漏的堆指针(指向先前创建的对象存储库的元数据中的keypath字符串),并pending_requests_.capacity设置为1。

现在,我们在已释放的IndexedDBDatabase上调用Open方法一次,这将会把新的OpenRequest附加到伪对象的pending_requests_队列中。 由于容量设置为1,因此代码会尝试重新分配后备缓冲区,并释放pending_requests_.buffer指向的内存,并将其替换为更大的缓冲区。

这将会释放泄漏的堆指针指向的内存,然后我们可以通过更改数据库名称重复调用Open方法,以便重新分配有效的IndexedDBDatabase对象。

这里我们会用到一个小trick,我们把创建的数据库名称设置为一个非常大的0x4000字节的字符串。 稍后,我们将泄漏其中一个创建的IndexedDBDatabase对象的内容。该过程不仅会泄漏出vtable指针,还会泄漏指向数据库的名称字符串的指针,然后它将返回我们一个指向巨大的slcak space(松弛空间)的堆指针,我们可以利用它来存储ROP链和shellcode。

但现在我们只读取先前创建的对象存储库的元数据,该对象存储将接收其中一个IndexedDBDatabase对象的内容。

然后,我们可以使用来自已泄漏的IndexedDBDatabase对象的vtable指针以及指向对象的名称字符串的指针,从而泄漏出一个指向大内存的指针,以便我们存储ROP链和shellcode。

 

远程代码执行

只要我们泄漏出指向松弛内存和Chrome基地址的指针,我们就可以在松弛内存中放置一个伪OpenRequest对象,然后使用它的虚拟化Perform方法来获取远程代码执行并启用ROP链。

然后,我们将精心构造的伪对象放入已释放的IndexedDBDatabase的内存中,并将processing_pending_requests_设置为0,将pending_requests_.buffer设置为我们放置伪OpenRequest指针的内存。 因为processing_pending_requests 为0,对已释放的IndexedDBDatabase调用Open方法,将会对存储在`pending_requests`中的请求调用Perform方法,从而启用我们的伪对象并且获取代码执行。

ROP链

通过调用IndexedDBDatabase::ProcessRequestQueue方法,我们可以控制程序的counter(计数)。

void IndexedDBDatabase::ProcessRequestQueue() {
  // Don't run re-entrantly to avoid exploding call stacks for requests that
  // complete synchronously. The loop below will process requests until one is
  // blocked.
  if (processing_pending_requests_)
    return;

  DCHECK(!active_request_);
  DCHECK(!pending_requests_.empty());

  base::AutoReset<bool> processing(&processing_pending_requests_, true);
  do {
    active_request_ = std::move(pending_requests_.front());
    pending_requests_.pop();
    active_request_->Perform();                                             [13]
    // If the active request completed synchronously, keep going.
  } while (!active_request_ && !pending_requests_.empty());
}

在[13]处,执行ConnectionRequest::Perform方法后我们可以已获得控制权。 在调用时寄存器x0指向我们先前分配的松弛空间,这样我们就获取到远程代码执行。 相应的汇编代码如下所示:

<content::IndexedDBDatabase::ProcessRequestQueue()+72>:   ldr x0, [x21]
<content::IndexedDBDatabase::ProcessRequestQueue()+76>:   ldr x8, [x0]
<content::IndexedDBDatabase::ProcessRequestQueue()+80>:   ldr x8, [x8,#16]
<content::IndexedDBDatabase::ProcessRequestQueue()+84>:   blr x8              [13]
<content::IndexedDBDatabase::ProcessRequestQueue()+88>:   ldr x8, [x21]

策略

由于我们已经泄露出分配的松弛空间(x0)地址和Chrome基地址,那我们可以构建一个简单的ROP链,用于将松弛空间的权限提升为read/write/executable(读写执行),最后跳转到松弛空间内存中的ROP链之后,即shellcode处。

Gadgets

我们使用下面六个ROP gadgets(基于我们测试时的偏移量):

* G1: 0x4959c14 : ldr x8, [x0, #0x48]! ; ldr x1, [x8, #0x100] ; br x1
* G2: 0x1df7f8c : ldr x9, [x8, #0x190] ; ldr x6, [x8, #0x80] ; blr x9
* G3: 0x3e7a4b0 : ldr x20, [x0, #0x68] ; ldr x9, [x8] ; mov x0, x8 ;
                  ldr x9, [x9, #0xf8] ; blr x9
* G4: 0x3f9152c : ldr x2, [x0, #0x18] ; ldr x0, [x0, #0x38] ; br x2
* G5: 0x2fbf400 : ldr x8, [x8, #0x10] ; blr x8 ; ldr x8, [x20, #0x3b8] ;
                  cbz x8, #0x2fbf424 ; blr x8
* G6: 0x3f0fd88 : ldr x5, [x6, #0x28] ; ldr x4, [x6, #0x20] ;
                  ldr x3, [x6, #0x18] ; ldr x2, [x6, #0x10] ;
                  ldr x1, [x6, #8] ; mov x8, x0 ; ldr x0, [x6] ; svc #0 ; ret

在目标二进制文件上查找相应的偏移量(比如使用ROPgadget)。 第一个gadget G1是用于启动ROP链。

内存布局

ROP链以及shellcode将会被放入松弛存储空间。 具体地放置方式如下:

| Offset              | Value               | Used By | Comment                                    |
| ------------------  |:-------------------:|:-------:| ------------------------------------------:|
| 0x50 (0x48+8)       | slackbase+0x100     | G1      | x8 = slackbase+0x100                       |
| 0xb8 (0x48+0x68+8)  | slackbase           | G3      | x20 = slackbase                            |
| 0x100               | slackbase+0x260     | G3      | x9 = slackbase+0x260, x0 = slackbase+0x100 |
| 0x110 (0x100+0x10)  | gadget addr G6      | G5      | x30 = addr of ldr x8                       |
| 0x118 (0x100+0x18)  | gadget addr G5      | G4      | x2 = addr of G5                            |
| 0x138 (0x100+0x38)  | 226                 | G4      | x0 = 226                                   |
| 0x180 (0x100+0x80)  | slackbase+0x300     | G2      | x6 = slackbase+0x300                       |
| 0x200 (0x100+0x100) | gadget addr G2      | G1      | x1 = addr of G2                            |
| 0x290 (0x100+0x190) | gadget addr G3      | G2      | x9 = addr of G3                            |
| 0x300               | slackbase (aligned) | G6      | x0 = aligned slackbase                     |
| 0x308 (0x300+0x8)   | 0x4000              | G6      | x1 = 0x4000                                |
| 0x310 (0x300+0x10)  | 7                   | G6      | x2 = 7                                     |
| 0x318 (0x300+0x18)  | 0                   | G6      | x3 = 0                                     |
| 0x320 (0x300+0x20)  | 0                   | G6      | x4 = 0                                     |
| 0x328 (0x300+0x28)  | 0                   | G6      | x5 = 0                                     |
| 0x358 (0x260+0xf8)  | gadget addr G4      | G3      | x9 = addr of G4                            |
| 0x3b8               | 0x1000              | G5      | x8 = slackbase+0x1000                      |
| 0x1000              | shellcode           |         |                                            |

更改松弛内存权限为read/write/executable后,跳转到该内存中的shellcode处。

关于进程运行问题,我们在虚拟函数调用之后直接返回,那里我们可以控制程序流,从而让程序继续安全运行。

Android Exploit

针对Android 64位的Chrome利用代码,你可以在这里找到。 它是用于测试Chromiunm渲染器新补丁而制作的,带有一些简单JavaScript参考代码。 如要使用,需要调整偏移和gadgets。如果成功,那么可以获取一个具有浏览器进程权限的反向shell。

(完)