chrome_v8_pwn入门

chrome_v8_pwn入门

前置知识

v8是什么

JS引擎是浏览器的一部分,用于解析前端Javascript代码,目前市面上有多个不同大厂开发的JS引擎,也有应用于除浏览器外的其他项目。JS代码从服务端获取,在客户端浏览器执行,如果JS引擎中包含漏洞,就能通过发送恶意JS代码给客户端对客户端机器发起攻击,如远程代码执行、DDOS。本文主要学习名为V8的JS引擎的漏洞挖掘与利用,因为v8市场占有率最高、相关资料多、开源且有完善的调试功能。

V8是Google开源的JS引擎,实现了ECMAScript和WebAssembly,采用C++开发,应用于chrome、node.js、Election等,功能包括:解释执行、编译执行、代码优化、垃圾回收等。

简单粗暴地讲,v8是源码编译后得到的名为d8的二进制可执行文件。当然,也可以取v8的源码嵌入到自己的项目中。

浏览器架构

梳理浏览器架构,进一步了解JS引擎在浏览器中的定位和作用

整体架构

从功能和组成的角度分析,主要包括以下部分:

  • 用户界面(Browser GUI)

  • 浏览器引擎(Browser Engine)

    在用户界面和渲染引擎之间传递数据和指令

  • 渲染引擎(Rendering Engine)

    解析HTML和CSS文本等,将网页内容渲染呈现出来

  • JS引擎(Javascript Engine)

    解析执行Javascript 语言,实现动态网页

  • 网络模块(Networking)

    处理网络请求,比如http请求网页、图片资源等

  • 数据存储(data storage)

    管理硬盘中保存的书签、cookie、缓存、偏好设置等各种用户数据

  • 用户界面后端(UI backend)

    为界面绘制提供图形库接口

layers

图片来源

浏览器内核包含了渲染引擎和JS引擎,目前主流浏览器内核有blink(Chrome)、webkit(Safari)、Gecko(FireFox)等。

网上许多文章将浏览器内核直接称作渲染引擎,笔者认为这并不妥当,容易引起误解,因为浏览器内核相当于内置了JS引擎的渲染引擎,并不是单纯的渲染引擎。当然,如果将JS引擎视作渲染引擎的一部分,这样称呼也没问题,但是随着JS引擎越来越独立,大部分文章默认二者是平级关系,而不是包含关系,这就引起了歧义。

多进程架构

从进程和线程的角度分析,chrome采用使用IPC通信的多进程架构,目前正在由原本的多进程架构向面向服务架构(Services Oriented Architecture,简称SOA)改进。

主要包括以下几种进程:

  • 浏览器主进程(Browser Process)

    主要负责界面显示, 用户交互, 子进程管理, 同时提供存储等功能。

  • 渲染进程(Render Process)

    核心任务是将HTML, CSS和JavaScript 转换为用户可以与之交互的网页,渲染引擎和JS引擎运行在该类进程中。Chrome默认一个渲染进程负责一个标签页。 但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点,则新页面复用父页面的渲染进程。

  • 插件进程(Plugin Process)

    运行扩展插件,为了避免插件崩溃影响其它组件,每个插件对应一个进程。

  • 基础服务进程

    包括GPU进程、网络进程、Audio进程、Profile进程等。基础服务进程在硬件资源受限的情况下能够整合到一个进程中以节省内存。GPU进程例外,不参与合并。

如图,包括渲染进程在内的大部分进程运行在沙箱

1

图片来源

v8工作原理

JS是一门解释型语言,其代码翻译成字节码后在v8内置的VM中运行。解释型语言跨平台性好,但是运行效率低。为了提高运行效率,V8采用了即时编译(JIT),结合使用编译器和解释器:对于执行次数较少的普通代码,由解释器(VM)执行;对于高频出现的热代码,优化并编译为机器码执行。V8主要包含以下组件:

  • Parse(解析器)

    将JS代码转换为抽象语法树(AST)

  • Ignition(解释器/VM)

    将AST转换成字节码并在VM中执行

  • TurboFan(编译器)

    将字节码编译优化为机器码并执行

2

图片来源

环境搭建及调试

v8开源仓库 v8官方文档 Chromium官网

环境:ubuntu2004虚拟机

VPN配置

要在虚拟机中访问外网,笔者采用VPN的方式,参考在虚拟机中使用clash科学上网VMware虚拟机网络配置-NAT篇

另外还需要配置git和curl的代理,如下,192.168.43.33改为宿主机ip,7890为clash的局域网代理端口

