home HOME PAGE

patch

9.4
[compiler] only handle side-effect free interrupts in loop stack checks (4614699) · Gerrit Code Review (googlesource.com)

分析

此patch涉及到多个文件的修改

除此以外还新添加了一个限制条件,那就是当当前stack_check节点的kind为循环迭代体kJSIterationBody时,在之后执行运行时函数就去执行不会进行堆写入的运行时函数,总体来讲此次path主要是为了防止在栈保护机制执行runtime函数时中断处理对某个堆进行写入从而导致的类型混淆。

总结

此漏洞可能是一个由于在执行stack_guard中断时有中断操作的副作用导致某个堆被错误的修改或者写入了某些内容从而最终导致类型混淆。
初步判断POC应该满足以下条件:

漏洞分析

9.26
漏洞poc以及相关分析文章已经公开,对于漏洞的前半部分触发流程与我之前的分析基本一致,当通过循环迭代触发kJSIterationBody类型的StackCheck节点时会导致Runtime_StackGraud函数被触发,只是之前对于Runtime_StackGraud函数的分析存在一些错误的理解,Runtime_StackGraud函数中的中断事件并不是由循环本身所触发或者创建的,在v8的线程中会维护一个中断事件列表,当执行到Runtime_StackGraud函数时会去检查此列表,当有中断事件(例如GC、编译等事件)需要执行时,就暂时中断之前的操作进入相关的中断事件进行执行,也就是说这些中断事件基本都是由其他线程创建,事实也确实如此GC、编译等事件并不是由同一个线程负责处理。

漏洞的前半部分分析基本正确,当Runtime_StackGraud函数执行时会检查当前是否有INSTALL_CODE事件需要执行,此事件通常是由于有其他线程在执行后台编译事件,当后台编译事件完成时就会将相关的INSTALL_CODE事件存入中断列表,随后当Runtime_StackGraud函数被调用时就去执行此INSTALL_CODE事件将编译好的code安装到对应的函数对象中。之前由于对中断事件的错误认知导致我以为INSTALL_CODE是需要在循环中反复调用函数使被调用的函数成热点函数触发编译事件才会触发,但是最终通过这种方式尝试去触发INSTALL_CODE事件时会发现此事件并不能稳定的被触发,而且似乎需要嵌套循环才会准确的在执行循环迭代时触发Runtime_StackGuard函数(之前我一直使用单层循环),以下代码前者可以触发,而后者却不可以:

let b = 0x500;
for(let j=0; j<b; j++){
	for(let i=0; i<b; i++){
		k[i] += 1;
	}
}
let b = 0x500;
for(let i=0; i<b*b; i++){
	k[i] += 1;
}

当INSTALL_CODE事件触发后会调用到FinalizeTurbofanCompilationJob函数来结束编译事件,在此函数的执行过程中会通过以下调用路径调用到CompilationDependencies::Commit函数来提交安装依赖项:
输入图片说明
CompilationDependencies::Commit函数会调用PrepareInstall来为依赖项的安装做准备,PrepareInstall函数中会遍历所有依赖内容并逐一处理,问题就出在这些依赖项的准备安装处理中:
输入图片说明
首先要明确Runtime_StackGuard函数是循环迭代正在执行时被执行的,然后当遇到PrototypePropertyDependency依赖项的准备处理时就会去调用EnsureHasInitialMap函数,而EnsureHasInitialMap函数会调用SetInitialMap函数将相关函数对象的Prototype字段的map修改为字典类型:
输入图片说明
所以现在就出现了问题,如果将循环包裹在foo函数中,再提前将foo函数编译好,然后再编写一个foo2函数,将其放在后台进行编译,如果在执行编译后foo函数中的循环时如果foo2函数刚好编译好的话,就会触发循环中Runtime_StackGuard函数的触发,但是如果只是如此的话还是无法达到触发漏洞的效果,现在还需要在提交安装依赖时保证会有一个PrototypePropertyDependency依赖被处理,只有这样才会使prototype的map被修改,通过对PrototypePropertyDependency类的引用查找会找到在CompilationDependencies::DependOnPrototypeProperty函数中会创建PrototypePropertyDependency对象并插入到依赖列表中:
输入图片说明
输入图片说明
再查找CompilationDependencies::DependOnPrototypeProperty函数的调用者会发现会有以下函数调用,暂且先排除Maglev中的内容,还有LoadNamed操作与OrdinaryHasInstance节点操作会触发PrototypePropertyDependency依赖的创建,也就是说只需要在foo2函数优化编译的过程中触发这两个节点的创建就可以创建PrototypePropertyDependency依赖:
输入图片说明
LoadNamed操作主要用于加载属性,并且属性的receiver必须得是一个HeapConstant(因为使用了HeapObjectMatcher对象),但要注意的是被创建的PrototypePropertyDependency依赖的prototype必须得是要被混淆的类的prototype,也就是说当前属性的receiver必须得是要被混淆的类,我试过很多方法都没有达到想要的结果,POC原作者在此处也没有使用LoadName而是用了OrdinaryHasInstance:
输入图片说明
输入图片说明
OrdinaryHasInstance可以在js代码中使用instanceof来触发,他会使用同样的HasResolvedValue()函数来检查当前node的constructor是否为HeapConstant:
输入图片说明
最后只要保证constructor是函数就可以触发PrototypePropertyDependency依赖的创建:
输入图片说明
此时已经可以在循环执行时修改prototype的map,尝试在map被修改后向prototype中的属性存储内容,通过以上的步骤总结可以大致得到以下代码:

