Corellium Empowers MidnightSunCTF to Add iOS Exploitation Challenges

HackingForSoju, organizers of MidnightSunCTF, challenged competitors to conquer the PAC mitigation, leveraging Corellium's virtual iPhone 15.
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. 

Midnight Sun CTF Winners

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).

Midnight Sun CTF

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

Pointer authentication for arm64e

Enhancing Chromium's Control Flow Integrity with Armv9

PACMAN: Attacking ARM Pointer Authentication with Speculative Execution