1
2
3
4
5
6
git config --global http.proxy 'socks5://192.168.43.33:7890'
git config --global https.proxy 'socks5://192.168.43.33:7890'
git config --global https.proxy 'http://192.168.43.33:7890'
git config --global http.proxy 'http://192.168.43.33:7890'
echo 'export http_proxy=http://192.168.43.33:7890' >> ~/.bashrc
echo 'export https_proxy=http://192.168.43.33:7890' >> ~/.bashrc

编译v8

这里需要了解git相关的术语和概念。

题目通常会给出commit哈希和存在漏洞的diff补丁文件,我们需要给对应commit版本的v8源码打上diff补丁后编译,获得存在漏洞的d8二进制程序。如果没有给出commit版本/哈希,给了chrome浏览器,可以打开浏览器输入chrome://version查看chrome版本,再在github根目录的DEPS文件中搜索v8_version查看commit版本。

安装ninja,用于编译

1
sudo apt install ninja-build

安装depot_tools,其中集成了一系列chromium开发工具:

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=/path/to/depot_tools:$PATH' >> ~/.bashrc

首次运行gclient,更新并初始化depot_tools,然后关闭自动更新

1
2
gclient
echo 'export DEPOT_TOOLS_UPDATE=0' >> ~/.bashrc

如下说明初始化成功

3

获取最新版v8源码到本地并安装依赖

1
2
3
4
fetch v8
cd v8
gclient sync -D
./build/install-build-deps.sh

做题时需要切换到题目的源码版本,这里以例题分析中的starctf2019 - OOB为例

1
2
3
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598  #切换源码分支
gclient sync -D #下载依赖并删去不需要的依赖
git apply < path_to/oob.diff #打上题目给的补丁

编译分为release版本和debug版本,debug版本输出的对象结构等调试信息更全面,但有很多检查导致调试漏洞时报错,因此一般使用release版本进行调试,debug版本辅助分析。32位将x64改为ia32

1
2
./tools/dev/gm.py x64.release
./tools/dev/gm.py x64.debug

编译结果位于v8/out/x64.releasev8/out/x64.debug

调试

向gdb中加入v8调试插件

1
2
source '/home/op1n/Desktop/browser_v8/v8/tools/gdbinit'
source '/home/op1n/Desktop/browser_v8/v8/tools/gdb-v8-support.py'

调试相关的d8运行参数

1
2
3
4
--allow-natives-syntax #允许在源代码中使用V8提供的原生API语法
--trace-turbo #跟踪生成TurboFan IR
--print-bytecode #打印生成的bytecode
--shell #运行脚本后切入交互模式
  • --allow-natives-syntax参数主要用于引入以下函数
1
2
%DebugPrint(obj) //打印对象相关信息,debug版本输出更详细
%SystemBreak() //下断点,结合gdb等调试器使用
  • --trace-turbo参数用于生成turbo-*.json格式的IR图数据文件,在turbolizer工具加载文件生成可视化IR图
1
npm install -g turbolizer  #安装turbolizer

4

效果如下

5

gdb调试技巧

  • 结合--allow-natives-syntax参数调试js代码
1
2
~/Desktop/browser_v8/v8/out/x64.release$ gdb ./d8
pwndbg> set args --allow-natives-syntax ./js_code/test.js
  • telescope查看指定地址的内存数据
1
telescope addr [length]
  • job

查看JavaScript对象的内存结构

1
job 对象地址
  • v8/tools/gdbinit中有更多命令的定义和注释

tips:由于指针标记机制,gdb中查看对象时,job命令的参数为对象地址,其它命令(x、telescope等)的参数为对象地址-1

v8基础

数据类型和对象

js 中,数据类型可以分为以下两类:

  • 基础数据类型:undefinednullNumberStringBooleanSymbol(ES6),BigInt(ES10)
  • 引用数据类型:ObjectArrayFunctionDataRegExpArrayBuffer

基础数据类型创建后不可修改,对其进行赋值操作实际上是销毁再创建;引用数据类型可修改,赋值操作相当于改变原来的内存数据。

对象存储在堆(实际上是anon段,非heap系统堆)中,栈中保存对象的引用地址(指针)。

除了Number中的Smis,v8中类型变量均以对象形式存储在堆上,栈上存放引用指针。

指针标记

v8中数据都以对象引用指针的形式表示,但如果最基本的小整数(Smis)都要作为对象存储,将会带来巨大的开支,比如:循环中递增索引时,每次都需要分配新的number对象。

为了解决这类问题,V8使用指针标记技术来区分小整数和指针,使得小整数(Smis)以立即数而非对象的形式与指针共同存储。

1
2
Smi: Represented as value << 32, i.e 0xdeadbeef is represented as 0xdeadbeef00000000
Pointers: Represented as addr & 1. 0x2233ad9c2ed8 is represented as 0x2233ad9c2ed9

