1

Chrome 沙箱绕过研究

 2 years ago
source link: https://paper.seebug.org/1947/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者: 启明星辰ADLab
原文链接:https://mp.weixin.qq.com/s/gqH0lqz1ey6IzT--UD9Jsg

01 研究背景

沙箱作为很多主流应用的安全架构的重要组成部分,将进程限制在一个有限的环境内,避免该进程对磁盘等系统资源进行直接访问。Chromium 中的沙箱进程通过pipe等方式和具有I/O等高权限的进程交互来完成进一步的操作,因此利用IPC绕过沙箱成为一种常见的方式,而渲染进程和无沙箱的browser进程之间的通信也成为了被关注的重点。Chromium IPC 包括Legacy IPC和 Mojo,本文主要介绍Mojo IPC机制,同时对Mojo IPC经典案例进行跟踪分析。

02 Mojo IPC介绍

在Mojo的文档的System Overview,超链接[1]中给出了Mojo的定义。

图片

Mojo 使得IPC通信成为可能。要想使用Mojo(参见超链接[2]),首先要定义一个Mojom的文件,这个文件中定义了接口(interface),每个接口中定义了消息(message)。定义mojom文件//services/db/public/mojom/db.mojom:

图片

添加BUILD.GN目标//services/db/public/mojom/BUILD.gn:

图片

向需要这个接口的目标添加依赖,这里添加到src/BUILD.gn:

图片

使用bindings generators 处理mojom文件,默认生成C++代码,通过指定后缀也可以生成其他语言(js或者java)的绑定。

图片

mojom和生成的C++绑定代码的对应如下:

图片

生成接口文件后,需要定义pipe以及pipe两端的handle 对象,这样才能发送消息。接收方如果想对消息进行接收处理,需要对接口进行实现。下面是C++定义pipe的两种方式:

图片

logger和receiver分别是pipe两端的handle对象,分别代表发送端和接收端。此时可以使用logger->Log(“Hello”) 发送消息。接收端想要接收消息,首先要对mojo::PendingReceiver<T>进行绑定,最常见的就是将其绑定为mojo::Receiver<T>。一旦pipe上有可读的消息,Receiver 就会读取消息,反序列化消息,然后将该消息派遣到T的实现上。下图是T的实现:

图片

如果发送一个消息希望得到返回信息,mojom文件应该像下面这样:

图片

生成的C++接口如下:

图片

发送端在发送消息时,可发送一个回调函数,而接收端在调用该消息的实现时,会在内部调用该回调函数,将这个消息的处理结果再发送给发送端。

图片

上面以C++绑定作为实例,同理其他语言的绑定。例如js绑定如下:

图片

在上面的实例中,echoServicePtr 相当于C++的 mojo::Remote<T>为发送端,echoServiceRequest 相当于C++的 mojo::PendingReceiver<T>为接收端。echoServiceBinding 相当于C++的 mojo::Receiver<T>已绑定的接收端,即可以处理接收的消息。

03 沙箱绕过案例分析

chromium的实现采用多进程方式,渲染进程和browser进程间就可以通过使用mojo IPC的方式进行通信。由于browser 是无沙箱运行的,通过与browser 的漏洞的交互,渲染进程就可以穿越沙箱执行任意代码。下面通过一个经典案例来详细跟踪沙箱绕过的过程。

首先,启用blink 特征参数“ --enable-blink-features=MojoJS,MojoJSTest ”,该参数可以模拟被妥协的渲染进程,使得js 可以直接访问Mojo。如果有一个真正的妥协的渲染进程,可以通过修改内存直接开启此功能,使得渲染进程具有MojoJS的能力。

下面是漏洞触发时对应的现场环境,以及源代码情况,可以看到,该漏洞是由于render_frame_host 对象被释放后,由于该对象在FilterInstalledApps方法中被引用并进行连续的方法调用导致的释放后重用:

图片
图片
图片

InstalledAppProviderImpl 是浏览器进程对接口InstalledAppProvider的实现:

图片

接口的定义文件为third_party/blink/public/mojom/installedapp/installed_app_provider.mojom:

图片

由此可见,浏览器进程中实现了InstalledAppProvider接口,渲染进程通过该接口与浏览器进程通信。在Create 静态方法中接收渲染进程发送的mojo::PendingReceiver。

图片
图片

在该方法中使用mojo::MakeSelfOwnedReceiver函数进行接收的绑定。在C++绑定API中查看该函数的使用,该函数会将接口的实现以及Receiver进行绑定。使得实现对象的生命周期和pipe的生命周期相同。一旦绑定的一端检测到错误或者pipe关闭的时候,就会将实现对象回收。

图片

在绑定的过程中会创建接口的实现实例,实例中保留了RenderFrameHost 对象:

图片

browser进程保留RenderFrameHost和渲染进程中的框架进行交互。当框架销毁时对应的RenderFrameHost也会随之销毁。

图片

browser进程在每个框架初始化时,会调用PopulateFrameBinders 将框架对应的接口的创建函数存入map中,表示当前框架可以使用的接口,当某个框架中使用创建某个接口的pipe时,就会在这个map中查找创建接口实现的函数:

图片

当创建好pipe,并设置好两端的handle对象,那该pipe就能正常使用收发消息。在处理FilterInstalledApp消息时,会引用接口实现的实例中保留的RenderFrameHost。

图片

如果在发送消息前,将该RenderFrameHost 对应的框架释放,会引起释放后重用。这个漏洞的触发可以通过navigator.getInstalledRelatedApps不断的向pipe发送消息,使得析构的框架和消息处理之间竞争,使得某个消息的处理在析构之后。但是非顶端的框架不能直接调用这个API。

