前段时间一直在忙大作业和考试,现在结束了,终于有时间继续自己的安排。这段时间分析了两个 V8 的漏洞,一个非常简单,基本看到漏洞就能利用;另一个则比较复杂,最终没有完成利用。分析过程中对 V8 JIT 流程也加深了一点理解。
CVE-2019-5782
这是一个非常简单的漏洞,漏洞位于 src/compiler/type-cache.h 中,在对 ArgumentsLength 节点的类型进行定义时,漏洞版本代码定义如下:
Type const kArgumentsLengthType = Type::Range(0.0, Code::kMaxArguments, zone());
而 kMaxArguments 的值为 65534。
但是实际上函数的参数个数是可以大于 65534 的(估计是早期在调用函数时只能支持最大 65534 个参数,但是后来能支持更多的参数,开发者在修改过程中忽视了这里的问题),因此这里存在类型判断错误,通过构造可以实现 CheckBounds 节点消除,从而实现漏洞利用。
这个漏洞利用起来比较容易,由于 ArgumentsLength 的节点类型在 Typer 阶段判断成 Range(0, 65534),因此对该节点的值使用右移操作符 >> 16,则节点类型最终会被判断成 0(Range0, 0()),之后即可对 CheckBounds 进行消除。
Turbolizer 工具生成的 Load Elimination 阶段(Load Elimination 阶段之后产生 ArgumentsLength 节点)的图如下所示:

这里的右移节点仍为 Range(-32768, 32767) 类型,但是在 Simplified Lowering 节点的 Type Propagation 阶段中,会调用 UpdateFeedbackType 函数(src/compiler/simplified-lowering.cc),该函数会再一次根据每个节点的输入值的类型,对 Feedback Type 进行更新,更新之后 SpeculativeNumberShiftRight 的类型为 Range(0, 0),如下图所示:

因此,在 Simplified Lowering 会完成 CheckBounds 节点的消除,如下图所示:

之后,通过越界构造稳定的可越界读写数组,跟之前的越界写利用方式一样,构造任意内存读写原语以及 addr_of 原语,完成漏洞利用。
CVE-2019-5755
CVE-2019-5755 是一个对 -0 参与的减法操作的类型判断错误的漏洞。「-0 – 0」 在 JavaScript 中得到的值是 -0,但是在有漏洞版本中,对相应的整数减法的判断类型如下(src/compiler/operation-typer.cc):
Type OperationTyper::SpeculativeSafeIntegerSubtract(Type lhs, Type rhs) {
Type result = SpeculativeNumberSubtract(lhs, rhs);
return Type::Intersect(result, cache_.kSafeInteger, zone());
}
可以看到该函数会将 SpeculativeNumberSubtract 得到的类型结果与 SafeInteger 类型进行交集之后返回(SafeInteger 不包括 -0),所以这里存在类型判断的错误。
至于 simplified-lowering.cc 中的修复,是为了插入一些对节点进行调整的节点,与漏洞本身的关系并不大。
这个漏洞与之前 Math.expm1 中的漏洞类似,因此想要借助类似的方式对这个漏洞进行利用,但是最后失败了,在此记录一下构造利用的过程。首先参考回归测试创建如下函数:
function hot(trigger) {
var idx = Object.is((trigger ? -0 : 0) - 0, -0);
return idx
}
然后多次调用,使其 JIT 之后,在调用 JIT 之后的 hot(true) 时,返回的是 false,证明漏洞存在。
但是每次都返回 false,不是漏洞利用所需要的。我们需要的是在 SimplifiedLowering 阶段通过这个漏洞将 CheckBounds 节点消除,但是在真正执行的时候返回的是 true。
每次都返回 false 的原因是在 Inlining 阶段,Object.is 函数会被替换成 SameValue 节点。Typer 阶段,会根据 SameValue 的输入判断出 SameValue 节点的类型,由于 SpeculativeSafeIntegerSubtract 类型被判断不能为 -0,因此 SameValue 的类型是 false,如下图。

这样到了 TypedLowering 阶段,SameValue 节点就会被直接替换成 HeapConstant (false) 节点。
因此,为了不每次都返回 -0,需要将 SameValue 的第二个输入(-0)的类型进行处理,不让 TurboFan 过早地发现他的类型。使用 Math.expm1 中的利用思想,借助 Escape Analysis 来对 -0 进行处理:
function(value, trigger) {
var obj = { a: -0 };
var result = Math.expm1(value);
var idx = Object.is((trigger ? -0 : 0) - 0, obj.a);
return idx;
}
for (var i = 0; i < 20000; ++i) {
hot(‘0’, false);
}
print(hot(-0, true));
其中,Math.expm1 是为了让 obj.a 不在 Load Elimination 就被优化(具体为什么添加上这句就不会被优化目前还不清楚),使得其留到 Escape Analysis 阶段。但是上述执行之后,输出的仍是 false。通过 Turbolizer 观察可以发现,Simplified Lowering 阶段完成之后,各个节点已经满足了我们利用的基本要求,但是 Int32Sub 与 SameValue 节点之间多了一个 ChangeInt31ToTaggedSigned 节点。

我们继续看看这样的情况下能否将 CheckBound 节点消除,将 Exp 进行如下修改:
function(value, trigger) {
let array = [1.1, 2.2, 3.3, 4.4[;
var obj = { a: -0 };
var result = Math.expm1(value);
var idx = Object.is((trigger ? -0 : 0) - 0, obj.a);
return array[idx * 10];
}
用 Turbolizer 查看生成的节点:

可以看到 CheckBounds 节点被消除了,因此在消除越界检查这一方面这个 Exp 已经完成了。但是为什么 SameValue 在这种情况下还是返回 false 呢?在这里我的猜想是:
在 SameValue 之前,添加了 ChangedInt31ToTaggedSigned 这个节点,这个节点会将输入值变为 Tagged 的形式,在放到 SameValue 中比较。而当输入是 -0 时,被变为 TaggedSigned 的过程中改变了其原来的值类型,因此在之后与 -0 进行比较的过程中仍为 false。由于暂时没有找到防止这里插入 ChangeInt31ToTaggedSigned 的方法,因此无法验证猜想,也无法进一步完成利用。