chrom-V8环境搭建

环境编译

chrome 里面的 JavaScript 解释器称为v8。

我们下载的源码称为V8,而V8经过编译之后得到的可执行文件为 d8。根据编译时选择的不同,编译出来的 d8 分为 debug版本 和 release版本,一般把这两个版本都编译出来。

下载源码

由于需要去谷歌的网站上下载源码,所以需要保证虚拟机能够走代理。我这里直接设置允许局域网连接,将虚拟机设置为NAT,那么就可以上梯子。

随后,依次安装

depot_tools

这个工具是用来得到v8源码的:

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo "export PATH=$(pwd)/depot_tools:${PATH}" >> ~/.bashrc

ninja

这个工具是用来编译v8的:

1
2
3
git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
echo "export PATH=$(pwd)/ninja:${PATH}" >> ~/.bashrc

然后就是重新加载环境变量:

1
source  ~/.bashrc

最后就是去下载V8 源代码:

设置环境变量后拉取 v8 的代码 但考虑到中英文问题和一些网络代理问题,这里不安装字体依赖,有需要的师傅可以试着去掉该参数

1
2
fetch v8
./v8/build/install-build-deps.sh --no-chromeos-fonts

写一个脚本去跑编译,方便以后直接换版本编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
VER=$1
if [ -z $2 ]; then
NAME=$VER
else
NAME=$2
fi
cd /path/depot_tools/v8
# /path/depot_tools/v8 换成自己的路径
git reset --hard $VER
gclient sync -D
gn gen out/x64_$NAME.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu ="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'
ninja -C out/x64_$NAME.release d8

执行

1
time ./build.sh 9.6.180.6

编译效率一般取决于自己的设备性能

加载补丁

上面的方法,只适用于编译最新的 V8代码。但是对于我们常见的调试漏洞或者解题,往往需要编译特定版本的 V8 或者加载相应的补丁。

需要用如下方法。

一般题目都会给出有漏洞的版本的commitid,所以编译之前先把源码的版本reset到和题目一致的版本,在把题目给出的diff文件应用到源码中:

1
2
3
4
5
6
7
8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff
# 编译debug版本
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
# 编译release版本
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

调试

将 “v8/tools/gdbinit” 保存到自己惯用的目录下,这里称之为 “path”,然后将路径写到 .gdbinit 下:

1
2
3
4
cp v8/tools/gdbinit /path/gdbinit_v8
cat ~/.gdbinit
#source /home/tokameine/Desktop/env/pwndbg/gdbinit.py
#source /path/gdbinit_v8

做完以后,就能够在源代码中插入如下代码进行调试了:

可以直接在 js代码中使用%DebugPrint();以及%SystemBreak();下断点。%SystemBreak()其作用是在调试的时候会断在这条语句这里,%DebugPrint() 则是用来打印对象的相关信息,在debug版本下会输出很详细的信息。

1
2
%DebugPrint(x); 打印变量 x 的相关信息
%SystemBreak(); 抛出中断,令 gdb 在此处断点

但这两条代码并非原有的语法,在执行时需添加参数 “–allow-natives-syntax”, 否则会提示 “SyntaxError: Unexpected token ‘%’”

调试样本

就用一个简单的 demo 测试一下调试能够正常进行:

1
2
3
4
5
6
7
8
9
10
//demo.js
%SystemBreak();
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%DebugPrint(wasmInstance);
%SystemBreak();

我们暂时不用在意这段代码在做什么,这无关紧要,我们现在只想知道调试环境是否能够正常工作而已,所以读者只需要知道有这么个变量名为 f 的变量即可

在 v8/out/x64_$name.release 目录下可以找到二进制程序 d8,它才是解析执行 js 代码的引擎,通过 gdb 去调试该程序,并将 demo.js 作为参数传给它

1
2
3
$ gdb d8
pwndbg> r --allow-natives-syntax /home/tokameine/Desktop/demo/test.js
pwndbg> c

可以看到 gdb 正常发生了中断,但由于我们调试的并非 js 脚本,所以自然不可能顺着脚本中断,而是在 d8 的某行机器码处中断了,此时它会打印出数组 f 的数据:

调试命令

job命令

用于可视化显示JavaScript对象的内存结构。

gdb下使用:job 对象地址

telescope命令

功能:查看一下内存数据

使用:telescope 查看地址 (长度)

基础知识

指针标记

V8 使用指针标记机制来区分 指针、双精度数 和 Smis(代表) immediate small integer

1
2
3
Double: Shown as the 64-bit binary representation without any changes
Smi: Represented as value << 32, i.e 0xdeadbeef is represented as 0xdeadbeef00000000
Pointers: Represented as addr & 1. 0x2233ad9c2ed8 is represented as 0x2233ad9c2ed9

因此,V8 中 如果一个值表示的是指针,那么会将该值的最低bit 设置为1,所以其实真实的值需要减去 1.

Job 直接给对象地址就行,telescope 的时候,需要给真实值,需要 -1.

V8 对象结构

V8 中的对象有如下属性:

1
2
3
4
5
map: 定义了如何访问对象
prototype: 对象的原型(如果有)
elements:对象的地址
length:长度
properties: 属性,存有map和length

分析:

对象里存储的数据是在elemnts指向的内存区域的,而且是在对象的上面。也即,在内存申请上,V8先申请了一块内存存储元素内容,然后申请了一块内存存储这个数组的对象结构,对象中的elements指向了存储元素内容的内存地址。

map属性详解

对象的map (数组是对象)是一种数据结构,其中包含以下信息:

1
2
3
4
5
对象的动态类型,即 String,Uint8Array,HeapNumber 等
对象的大小,以字节为单位
对象的属性及其存储位置
数组元素的类型,例如 unboxed 的双精度数或带标记的指针
对象的原型(如果有)

属性名称通常存储在Map中,而属性值则存储在对象本身中几个可能区域之一中。然后,map将提供属性值在相应区域中的确切位置。

本质上,映射定义了应如何访问对象:

对于对象数组:存储的是每个对象的地址

对于浮点数组:以浮点数形式存储数值

所以,如果将对象数组的map换成浮点数组 -> 就变成了浮点数组,会以 浮点数的形式存储对象的地址;如果将对 浮点组的 map 换成对象数组 -> 就变成了对象数组,打印浮点数存储的地址。

对象和对象数组

也就是说,对象数组里面,存储的是别的对象的地址。