栏目分类
American pepe中文网
chrome v8漏洞CVE-2023-2033分析-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
发布日期:2025-01-04 16:29 点击次数:101
chrome v8漏洞CVE-2023-2033分析
作者: coolboy
前言
这篇文章比较深入的介绍了v8漏洞CVE-2023-2033成因、原理、利用细节以及v8 sandbox对利用的缓解效用。介绍过程中会提及较多源码片段,结合源码享用风味更佳。与此同时提供了原创完整可用exp,这是笔者在其他地方没有找到的。这是一个系列文章,本文是第六篇。前五篇:
第一篇:chrome v8漏洞CVE-2021-30632浅析
第二篇:chrome v8漏洞CVE-2021-37975浅析
第三篇:chrome v8漏洞CVE-2023-3420浅析
第四篇:chrome v8漏洞CVE-2020-16040浅析
第五篇:chrome v8 issue 1486342浅析
POC
先试为快。
编译v8
POC
执行./out/x64.release/d8 poc.js将会得到一个sh(由于并行编译,成功率80%左右),如下:
漏洞分析
有漏洞的版本执行这段代码将得到hole,而打过补丁的版本将得到undefine。hole是什么呢?hole是一个v8实现的内部的值,不应该暴露给js。如果暴露给js,那么将会导致漏洞。更多hole的解释,参考can-anyone-explain-v8-bytecode-ldathehole。下面,将详细分析hole是如何泄露到js中的。
globalThis
什么是globalThis?完整信息可以参考MDN文档globalThis。简单的讲,它是一个对象,代表了一个集合,这个集合包含了所有的全局对象:属性、函数、变量等。申明一个全局变量obj,它也可以通过globalThis.obj来访问,两者是等价的。
Error
Error.captureStackTrace(globalThis) 这是一个非标准的JS api的调用,为globalThis对象添加stack属性。调用console.log(globalThis.stack)将会打印调用堆栈。Error.prepareStackTrace是一个回调函数,在globalThis.stack发生修改操作的时候被调用。参考preparestacktrace
hole泄露过程
代码1处调用defineProperty对globalThis.stack属性进行修改,此时优先调用prepareStackTrace回调函数,当回调函数执行完之后继续执行defineProperty操作。
调用deleteProperty,删除回调函数,防止代码2处调用时对stack做修改时进行无限递归。
调用Reflect.deleteProperty,将stack属性从globalThis对象中移除。Error.captureStackTrace创建属性时,默认属性的configurable值为true,因此此处deleteProperty可以成功。
调用Reflect.defineProperty又重新定义stack属性,此时stack属性的configurable值被重新赋值为false,value被赋值为1.
stack = undefined; stack的value从1被定义为undefined。
代码3处重复调用rGlobal,触发jit优化,将对rGlobal函数做优化。
sleepSync(2000); 睡眠2秒钟
此时回调函数执行完毕,接着执行1处代码后续操作,将globalThis.stack的configurable设置为true。
4处代码得益于1处代码修改了configurable为true,因此可以delete成功。
4处代码delete并没有导致rGlobal解优化,此时globalThis.stack为hole,于是h0le[0]变为了hole泄露给js了。上述过程中,看似平平无奇,实则内藏玄机。还有下面几个问题需要回答。
globalThis可以替换成其他全局对象吗?
答案是不能。将globalThis替换成global_obj,上述代码将打印undefined。为什么呢?我们对比查看两份代码rGlobal的jit汇编。 左右两边分别对应globalThis及global_obj。
globalThis.stack jit优化之后,变成了对PropertyCell的直接访问,PropertyCell对应属性的值,直接就是stack的值。
global_obj.stack jit优化之后,变成了LoadICTrampoline函数调用,这个函数的作用是对global_obj对象,进行stack属性索引。
这两者的差别将导致4处代码delete globalThis.stack执行之后,一个返回hole,另外一个返回undefinedglobalThis和global_obj,两者都是全局对象,为什么导致了jit优化代码的不同呢?经过一番调试了,找到了原因。
ReduceNamedAccess 函数调用发生在jit编译的InliningPhase阶段(关于更多jit知识,可以查看前面几篇系列文章,或者其他公开文档)。参考注释可知,节选出来的代码做了一个优化,对于xxx.stack这样的属性访问,如果xxx是一个global proxy,即globalThis,那么globalThis.stack访问将被转换为对stack全局变量的直接访问。而global_obj.stack进不了这个分支,将进入到其他分支,被优化成对LoadICTrampoline函数的调用。
2处代码的作用是什么?
这两行代码产生了下面的效用:
给globalThis对象增加了stack属性
stack属性的configurable值为false
stack属性的value为1
将stack赋值为undefined,由于undefined(NULL)和1(SMI)属于不相同的类型,于是stack的cell_type为PropertyCellType::kMutable。(关于这部分知识,参考CVE-2021-30632关于PropertyCellType的介绍)总结一下,给globalThis对象增加stack属性,同时stack的configurable为false,且value类型为kMutable。这都是为了通过上面调用的ReduceGlobalAccess函数的检查,见下面代码:
满足“stack的configurable为false,且value类型为kMutable”这两个条件才不会进入到dependencies()->DependOnGlobalProperty的调用,而这个函数的作用是,当stack的值发生变化时,将对当前函数进行解优化。回顾前面的代码:
至此,我们可以回答这个问题了。2处代码的作用就是给stack设置一个合适的值,使得DependOnGlobalProperty函数不被调用,从而在delete globalThis.stack执行的时候不对rGlobal函数做解优化(deopt)。 通过给d8传递参数--trace-deopt可以观察到解优化的日志,可以自行修改configurable为true进行观察。
1处代码执行时stack的configurable已经被回调置为false,为何configurable还能被修改为true?
如上代码,delete obj.abc和第二次Reflect.defineProperty调用都会失败。因为configurable已经被置为false了。那么为什么1处代码在configurable已经为false的情况下,对configurable置为true仍然可以成功呢?
通过lldb调试d8,得到下面的调用堆栈:
GetPropertyDescriptorWithInterceptor函数获取了当前stack属性,存放在desc中。
Object::GetProperty(it).ToHandle 函数调用将触发Error.prepareStackTrace回调。
也就是说执行回调前,v8已经将先前的stack属性缓存在desc变量中了。
GetOwnPropertyDescriptor 获取先前的stack属性,存入current变量,并且调用Error.prepareStackTrace注册的回调
ValidateAndApplyPropertyDescriptor通过current做configurable是否为true的判断,从而决定configurable是否可以修改总结一下:
Error.captureStackTrace定义了stack,此时stack默认configurable为true。
调用Reflect.defineProperty 意图修改configurable为true。下面将Reflect.defineProperty的内部动作进一步拆解。
此时优先获取configurable的值,并且缓存进current变量。
然后执行回调Error.prepareStackTrace,将configurable修改为false(目的是为了优化代码不被解优化)。
根据current变量进行判断,current变量中configurable为true,因此可以做任意修改。
最后Reflect.defineProperty将configurable从false修改为true。
sleepSync(2000)的作用是什么,可以不要或者睡眠其他时间吗?
在d8的release编译模式下,jit编译是同步进行的。rGlobal循环1万次,在某次(比如6000)执行之后,v8会开启一个新的线程对rGlobal函数进行编译,编译成功之后通知主线程,下一次执行就直接执行编译之后的代码。这个时机可能是9000次,也有可能到程序结束也得不到执行。因此这里睡眠2秒钟或者更长时间,等待编译结束。保证下一次执行的时候,是执行的jit代码。
POC详解
POC介绍一下跟此漏洞相关的地方。剩余的部分跟对象内存布局相关,可以通过