Search code examples
assemblyarm

How to enable the Arm Pointer Authentication Code (PAC) on macOS?


How do you enable the Arm Pointer Authentication Code (PAC) on macOS?

I have a MacBook Air with an Apple M1 chip. The CPU implements the Arm architecture version v8.5-A, which includes the Pointer Authentication Code (PAC) instructions. This feature is typically used to prevent malware code injection through ROP chains, typically exploiting a buffer overflow on the stack.

I try to demonstrate the behavior of some PAC instructions through simple code.

On the native macOS, the authentication instructions (PACIA, PACDA, etc) seem to have no effect, as if the feature was not implemented in the CPU. This could be possible since most Arm features are optional. However, after installing a Linux virtual machine on the same MacBook, the same PAC instructions work inside the Linux VM. On the same physical CPU, which consequently supports the PAC feature.

So, there must be some way to disable the behavior of PAC on a process basis: not active in native macOS applications, active in applications running in the Linux VM.

How would you enable PAC on macOS?

The sample code pacia.c below

  1. displays an instruction address,
  2. adds a PAC using PACIA instruction and displays it
  3. "authenticate" it (restore its original value if the PAC is correct) using AUTIA instruction and displays it.

We expect that the second address has its MSB part altered by the PAC. We expect that the third address is identical to the first one.

#include <stdio.h>
#include <inttypes.h>

// noinline for easier inspection of generated code in main
__attribute__((noinline)) void report(uint64_t value)
{
    printf("%016" PRIX64 "\n", value);
}

int main(int argc, char* argv[])
{
    uint64_t data = (uint64_t)(&&lab);
    uint64_t modifier = 2;
 lab:
    report(data);
    asm("pacia %[reg], %[mod]" : [reg] "+r" (data) : [mod] "r" (modifier) : );
    report(data);
    asm("autia %[reg], %[mod]" : [reg] "+r" (data) : [mod] "r" (modifier) : );
    report( data);
}

Compilation:

cc -O2 -march=armv8.5-a pacia.c -o pacia

On the host system, macOS 13.1, the PACIA instruction does not modify the address to authenticate.

$ ./pacia 
00000001028B3F50
00000001028B3F50  <-- not modified, no PAC
00000001028B3F50
$ ./pacia 
000000010080FF50
000000010080FF50
000000010080FF50
$ ./pacia 
0000000102A7FF50
0000000102A7FF50
0000000102A7FF50
$ 

On the Ubuntu 22.10 virtual machine, the MSB part of the address is updated with a PAC by PACIA and correctly removed by AUTIA.

$ ./pacia
0000AAAACF3D0680
0043AAAACF3D0680  <-- 0043 PAC added
0000AAAACF3D0680  <-- PAC removed, address restored
$ ./pacia
0000AAAAD7CF0680
0023AAAAD7CF0680
0000AAAAD7CF0680
$ ./pacia
0000AAAAAAE00680
0036AAAAAAE00680
0000AAAAAAE00680

Just to make sure, I inspected the generated code on macOS. The PACIA instruction is actually used.

cc -O2 -march=armv8.5-a pacia.c -S -o pacia.s

Generated code of main() on macOS with clang 14.0.0:

_main:                                  ; @main
        .cfi_startproc
; %bb.0:
Ltmp0:                                  ; Block address taken
; %bb.1:
        stp     x20, x19, [sp, #-32]!           ; 16-byte Folded Spill
        stp     x29, x30, [sp, #16]             ; 16-byte Folded Spill
        add     x29, sp, #16
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
        .cfi_offset w29, -16
        .cfi_offset w19, -24
        .cfi_offset w20, -32
Lloh2:
        adrp    x19, lCPI1_0@PAGE
Lloh3:
        ldr     x19, [x19, lCPI1_0@PAGEOFF]  <--- data = (uint64_t)(&&lab) in x19
        mov     x0, x19       <--- x19 is printed (first time)
        bl      _report
        mov     w20, #2       <--- modifier = 2 in x20
        ; InlineAsm Start
        pacia   x19, x20      <--- x19 should receive a PAC code
        ; InlineAsm End
        mov     x0, x19       <--- x19 is printed (second time)
        bl      _report
        ; InlineAsm Start
        autia   x19, x20
        ; InlineAsm End
        mov     x0, x19
        bl      _report
        mov     w0, #0
        ldp     x29, x30, [sp, #16]             ; 16-byte Folded Reload
        ldp     x20, x19, [sp], #32             ; 16-byte Folded Reload
        ret

Solution

  • After a suggestion from @fuz, I added -arch arm64e in addition to -march=armv8.5-a. This marks the binary as arch64e instead of arm64, as seen with file for instance:

    $ file pacia
    pacia: Mach-O 64-bit executable arm64e
    

    Trying to run it failed with Killed: 9. Googling a bit, I see that you need to enable the arm64e architecture system wide, using the boot flag -arm64e_preview_abi:

    sudo nvram boot-args=-arm64e_preview_abi
    

    However, you can do that only after disabling the system integrity. So, I rebooted in recovery mode, did csrutil disable, rebooted in normal mode, set the boot flags, rebooted in recovery mode, did csrutil enable, rebooted in normal mode. No luck, still Killed: 9. Checked the boot flags using nvram -p, the option -arm64e_preview_abi is there.

    In fact, it seems that you need to stay with system integrity disabled to be able to run an arm64e binary. This is a pity since the PAC instructions are supposed to increase the security of the application but you need to disable the system integrity (and consequently reduce the system security) to run more secure applications. Weird...

    Anyway, after a couple of reboots and system integrity permanently disabled, the application runs.

    However, it initially produces strange results:

    $ ./pacia
    5F3A800100787F3C
    0000000100787F3C
    2000000100787F3C
    $ ./pacia
    B273800102563F3C
    0080000102563F3C
    BFFF800102563F3C
    $ ./pacia
    555C80010065FF3C
    000000010065FF3C
    200000010065FF3C
    $
    

    The input addresses already have extra info in the MSB part. PACIA mostly removes it (0000 in two cases, 0080 in one case) and AUTIA does not restore the previous address.

    In fact, using -arch arm64e also affected the code generation. Taking a real code address as in &&lab generates an implicit pointer authentication:

        add x16, x16, Ltmp0@PAGEOFF
        mov x17, #13503
        pacia   x16, x17. <-- added PACIA due to -arch arm64e
        mov x19, x16
        mov x0, x16
        bl  _report
        mov w20, #2
        ; InlineAsm Start
        pacia   x19, x20.  <-- my explicit PACIA
        ; InlineAsm End
        mov x0, x19
        bl  _report
    

    So, my explicit PACIA worked on an address with already a PAC in it. This destroyed the previous PAC.

    To exhibit the exact behavior of PACIA, I simply used an integer value data = 0x12345678 to control the exact input value of my explicit PACIA.

    This time, it works as expected:

    0000000012345678
    2503000012345678
    0000000012345678
    

    So, we now know how to enable PAC on macOS. The disturbing point is that you need to keep the system integrity permanently disabled. Fine for some developers tests, not for production usage.