Kept you waiting huh?

So finally we got the final straight of HENkaku CTF. If you don’t remember it, this is the CTF made months ago by the Molecule Team for everyone that is interested in learning more about PS Vita security. Before anything, make sure that you already read both my point of view from stage2 and the xyz write up about the Vita kernel exploit that made all of this possible. Let’s get started!

Stage 3: Cryptanalysis

After finish the stage 2, I started to analyze the stage 3 payloads. As I explained before these payloads are encrypted with AES-ECB and I discovered it because you can leak some information about the plaintext by observing the ciphertext. It’s one of some weakness of ECB mode (and that is one of many reasons that ECB mode is really not recommended). Exploring this weakness in both payload 1 (loader.enc) and payload 2 (payload.enc) I noticed it in payload 1:

In ECB mode if you encrypt with the same key two plaintext with few changes, the ciphertext will only change where plaintext was changed. As the payloads 2 really changed a lot by HENkakus versions (you can notice it doing hex diff between them), I guessed that the last bytes from the payload 1 is the key used to encrypted/decrypted the payload 2! So it has different key per-version. I tried to explore this weakness and try to find a way to get the plaintext, but I didn’t have success. Even with crazy ideas like modifying the key in the end of payload 1 and trying to craft a branch instruction that will run my code in payload2 and others craziness, I considered giving up. As far as I know, only with a known plaintext we could do something. Anyway it gave me some important informations about what I’m dealing and was useful to the next approach.

Stage 3: ROP-chain brute-force

This was the second approach that I tried. To this I needed the max possible of information about the HENkaku stage 3, with the cryptanalysis I determined that we are dealing with payload 1 with a key fixed and a payload 2 that has a key-per-version. Another goods source of information was the xyz writeup about the stage 2 exploit that explained that the leaked addresses used is from SceSysMem and after the stage2 analysis release, Team Mocule updated the HENkaku repository, what was a nice place to look for information! After sometime looking into the loader.rop.in file and krop.rop I found this:

store(&return, mybuf + 3 * 4); // sp add([sysmem_base], 0x347); // pop {pc} to kick off the ropchain store(&return, mybuf + 4 * 4);

Github Link

That’s nice! We have a rop-gadget to start, another good source of information was the end of krop.rop that has some information about the payload 1 (loader.enc) and payload 2(payload.enc).

So gathering all the information I had that:

ROP will decrypt loader.enc if ROP will decrypt loader.enc, there is a gadget to aes calls (like init and decrypt), it needs to allocate memory and *somehow* run the loader.enc after decryption, it needs to copy the memory from stack to the allocated buffer loader.enc has a key per version in the end that decrypt the payload.enc, All gadgets are in the SceSysMem SceSysMem_base+0x347 is the pop{pc} gadget payloader.enc will patch kernel and install vitashell

Another useful hint is that the rop need to make function call. So we will have a lot of pop {reg, pc} gadgets and blx gadgets, I tried to use this to determine some gadgets (what worked with few). With it in mind I started to brute-force the ROP-chain (what is really boring).To do so, we only need to change the krop.rop. We need to determine what is used and what is not. to it, you only need to change the &return after an add, if this is not used, probably HENkaku will boot normal and probably we have a value used in a pop {reg, pc} gadget, otherwise the system will crash and reboot. So you do it again and again […].

