0day漏洞:Chromium v8引擎最新UAF代码执行漏洞分析

介绍

在Chromium v8中x64平台的指令优化中发现了UAF漏洞。成功利用此漏洞可以允许攻击者在浏览器上下文中执行任意代码。

该漏洞是由于v8在优化结束之后,指令选择阶段,选择了错误的指令,导致的内存破坏漏洞,成功利用此漏洞,可以达到代码执行的效果。

该漏洞发生在

https://chromium.googlesource.com/v8/v8/+/71a9fcc950f1b8efb27543961745ab0262cda7c4%5E

之前(含本次提交),如果想重现此漏洞,可以同步代码到此次提交之上。

注:Google已发布紧急补丁更新,请及时下载安装。

代码主要的功能及原理:

1.首先创建一个typed array数组,大小为0x10000

2.然后判断a[0]的值是否为非零,然后结果赋值给b

3.然后进行垃圾回收

4.如果b为true,打印字符串boom

运行上面的代码输出的结果:

1653925991_6294e86749d992f78bc38.png!small?1653925991325

图1:内存泄漏错误

可以看到,访问违规的地址就是a的data_ptr: 0x7f71576a0000

1653926001_6294e87121d9a2a52e239.png!small?1653926000974

图2: 访问违规地址数组a

代码首先,调用了runtime函数gc,然后[r8],0做对比:

1653926021_6294e885e6513e9668896.png!small?1653926021998

图3: 调用gc时的汇编指令

漏洞产生原因

在前两次运行的时候,v8知道对数组a的使用只是在步骤2处,后续并没有访问数组a,所以在优化编译阶段,gc把v8的数组存放的内存给回收了,数组a的内存被释放了,但是由于v8在x64平台上错误的生成了指令,导致在4处访问到了已经被回收的页面,造成了内存访问异常,也就是在0x7f6e29984162地址处,又一次的访问已经被释放的数组a的内存,从而导致crash。

优化过程:

if(b)=>if(a[0]!=0)=>Word32Equal(a[0],0) =>[cmp [r8],0 ; jnz xxxx]

因此,在执行第4步时,它正在访问已回收的同一位置,导致地址0x7f6e29984162的内存访问异常,其中再次访问已释放的数组a的内存,导致内存损坏和崩溃。

1653926048_6294e8a0e7edfb06c8662.png!small?1653926048670

图4 :漏洞出发流程图

补丁对比

修补代码删除了CanCoverForCompareZero函数,恢复了CanCover的使用CanCoverForCompareZero的作用是用来代替在函数VisitWordCompareZero中canCover。如果canCover返回了false,但是这个节点是一个比较的节点,就不需要其他的任何寄存器,可以被user节点给cover住。

一般情况下这个没有问题,但是问题恰恰出现在当访问内存的时候,此时生成的代码会直接去比较内存,而不是先获取比较结果,在去进行比较,但是当在第二次访问内存的时候,调用gc,就会造成UAF。

这时候会调用这个函数,用来访问a的数组buffer,如下图所示:

1653926095_6294e8cf48abfe3a728c1.png!small?1653926095234

在这里生成转化为具体的指令1653926107_6294e8db6b62e4acbe68b.png!small?1653926107350

因为易受攻击的代码采用CanCoverForCompareZero为true的路径,从而生成不同的指令序列(易受攻击版本VS固定版本)。

指令序列的详细分析

在分析了Turbofan的优化阶段后,我们知道这个问题发生在最后两个阶段:

在计划阶段,节点信息完全一致,但之后变得不同,我们找到了相关的指令序列。

事实上,相应的js代码是const b=a[0]!=0;

修补前:

1653926124_6294e8ec2008cdb4de719.png!small?1653926123832

修补之后:

1653926134_6294e8f66491bf3ddd549.png!small?1653926134276

