34c3 v9 writeup
很久之前做的了,和*CTF那题差不多,顺便就发出来。
环境搭建
1 | mkdir v9 && cd v9 |
exploit
工具类准备
这部分就是一些可复用的代码。
1 | String.prototype.padLeft = |
在这次exp编写中,用到的主要是
1 | Int64.fromDouble(double num); |
Int64.fromDouble(double num)
Constructs a new Int64 instance with the same bit representation as the provided double.
例如:1
2
3
4
5
6print(Int64.fromDouble(1.1));
print(typeof(Int64.fromDouble(1.1)));
...
...
0x3ff199999999999a
objectnew Int64(int num).asDouble();
Return a double whith the same underlying bit representation.
例如1
2
3
4
5
6print(new Int64(0x3ff199999999999a).asDouble());
print(typeof(new Int64(0x3ff199999999999a).asDouble()));
...
...
1.1000000000000227
number
root cause
1 | diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc |
每一个对象都有一个map来标记这个对象的类型,而checkmap就是用来检查这个对象的类型有没有变化的。
如果没变的话就可以一直走fast path,否则就要baliout。
根据给出的含漏洞的patch可知,JIT优化中的函数调用层次如下:
1 | Reduction RedundancyElimination::Reduce(Node* node) { |
首先在Reduce里遇到CheckMaps的时候
1 | case IrOpcode::kCheckMaps: |
为了找到最优的dominates,会去遍历其他的check
1 | for (Check const* check = head_; check != nullptr; check = check->next) { |
如果找到其他的CheckMaps的话,会检查是否“兼容”,会去看它们的maps,如果第一个检查已经包含第二个检查的话,就会把第二个检查给去掉。
1 | if (Node* check = checks->LookupCheck(node)) { |
利用思路
type confusion可以让我们得到对于用户空间任何object的读写权限,可以将任意一个对象的指针当成一个double读出来,也可以将任意一个double当成一个对象的指针写进去,这样我们就可以在一个地址伪造一个对象。
通过type confusion去fake map,fake ArrayBuffer,然后通过改我们fake的ArrayBuffer的BackingStore得到任意地址读写的原语。
fake map prototype&&constructor
PS.事实上这步可能不需要。只是当时学习别人exp的时候写的
通过type confusion去leak ab的prototype地址,且由于prototype和constructor的地址偏移是固定的,所以可以去通过prototype的地址去计算出constructor的地址,然后将他们写入我们要fake的map对应的位置。
不过也可以直接用ab.__proto__.constructor
得到constructor的地址。
1 | var ab=new ArrayBuffer(0x20); |
log
1 | 要被leak的ArrayBuffer |
fake map并leak出来
前后两次gc(),让ab_map_obj这个double array移动到old space里,并且让其和它的elements地址偏移恒定。
1 | gc(); |
1 | DebugPrint: 0x3e0338a149e9: [JSArray] in OldSpace |
然后将其ab_map_obj的地址leak出来,加上0x70就是我们fake的map的地址。
1 | print("要leak出ab_map_obj的数组"); |
这里顺便说一句,无论是leak还是fake的时候,得到的都是double,写入的也是按照double写入,这个调试一下就知道了。
fake ArrayBuffer并leak出来
在map被fake好了之后,我们就可以fake ArrayBuffer得到任意地址读写的原语了。
依然是前后两次gc(),然后fake一个ArrayBuffer结构。
1 | gc(); |
然后将这个fake好的ArrayBuffer的地址leak出来,依然是先leak fake_ab这个JSArray的地址,然后根据偏移0x70找到我们在elements里fake的ArrayBuffer的地址。
1 | arr2=[1.1,2.2,3.3,4.4]; |
log
1 | leak出的map地址是810f1c94a01 |
将我们fake的ArrayBuffer当成一个JSObject读出来
我们可以在callback里改掉array的类型,比如将一个double array改成了object array,但是由于type confusion,我们在第二次对arr[0]重新写入值的时候,依然把arr当成一个double array,并将其写入。
这样实际上,我们把一个double的数值当成一个object指针写入。
如下,写入之后,arrr[0]将由于我们fake的arraybuffer的map,被视作一个arraybuffer对待,于是可以用它来初始化一个DataView。
DataView就可以操作这个fake的ArrayBuffer的BackingStore地址对应的内存。
1 | arrr=[1.1,2.2,3.3,4.4]; |
leak一个function的code指针的地址,并将其写入fake ArrayBuffer的BackingStore
由此,我们就可以读取对应于code指针所在地址的code指针的值。
如下图log,我需要得到code的地址,
1 | gdb-peda$ job 0xac9a5c986c9 |
从图中可以看出来,就是function-1(这个减一是因为v8中指针末位都置为1,需要去掉)+0x38,我们把它leak出来。
1 | gc(); |
所以找到这个地址后,我们将其写入fake arraybuffer的backingstore,就能用dataview把这个地址对应的数据读出来。
1 | fake_dv = new DataView(arrr[0],0,0x4000); |
但是这个地址,并不是真正的函数对应的执行的代码的入口,它还需要加上0x5f,如图:
1 | gdb-peda$ job 0x19d27c522f01 |
于是我们还要再加上0x5f
1 | shellcode_address=shellcode_address+new Int64(0x5f).asDouble(); |
向函数要执行的代码的地址,写入我们的shellcode
同上,将函数要执行的代码的地址写入到BackingStore,并用dataview向这个地址写入shellcode。
1 | fake_ab[4]=shellcode_address; |
exploit
附录
JSArrayBuffer
ArrayBuffer and TypedArray
- Originally ArrayBuffer
- 一个可以直接从JavaScript访问内存的特殊数组
- 但是,ArrayBuffer仅准备一个buffer
- BackingStore——可以使用TypedArray/DataView,指定的类型读取和写入该区域,例如作为原始数据数组访问的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中常用的方法。
完整exp
我写了两个版本的exp,思路一样,但是写法稍微有点不同,版本一相对简洁舒服一些,版本二感觉会稳定一点。
版本1
1 | function gc(){ |
版本2(工具类在上面)
1 | // leak出object的地址,即将一个object当成double读出来 |