因为v8分配的堆对象地址是字对齐的(4byte/8byte),所以指针最低2/3位始终为0,可以用来存储其它信息:最后一位区分指针和Smi(1表示指针,0表示Smi),倒数第二位区分强引用和弱引用。

在64位机器中,Smi的高32位表示数值,低32位始终为0,如下图

1
2
3
            |----- 32 bits -----|----- 32 bits -----|
Pointer: |________________address______________w1|
Smi: |____int32_value____|0000000000000000000|

内存布局

在gdb中使用vmmap查看内存布局,可以发现除了栈和系统堆heap,低地址内存中还有大片由v8通过mmap申请的anon区域,创建的对象都存储在该片区域。

6

接着我们观察v8堆中对象的布局,我们采用不同方式创建两个数组对象,观察它们对象结构和元素的存储地址

1
2
3
4
5
var a = [1,2,3]
var b = new Array(3);
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();

可以分析出由低地址向高地址拓展,布局如下:

1
2
3
4
5
6
--------低地址--------
- elements: 0x0d2d08d8dd21 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] //a元素地址
DebugPrint: 0xd2d08d8dd91: [JSArray] //a对象地址
DebugPrint: 0xd2d08d8ddb1: [JSArray] //b对象地址
- elements: 0x0d2d08d8dde1 <FixedArray[3]> [HOLEY_SMI_ELEMENTS] //b元素地址
--------高地址--------

直接创建的Array,元素存储在低地址,对象存储在高地址;new创建的Array,元素存储在高地址,对象存储在低地址。

对象的结构

在debug版本中调试分析,主要分析String、Array和ArrayBuffer的对象结构

String

1
2
3
var a = "AAAABBBBCCCC";
%DebugPrint(a);
%SystemBreak();

%DebugPrint输出了变量所在的函数栈帧和map信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#AAAABBBBCCCC
fp = 0x7fff2dcce0d0, sp = 0x7fff2dcce090, caller_sp = 0x7fff2dcce0e0: 0x10ea1d880461: [Map]
- type: ONE_BYTE_INTERNALIZED_STRING_TYPE
- instance size: variable
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x10ea1d8804d1 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x10ea1d880259 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x10ea1d8801d9 <null>
- constructor: 0x10ea1d8801d9 <null>
- dependent code: 0x10ea1d8802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

我们在栈帧中找到对象地址并查看,发现从低地址到高地址其结构如下

  • 字符串长度
  • 字符串本体,使用0xdeadbeed填充至字对齐
  • map属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> job 0x3b515481f211
#AAAABBBBCCCC
pwndbg> x/20gx 0x3b515481f210
0x3b515481f210: 0x000010ea1d880461 0x0000000c5e39f07e <-----
0x3b515481f220: 0x4242424241414141 0xdeadbeed43434343 <-----
0x3b515481f230: 0x000010ea1d880461 0x0000000a4e921a1a
0x3b515481f240: 0x6972506775626544 0xdeadbeedbead746e
0x3b515481f250: 0x000010ea1d880461 0x0000000b98f2aa1e
0x3b515481f260: 0x72426d6574737953 0xdeadbeedbe6b6165
0x3b515481f270: 0x000010ea1d880991 0x0000000700000000
0x3b515481f280: 0x00000c4300000000 0x0000000000000000
0x3b515481f290: 0x0000000000000000 0x000010ea1d880751
0x3b515481f2a0: 0xffffffff00000000 0x0000000000000000
pwndbg> job 0xc5e39f07e
Smi: 0xc (12)

Array

1
2
3
var a = [1,2,3];
%DebugPrint(a);
%SystemBreak();

其结构如下:

  • map属性
  • prototype - 对象的原型
  • elements - 数组中元素的存储地址(对象结构体的低地址不远处)
  • length - 数组中的元素个数
  • properties - 对象的属性描述符

