Corellium Empowers MidnightSunCTF to Add iOS Exploitation Challenges
On June 15-16th, Midnight Sun's HackingForSoju hosted their Capture The Flag Finals. CTF finalists from around the world were invited to compete head to head, after qualifying in April.
Two of the challenges centered around PAC on iOS with Corellium.
They had to be solved remotely, with only binary code provided. Each team was provided their own virtual iPhone 15 as a debug host by Corellium. The teams did great and solved both challenges during the contest.
Challenge Accepted: See how the top teams stacked up in the CTF Finals.
CTF Victory: celebrating the champions, Tokyo Westerns.
Overview of PAC
iOS Applications like Messages are executed with hardening for memory corruption. One of these layers is Pointer Authentication (or PAC).
It's akin to Address Space Layout Randomization where the addresses of code and data are randomized to extend the work of attackers when they exploit memory corruption.
One of the weaknesses of ASLR, though, is that memory corruption often leads to arbitrary memory reads, which in turn defeat the protection mechanism. Guard pages can help mitigate this but are not a complete solution.
Pointer Authentication uses a key that's not readable by userland processes to "sign" pointers and later "authenticate" them.
The keys are also 128-bits long, so they are sufficiently large enough that a memory leak can't be used to brute force keys offline.
ARMv8.3 provides four abstract signing keys: IA, IB, DA, and DB. The architecture designates IA and IB for signing code pointers and DA and DB for signing data pointers.
The "B" keys can be thought of as "ROP killers" since they sign return addresses or potentially frame pointers, and are randomized per-process execution.
The "A" keys are more of a stop-gap against remote exploitation only since a local program escalating userland privileges would likely be able to forge everything needed with a stack leak.
Still PAC has some notable weaknesses. First, that key signing oracles can appear where code gadgets can authenticate a pointer, often due to a callback or a more unusual integration between an external library call.
Next, PAC’s small signatures mean that PAC fundamentally does not eliminate attacks, but instead it slows them down significantly. And in practice the number of attempts and crashes should make an attack attempt with brute force unlikely, but this may not always be true.
Lastly, side channels may exist which can be used to brute force PAC signatures or leak keys.
Signing Oracles
For the first task, a hand-rolled structure has been created with function pointers signed and stored in memory. To call them they are authenticated in-memory and then branched into.
If a flaw exists in the callee then that callee can modify the naked function pointer, which will be re-signed for the subsequent execution.
Challenge 1
inline void signpacky(void *t, uint64_t modifier) {
uint64_t *target = (uint64_t *)t;
asm volatile("pacib %[reg], %[mod]" : [reg] "+r" (*target) : [mod] "r" (modifier) : );
}
inline void authpacky(void *t, uint64_t modifier) {
uint64_t *target = (uint64_t *)t;
asm volatile("autib %[reg], %[mod]" : [reg] "+r" (*target) : [mod] "r" (modifier) : );
}
void do_work(char *buf) {
char input[64];
ctf_writef(s1, "All these windows. But no fresh air\n");
ctf_readn(s, input, atoi(buf));
}
void drink_soap(char *buf) {
char bubbles[256];
buf[atoi(buf)] = ctf_readsn(s, bubbles, sizeof(bubbles));
}
...
void get_money(char *buf) {
ctf_writef(s1, "Can finally afford tuition\n");
char flagbuf[512];
memset(flagbuf, 0, sizeof(flagbuf));
int fd = open("flag.txt", O_RDONLY);
read(fd, flagbuf, sizeof(flagbuf));
ctf_writef(s1, "%s\n", flagbuf);
return;
}
int child_main()
{
struct command tasks[] = {
{"do_work", do_work},
{"eat_feelings", eat_feelings},
{"find_meaning", find_meaning},
{"drink_soap", drink_soap},
{"get_money", get_money},
};
//The `get_money` routine is not signed and can't be called directly.
for (int i = 0; i < NUM_TASK - 1; i++) {
signpacky(&tasks[i].target, dork);
}
...
for (int i = 0; i < NUM_TASK; i++) {
if (!strcmp(tasks[i].name, buf)) {
found = 1;
authpacky(&tasks[i].target, dork);
tasks[i].target(sep);
signpacky(&tasks[i].target, dork);
break;
}
}
}
Signature Forging
For the second task, players forged a signature with brute force against a forking server.
Challenge 2
int child_main(int s, int s1) {
char buf[256];
ssize_t len;
memset(buf, 0, sizeof(buf));
for (;;) {
len = ctf_readn(s, buf, 1024);
if (len < 1) break;
ctf_writes(s1, buf);
}
return 0;
}
/* easy variant */
void flag() {
ctf_writef(s1, "oh...\n");
char flagbuf[256];
memset(flagbuf, 0, sizeof(flagbuf));
int fd = open("flag.txt", O_RDONLY);
read(fd, flagbuf, sizeof(flagbuf));
ctf_writef(s1, "%s\n", flagbuf);
return;
}
int main(int argc, const char * argv[]) {
int sd;
sd = ctf_listen(1336, IPPROTO_TCP, NULL);
ctf_server(sd, NULL, child_main);
return 0;
}
/*
; pac-ret
; sign link register with the key I B, using the 64-bit stack pointer register as the discriminator.
pac[0x1000047ac] <+0>: pacibsp
pac[0x1000047b0] <+4>: sub sp, sp, #0x150
pac[0x1000047b4] <+8>: stp x26, x25, [sp, #0x100]
pac[0x1000047b8] <+12>: stp x24, x23, [sp, #0x110]
pac[0x1000047bc] <+16>: stp x22, x21, [sp, #0x120]
pac[0x1000047c0] <+20>: stp x20, x19, [sp, #0x130]
; load frame pointer and link register
pac[0x1000047c4] <+24>: stp x29, x30, [sp, #0x140]
...
; restore frame pointer and link register from stack
pac[0x100004898] <+236>: ldp x29, x30, [sp, #0x140]
pac[0x10000489c] <+240>: ldp x20, x19, [sp, #0x130]
pac[0x1000048a0] <+244>: ldp x22, x21, [sp, #0x120]
pac[0x1000048a4] <+248>: ldp x24, x23, [sp, #0x110]
pac[0x1000048a8] <+252>: ldp x26, x25, [sp, #0x100]
pac[0x1000048ac] <+256>: add sp, sp, #0x150
; authenticate link register and set program counter ($pc = $lr)
pac[0x1000048b0] <+260>: retab
*/
Pointer signatures are about 17 bits. That makes 2^17 = 131072 different values to sweep through. The server further forks, which means the IB key is not randomized.
This is useful for chaining multiple gadgets without restarting across attempts, and also greatly reduces the number of attempts.
With key randomization, roughly 400,000 tries would be needed for a 95% success rate. Without key randomization, the total search is then 1/4th the size.
What's next for PAC
ARMv9 improves support for BTI with PAC. BTI is Branch Target Identification, which ensures that dynamic breaches land onto entry point opcodes specified by the compiler.
The br instruction expects to land on a bti j opcode. The blr instruction expects to land on a bti c instruction. The paciasp and pacibsp instructions can also be used as targets with BTI.
Are you interested in learning more about Corellium? The Corellium Virtual Hardware platform provides never-before-possible security vulnerability research for iOS and Android phones. See all our mobile security research capabilities to discover more.
References:
Pointer authentication for arm64e
Enhancing Chromium's Control Flow Integrity with Armv9
PACMAN: Attacking ARM Pointer Authentication with Speculative Execution
Advance Your Mobile Security Research with Corellium
Experience Corellium’s groundbreaking virtualization technology for mobile devices and discover never-before-possible mobile vulnerability and threat research for iOS and Android phones. Book a meeting today to explore how our platform can optimize mobile security research and malware analysis.