add([sysmem_base], 0x31); // addr store(&return, krop + 0x0); // POP {R0, PC} store(0x8106803, krop + 0x4); // any different value add([sysmem_base], 0x1eff1); // addr store(&return, krop + 0x8); // function ptr ? store(0x38, krop + 0xc); // 0x38 not used ? add([sysmem_base], 0x1efe1); // addr store(&return, krop + 0x10); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x14); // POP {PC} add([sysmem_base], 0x39eb); // addr store(&return, krop + 0x18); // no idea, but used add([sysmem_base], 0x1b571); // addr store(&return, krop + 0x1c); // no idea, but used store(0x0, krop + 0x20); // not used, probably REG POP add([sysmem_base], 0x1e43); // addr store(&return, krop + 0x24); // no idea, but used store(0x0, krop + 0x28); // no idea, but used add([sysmem_base], 0x1fc6d); // addr store(&return, krop + 0x2c); // not used, REG POP add([sysmem_base], 0xea73); // addr store(&return, krop + 0x30); // not used, REG POP add([sysmem_base], 0x31); // addr store(&return, krop + 0x34); // POP {R0, PC}, not used ? add([sysmem_base], 0x27913); // addr store(&return, krop + 0x38); // not used, REG POP add([sysmem_base], 0xa523); // addr store(&return, krop + 0x3c); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x40); // POP {PC} add([sysmem_base], 0xce3); // addr store(&return, krop + 0x44); // not idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x48); // POP {PC} add([sysmem_base], 0x1f2b1); // addr store(&return, krop + 0x4c); // not used, REG POP add([sysmem_base], 0x67); // addr store(&return, krop + 0x50); // not used, REG POP add([sysmem_base], 0x587f); // addr store(&return, krop + 0x54); // no idea, but used add([sysmem_base], 0x19713); // addr store(&return, krop + 0x58); // no idea, but used add([sysmem_base], 0x1605); // addr store(&return, krop + 0x5c); // not used, REG POP add([sysmem_base], 0x1e1d); // addr store(&return, krop + 0x60); // no idea, but used store(0x0, krop + 0x64); // not used, REG POP add([sysmem_base], 0x1efe1); // addr store(&return, krop + 0x68); // not used, REG POP add([sysmem_base], 0x347); // addr store(&return, krop + 0x6c); // POP {PC} add([sysmem_base], 0x1603); // addr store(&return, krop + 0x70); // no idea, but used add([sysmem_base], 0x1f2b1); // addr store(&return, krop + 0x74); // not used, REG POP add([sysmem_base], 0x1f17); // addr store(&return, krop + 0x78); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x7c); // POP {PC} add([sysmem_base], 0x31); // addr store(&return, krop + 0x80); // POP {R0, PC} add([sysmem_base], 0xb913); // addr store(&return, krop + 0x84); // no idea, R0 add([sysmem_base], 0x23b61); // addr store(&return, krop + 0x88); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x8c); // POP {PC} add([sysmem_base], 0x39eb); // addr store(&return, krop + 0x90); // no idea, but used add([sysmem_base], 0x232eb); // addr store(&return, krop + 0x94); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0x98); // POP {PC} add([sysmem_base], 0x1b571); // addr store(&return, krop + 0x9c); // no idea, but used add([sysmem_base], 0x23b61); // addr store(&return, krop + 0xa0); // not used, REG POP add([sysmem_base], 0x232f1); // addr store(&return, krop + 0xa4); // no idea, but used add([sysmem_base], 0x1411); // addr store(&return, krop + 0xa8); // not used, REG POP add([sysmem_base], 0xae1); // addr store(&return, krop + 0xac); // no idea, but used add([sysmem_base], 0x347); // addr store(&return, krop + 0xb0); // POP {PC} add([sysmem_base], 0x50e9); // addr store(&return, krop + 0xb4); // no idea, but used add([sysmem_base], 0x1411); // addr store(&return, krop + 0xb8); // no idea, but used store(0x10, krop + 0xbc); // overwrite with 0x90, useless add([sysmem_base], 0x1f2b1); // addr store(&return, krop + 0xc0); // no idea, but used add([sysmem_base], 0x12b11); // addr store(&return, krop + 0xc4); // no idea, but used add([sysmem_base], 0xce3); // addr store(&return, krop + 0xc8); // not used, POP REG add([sysmem_base], 0xd1); // addr store(&return, krop + 0xcc); // not used, POP REG add([sysmem_base], 0x347); // addr store(&return, krop + 0xd0); // POP {PC} add([sysmem_base], 0x1f2b1); // addr store(&return, krop + 0xd4); add([sysmem_base], 0x347); store(&return, krop + 0xd8); add([sysmem_base], 0x39eb); store(&return, krop + 0xdc); add([sysmem_base], 0x1fdc5); store(&return, krop + 0xe0); add([sysmem_base], 0x1d8db); store(&return, krop + 0xe4); add([sysmem_base], 0x19399); store(&return, krop + 0xe8); add([sysmem_base], 0x19399); store(&return, krop + 0xec); add([sysmem_base], 0x11c5f); // addr store(&return, krop + 0xf0); // no idea but used add([sysmem_base], 0x19399); store(&return, krop + 0xf4); add([sysmem_base], 0x347); store(&return, krop + 0xf8); add([sysmem_base], 0xb913); store(&return, krop + 0xfc); store(0x0, krop + 0x100); add([sysmem_base], 0x1efe1); store(&return, krop + 0x104); add([sysmem_base], 0x347); store(&return, krop + 0x108); add([sysmem_base], 0x1861); store(&return, krop + 0x10c); add([sysmem_base], 0x1fc6d); store(&return, krop + 0x110); add([sysmem_base], 0x1f2b1); store(&return, krop + 0x114); add([sysmem_base], 0x347); store(&return, krop + 0x118); add([sysmem_base], 0x39eb); store(&return, krop + 0x11c); add([sysmem_base], 0x19399); store(&return, krop + 0x120); add([sysmem_base], 0x347); store(&return, krop + 0x124); add([sysmem_base], 0x19399); store(&return, krop + 0x128); add([sysmem_base], 0x347); store(&return, krop + 0x12c); add([sysmem_base], 0x39eb); store(&return, krop + 0x130); add([sysmem_base], 0x1614d); store(&return, krop + 0x134); add([sysmem_base], 0x233d3); store(&return, krop + 0x138); add([sysmem_base], 0x1f2b1); store(&return, krop + 0x13c); add([sysmem_base], 0x347); store(&return, krop + 0x140); add([sysmem_base], 0xaf); store(&return, krop + 0x144); add([sysmem_base], 0x1605); store(&return, krop + 0x148); add([sysmem_base], 0x1efe1); store(&return, krop + 0x14c); add([sysmem_base], 0x347); store(&return, krop + 0x150); add([sysmem_base], 0x50e9); store(&return, krop + 0x154); add([sysmem_base], 0x39eb); store(&return, krop + 0x158); add([sysmem_base], 0x1347); store(&return, krop + 0x15c); add([sysmem_base], 0x347); store(&return, krop + 0x160); add([sysmem_base], 0xb9); store(&return, krop + 0x164); add([sysmem_base], 0x1f2b1); store(&return, krop + 0x168); add([sysmem_base], 0x1347); store(&return, krop + 0x16c); add([sysmem_base], 0x347); store(&return, krop + 0x170); add([sysmem_base], 0x39b); store(&return, krop + 0x174); store(0x0, krop + 0x178); add([sysmem_base], 0x1cb95); store(&return, krop + 0x17c); add([sysmem_base], 0x1ea93); store(&return, krop + 0x180); add([sysmem_base], 0x1411); store(&return, krop + 0x184); add([sysmem_base], 0x347); store(&return, krop + 0x188); add([sysmem_base], 0x209d7); store(&return, krop + 0x18c); add([sysmem_base], 0x209d3); store(&return, krop + 0x190); add([sysmem_base], 0x1411); store(&return, krop + 0x194); add([sysmem_base], 0x347); store(&return, krop + 0x198); add([sysmem_base], 0x1baf5); store(&return, krop + 0x19c); add([sysmem_base], 0x1605); store(&return, krop + 0x1a0); add([sysmem_base], 0x347); store(&return, krop + 0x1a4); add([sysmem_base], 0x652b); store(&return, krop + 0x1a8); add([sysmem_base], 0x347); store(&return, krop + 0x1ac); add([sysmem_base], 0x1baf5); store(&return, krop + 0x1b0); add([sysmem_base], 0x22a49); store(&return, krop + 0x1b4); store(0xfffffeb0, krop + 0x1b8); add([sysmem_base], 0x39b); store(&return, krop + 0x1bc); store(0x40, krop + 0x1c0); add([sysmem_base], 0x22a49); store(&return, krop + 0x1c4); add([sysmem_base], 0x347); store(&return, krop + 0x1c8); add([sysmem_base], 0x652b); store(&return, krop + 0x1cc); add([sysmem_base], 0x347); store(&return, krop + 0x1d0); add([sysmem_base], 0x39b); store(&return, krop + 0x1d4); store(0x40, krop + 0x1d8); add([sysmem_base], 0x1605); store(&return, krop + 0x1dc); add([sysmem_base], 0x347); store(&return, krop + 0x1e0); add([sysmem_base], 0x1d9eb); store(&return, krop + 0x1e4); add([sysmem_base], 0x39eb); store(&return, krop + 0x1e8); add([sysmem_base], 0x853); store(&return, krop + 0x1ec); add([sysmem_base], 0x1d8db); store(&return, krop + 0x1f0); store(0x38, krop + 0x1f4); add([sysmem_base], 0xab); store(&return, krop + 0x1f8); add([sysmem_base], 0xd1); store(&return, krop + 0x1fc); add([sysmem_base], 0x2328b); store(&return, krop + 0x200); add([sysmem_base], 0x22fcd); store(&return, krop + 0x204); add([sysmem_base], 0xd1); store(&return, krop + 0x208); add([sysmem_base], 0x1eff1); store(&return, krop + 0x20c); add([sysmem_base], 0x2a117); store(&return, krop + 0x210); add([sysmem_base], 0x347); store(&return, krop + 0x214); add([sysmem_base], 0x1605); store(&return, krop + 0x218); add([sysmem_base], 0x19399); store(&return, krop + 0x21c); add([sysmem_base], 0x347); store(&return, krop + 0x220); add([sysmem_base], 0x39eb); store(&return, krop + 0x224); add([sysmem_base], 0x1bf1f); store(&return, krop + 0x228); store(0xfffffeb0, krop + 0x22c); add([sysmem_base], 0x39b); store(&return, krop + 0x230); store(0x40, krop + 0x234); add([sysmem_base], 0x22a49); store(&return, krop + 0x238); add([sysmem_base], 0x39eb); store(&return, krop + 0x23c); add([sysmem_base], 0x3d73); store(&return, krop + 0x240); store(0x0, krop + 0x244); add([sysmem_base], 0x21fd); store(&return, krop + 0x248); add([sysmem_base], 0x347); store(&return, krop + 0x24c); add([sysmem_base], 0x50e9); store(&return, krop + 0x250); add([sysmem_base], 0xae1); store(&return, krop + 0x254); add([sysmem_base], 0x347); store(&return, krop + 0x258); add([sysmem_base], 0x2a117); store(&return, krop + 0x25c); add([sysmem_base], 0x347); store(&return, krop + 0x260); add([sysmem_base], 0x1f2b1); store(&return, krop + 0x264); add([sysmem_base], 0x67); store(&return, krop + 0x268); add([sysmem_base], 0x39eb); store(&return, krop + 0x26c); add([sysmem_base], 0x1bf47); store(&return, krop + 0x270); add([sysmem_base], 0x347); store(&return, krop + 0x274); add([sysmem_base], 0x50e9); store(&return, krop + 0x278); add([sysmem_base], 0xaf33); // addr store(&return, krop + 0x27c); // used add([sysmem_base], 0x347); // addr store(&return, krop + 0x280); // pop {PC} add([sysmem_base], 0x1d9eb); // addr store(&return, krop + 0x284); // not used store(0x0, krop + 0x288); // payload 2 addr add([sysmem_base], 0x1fc6d); store(&return, krop + 0x28c); add([sysmem_base], 0xea73); store(&return, krop + 0x290); add([sysmem_base], 0x39b); // addr store(&return, krop + 0x294); // not used add([sysmem_base], 0x853); // addr store(&return, krop + 0x298); // used, probably POP{REG1, REG2, PC} store(0xffffffff, krop + 0x29c); // not used store(0x8106803, krop + 0x2a0); // used add([sysmem_base], 0x233d3); store(&return, krop + 0x2a4); add([sysmem_base], 0x347); store(&return, krop + 0x2a8); add([sysmem_base], 0x433); store(&return, krop + 0x2ac); add([sysmem_base], 0x233d3); store(&return, krop + 0x2b0); add([sysmem_base], 0x150a3); store(&return, krop + 0x2b4); store(0x0, krop + 0x2b8); add([sysmem_base], 0xa74d); store(&return, krop + 0x2bc); add([sysmem_base], 0x0); store(&return, krop + 0x2c0); add([sysmem_base], 0x853); // addr store(&return, krop + 0x2c4); // used add([sysmem_base], 0x1bf1f); // addr store(&return, krop + 0x2c8); // used store(0x0, krop + 0x2cc); // used add([sysmem_base], 0x1605); // addr store(&return, krop + 0x2d0); // used add([sysmem_base], 0x347); // addr store(&return, krop + 0x2d4); // POP {PC} add([sysmem_base], 0x50e9); // addr store(&return, krop + 0x2d8); // used add([sysmem_base], 0x1605); // addr store(&return, krop + 0x2dc); // used add([sysmem_base], 0x22fcd); // addr store(&return, krop + 0x2e0); // not used add([sysmem_base], 0x39eb); // addr store(&return, krop + 0x2e4); // used add([sysmem_base], 0x853); // addr store(&return, krop + 0x2e8); // not used add([sysmem_base], 0x11c5f); // addr store(&return, krop + 0x2ec); // function pointer ? // loader.enc (payload1 used to loader the big payloader) store([kx_loader_addr], krop + 0x178); // used // no idea store(0x90, krop + 0xbc); // not used ? // no idea store(0x240, krop + 0x234); // used // no idea store(0x200, krop + 0x2cc); // used // payload 2 store(second_payload, krop + 0x288); // used