也就是说,在内存申请上,v8先申请了一块内存存储元素内容,然后申请了一块内存存储对象结构

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
DebugPrint: 0x29eb68f8dd71: [JSArray]
- map: 0x057f96042fc9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1fc0fca11111 <JSArray[0]>
- elements: 0x29eb68f8dda1 <FixedArray[3]> [HOLEY_ELEMENTS]
- length: 3
- properties: 0x15b1b8dc0c71 <FixedArray[0]> {
#length: 0x095c4f0001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x29eb68f8dda1 <FixedArray[3]> {
0: 0x1fc0fca1f211 <String[#4]: str1>
1: 233
2: 0x1fc0fca1f361 <HeapNumber 1.1>
}
0x57f96042fc9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x057f96042f79 <Map(PACKED_ELEMENTS)>
- prototype_validity cell: 0x095c4f000609 <Cell value= 1>
- instance descriptors (own) #1: 0x1fc0fca11f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- prototype: 0x1fc0fca11111 <JSArray[0]>
- constructor: 0x1fc0fca10ec1 <JSFunction Array (sfi = 0x95c4f00a9b9)>
- dependent code: 0x15b1b8dc02c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

ArrayBuffer

ArrayBuffer表示一块定长的原始二进制数据缓冲区,不能直接操作,需要通过TypeArray(如Uint8Array、Int16Array、Float64Array等)将缓冲区与数据类型相关联,才能以特定的数据类型格式访问内存空间。

1
2
3
4
5
6
var a = new ArrayBuffer(20);
let view = new Uint32Array(a);
view[0] = 0x1234;
view[1] = 0x5678;
%DebugPrint(a);
%SystemBreak();

结构如下,其中需要关注的是backing_storebyte_length

backing_store指向ArrayBuffer开辟的内存空间,该内存位于系统堆上,我们可以使用TypedArray指定的类型读写该区域。

byte_length表示缓冲区的长度。

针对ArrayBuffer的常见利用:

  • 修改byte_length造成越界访问(oob)
  • 修改backing_store指针实现任意地址读写
  • 通过backing_store指针泄露堆地址
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
DebugPrint: 0x242a6484ddc9: [JSArrayBuffer]
- map: 0x06145b4021b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x20a6f9e0e981 <Object map = 0x6145b402209>
- elements: 0x341e7d1c0c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x55c0900ce200
- byte_length: 20
- detachable
- properties: 0x341e7d1c0c71 <FixedArray[0]> {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
0x6145b4021b9: [Map]
- type: JS_ARRAY_BUFFER_TYPE
- instance size: 64
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x341e7d1c04d1 <undefined>
- prototype_validity cell: 0x10af3bbc0609 <Cell value= 1>
- instance descriptors (own) #0: 0x341e7d1c0259 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x20a6f9e0e981 <Object map = 0x6145b402209>
- constructor: 0x20a6f9e0e7e9 <JSFunction ArrayBuffer (sfi = 0x10af3bbd1221)>
- dependent code: 0x341e7d1c02c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

map属性

不同类型数据的map属性结构相同。其中几个字段的含义如下:

  • type:对象的动态类型,如String、Uint8Array、Array等
  • instance size:对象的大小(以字节为单位)
  • elements kind:元素类型,如浮点数、指针

map属性定义了v8对js对象的解析方式(比如将对象作为什么类型访问),通过修改对象的map为其它类型对象的map,我们能够实现类型混淆,继而实现地址泄露和内存读写。

这里我们暂时不需要更深入地了解每个字段的含义,因为是对整个map进行替换。

例题分析

一般浏览器的出题有两种,一种是diff修改v8引擎源代码,人为制造出一个漏洞,另一种是直接采用某个cve漏洞。一般在大型比赛中会直接采用第二种方式,更考验选手的实战能力。

同时,v8 pwn的目标是任意代码执行,而不是单纯的getshell,因此传统pwn中onegadget等手法不适用

starctf2019 - OOB

远程nc后给出commits哈希,附件里有diff文件

1
the v8 commits is 6dc88c191f5ecc5389dc26efa3ca0907faef3598.

分析diff文件

为Array对象注册oob函数,内部表示为kArrayOob

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- 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);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

ArrayOob函数的具体实现,漏洞出现在这里

C++中成员函数的第一个参数必定是this指针,因此oob函数无参数时len为1,函数返回Array[length],存在一个元素的越界读;oob函数有一个参数时将该参数存入Array[length]并返回undefine,存在一个元素的越界写。

结合先前对内存布局的分析,对于非new创建的Array,这里越界读写的就是当前Array对象的map地址

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- 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)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);

关联kArrayOob类型和实现函数ArrayOob

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
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- 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:

构造原语

addressOf

fakeObject

实现任意地址读写

利用wasm机制执行shellcode

Reference

Chrome Browser Exploitation, Part 1: Introduction to V8 and JavaScript Internals

How browsers work - Behind the scenes of modern web browsers

浏览器架构的前生今世

Chrome浏览器引擎 Blink & V8

[sky123]Chrome v8 pwn

[Shuwen’s blog]chrome study by v8 oob

V8 编译浅谈

[译]了解 V8 内存管理

v8 exploit入门[PlaidCTF roll a d8]


chrome_v8_pwn入门
https://lkliki.github.io/2024/11/11/chrome-v8-pwn入门/
作者
0P1N
发布于
2024年11月11日
许可协议