文章首发于安全KER https://www.anquanke.com/post/id/227284
0x00 前言
chrome issue 1051017是2020年2月公布的一个v8漏洞,该漏洞是在JIT优化时对循环变量的类型估算考虑不周导致的compiler阶段的类型混淆,通过compiler阶段的类型混淆进一步构造OOB溢出。
0x01 前置知识
induction variable指循环中的一个变量,其值在每一次循环迭代过程中增加(或减少)固定的值,也就是循环中的i变量等。有关编译器确定程序中循环变量的算法,可以阅读论文INTERPROCEDURAL INDUCTION VARIABLE ANALYSIS
。
0x02 issue 1051017 分析
patch分析
1 | diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc |
该patch是用于修复ISSUE 1051017
漏洞的,该patch的批注如下
1 | The bug is that induction variable typing does not take into account |
该patch位于src/compiler/typer.cc
源文件的Typer::Visitor::TypeInductionVariablePhi
函数,从文件名和函数名可以推出,该函数属于JIT编译器的一部分,并且可能是在Typer
阶段被调用,且与循环变量(induction variables)有关。
调试分析
为了弄清楚漏洞原理,我们回退到parent
版本,编译v8引擎以后进行调试,我们使用其给出的poc进行调试
1 | function foo() { |
在该poc中,i就是induction variables
,而x就是increment
首先Typer::Visitor::TypeInductionVariablePhi
设置断点,然后运行poc,来到both_types_integer
的判断
1 | 857 const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) && |
因为poc里,induction variables
i初始值为0,属于typer_->cache_->kInteger
类型,然后increment
x初始值为-Infinity
,也属于typer_->cache_->kInteger
类型,因此,接下来会进入if分支
1 | 862 if (both_types_integer) { |
执行后,maybe_nan
为false,这样程序顺利绕过了下面的if
1 | // We only handle integer induction variables (otherwise ranges |
接下来就开始正式处理循环逻辑了
1 | 897 double increment_min; |
由于poc里,i执行的操作是+=
,满足条件arithmetic_type == InductionVariable::ArithmeticType::kAddition
,因此,increment_min
等于-inf
,而increment_max
等于inf
,那么就直接进入下面的else
分支,返回typer_->cache_->kInteger;
类型
1 | if (increment_min >= 0) { |
回到上层调用,最终发现,该函数在v8::internal::compiler::Typer::Run
时调用。
1 | pwndbg> return |
从以上调试情况来看,我们可以知道Typer::Visitor::TypeInductionVariablePhi
函数是在Typer
阶段用于确定induction variables
循环变量的最终类型的。通过调试知道,JIT编译器认为poc里的这个循环,i最终类型为typer_->cache_->kInteger;
,然而,在实际的普通js层,测试发现,i最终类型为NaN
1 | function foo() { |
由此,可以知道,该漏洞使得JIT
层面和普通JS
层面对循环变量i的类型判断不一致,也就是在JIT
层面有一个类型混淆。
漏洞修复分析
我们来看一下该漏洞是如何被修复的
1 | - // We only handle integer induction variables (otherwise ranges |
主要是在原来这个if里面增加了两个条件,判断 increment_type.Min()
和 increment_type.Max()
的值
1 | // We only handle integer induction variables (otherwise ranges |
如果两个值分别为 -V8_INFINITY
和 +V8_INFINITY
,那么经过type = Type::Union(type, Operand(node, i), zone());
操作,type类型为NaN
与JS层面保持一致。
0x03 issue 1051017 漏洞利用
OOB数组构造
首先,在原有的基础上加入一个数组
1 | function opt(index) { |
运行结果并无差异
1 | root@ubuntu:~/Desktop/v8/out.gn/x64.debug# ./d8 p.js --allow-natives-syntax |
我们查看一下IR图
可以发现在Typer
阶段,var x = Math.max(i,1);
这句已经形成了一个节点为Range(1,inf)
我们再来看一下加入修复补丁以后的v8运行的IR图,修复后其值为NaN
现在的情况是编译器认为其值为Range(1,INF)
,而真实值为NaN
1 | //compiler:Range(1,INF) |
现在,我们需要利用某种方法,使得compiler
形成的Range在数组长度之内,而reality
真值则实际大于数组长度。考虑做如下运算
1 | //compiler:Range(-INF,-1) |
首先,将区间取反,这样,对于编译器来说是Range(-INF,-1)
而真值却为NaN
,接下来再用max函数,使得Range估算为(-2,-1)
,真值却仍然为NaN
,然后利用>>
运算,>> 0
运算可以使得NaN
的值变为0,使得编译器认为Range(-2,-1)
,而真值为0。
然后,我们查看IR图
正如预料的那样,编译器的最终评估为Range(-2,-1)
。
为了进一步调试真实值的计算过程,我们使用如下代码进行调试
1 | function opt(index) { |
使用如下参数进行调试
1 | set args --allow-natives-syntax ./p.js -print-opt-code |
在打印出JIT代码和地址后,我们在JIT代码地址出断点然后调试
这里是for循环的逻辑
1 | pwndbg> u rip |
当for循环逻辑结束后,此时查看循环变量i的值
1 | ► 0x257b00082bf2 vmovapd xmm3, xmm2 |
i现在是-NaN
,执行x = -x
以后·,来到x = Math.max(x,-2);
逻辑
1 | pwndbg> p $xmm3 |
最后>> 0
运算被转换为了如下代码
1 | 0x257b00082c5e vcvttsd2si edx, xmm3 |
通过调试,我们发现,生成的JIT代码是没有问题的,确实是按照NaN
来运算,bug
仅出现在IR
分析阶段。接下来,我们继续构造
1 | function opt(index) { |
这样可以使得编译器的估测值比真实运算结果小,由此发生溢出。运行发现程序直接崩溃
1 | root@ubuntu:~/Desktop/v8/out.gn/x64.bug# ./d8 poc.js --trace-turbo --allow-natives-syntax |
分析IR图,checkbounds
的Range(0,7)
在数组长度之内,在后续,该checkbounds
会被移除
在V8.TFEffectLinearization 369
阶段,已经没有了边界检查,因此也可以溢出
从IR图中,未分析出任何异常,因此,我们继续调试JIT代码
1 | R8 0x80000000 |
调试中看出,在执行x += 2;
时,x(寄存器r8)的值仍然为0x80000000
,最终使得运算的下标为 R8 0x8000000a
,即有一个符号位的存在,因此,我们可以在最后添加一个移位操作,用于移除NaN
计算造成的符号位。完整的OOB构造方法如下
1 | function opt(index) { |
运行后发现成功溢出
1 | root@ubuntu:~/Desktop/v8/out.gn/x64.bug# ./d8 p.js --allow-natives-syntax |
疑难问题
在构造过程中,var x = Math.max(i,1);
和x = Math.max(x,-2);
语句中的参数,位置不能调换,否则利用失败。
这是因为max
函数最终是会被转换为Float64LessThan
函数,
而对于一个NaN
,任何的比较都是false,因此在这个情况下,max
运算的真实结果将会是第一个参数
1 | NaN == NaN |
而我们的目的就是要让NaN
参与真实值的计算,因此,不能调换参数的位置。
能否使用var x = i < 1 ? 1 : i
来代替max
函数?答案是不行。
这将导致var x = i < 1 ? 1 : i
这个Phi
节点与i
的估测一致,同为Range(-INF,INF)
,因为从程序的流程分析来看,显然i < 1
是恒不成立的,因为刚刚循环退出的条件就是i >= 1
,因此,var x = i < 1 ? 1 : i
就相当于var x = i
,在后面,编译器直接评估它与i的情况一样,同为Range(-inf,inf)
,由此不能达到我们的利用目的。同理,var x = i > 1 ? i : 1;
也不可行,它将使得i为NaN
时,x的值为1。
exp编写
控制好对象布局,利用JIT的oob,覆写后方Array
的length,从而构造一个自由溢出的OOB Array
,然后后续就是简单的利用了。
1 | var buf = new ArrayBuffer(0x8); |
0x04 感想
最近研究v8越来越上手了,以后还得继续努力。
0x05 参考
论文Interprocedural Induction Variable Analysis
chromium commit