class B{}
B.prototype.a = 1;
B.prototype.b = {};

function foo2(x){
	return x instanceof B;
}

function foo(b, proto){
	let k = new Array(0x1000);
	k.fill(0xA);
	// trigger Runtime_StackGraud function
	for(let j=0; j<b; j++){
		for(let i=0; i<b; i++){
			k[i] += 1;
		}
	}
	proto.a = 0x1111;
	proto.b = 0x1212;
	return k;
}

// compile foo
%PrepareFunctionForOptimization(foo);
foo(0x1, B.prototype);
%OptimizeFunctionOnNextCall(foo);
foo(0x1, B.prototype);

%PrepareFunctionForOptimization(foo2);
// backgound compile foo2
%OptimizeFunctionOnNextCall(foo2, "concurrent");
foo2({b: 1});

foo(0x500, B.prototype);
%DebugPrint(B.prototype);

但只是如此会发现还是没有触发崩溃,通过观察turbolizer图会发现当在向属性写入值的时候会插入一个CheckMaps节点,此节点生成的优化代码会去检查prototype的map,从而导致漏洞在此处被修正,所以还需要想办法绕过这个CheckMaps检查:
输入图片说明
对象字段的存储操作基本都会触发CheckMaps的创建,通过查找ReduceCheckMaps函数可以找到一些此节点被优化删除的逻辑,ReduceCheckMaps函数有两个,LoadElimination::ReduceCheckMaps只在LoadElimination阶段会被调用,TypedOptimization::ReduceCheckMaps则会分别在TypedLowering与LoadElimination阶段被调用:
输入图片说明
通过调试LoadElimination阶段的代码会发现在调用ReduceCheckMaps函数前会先调用ReduceCheckpoint函数,将Checkpoint节点替换为它的Effect节点:
输入图片说明
此时CheckMaps节点的Effect节点已经变成了SpeculativeNumberLessThan节点,随后去执行LoadElimination::ReduceCheckMaps函数,此函数会先通过CheckMaps节点的op来获取maps,然后检查是否已经有UpdateState函数已经处理过SpeculativeNumberLessThan节点,如果没有的话那就会因为无法根据SpeculativeNumberLessThan节点获取State对象而直接退出函数,此处会直接退出,如果state对象不为空时则会从State对象中获取object的map,并尝试将object_maps与前面得到的maps进行比较,如果相同说明CheckMaps节点时可以用其Effect节点替换的:
输入图片说明
之后会去执行TypedOptimization::ReduceCheckMaps函数,此函数替换CheckMaps节点的前提是CheckMaps所检查的Object必须具有一个稳定的map,并且还要有一个HeapConstant表示:
输入图片说明
所以现在需要一种操作使proto触发在执行循环之前生成一个HeapConstant节点,通过obj.x=ptotoobj['x']=proto可以使proto触发创建HeapConstant节点:

function foo(b, proto){
	// Allocate[Any, Young]
	let obj = {};
	let k = new Array(0x1000);
	k.fill(0xA);
	// StoreField[0]
	obj['x'] = proto;
	for(let j=0; j<b; j++){
		for(let i=0; i<b; i++){
			k[i] += 1;
		}
	}
	proto.b = 0x1212;
	return k;
}

输入图片说明
B.prototy的map本身也是一个stable_map
输入图片说明
根据以上分析可以得到一个与原漏洞提交者公开的poc类似的代码用例:

class B{}
B.prototype.a = 1;
B.prototype.b = {};

function foo2(x){
  return x instanceof B;
}

function foo(b, proto){
  let obj = {};
  let k = new Array(0x1000);
  k.fill(0xA);
  obj['x'] = proto;
  for(let j=0; j<b; j++){
    for(let i=0; i<b; i++){
      k[i] += 1;
    }
  }
  proto.b = 0x1212;
  return k;
}

// compile foo
%PrepareFunctionForOptimization(foo);
foo(0x1, B.prototype);
%OptimizeFunctionOnNextCall(foo);
foo(0x1, B.prototype);

%PrepareFunctionForOptimization(foo2);
// backgound compile foo2
%OptimizeFunctionOnNextCall(foo2, "concurrent");
foo2({b: 1});

foo(0x500, B.prototype);
%DebugPrint(B.prototype);

总结与思考

  1. 代码审计能力较弱,阅读代码时对代码可能会存在错误的理解,由于无法正确的理解代码逻辑导致最终无法根据patch分析写出poc。
  2. 无法完全正确的根据v8 c++源码编写出对应可以触发c++代码执行的js源码用例。

我认为漏洞提交者在通过代码审计挖掘此漏洞时着重关注了在创建与使用某些对象/内容之间可能会对此对象/内容做出修改的情况。