作者: 启明星辰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
[3]https://bugs.chromium.org/p/chromium/issues/detail?id=1062091
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1947/