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函数,因此也公开了不同的接口集。我们可以在PopulateServiceWorkerBinders
和PopulateFrameBinders
中观察到这一点。
说明:可以通过关注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。这意味着,它的生命周期要比WebContentsImpl
和RenderFrameHostImpl
还要长!
我们还了解到,并不是每次都会创建新的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漏洞。我们期待能为漏洞防御做出贡献,以使这些漏洞更难以被利用。