以下是对2021年chrome的issue-1196683和issue-1195777进行的粗略的分析(主要是对v8没有那么的熟悉)
环境搭建
编译一个8.x版本的v8(反正找一个2021年4月份前的),还需要准备好turbolizer:
1 | cd tools/turbolizer |
接着在浏览器访问:http://127.0.0.1:8000/ 就可以看到turbolizer了
issue-1196683
相关链接:https://chromium-review.googlesource.com/c/v8/v8/+/2820971
我们稍微改改其提供的regress-1196683.js :
1 | const arr = new Uint32Array([2**31]); |
运行一下:
1 | >./d8 issue-1196683.js |
正常情况下结果应该是-2147483647
,但是其在生成优化代码后变成了2147483649
用turbolizer来看看优化过程,先生成其json文件:
1 | ./d8 issue-1196683.js --trace-turbo |
用turbolizer打开生成的json文件(turbo-foo-0.json)
TyperPhase
点箭头标记的那个三角,可以显示出与该异或操作相关的节点(30),其输入的两个节点为常数0(节点22)和arr[0](节点44)
SimplifiedLoweringPhase
在SimplifiedLowering
阶段,原先在Typer
阶段的结点30:SpeculativeNumberBitwiseXor
被优化成了Word32Xor
节点,而且与patch相关的ChangeInt32ToInt64
节点也出现了,patch:
EarlyOptimizationPhase
到了EarlyOptimization
阶段,原本的Word32Xor
节点已经被Reduce了,导致原本ChangeInt32ToInt64
节点的输入类型由原本的Signed32
(见SimplifiedLoweringPhase)变成了Unsigned32
,和这里的逻辑相关的代码为:
1 | template <typename WordNAdapter> |
我们的情况真好是 x^0 , 所以用左边节点来替换当前节点,导致了类型由Signed32
变成了Unsigned32
schedule
再到register allocation:
register allocation
imm:20 的值为1,红框对应的就是我们js代码中的 (arr[0] ^ 0 + 1)
这里应该是对应了patch中的指令选择阶段,我们的输入类型由于是Unsigned32
,所以在指令选择阶段选择了kX64Movl
指令,而不是会做符号拓展的kX64Movsxlq
指令,导致结果变成了无符号的0x80000000+1
也就是2147483649
我们还可以调试一下,给代码加一个断点:
1 | const arr = new Uint32Array([2**31]); |
加一个--print-opt-code
来确定优化代码的位置并下断点:
1 | pwndbg> r --allow-natives-syntax --print-opt-code ./issue-1196683.js |
下好断点跳过去查看:
后续的add rcx,1
也就是我们的返回结果了
issue-1195777
相关链接:https://chromium-review.googlesource.com/c/v8/v8/+/2826114
我们稍微改改其提供的regress-1195777.js :
1 | function foo(b){ |
运行之后:
1 | >./d8 issue-1195777.js |
可以看到在优化后,返回了一个错误的false,关键逻辑是在 -1 < Math.max(0, x, -1)
,用turbolizer来查看其优化过程
TyperPhase
这是与-1 < Math.max(0,x,-1)
相关的节点
TypeLoweringPhase
原本的53和56节点被替换成常数0(节点31)和-1(节点13)
SimplifiedLoweringPhase
到了SimplifiedLowering
阶段,NumberMax
节点被替换成了Int64LessThan
和Selec
节点,SpeculativeNumberLessThan
被替换成了Int32LessThan
;还可以看到最后生成了TruncateInt64ToInt32
节点,该节点就是漏洞触发的地方,我们查看patch:
但是调试的时候无法断到此处,先留个坑在这。。。
Math.max(0, x, -1)
的结果被truncate成了Signed32,导致会计算出错误的结果
漏洞利用
经过上面的分析,这两个漏洞都能使我们获得一个错误的值为1的变量。通过分析在野利用的exp,利用步骤为:
- 用该错误的变量作为长度创建一个Array
- 通过
Array.prototype.shift
来使上一步创建的Array的长度变成0xFFFFFFFF
第二步的利用方法在4月份的时候也被谷歌修了:https://chromium.googlesource.com/v8/v8/+/d4aafa4022b718596b3deadcc3cdcb9209896154
poc1:
1 | function foo() { |
poc2:
1 | // use issue-1195777 |
可以看下其生成的优化代码:
有两处对长度的赋值,分别对应flag == true
和 flag == false
,第二处的赋值直接把0xfffffffe(Int32(0xffffffff << 1))赋值给长度,所以我们能得到一个长度为-1的数组,具体为啥会这样还得等后面熟悉了再回头看看。。。
后续步骤:
- 修改corrupt_arr的length
- 利用oob_arr和corrupt_arr来构造addrof和fakeobj原语
- 泄露出corrupt_arr的map
- 申请一个FixedDoubleArray对象A,布置好其内容(为下一步伪造对象做准备)
- 利用fakeobj原语fake一个FixedDoubleArray对象B
- 利用对象A和B实现任意地址读写
具体代码:
1 | var buf = new ArrayBuffer(16); |
调试的时候注意有指针压缩
参考链接
https://iamelli0t.github.io/2021/04/20/Chromium-Issue-1196683-1195777.html
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/#control-edges
https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js