怎样分析Chrome 1day 漏洞CVE-2021-21224


本篇文章给大家分享的是有关怎样分析Chrome 1day 漏洞CVE-2021-21224,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。2021年4月4日 – tr0y4 在 Chromium issue tracker 中提交该漏洞;2021年4月12日 – Chromium 修复该漏洞,除了补丁外还公开了相关的poc;2021年4月14日 – 国内的研究员 frust93717815 公开了此漏洞的exp[1][2],影响未开沙箱的稳定版 Chrome 浏览器;2021年4月20日 – Chrome发布更新及致谢,对应的CVE编号为 CVE-2021-21224。该漏洞发生在v8的优化编译器 TurboFan 中,具体发生在 JIT 的 Simplified-Lowering 阶段。关于TurboFan的介绍可以参考 ”Introduction to TurboFan”[3]。有关 Simplefied-Lowering 阶段的具体细节可以参考 CVE-2020-16040 的分析[4]。此外,本文使用Turbolizer 展示 TurboFan 不同优化阶段的 sea of nodes graph。下面简单介绍一下sea of nodes,它是TurboFan运行时的程序表示形式。TurboFan在优化代码时,整个代码是以graph的形式存储的,每个节点就是一个node,包括数学运算、加载、存储、调用、常数等形式。每个node的具体信息如下:每个 node 都有一个restriction type和一个feedback type,前者可以理解为节点初始化后的类型,后者则是在实际优化过程中反馈的真实类型。Node 的representation则表示节点的表示类型,具体如下:
根据commit信息得知,漏洞发生在RepresentationChanger:: GetWord32RepresentationFor函数内。该函数的调用栈如图 1,对该函数的调用主要发生在 Simplified-Lowering 阶段:图1. GetWord32RepresentationFor 调用栈具体补丁如图 2,output_rep为传入节点的Representation类型,output_type为当前节点的feedback type,use_info为当前节点的后继节点信息(主要用于区分32位或64位)。所以修复之前的逻辑是满足两种情况之一可以更新op:一是当前节点的feedback type是Signed32或者UnSigned32;二是当前节点feedback type为SafeInteger且其后继节点被用作32位使用(IsUsedAsWord32)。修复后的逻辑则是满足以下三个条件之一便可以更新:一是当前节点的feedback type是Signed32;二是当前节点feedback type是UnSigned32并且后继节点类型是None;三是当前节点为SafeInteger并且后继节点被用作32位使用。图2. 补丁信息对比修复前后的逻辑可以发现,在当前节点的feedback type为UnSinged32时,并不能直接修改op,需要保证后继节点类型为None才能修改。漏洞正是利用了这一条件,在当前节点满足UnSigned32时,其后继节点类型不为None(exp中后继节点类型如图 3所示,为SingdeSmall,是一个有符号数),最后op被赋值为TruncateInt64ToInt32。因此,所有传入到当前TruncateInt64ToInt32节点的数字都会直接转为一个有符号32位数,如果传入的数字恰好使用到了对应32位数的符号位(比如exp中使用的0xffffffff),那么就会发生整数下溢。图3. 后继节点的 use_info为了验证漏洞,本文使用补丁发布前一个版本的v8代码进行实验,commit哈希为f87baad0f8b3f7fda43a01b9af3dd940dd09a9a3。为了便于理解,本文简化了 exp 中的foo函数如下:这里我们通过对比 Simplified Lowering 及其前一阶段 EscapeAnalysis 的graph结果来展示修复前后turbolizer graph的变化。如图 1所示,在EscapeAnalysis 阶段70号节点对应到Max的返回结果,它作为51号节点的输入,继续进行下一步的减法操作(SpeculativeSafeIntegerSubtract)。而经过Simplified Lowering 阶段后,70号节点和51号节点之间被插入了一个新的95号节点TruncateInt64ToInt32,直接将70号节点的输出转为一个32位有符号数,再进行减法操作。此时再来看本节使用的例子,Max的返回值是0xffffffff,即y=0xffffffff,经过TruncateInt64ToInt32节点后,y被转成32位有符号数,也就是-1,发生了整数下溢。因此z最后经过sign函数得出的结果是1。图 4. 修复前EscapeAnalysis阶段的turbolizer graph图 5. 修复前Simplified Lowering阶段的turbolizer graph修复之后得到的 turbolizer graph如图 6,可以看出原本的TruncateInt64ToInt32 节点现在变成了CheckedUint64ToInt32,在将64位无符号数转为32位有符号数时会进行检查,避免了整数溢出。图6. 修复后Simplified Lowering阶段的turbolizer graph单独的整数溢出并不能实现 RCE,与之前 CVE-2020-16040[5]和CVE-2021-21220[6]的exploit类似,该 exploit 的作者结合shift函数获得一个长度为-1的数组,实现 OOB。exp 中触发漏洞的地方在 foo 函数,下面的代码只保留了原始 exp 中获取长度为 -1 数组的部分。注释中标注出了每一行代码执行后的结果,可以看到最后声明的 arr 数组实际长度为 1,但是经过 shift 操作后长度直接变成了-1:下面通过对turbolizer graph(Simplified-Lowering阶段)中各个节点的分析来详细说明产生上述情况的原因。图7是2-4行代码中包含的关键节点,首先,前两行代码决定x的范围是(-1,4294967295),接着到Math.max函数中,x首先和0比较取最大值,所以更新后的范围是(0, 4294967295),然后再与-1比较取最大值,范围没有变化,仍然是(0, 4294967295)。图7. Math.max函数输出结果在此之后90号节点的值会先被转为Int32然后进行一个减法操作,对应代码就是第5行中的0-y,对应的范围是(-4294967295,0)。需要注意的是这里的范围并不是真实的范围,后续还会有 feedback type 的更新会进一步更新这个范围,但是这里对后续的结果并没有影响。Math.sign函数会根据根据参数的正负,返回+1/-1,此外还有+0/-0/NaN,由于这里+-0并不会对最终结果有影响,所以本文不做过多讨论。由于0-y的值小于等于零,所以该函数输出结果只能是(-1,0),即z的范围是(-1,0)。接着到第6行代码声明数组,会对数组的边界做检查(长度必须要大于零),所以把z的范围和0取交集得到最后数组长度的范围是(0,0)。Shift操作会移除数组中的一个元素,并把数组长度减一。由于数组的长度只能是0,所以jit在优化过程中直接把shift之后数组的长度固定为0-1 = -1,这就是为什么shift之前数组长度是1,shift之后长度直接变成了-1。这里其实利用了shift函数中存在的一个bug,即该函数未对数组的边界做检查,无论原始数组的长度如何都直接进行减一操作。由于数组长度为0,所以shift之后长度固定为-1,这一值被直接写入到jit优化后的代码中(在图 9中,长度被直接赋值为0xfffffffe,也就是-2,经过调试发现jsarry在内存中存储的长度数值左移了1位,这是为了保证其内存中所有的数字都是以0结尾,指针以1结尾,具体可以参考JS类型对象的内存布局[8]),因此最终得到数组的长度为-1。图 8. 边界检查结果还有一点需要注意的是,由于shift操作还会移除数组中的的一个元素,所以如果第6行声明的数组中不包含任何元素,则移除元素操作会导致v8直接崩溃。exp中利用溢出恰好声明了一个长度为1的数组,满足了这一限制条件。这也解释了另一个问题,如果不利用整数溢出漏洞,直接声明一个长度为0的数组,那么根据优化后的jit代码是不是也能得到一个长度为-1的数组呢。实际上jit代码中在shift之前对长度做了验证,如图 9所示,rdi为数组的长度,如果长度为0的话会直接跳过数组长度更新而直接返回,所以无法获得一个长度-1的数组。图9. 调用shift前的边界检查及长度赋值操作当然,谷歌也在4月15日的更新中修复了该bug[7]。在该补丁中,shift以及类似的pop函数在计算出新的数组长度后会首先进行边界检查,基本上杜绝了类似的利用方式。图 10. [turbofan] Harden ArrayPrototypePop and ArrayPrototypeShift补丁1) 越界访问rwarr数组exp在arr数组之后又声明了一个长度为2的数组和一个长度为0x1000的ArrayBuffer,接着修改了corrput_arr[12]的值,这里其实就是利用溢出修改了rwarr数组的长度。根据调试信息可以得知,corrput_arr中元素的位置在0x18b80828216c,每个元素的长度是4字节,rwarr中元素的位置在0x18b8082821a8,根据二者之间的距离,减去corrput_arr前八个字节(分别为元素的map和长度),就可以计算出corrput_arr数组第13个元素对应的就是rwarr数组的长度。通过将该长度修改为一个较大的值(比如0x22444),可以实现对rwarr数组的越界访问。2) 越界访问corrupt_buffsetbackingStore 就是设置corrput_buf中backing_store的值,leakObjLow则是泄露地址o处的信息。rwarr设置的第一个元素值为5.1,长度8字节,所以数组中元素都是8字节,因此rwarr[4]的位置在0x18b8082821a98+8+4*8=0x18b8082821ac0,该地址再加4便是corrupt_buff的backing_store的地址, 因此这里还需要用到rwarr[5],分别修改rwarr[4]的高四个字节和rwarr[5]的低4个字节,就可以实现对backing_store的修改。后续exp会从corrupt_buff中生声明一个Dataview,而backing_store记录的就是实际DataView的内存地址。如果我们将这个backing_store指针修改为我们想要写入的内存地址,那么我们再调用view.setUint32(0, poc, true)类似指令时,实际上就是向指定内存地址处写入了poc,从而达到任意地址写。leakObjLow函数使用corrupt_buff的slot属性,修改该属性为某一对象o,那么o的地址就会被写入到corrupt_buff所在的内存区间中,然后利用rwarr的溢出访问该值,实现泄露。代码免费云主机域名执行的方法利用了WASM的RWX段。通过泄露该RWX段的地址,将shellcode写入,最后调用WASM的导出函数即可实现代码执行。以上就是怎样分析Chrome 1day 漏洞CVE-2021-21224,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注云编程开发博客行业资讯频道。

相关推荐: chrome 下使用维基百科

维基百科是非常好用的查询工具生活在国内怎么使用了下面提供几种方法: 使用环境: window 浏览器推荐: chrome 1. 插件: 使用wikiwand插件 Wikiwand是一款能够改变维基百科条目界面的软件,2013年由利奥尔格罗斯曼和依 兰列文创建,…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 02/05 16:47
下一篇 02/05 16:47