乍一看,它似乎完全相同,但由于漏洞,这里生成的代码不同(请注意,114的左侧,修复前是一个点,修复后是一个圆圈和一个点,这意味着修复前的114行不是直接生成的指令,而是从33个输入中提取,以生成优化的指令序列——指令折叠。

在易受攻击代码中,当调用VisitWordCompareZero时,CanCoverForCompareZero将返回true,因为Word32Equal节点是一个比较类型位置。由于后续访问是cmp指令,v8不需要在注册表中存储比较结果,但假设后续访问(即if(b)语句)可以再次直接访问数组a的内存,并最终生成包含更少指令序列的优化代码。

这就是注册表分配之前发生的事情:

修复前:

1653926154_6294e90ad9c5087055916.png!small?1653926154767

修复前生成的相关指令:

1653926169_6294e91911165bf8f8048.png!small?1653926168709

修复后:

1653926178_6294e9224b941856f5895.png!small?1653926178021

修复后生成的相关说明:

生成25行指令序列:

1653926187_6294e92b053436a4ce52e.png!small?1653926186667

生成26行指令序列:1653926192_6294e930ee921624a1c99.png!small?1653926192578

比较修复前后的2组指令,条件指令的差异与setnzl VS setzl相反,因为修复前的代码在函数VisitWordCompareZero中执行cont->OverwriteAndNegateIfEqual(kEqual)。

1653926214_6294e94690dd5b255462b.png!small?1653926214381

以下显示了修复漏洞后的TurboFan程序集指令(注意生成的代码指令序列比修复之前更长):

1653926223_6294e94f55397365e907d.png!small?1653926223316

图5: 漏洞修补后的TurboFan程序集说明

在图5中,我们可以看到比较结果首先放在堆栈[rbp-0x28]上,然后将[rbp-0x28]与0进行比较,在这种情况下,它将不会访问gc中的内存,因此不会出现UAF问题。

修补代码对比

-// Used instead of CanCover in VisitWordCompareZero: even if CanCover(user,
-// node) returns false, if |node| is a comparison, then it does not require any
-// registers, and can thus be covered by |user|.
-bool CanCoverForCompareZero(InstructionSelector* selector, Node* user,
-                            Node* node) {
-  if (selector->CanCover(user, node)) {
-    return true;
-  }
-  // Checking if |node| is a comparison. If so, it doesn't required any
-  // registers, and, as such, it can always be covered by |user|.
-  switch (node->opcode()) {
-#define CHECK_CMP_OP(op) \
-  case IrOpcode::k##op:  \
-    return true;
-    MACHINE_COMPARE_BINOP_LIST(CHECK_CMP_OP)
-#undef CHECK_CMP_OP
-    default:
-      break;
-  }
-  return false;
-}
-
 }  // namespace
 
 // Shared routine for word comparison against zero.
@@ -2516,7 +2494,7 @@
     cont->Negate();
   }
 
-  if (CanCoverForCompareZero(this, user, value)) {
+  if (CanCover(user, value)) {
     switch (value->opcode()) {
       case IrOpcode::kWord32Equal:
         cont->OverwriteAndNegateIfEqual(kEqual);
@@ -2536,7 +2514,7 @@
       case IrOpcode::kWord64Equal: {
         cont->OverwriteAndNegateIfEqual(kEqual);
         Int64BinopMatcher m(value);
-        if (m.right().Is(0) && CanCover(user, value)) {
+        if (m.right().Is(0)) {
           // Try to combine the branch with a comparison.
           Node* const eq_user = m.node();
           Node* const eq_value = m.left().node();
@@ -2646,6 +2624,7 @@
         break;
     }
   }
+
   // Branch could not be combined with a compare, emit compare against 0.
   VisitCompareZero(this, user, value, kX64Cmp32, cont);
 }

(https://chromium.googlesource.com/v8/v8/+/71a9fcc950f1b8efb27543961745ab0262cda7c4%5E%21/#F0)

对比修补的代码,我们看到,恢复了canCover函数,删除了CanCoverForCompareZero,这样就不会将对比的节点这种情况来进去到这个switch case里,也就不会产生错误的指令生成。

利用思路

本漏洞通过堆喷的方式,覆盖a的数组内存,然后转化为类型混淆漏洞,之后再利用类型混淆,来实现读写的能力,最后达到RCE的效果,感兴趣的话可以自行尝试构造。

关于作者

Weibo Wang(Nolan)是一名知名安全研究员,目前在总部位于新加坡的网络安全公司Numen Cyber Technology工作。他在著名的区块链项目上发现了许多高危漏洞,如ETH、EOS、Ripple、TRON以及苹果、微软、谷歌等的热门产品。

本文作者:numencyber, 转载请注明来自FreeBuf.COM

© 版权声明
THE END
喜欢就支持一下吧
点赞12赏点小钱 分享