Liam Wachter, Julian Gremminger
Karlsruhe Institute of Technology
KIT Computer Science Student
pwn/rev main CTF player @ KITCTF and orgakraut
Found one V8 bug and counting
KIT Computer Science Student
rev/pwn main CTF player @ KITCTF, polygl0ts, orga{niser,kraut}
Uses ML for vulnerability discovery @ SAP Security Research
//renderer/bindings
FunctionTemplate
and Object Template
Parsed two times, fast partial approximate, slow exact
E.g. also used by Node.js
v8::Isolate
v8::Context
s[CustomToV8]
interface Node {
const unsigned short ELEMENT_NODE = 1;
attribute Node parentNode;
[TreatReturnedNullStringAs=Null] attribute DOMString nodeName;
[Custom] Node appendChild(Node newChild);
void addEventListener(DOMString type, EventListener listener,
optional boolean useCapture);
};
function printA(obj) {
console.log(obj.a);
}
for (let i = 0; i < 10000; i++) {
printA({a: i});
}
V8 gets the script as text (or as StreamedSource
)
Hand written recursive descent parser
Separate passes for AST building and scope analysis (hard)
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . FUNCTION "printA" = function printA
IGNITION_HANDLER(Star, InterpreterAssembler) {
TNode<Object> accumulator = GetAccumulator();
StoreRegisterAtOperandIndex(accumulator, 0);
Dispatch();
}
void InterpreterAssembler::Dispatch() {
Comment("========= Dispatch");
DCHECK_IMPLIES(Bytecodes::MakesCallAlongCriticalPath(bytecode_), made_call_);
TNode<IntPtrT> target_offset = Advance();
TNode<WordT> target_bytecode = LoadBytecode(target_offset);
DispatchToBytecodeWithOptionalStarLookahead(target_bytecode);
}
Insanely long call chain to arrive at the code doing the actual work
What kind of types do we encounter during execution?
function printAx2(obj) {
console.log(obj.a + obj.a); // <-- int + int
}
for (let i = 0; i < 10000; i++) {
printAx2({a: i});
}
function printAx2(obj) {
console.log(obj.a + obj.a); // <-- str + str
}
for (let i = 0; i < 10000; i++) {
printAx2({a: i.toString()});
}
function printAx2(obj) {
console.log(obj.a + obj.a); // <-- int + int
}
for (let i = 0; i < 10000; i++) {
printAx2({a: i});
}
Monomorphic
In bytecode this is:
GetProperty(obj, "a", feedback_cache)
function printAx2(obj) {
console.log(obj.a + obj.a); // <-- int + int | str + str
}
for (let i = 0; i < 10000; i++) {
printAx2({a: i});
}
for (let i = 0; i < 10000; i++) {
printAx2({a: i.toString()});
}
Polymorphic
function printAx2(obj) {
console.log(obj.a + obj.a);
}
printAx2({a: 42});
printAx2({a: 0.23});
printAx2({a: "asdf"});
printAx2({a: {lmao: 7}});
printAx2({a: BigInt(1337)});
Megamorphic
Functions that get executed a lot with the same feedback get hot 🔥
for (let i = 0; i < 10000; i++) {
printA({a: i});
}
What happens when assumptions are broken?
function printAx2(obj) {
console.log(obj.a + obj.a);
}
// optimize printAx2
for (let i = 0; i < 10000; i++) {
printAx2({a: i}); // <-- int
}
printAx2({a: "asdf"}); // <-- str
Optimized code is not correct anymore!
__ CodeEntry();
{
RCS_BASELINE_SCOPE(Visit);
Prologue();
AddPosition();
for (; !iterator_.done(); iterator_.Advance()) {
VisitSingleBytecode();
AddPosition();
}
}
void BaselineCompiler::VisitLdaZero() {
__ Move(kInterpreterAccumulatorRegister,
Smi::FromInt(0));
}
void BaselineAssembler::Move(
interpreter::Register output,
Register source) {
return __ movq(RegisterFrameOperand(output),
source);
}
void MaglevGraphBuilder::VisitAdd() { VisitBinaryOperation<Operation::kAdd>(); }
template <Operation kOperation>
void MaglevGraphBuilder::VisitBinaryOperation() {
...
switch (feedback_hint) {
case BinaryOperationHint::kNone:
RETURN_VOID_ON_ABORT(EmitUnconditionalDeopt(
DeoptimizeReason::kInsufficientTypeFeedbackForBinaryOperation));
case BinaryOperationHint::kSignedSmall:
case BinaryOperationHint::kSignedSmallInputs:
case BinaryOperationHint::kNumber:
case BinaryOperationHint::kNumberOrOddball: {
... // Optimized for number operation
}
case BinaryOperationHint::kString:
... // Optimized for string operation
}
}
function f(obj, i) {
if (i % 2 == 0) {
obj.a = obj.a + 1;
}
}
What happens when side effects are modeled incorrectly?
CVE-2018-17463- V(CreateObject, Operator::kNoWrite, 1, 1) \
+ V(CreateObject, Operator::kNoProperties, 1, 1) \
Object.create
did not write to the side effect chain but could have had side effects!
Incorrect removal of important checks!
Most passes consist of n fixpoint iteration reducers on the graph
struct TypedLoweringPhase {
DECL_PIPELINE_PHASE_CONSTANTS(TypedLowering)
void Run(PipelineData* data, Zone* temp_zone) {
...
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &simple_reducer);
AddReducer(data, &graph_reducer, &common_reducer);
...
graph_reducer.ReduceGraph();
}
}
DeadCodeElimination
, ConstantFoldingReducer
, MachineOperatorReducer
, ...
Reduction MachineOperatorReducer::Reduce(Node* node) {
...
case IrOpcode::kInt32Mul: {
Int32BinopMatcher m(node);
if (m.right().Is(0)) return Replace(m.right().node()); // x * 0 => 0
if (m.right().Is(1)) return Replace(m.left().node()); // x * 1 => x
if (m.right().Is(-1)) { // x * -1 => 0 - x
node->ReplaceInput(0, Int32Constant(0));
node->ReplaceInput(1, m.left().node());
NodeProperties::ChangeOp(node, machine()->Int32Sub());
return Changed(node);
}
...
}
}
function g(i: int) {
return i & 3;
}
function f(i: int) {
if (g(i) <= 3) {
i = 0;
}
return i;
}
function f(i: int) {
if (i & 3 <= 3) {
i = 0;
}
return i;
}
Enables strong optimization possibilities
function f(i: int) {
return 0;
}
function f(i) {
return i & 3;
}
function f() {
var obj = {a: 1337};
console.log(obj.a);
}
function f() {
console.log(1337);
}
String.indexOf
Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
...
case kStringLastIndexOf:
return Type::Range(-1.0, String::kMaxLength - 1.0, t->zone());
Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
...
case kStringLastIndexOf:
return Type::Range(-1.0, String::kMaxLength - 1.0, t->zone());
const kMaxLength = 0x1fffffe8;
function hax() {
let s = "_".repeat(kMaxLength);
let bad = s.indexOf("", kMaxLength); // Type = (-1.0, 0x1fffffe7) | Actual = 0x1fffffe8!
...
}
Mismatch between assumed type and actual value
Array.pop()
, Array iterator .next()
https://bugs.chromium.org/p/chromium/issues/detail?id=1423487 (still restricted!)
var arr = [1.1, 2.2, , , 5.5];
arr[2]; // internally represented by TheHole
Currently very hot topic
Recently found in-the-wild bugs
Public (old) way via Map.set()
+ Map.delete()
(now patched)
addrof = getaddrOf();
fakeObj = getfakeObj();
var testObj = {a: 1337, b: 420};
if (!fakeObj(addrof(testObj)) === testObj) throw "fakeObj/addrof bricked!";
print("fakeobj/addrof working as expected!");