Create a simple hello world type application to demonstrate how pointer authentication works.
The example contains a function func2() to start a shell, that is never called by the application.
Using your preferred text editor, create main.c containing the below.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func1(char *s)
{
char buffer[16];
strcpy(buffer, s);
}
void func2(void)
{
printf("Hello from func2!\n");
system("/bin/sh");
}
int main(int argc, char **argv)
{
if (argc > 1)
{
func1(argv[1]);
printf("Hello World!\n");
}
return 0;
}
Create a Makefile with the contents below.
CROSS_COMPILE ?= aarch64-linux-gnu-
CC=$(CROSS_COMPILE)gcc
CFLAGS += -march=armv8.5-a -fPIC -pedantic -Wall -Wextra -ggdb3 -O0
LDFLAGS += -fPIE -static
PROJ := main_pac
PROJ_NOPAC := main_nopac
.PHONY: all clean
all: none pac
clean:
rm -f $(PROJ) $(PROJ_NOPAC)
none: CFLAGS+=-fno-stack-protector
none: $(PROJ_NOPAC)
pac: CFLAGS+=-mbranch-protection=standard -fno-stack-protector
pac: $(PROJ)
$(PROJ): main.c
$(CC) $(CFLAGS) $(LDFLAGS) main.c -o $@
$(PROJ_NOPAC): main.c
$(CC) $(CFLAGS) $(LDFLAGS) main.c -o $@
dump_none: $(PROJ_NOPAC)
gdb-multiarch -batch -ex "disassemble main" $<
gdb-multiarch -batch -ex "disassemble func1" $<
dump_pac: $(PROJ)
gdb-multiarch -batch -ex "disassemble main" $<
gdb-multiarch -batch -ex "disassemble func1" $<
Arm Compiler for Embedded, GCC, and LLVM can all produce code making use of pointer authentication.
Generation of such code is controlled by:
-mbranch-protection=<protection>
Where <protection> can have different inputs:
none means that no branch protection enabled. This is the default.standard enables all types of PAC return address signing and branch protection.Build with the command:
make all
This will create 2 executables, main_nopac and main_pac.
| Application | -mbranch-protection |
|---|---|
| main_nopac | none |
| main_pac | standard |
Inspect the difference in the generated code used for applications built with and without pointer authentication and BTI.
Inspect the disassembled contents of functions main and func1 in main_nopac:
make dump_none
You will see the similar to the following:
Dump of assembler code for function main:
0x0000000000400718 <+0>: stp x29, x30, [sp, #-32]!
0x000000000040071c <+4>: mov x29, sp
0x0000000000400720 <+8>: str w0, [sp, #28]
0x0000000000400724 <+12>: str x1, [sp, #16]
0x0000000000400728 <+16>: ldr w0, [sp, #28]
0x000000000040072c <+20>: cmp w0, #0x1
0x0000000000400730 <+24>: b.le 0x400750 <main+56>
0x0000000000400734 <+28>: ldr x0, [sp, #16]
0x0000000000400738 <+32>: add x0, x0, #0x8
0x000000000040073c <+36>: ldr x0, [x0]
0x0000000000400740 <+40>: bl 0x4006d4 <func1>
0x0000000000400744 <+44>: adrp x0, 0x459000 <do_release_all+32>
0x0000000000400748 <+48>: add x0, x0, #0x710
0x000000000040074c <+52>: bl 0x4076d0 <puts>
0x0000000000400750 <+56>: mov w0, #0x0 // #0
0x0000000000400754 <+60>: ldp x29, x30, [sp], #32
0x0000000000400758 <+64>: ret
...
Dump of assembler code for function func1:
0x00000000004006d4 <+0>: stp x29, x30, [sp, #-48]!
0x00000000004006d8 <+4>: mov x29, sp
0x00000000004006dc <+8>: str x0, [sp, #24]
0x00000000004006e0 <+12>: add x0, sp, #0x20
0x00000000004006e4 <+16>: ldr x1, [sp, #24]
0x00000000004006e8 <+20>: bl 0x415880 <strcpy>
0x00000000004006ec <+24>: nop
0x00000000004006f0 <+28>: ldp x29, x30, [sp], #48
0x00000000004006f4 <+32>: ret
Both the functions in this case use the stp to push pairs of 64-bit registers onto the stack at entry, and retto return from the functions.
Now let us inspect the disassembled contents of the same functions in main_pac
make dump_pac
You will see similar to:
Dump of assembler code for function main:
0x0000000000400720 <+0>: paciasp
0x0000000000400724 <+4>: stp x29, x30, [sp, #-32]!
0x0000000000400728 <+8>: mov x29, sp
0x000000000040072c <+12>: str w0, [sp, #28]
0x0000000000400730 <+16>: str x1, [sp, #16]
0x0000000000400734 <+20>: ldr w0, [sp, #28]
0x0000000000400738 <+24>: cmp w0, #0x1
0x000000000040073c <+28>: b.le 0x40075c <main+60>
0x0000000000400740 <+32>: ldr x0, [sp, #16]
0x0000000000400744 <+36>: add x0, x0, #0x8
0x0000000000400748 <+40>: ldr x0, [x0]
0x000000000040074c <+44>: bl 0x4006d4 <func1>
0x0000000000400750 <+48>: adrp x0, 0x459000 <do_release_all+32>
0x0000000000400754 <+52>: add x0, x0, #0x710
0x0000000000400758 <+56>: bl 0x4076d0 <puts>
0x000000000040075c <+60>: mov w0, #0x0 // #0
0x0000000000400760 <+64>: ldp x29, x30, [sp], #32
0x0000000000400764 <+68>: retaa
...
Dump of assembler code for function func1:
0x00000000004006d4 <+0>: paciasp
0x00000000004006d8 <+4>: stp x29, x30, [sp, #-48]!
0x00000000004006dc <+8>: mov x29, sp
0x00000000004006e0 <+12>: str x0, [sp, #24]
0x00000000004006e4 <+16>: add x0, sp, #0x20
0x00000000004006e8 <+20>: ldr x1, [sp, #24]
0x00000000004006ec <+24>: bl 0x415880 <strcpy>
0x00000000004006f0 <+28>: nop
0x00000000004006f4 <+32>: ldp x29, x30, [sp], #48
0x00000000004006f8 <+36>: retaa
See how in this case the instructions used at function entry (paciasp) and return (retaa) have changed.
lr) with Stack Pointer(sp) as the modifier.In the next section, you will exploit main_nopac and show how the same technique does not work on main_pac due to pointer authentication.