图片

在实际的trigger文件中,通过在子框架中创建pipe,并且接收端绑定为接口InstalledAppProvider,将发送端绑定为一个临时的全局接口名pwn。

图片

在主框架中,分配一个子框架,在子框架绑定pwn接口时,使用interceptor.oninterfacerequest 截获该绑定,创建实际的InstalledAppProvider的发送端。这样就创建好了pipe和两端的绑定,也就创建了browser对该接口的实现。这样主框架就能有效地控制了子框架的pipe消息发送。在实现对象中包含对子框架的引用。在子框架析构后,browser对子框架的引用还在。

图片

当发送filterInstalledApps的消息时,触发UAF:

图片

每次创建一个框架,browser进程会为该框架注册可使用的接口。当一个框架申请接口时,browser进程会根据注册的接口查找该接口的创建函数。创建函数需要接收发送端的pendingReceiver,并对其绑定。对pendingReceiver的绑定有多种,在InstalledAppProvider接口中使用mojo::MakeSelfOwnedReceiver将接口的实现和接收端进行绑定。这个函数会将接口的实现对象的生命周期和pipe的生命周期进行捆绑。这个函数在调用的过程中会创建实现对象。在实现对象中保存了框架指针。当收到FilterInstalledApps的消息时,该函数调用了框架指针的虚函数。如果创建了pipe并且绑定完毕,删除框架,但pipe依然存在。在这个时候发送消息,就会引发框架的释放后重用。在漏洞触发上,能够保证框架在销毁后,pipe还能保持连接很重要。这样才有机会触发漏洞。通过在子框架中创建pipe,并且截获接口发送端的绑定,将子框架中的发送指针传递到主框架中,这样就保证了pipe的连接。pipe的消息发送也就得到了有效控制。

chrome的每个进程都有一个全局对象CommandLine,这个对象保存了这个进程运行时传递的参数,如果能向这个对象中传递--no-sandbox参数,当渲染进程重新加载后就会在无沙箱的进程中执行程序。

图片

从漏洞分析上可以漏洞利用需要首先控制对象的虚表指针。但在没有泄漏任何地址的情况下,如何控制RenderFrameHost的虚表指针呢?由于在Windows上每次加载chrome.dll的基址基本不变,而这个库包含了chrome的大部分代码 ,作为渲染进程和浏览器进程共享的库。实际上结合js的漏洞可以泄漏这个库的基址。这个库为漏洞基础的搭建创造了条件。

对于RenderFrameHost对象的替换,使用RenderFrameHost在调试的版本中对象的大小是0xc38,使用可控大小的blob对象进行占用。

图片

使用mojo bindings创建blob对象,实现了对blob对象的创建 、释放、以及数据的读取。

图片

因为共有三次连续的虚函数调用,必须保证前一次调用的虚函数返回内容是可控的。这里找到可以返回对象成员的指针,这样返回内容就是可控的。在这里找到的虚函数如下:

图片

该函数返回的内容正是在对象偏移8的位置上:

图片

调用流程如下:

图片

三次调用的偏移分别为0x48,0x0d0和0x18。

图片

allocReadable函数中触发两次漏洞。在触发漏洞之前,会将要写入缓存的内容写入占位缓存buf1的末端:

图片

第一次触发漏洞,控制程序执行调用chrome!content::WebContentsImpl::GetWakeLockContext,该函数调用chrome!content::WakeLockContextHost::WakeLockContextHost创建WakeLockContextHost对象,并将WakeLockContextHost对象的地址写入占位的buf1+0x10+0x650处。

图片

图片

第二次触发漏洞,控制程序执行调用chrome!`anonymous namespace'::DictionaryIterator::Start,该函数将第一次触发漏洞时创建的占位的buf1缓存地址写回到第二次触发漏洞的占位缓存buf2+0x10+0x18处,这样就泄漏this指针,从而泄漏写入内存内容的地址。

图片

另一个重要的函数是callFunction实现任意函数调用。该函数触发漏洞,调用函数chrome!content::responsiveness::MessageLoopObserver::DidProcessTask,该函数执行回调:

图片

图片

在调用任意函数之前需要伪造bindstate,bindstate的布局如下:

图片

Polymorphic_invoke 是一个函数指针,该函数负责调用functor。Polymorphic_invoke 必须知道函数参数个数以及参数类型。找到一个可以调用多个参数的invoker实现任意函数调用。

图片

在函数getCurrentProcessCommandLine中首先泄漏一个堆地址:

图片

泄漏 current_process_commandline_全局变量:

图片

图片

图片

调用一个具有拷贝功能的函数,将commandline的地址拷贝到泄漏的堆地址上,从而获得commadline的地址。

图片

调用SetCommandLineFlagsForSandboxType 关闭沙箱:

图片
图片
图片

结合js漏洞,绕过沙箱,打开本地的记事本。

图片

04 小结

有关Mojo IPC漏洞,最常见到的就是对象生命周期管理不当所带来的安全问题。本文结合Mojo的背景知识,对照Mojo的安全问题,深入研究chrome的IPC机制。同时本文跟踪了Mojo IPC的一个经典漏洞,漏洞的触发思路从可能的条件竞争到有效控制消息的发送,通过在渲染进程的commandline全局变量中,添加关闭沙箱的选项。

参考链接:

[1]https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#system-overview

[2]https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/mojo/public/cpp/bindings/README.md

[3]https://bugs.chromium.org/p/chromium/issues/detail?id=1062091


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1947/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK