v8 Base
阅读辅助:
IE相关漏洞:Shellcode编写->IE的UAF分析-> IE堆溢出利用
JS Engine相关:V8基础->jscore exloit>roll a d8->对V8进行插桩
CTF相关漏洞:how2heap One->how2heap Two->how2heap Three->how2heap Four
学习v8过程中参考了很多资料以及师傅们的博客,在这里把自己实验过的/或者需要速查的内容整理记录下来,里面引用各位师傅的博客的内容都注明了出处,特此致谢。
V8环境配置
终端代理
编译v8必须科学上网,本地使用crashx作为代理。
终端挂代理,将下面的代码写入/etc/bashrc,每次生成终端都会自动进行代理。
1 | export https_proxy=http://127.0.0.1:7890 |
或者参考自动化命令
1 | alias setproxy="export https_proxy=http://127.0.0.1:7890;export http_proxy=http://127.0.0.1:7890;export all_proxy=socks5://127.0.0.1:7891;echo \"Set proxy successfully\" " |
curl cip.cc
检验代理(crash x开全局)
编译v8
安装依赖
1 | apt-get install binutils python2.7 perl socat git build-essential gdb gdbserver |
1. 准备depot_tools工具
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
2. ninja准备
1 | git clone https://github.com/ninja-build/ninja.git |
3. v8编译
注意要使用depot_tools下的ninja而不是/usr/bin下的ninja,使用v8gen脚本快速构建toolchain
1 | fetch v8 && cd v8&& gclient sync #获取v8源代码同步最新源代码,如果直接clone了fetch命令可以不用 |
4. 运行d8
1 | ./out.gn/x64.debug/d8 |
v8漏洞利用在browser pwn中一般有两种形式,都需要对v8进行patch然后再进行编译。
直接使用真实的cve,一般提供commit的hash值。
在编译v8以前,需要使用
git reset --hard hash值
来对版本进行回溯–>roll a d8出题人给出一个patch,人为地在v8制造一个漏洞,一般提供一个diff文件/编译好的v8可执行文件–>oob-v8
需要
git apply < diff文件
将diff文件加入v8源码分支。
范例题目
例如Star CTF 2019 oob-v8,题目信息,我们需要使用对应版本的v8,然后打上我们的patch。
1 | Yet another off by one |
下载v8源代码并且回溯版本,不过因为编译失败,后来还是选择用fetch。
1 | git clone https://github.com/v8/v8.git && cd v8 #这一步改为fetch v8 |
将diff补丁写入代码,然后编译即可。
1 | git apply < oob.diff |
diff补丁
oob.diff
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
编译v8的问题汇总
chromium-cipd : golang net/http proxy代理
gclient sync的时候提示代理连接失败
1 | [P5912 11:57:20.290 client.go:347 W] RPC failed transiently. Will retry in 1s {"error":"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: proxyconnect tcp: EOF", "host":"chrome-infra-packages.appspot.com", "method":"GetInstanceURL", "service":"cipd.Repository", "sleepTime":"1s"} |
我是通过仅代理http(不使用https和sock5)来实现的(192.168.1.106是我mac主机的地址)我在Ubuntu下执行这个命令
export https_proxy=http://192.168.1.106:7890
提示Error: client not configured; see ‘gclient config’
在v8目录下创建一个.gclient文件,然后执行gclient sync
1 | solutions = [ |
v8调试基础
Google提供的gdb插件 gdb-v8-support.py
可以在/tools目录下找到.gdbinit和gdb-v8-support.py文件,然后在~/.gdbinit文件中source引入py文件,同时将.gdbinit的内容拷贝到~/.gdbinit中
其中定义了一些方法比如job,可以在gdb中作为辅助使用
Allow-natives-syntax参数
在运行v8时附加
--allow-natives-syntax
参数,可以通过该选项调用一些有助于调试到本地运行时函数。- %DebugPrint(obj) 输出对象地址
- %SystemBreak()插入调试中断,重新调试时还会断在此处。
- Print在遇到CodeStubAssembler的代码时,可以用于输出变量值。
***readline()***添加在js代码中,让程序停下等待输入,方便gdb调试。
Polyfill 包含很多js本身实现的js函数,实现逻辑和v8很类似,但是比C++阅读难度低很多。
v8 DEBUG
V8 Object入门
这部分流程基本按照THORN大神的文章思路走下来,增加了从内存角度看的内容,很大一部分大神写的太好了就直接引用了,在这里做声明。
在Chrome中查看快照
在console中运行一段js代码
1
2
3function Fool(){}
var a=new Fool(); //构造函数主要是为了更方便地在快照中找到对象
a.name='aaa'在Memory中,设置Profiles为Heap snapshot(堆快照),点击左边的小圆点。
v8对象结构
v8的对象主要由三个指针构成
- Hidden Class(隐藏类)
- Property
- Element
隐藏类用于描述对象的结构,Property和Element用于存放对象的属性,两者的区别主要体现在键名能否被索引。仔细的读者应该发现,在对象中还包含着名为__proto__
的属性,这是JS对象中的原型链,与JS中NEW的用途有关。
Property和Elememt
1 | // 可索引属性会被存储到 Elements 指针指向的区域 |
参照ECMA规范中的要求,可索引属性需要按照索引值大小升序排列,而命令属性根据创建的顺序升序排列。
让我们运行以下代码。
1 | var a = { 1: "a", 2: "b", "first": 1, 3: "c", "second": 2 } |
对象a以可索引属性开头,存储时按照可索引对象升序排列,命名属性first比second要早创建,所以first排列在secnod前。对象b以命名属性开头,存储时与可索引部分与a相同,命名属性按照创建先后排列,second在first之前。而可索引属性和命名属性都很明显是分开的,也验证了两者时分开存储的理论。
结论
- 索引的属性按照升序排列,命名属性根据创建的顺序升序排列
- 同时使用两种属性时,两个属性分开存储
让我们来实际论证一下这个理论,通过之前的Heap快照来查看内存中的对象。
1 | function Foo1 () {} |
a和b都包含命名属性name和text,a还拥有两个可索引属性。
a和b两个对象指向的隐藏类是同一个(图中的Map @111025),即说明a和b的对象具有相同的结构。
思考一下,为什么a和b的属性并不相同,但是两者却拥有相同的结构?THORN大神的文章中解释,可索引属性本身就是有序排列,并不需要通过结构去查找,所以也就不会和隐藏类有什么关系了。两者拥有相同隐藏类的原因还是取决于包含相同的命名属性name和text。
大神还给出另外一个案例
1 | a[1111]='aaa' |
在a对象增加了一个可索引属性[1111]。a对象和b对象,两者的隐藏类发生了变化。可索引数组Elememet中的数组也忽然没有了规律。因为写入了a[1111]之后,数组变成了稀疏数组,而为了节省空间,稀疏数组会转换为哈希存储的方式,而不是用一个完整的数组来描述这块空间。有趣的事,上文隐藏类似乎与可索引属性并没有什么关系,但是在这里,Elements从数组变成了稀疏数组,Property并没有变化,但是隐藏类却发生了变化。
大神的分析(咱也不懂,咱也不敢问)
可能是为了描述
Element
的结构发生改变
让我们从调试角度再来回顾一下上文的内容
通过*%DebugPrint(a)来查看对象a在内存中的分布,也可以通过job 0x2c671120f451*来查看这个结构。
1 | d8> %DebugPrint(a) |
1 | d8> %DebugPrint(b) |
通过telescope命令(可见显示指针关系),比较接近在Chrome中的观感。需要注意的是v8为了区别对象和值类型的区别,在对象的最低位设置为1,所以使用命令时需要-1才能获得正确的值。
1 | pwndbg> telescope 0x2c671120f451-1 |
验证前文property和element属性的分开存放的假设,初始时两者的值是相同的。properties数组中存放的值如果相同(例如都是’aaa’),也会引用同一块内存用来节省空间,类似隐藏类的实现。
命名属性(property)的不同存储方式
V8 中命名属性有三种的不同存储方式:对象内属性(in-object)、快属性(fast)和慢属性(slow)
- 对象内属性,保存在对象本身,访问速度最快
- 快属性,比前者多一次寻址次数
- 慢属性速度最慢,将属性的完整结构存储在内(前两种属性的结构会将结构放在隐藏类中描述)
1 | //三种不同类型的 Property 存储模式 |
对象内属性和快属性的结构基本相同,对象内属性因为对象存储空间的限制,所以在超过10个属性之后多余的部分就会进入property(命名属性)中。
对象内属性
快属性:多出的两个属性被存入property中,并且有序索引。
慢属性:property中的索引变得无序,说明这个对象已经采用了hash存取结构了。
至于为什么要采用三种方式进行存储,无非是为了让v8更快一些。这里就不做记录了,有兴趣可以看参考文献。
隐藏类
Hidden Class,即隐藏类,V8借用了类和偏移位置的思想,将本来通过属性名匹配来访问属性值的方法进行了改进,使用类似C++编译器的偏移位置机制来实现。在V8的Memory检查器中,隐藏类被称为Map。在SpiderMonkey中,类似的设计被称为Shape。
隐藏类的目的只有两个,运行更快和占内存空间更小。我们这里从节省内存空间讨论。
在 ECMAScript 中,对象属性的 Attribute被描述为以下结构。
[[Value]]
:属性的值[[Writable]]
:定义属性是否可写(即是否能被重新分配)[[Enumerable]]
:定义属性是否可枚举[[Configurable]]
:定义属性是否可配置(删除)
隐藏类的引入,将属性的 Value
与其它 Attribute
分开。一般情况下,对象的 Value 是经常会发生变动的,而 Attribute
是几乎不怎么会变的。没有没有必要重复Attribute的剩余部分。
隐藏类的创建
对象创建过程中,每添加一个命名属性,都会对应一个生成一个新的隐藏类。在 V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。
下面的例子中,a 在空对象时、添加 name
属性后、添加 text
属性后会分别对应不同的隐藏类。
1 | function Foo3 (){}; |
隐藏类生成概念图
查看堆内存快照,在创建name时,Hidden Class1创建了一个Attribute,当Hidden Class2创建的时候,name部分的属性会直接饮用Hidden Class1的Attribute。快照中可以看到back_pointer指向前一个Hidden Class。
隐藏类的结构
让我们从上文DebugPrint(a)的数据来看
1 | function Foo1 () {} |
a对象中的map,各个部分的意义在注释中
1 | 0x3ef5ef90ce91: [Map] |
job查看其中的对象实例
1 | pwndbg> job 0x2c671120f831 |
通过内存实验,验证一下MAP创建的过程。
1 | function Foo3 (){}; |
创建a.name之后,a.text之前的MAP
1 | 0x296dce70cf41: [Map] |
创建a.text之后的MAP
1 | 0x296dce70cff1: [Map] |
V8内存模型
V8的Object内存继承关系如下,继承比较复杂看源代码的时候需要注意从基类继承下来的变量。
1 | Object |
Object
Object主要包含两种类型
- Smi(Samll Integer) <–整数
- 整数由带符号的31位范围表示
- 整数由带符号的32位范围表示
- 最后一bit(LSB)始终为0
- HeapObject. <–V8大部分存储类型的基类
- 除了整数以外的其他类(包括范围在Smi表达以外的整数,例如Double)
- 始终有个指向Map的指针。
- 最后一bit位始终是1,在解引用的时候要做-1操作
- HeapObject基本上由GC管理,因此位于GC区域,而不是HEAP的区域(堆喷射中一连串均匀的非HEAP内存,那个一般就是JS的GC堆)
- 除了整数以外的其他类(包括范围在Smi表达以外的整数,例如Double)
Tagged Values
同时表示指向Smi和HeapObject的指针的机制(gdb中通过job查看内存会自动进行区分)
- 指向SMI时(LSB=0)
- 32bit :通过右移一位获得原始值
- 64bit:通过右移32位获得原始值(64bit原始值在高32位)
指向HeapObject时
- 32bit:Ptr-1直接指向HeapObject
- 64bit: Ptr-1 直接指向HeapObject
因为GC上的CHUNK都是4/8字节对齐的,所以指针在不人为改动的情况下HeapObject最低位LSB始终是0,所以Pointer不需要和Smi一样左移来空出多余的一位。
HeapNumber
当对象的值位Double,数据无法在Smi范围内表达,就需要借用HeapObject来存储数据。
V8中的HeapObject内部没有定义成员变量,这些内部指针是通过偏移量实现的。
也同样做一遍实验,存储Smi(0xdeadbee)和Double(0xdeadbeef),后者要大于0x7fffffff,所以需要放在数组中。
1 | pwndbg> job 0x10bf2190d441 |
写入内存之后,通过job可以看到这个数组内容,不过这次我们需要直接用x/看
Smi类型是直接存在FixedArray中高32位置
非Smi类型,存放在指针中,解引用的时候需要-1
0x41ebd5bdde0000是0xdeadbeef的double存储方式
HeapNumber对象和其他对象连续,中间没有任何间隙(自己做的图好丑)
这部分还有一些坑,之后再填,先学下面的部分
JSObject
这部分在上文有过比较详细实验,简单总结一下。
JSObject用于表示Javascript中的对象
继承自Object->HeapObject->JSReceiver
- JSObject从JSReceiver继承Property(命名属性),用于存放命名属性
- JSObject定义了Elements(可索引属性),用于存放可索引属性
JSFunction
用于指向Javascript Function的对象
继承自 Object->HeapObject->JSRecviver->JSObject
实验一下
1 | pwndbg> x/20xg 0x6d0e21a7308 <-- function f() |
kCodeEntryOffset是一个指向JIT code(可读可执行的区域),很多exploit都会在这个部分写入shellcode。当然前提是这个funciton变热才会使用JIT
V8 6.7版本之后JIT的区域不再可写,可能需要结合JIT Spray才能利用。(这个知识点还不太懂,没试过)
向JIT的RWX内存实现利用可以看我写的JS Engine Exploit-qwn2own
GC区域中 标注位Code的部分大概就是JIT的执行区域
JSArray
继承自 Object->HeapObject->JSReceiver->JSObject
JSArrayBuffer
ArrayBuffer & TypedArray
ArrayBuffer是一个直接从Javascript访问内存的特殊数组
ArrayBuffer仅仅准备了一个内存缓冲区
BackingStore,可以使用TypedArray指定的类型读取和写入该区域
- BackingStore指针,属于HEAP而不是GC管理区域
实际应用中,有必要和TypedArray和DataView一起使用
一些Demo
不使用TypeArray
1
d8> var a=new ArrayBuffer(8);
1
2
3d8> %DebugPrint(a)
DebugPrint: 0x2be37c28d469: [JSArrayBuffer]
- map: 0x2ed11a983fe9 <Map(HOLEY_ELEMENTS)> [FastProperties]prototype: 0x34a749712981
- elements: 0x9d3c4182251 <FixedArray[0]> [HOLEY_ELEMENTS]
embedder fields: 2
- backing_store: 0x55b2de6d7c40 –>指向HEAP区而不是GC区
- byte_length: 8
…
}arr_buf=new ArrayBuffer(8);1
2
3
4
5
6
7
8
9
10
11
12
13
- 使用TypeArray
*制定长度,初始化为0*
`t_arr=new Uint8Array(128)`//ArrayBuffer被创建在内部
*使用特定值初始化*
`t_arr=new Uint8Array([1,2,3,4])`//ArrayBuffer被创建在内部
*事先构建缓冲区并使用它*
t_arr1=new Uint16Array(arr_buf);//创建一个Uint16数组
t_arr2=new Uint16Array(arr_buf,0,4);//或者,您也可以指定数组的开始和结束位置1
[此图参考Hpasserby师傅文章的绘制]:(https://xz.aliyun.com/t/5190#toc-9) “,继承的C++ CLASS太多,内容太复杂了。这个结构肉眼看起来好累。。还容易画错”
BackingStore指向存储ArrayBuffer数据的HEAP。
- 所以在攻击中,覆盖BackingStore指针来实现一个oob是一个很好的思路
1
2
3
4
5arr=new ArrayBuffer(0x20);
u32=new Uint32Array(arr);
u32[0]=0x1234;
u32[1]=0x5678;
%DebugPrint(u32);
ArrayBuffer也可以在不同的TypedArray之间共享
- 准备ArrayBuffer
var ab = new ArrayBuffer(0x100);
- 向ArrayBuffer写入Float64的值
1 | var t64 = new Float64Array(ab); |
当某些地址在V8上泄露时,通常在大多数情况下被迫将其解释为双精度值,为了正确计算偏移量等,需要将其转换为整数值。 对于完成该转换,ArrayBuffer是最佳的。
从ArrayBuffer读取两个unit32
1
2var t32 = new Uint32Array(ab);
k = [t32[1],t32[0]]k是6.953328187651540e-310,将字节序列按照4个字节去分开,然后解释为Uint32,于是得到:
k=[0x00007fff,0xdeadbeef]
代码阅读引导
摘自
目录:
1 | src ---+ |
- A:存储汇编代码,反汇编程序,宏汇编程序,模拟器等,对于不同CPU不同。
- B:code generation系统,例如parse, compile, interpreter, etc.
- C:JS built-in function和runtime helper function
- D:Inline Cache code
- E:object model(对象模型)和memory(内存)相关代码
- F:Inspector
- G:Debugging and platform abstraction layer codes are stored.
必读部分
- api.h/api.cc
An API for Embedder is defined. - objects.h/objects.cc
定义了v8的所有对象模型 - compiler/compiler.cc
编译的入口点 - compiler/pipeline.cc
和compiler.cc关联,放置TurboFan - runtime/runtime-*.cc
A runtime function is defined. - builtins/builtin-*.cc
A faster runtime function group. It is described in CodeStubAssembler (commentary) or Assembler. - interpreter/*.cc
Ignition解释器 - ic/*.cc
Inline Caching的实现
存储Runtime(?)
扩展阅读:
V8 是怎么跑起来的 —— V8 的 JavaScript 执行管道
探究JS V8引擎下的“数组”底层实现 <–JSArray是个特殊的JSObject
Array.from() <–比起内容,更重要的是认识到Polyfill这个开源项目
nodejs深入学习系列之v8基础篇 <–v8编译的几种方式