After nearly two weeks and countless frustrations in the process I gave up on this approach. But I was forgetting something very useful that HENkaku let us do.

Stage 3: Kernel exploit and loader.enc analysis

Since HENkaku was released it give us a way to run homebrews and it really was very useful to fuzzy the system. I was reading this while I was doing the rop brute-force and decided to give a try and I found a vuln that when exploited gave me a way to dump the kernel, I won’t explain details about it, but when Sony fix, I promise I will make another writeup explaining it.

Once you have the SceSysmem module dumped, it’s not very difficulty to reverse the ROP-chain, here is my pseudo code from the ROP-chain:

void rop_stage3() { // alloc a memory block unsigned int memid = sceKernelAllocMemblock("SceMagic", 0x1020D006, 0xA0000, NULL); void *membase = NULL; // get mem ptr sceKernelGetMemblockBase(memid, &membase); // initialize the AES // ctx ? // memblock size // key size ? // key // that is the key to decrypt loader.enc init_aes(ctx, 0x80, size, key); // here start the undocumented feature, aka rop-loop-chain // it will decrypt each block of loader // and store into the membase // the rop will self modify on the fly and is a bit complex // so probably my pseudo-C has mistakes ( : while (...) { aes_decrypt(ctx, loader_addr, membase); loader_addr += 0x10; membase += 0x10; } // get memory base uid ? unsigned int uid = getuid(membase, 0x0); // remap memory to R-X ? or RWX ? remap_mem(uid, 0x1020D005); // flush data and instruction cache flush_cache(membase, 0x200); // function ptr to membase that will run our payload (*membase)(base_addr, payload2_addr); }

There is something very interesting in this ROP, it has a loop that is used to decrypt each block of the loader.enc, what is amazing in my opinion. Anyway the ROP is very direct, it basically allocate memory, init the AES and finally get inside the loop that will decrypt each block, the ROP will self-modify while it’s doing the decrypt. When the loop end, the system will remap the memory to R-X, flush cache memory and finally jump to the loader.enc code with base_addr and the address of payloadr.enc in the user-land space. If you’re curious to know about the ROP-chain, you can find it here. After it the system will run the loader.enc, here is the loader.enc pseudo code:

/* * load payload 2 * decrypt with AES-128-ECB * run in kernel mode */ void loader(unsigned int base_addr, unsigned int payload2_addr) { /* alloc mem block */ unsigned int (*sceKernelAllocMemBlock)(char *, int, int, void*) = (base_addr+0xA521); /* get memblock base */ unsigned int (*sceKernelGetMemBlockBase)(unsigned int, void**) = (base_addr+0x1F00); /* aes decrypt 128 ecb */ unsigned int (*aes_decrypt)(void *, void*, void*) = (base_addr+0x1BAF5); /* aes init */ unsigned int (*aes_init)(void*, unsigned int, unsigned int, void*) = (base_addr+0x1D8D9); /* memcpy_u2k */ unsigned int (*memcpy_u2k)(unsigned int*, unsigned int*, int) = (base_addr+0x825D); /* memcpy */ unsigned int (*epic_memcpy)(unsigned int*, unsigned int*, int) = (base_addr+0x23095); /* alloc RW- memblock */ unsigned int memid_1 = sceKernelAllocMemBlock("", 0x1020D006, 0xB000, NULL); /* alloc R-X memblock */ unsigned int memid_2 = sceKernelAllocMemBlock("", 0x1020D005, 0xB000, NULL); /* ptr */ void *membase_1 = NULL; void *membase_2 = NULL; /* get base addr to both memory allocated */ sceKernelGetMemBlockBase(memid_1, &membase_1); sceKernelGetMemBlockBase(memid_2, &membase_2); /* copy payload from the user address space to kernel */ memcpy_u2k(membase_1, payload2_addr, 0xA000); /* setup aes using the aes key in .data segment */ init_aes(membase_1 + 0xA000, 0x80, 0x80, aes_key); /* decrypt payload */ for (unsigned int i = 0; i < 0xA000; i += 16) { aes_decrypt(membase_1+0xA000, membase_1+i, membase_1+i); } /* disable DACR, do a memcpy, flush cache and enable DACR again */ epic_memcpy(membase_2, membase_1, 0xB000); /* function pointer to payload */ (*membase_2)(base_addr); }

There are some interesting details here, now it will allocate memory with R-X permission and RW- permission, use a memcpy (using special instructions to read from current pid userland virtual memory space) that will copy the payload.enc to the allocated memory in kernel space, initialize the aes but now using AES-128-ECB, run a loop until decrypt all memory and finally use another memcpy that will copy from RW- to R-X, it will disable some domain from DACR, copy the memory, flush cache and enable again the domains from DACR and finally jump to the payload.enc. Inside the payload.enc we have the code used to install the shell and patch the kernel.

So is this the end?

No, it’s far from the end, there is still a long way to hack the vita system. We are now in the Vita Kernel and we need to explore and study the max possible here, but there are the Trust Zone and Security Core, this saga is far from finished and I hope be back here soon to talk more about Vita.

Anyway, I really enjoyed with the CTF, I want to thank again the Molecule Team (Davee, Proxima, xyz, and YifanLu) for making this amazing challenge, I think not only me but other members from the community learned and enjoyed with it. That’s all folks!