Back to Technology

ARM Assembly Part 19: Reverse Engineering & ARM Binary Analysis

May 14, 2026 Wasil Zafar 23 min read

Reverse engineering flips the assembly workflow: instead of writing instructions, you read them to understand what a binary does. ARM's ubiquity across Android, iOS, and embedded firmware makes it the most important architecture for binary analysis today. Mastering ELF layout, symbol conventions, platform-specific quirks, and tool workflows separates shallow analysis from genuine understanding.

Table of Contents

  1. Introduction & RE Mindset
  2. ELF Section Layout for RE
  3. Ghidra ARM64 Workflow
  4. Binary Ninja MLIL
  5. iOS Mach-O Binaries
  6. Android NDK .so Quirks
  7. Identifying Compiler Idioms
  8. Case Study: IoT Firmware RE
  9. Hands-On Exercises
  10. RE Analysis Worksheet Tool
  11. Conclusion & Next Steps

Introduction & RE Mindset

Series Overview: Part 19 of 28. Parts 1–18 taught writing assembly. Part 19 teaches reading it — in binaries you didn't write, on platforms you may not control, with symbols stripped and optimizations enabled.

ARM Assembly Mastery

Your 28-step learning path • Currently on Step 19
1
Architecture History & Core Concepts
ARMv1→v9, RISC philosophy, profiles
2
ARM32 Instruction Set Fundamentals
ARM vs Thumb, registers, CPSR, barrel shifter
3
AArch64 Registers, Addressing & Data Movement
X/W regs, addressing modes, load/store pairs
4
Arithmetic, Logic & Bit Manipulation
ADD/SUB, bitfield extract/insert, CLZ
5
Branching, Loops & Conditional Execution
Branch types, link register, jump tables
6
Stack, Subroutines & AAPCS
Calling conventions, prologue/epilogue
7
Memory Model, Caches & Barriers
Weak ordering, DMB/DSB/ISB, TLB
8
NEON & Advanced SIMD
Vector ops, intrinsics, media processing
9
SVE & SVE2 Scalable Vector Extensions
Predicate regs, gather/scatter, HPC/ML
10
Floating-Point & VFP Instructions
IEEE-754, scalar FP, rounding modes
11
Exception Levels, Interrupts & Vector Tables
EL0–EL3, GIC, fault debugging
12
MMU, Page Tables & Virtual Memory
Stage-1 translation, permissions, huge pages
13
TrustZone & ARM Security Extensions
Secure monitor, world switching, TF-A
14
Cortex-M Assembly & Bare-Metal Embedded
NVIC, SysTick, linker scripts, low-power
15
Cortex-A System Programming & Boot
EL3→EL1 transitions, MMU setup, PSCI
16
Apple Silicon & macOS ABI
ARM64e PAC, Mach-O, dyld, perf counters
17
Inline Assembly, GCC/Clang & C Interop
Constraints, clobbers, compiler interaction
18
Performance Profiling & Micro-Optimization
Pipeline hazards, PMU, benchmarking
19
Reverse Engineering & ARM Binary Analysis
ELF, disassembly, CFR, iOS/Android quirks
You Are Here
20
Building a Bare-Metal OS Kernel
Bootloader, UART, scheduler, context switch
21
ARM Microarchitecture Deep Dive
OOO pipelines, reorder buffers, branch predict
22
Virtualization Extensions
EL2 hypervisor, stage-2 translation, KVM
23
Debugging & Tooling Ecosystem
GDB, OpenOCD/JTAG, ETM/ITM, QEMU
24
Linkers, Loaders & Binary Format Internals
ELF deep dive, relocations, PIC, crt0
25
Cross-Compilation & Build Systems
GCC/Clang toolchains, CMake, firmware gen
26
ARM in Real Systems
Android, FreeRTOS/Zephyr, U-Boot, TF-A
27
Security Research & Exploitation
ASLR, PAC attacks, ROP/JOP, kernel exploit
28
Emerging ARMv9 & Future Directions
MTE, SME, confidential compute, AI accel

Approach a binary bottom-up: first understand structure (what sections exist, what symbols are exported), then understand control flow (what functions call what), then understand data flow (what registers carry meaningful values through basic blocks). Tools accelerate the process but don't replace understanding the ISA — which is why Parts 1–18 came before this one.

Real-World Analogy — Archaeology: Reverse engineering a binary is like excavating an ancient city. The ELF headers are your site survey — they tell you where the walls (sections) are and how big the settlement was. Symbol tables are like inscriptions on buildings: they name the important structures, but many have been weathered away (stripped). Disassembly is brushwork on pottery shards — you reconstruct the original form from fragments, recognising patterns (compiler idioms) the way an archaeologist recognises a kiln or a granary from its layout. Ghidra's decompiler is your Rosetta Stone: it translates the raw artifacts into something closer to the original language (C pseudocode), but like any translation, it's an approximation that requires expert interpretation.

ELF Section Layout for RE

Key ELF Sections

.text — executable code (functions)
.plt / .plt.got — Procedure Linkage Table stubs; each entry = 3 instructions. First call resolves symbol via dynamic linker; subsequent calls jump directly.
.got / .got.plt — Global Offset Table: pointer-sized entries patched at load time for PIC code and lazy-bound symbols
.rodata — read-only data: string literals, const arrays, jump tables
.data — initialized mutable globals
.bss — zero-initialized globals (no bytes in file; just size recorded in section header)
.init_array / .fini_array — arrays of constructor/destructor function pointers called by crt0
.dynsym / .dynstr — dynamic symbol table + string pool (exported/imported symbols)
.rela.dyn / .rela.plt — RELA relocation tables with addend (type, symbol, offset, addend)
.note.gnu.build-id — 20-byte SHA1 of the binary; stable identifier even after stripping

readelf Command Reference

# Full overview: ELF header + section headers + symbol tables
readelf -a ./binary | head -200

# Section headers only (shows offsets, sizes, types, flags)
readelf -S ./binary

# Symbol table (T=text/function, D=data, U=undefined/imported)
readelf -s ./binary

# Dynamic symbols (stripped binaries still have these for shared libs)
readelf -D --dyn-syms ./binary

# Relocation entries — identify all PLT-resolved calls and GOT pointers
readelf -r ./binary

# Program headers (segments: LOAD, DYNAMIC, GNU_STACK, etc.)
readelf -l ./binary

# Build ID (useful for locating debug symbols in dSYM or breakpad)
readelf -n ./binary | grep Build

# Check ASLR/NX/PIE mitigations
readelf -l ./binary | grep -E "GNU_STACK|RELRO|GNU_RELRO"
checksec --file=./binary     # checksec tool wraps all mitigation checks

objdump Disassembly

# Disassemble all sections (ARM64)
objdump -d -Mno-aliases --arch=aarch64 ./binary | less

# With source interleaving (requires unstripped binary or debug info)
objdump -d -S --arch=aarch64 ./binary | less

# llvm-objdump (better ARM64 output, supports relocations inline)
llvm-objdump -d --no-show-raw-insn ./binary | less

# Show only one function by name
objdump -d ./binary | awk '/^[0-9a-f]+ /,/^$/'

# Disassemble + show relocation sites (helps identify PLT calls)
objdump -d -r ./binary | grep -A3 ""

# Find all BL/BLR instructions (call sites)
objdump -d ./binary | grep "bl\s"

# Dump string pool (rodata, .data strings)
objdump -s -j .rodata ./binary | head -80
strings -n 6 ./binary | sort -u

Ghidra ARM64 Workflow

# Command-line headless analysis (batch RE)
$GHIDRA_HOME/support/analyzeHeadless /tmp/ghidra_proj MyProject \
    -import ./binary \
    -postScript ExportFunctions.java \
    -deleteProject

# Useful Ghidra Script API (scripting console or .java scripts)
# List all functions and their addresses
currentProgram.getFunctionManager()
    .getFunctions(true)
    .forEach { f -> println("${f.getName()} @ ${f.getEntryPoint()}") }

# Decompile a specific function
def f = currentProgram.getFunctionManager().getFunctionAt(toAddr(0x10001234L))
def dc = new DecompInterface()
dc.openProgram(currentProgram)
def result = dc.decompileFunction(f, 60, TaskMonitor.DUMMY)
println(result.getDecompiledFunction().getC())
Ghidra ARM64 Tips: Enable ARM/THUMB Interworking analysis pass for binaries that mix ARM32 and Thumb. For PAC-protected binaries (Apple ARM64e), Ghidra <11.0 does not strip PAC bits from addresses — pointers show with upper bits set (0x000000xxxxxxxxxx vs correct 0x1000xxxxxxxxxx). Use the Auto-Analysis → Clear Code & Disassemble workflow after manually marking PACIASP sequences. Binary Ninja handles ARM64e better out of the box.

Binary Ninja MLIL

IL Levels

Disassembly (DISASM) — raw instruction mnemonics
Lifted IL (LIL) — semantic expansion: each instruction → 1–5 IL operations on abstract registers and flags
Low-Level IL (LLIL) — register-sized operations; conditions expressed as flag reads; still 1:1 with instructions
Medium-Level IL (MLIL) — SSA form; stack accesses replaced with named variables; types inferred; calling convention applied (parameters named x0=arg1 etc.)
High-Level IL (HLIL) — structured control flow (if/for/while); pointer arithmetic shown as array indexing; closest to C pseudo-code

# Binary Ninja command-line API (bnpy)
python3 - <<'EOF'
import binaryninja as bn

bv = bn.open_view("./binary")
bv.update_analysis_and_wait()

# Find function by name
f = bv.get_function_at(bv.get_symbols_by_name("target_fn")[0].address)

# Print MLIL SSA for each basic block
for block in f.mlil.ssa_form:
    for insn in block:
        print(insn)

# Find all cross-references to a symbol
sym = bv.get_symbols_by_name("malloc")[0]
for ref in bv.get_code_refs(sym.address):
    caller = bv.get_functions_containing(ref.address)
    print(f"malloc called from {caller[0].name} @ {hex(ref.address)}")
EOF

iOS Mach-O Binaries

# otool — macOS/iOS equivalent of readelf+objdump
otool -l ./MyApp.app/MyApp | grep -A4 LC_ENCRYPTION_INFO  # Check FairPlay DRM
otool -L ./MyApp.app/MyApp                                # Linked dylibs
otool -tV ./MyApp.app/MyApp | head -100                   # Disassemble __TEXT,__text

# Universal (fat) binary — list architectures
lipo -info ./binary          # e.g., arm64 arm64e
lipo -thin arm64 -output ./binary_arm64 ./binary   # Extract single arch

# Class-dump — reconstructs @interface declarations from ObjC runtime metadata
class-dump ./MyApp.app/MyApp -H -o ./headers/
# Look for method names, properties, protocols, ivar offsets

# nm — symbol list (after decryption; encrypted sections read as zeros)
nm -arch arm64 ./binary | grep -v "^0000" | head -50

# Strings in Mach-O sections
otool -s __TEXT __cstring ./binary | xxd | strings
otool -s __DATA __cfstring ./binary  # CFString objects
FairPlay Encryption: iOS app binaries from the App Store have LC_ENCRYPTION_INFO_64 load command with cryptid=1, meaning the __TEXT segment is encrypted. Static analysis tools see only the stub sections. To analyze encrypted apps you need a jailbroken device with dumpdecrypted or frida-ios-dump to capture the decrypted memory image. This is legal only for apps you own or have permission to analyze.

Android NDK .so Quirks

# APK is a ZIP — extract first
unzip -o app.apk -d app_extracted/
ls app_extracted/lib/arm64-v8a/   # AArch64 native libraries

# NDK .so analysis
readelf -a app_extracted/lib/arm64-v8a/libcore.so | head -100
objdump -d app_extracted/lib/arm64-v8a/libcore.so | grep "bl\s" | head -30

# Version scripts — Android NDK exports use versioned symbols
# Look for LIBNAME_V1 {} in .version_gnu sections
readelf -V app_extracted/lib/arm64-v8a/libcore.so

# JNI entry points follow Java_<pkg>_<class>_<method> naming
nm app_extracted/lib/arm64-v8a/libcore.so | grep "^[0-9a-f].*T Java_"

# .rela.dyn vs .rela.plt distinction in Android NDK
# .rela.dyn: absolute relocations for global data and ifuncs
# .rela.plt: PLT jump slot relocations for external functions
readelf -r app_extracted/lib/arm64-v8a/libcore.so | grep "R_AARCH64"

# frida — dynamic instrumentation (works on non-jailbroken if app is debuggable)
# frida -U -f com.example.app --no-pause -l hook.js
# hook.js example: intercept JNI RegisterNatives to log method mappings

Identifying Compiler Idioms in Disassembly

IdiomInteger Division by Constant

Compiler replaces x / N with multiply-by-magic-number + shift. Pattern:

movz  x2, #0xaaab       ; magic number lower
movk  x2, #0xaaaa, lsl #16
smulh x0, x0, x2        ; high 64 bits of signed multiply
asr   x0, x0, #1        ; arithmetic right shift = divide by 3

Recognising this tells you the original code was n / 3, not a hash or encryption function.

Idiommemset / memcpy Replacement

Small fixed-size memset(buf, 0, 16) → two STP XZR, XZR, [x0]. Fixed-size memcpy(dst, src, 32) → four LDP/STP pairs. Larger or variable sizes → call to __memset_chk or memset@plt. The pair/XZR pattern unmistakably indicates zero-fill.

IdiomSwitch Statement

Switch with dense integer cases → jump table in .rodata. Pattern: CMP Wn, #max_caseB.HI defaultADRP/ADD x_tbl, .LjumptableLDR Woff, [x_tbl, Wn, SXTW #2]ADD x_tbl, x_tbl, Woff, SXTWBR x_tbl. Recognising this avoids analysing it as an indirect function call.

IdiomStack Canary (SSP)

ADRP + LDR x8, [x8, #:lo12:__stack_chk_guard]LDR x8, [x8]STR x8, [sp, #N] at function entry; LDR / EOR / CBNZBL __stack_chk_fail at exit. Seeing these bookends tells you the function has a stack buffer and was compiled with -fstack-protector. The local at [sp+N] is the canary, not real data.

Case Study: Reverse Engineering IoT Firmware

IoTSecurityReal-World
Analyzing a Wi-Fi Router's ARM Firmware

In 2020, security researchers at Synacktiv reverse-engineered a popular consumer Wi-Fi router running a Cortex-A53 SoC. The firmware was distributed as a single encrypted blob with no source code available. Their methodology followed the exact workflow taught in this article:

  1. Extraction: Used binwalk -e firmware.bin to identify and extract a SquashFS filesystem containing the ARM64 ELF binaries. The main HTTP server was a stripped 2.3 MB binary.
  2. Structure analysis: readelf -S httpd revealed standard sections plus a suspicious .enc_config section (custom, encrypted configuration). readelf -d showed dependencies on libcrypto.so, libnvram.so (NVRAM access), and libshared.so.
  3. Entry point tracing: Ghidra's auto-analysis identified 847 functions. Cross-referencing strings ("Content-Type", "POST", "admin") narrowed the attack surface to 23 HTTP handler functions.
  4. Vulnerability discovery: One handler read a URL parameter into a stack buffer using sprintf (no bounds check). The compiler had inserted a stack canary (__stack_chk_guard), but the team noticed the canary was loaded from a constant NVRAM address — making it predictable. This led to a pre-auth remote code execution CVE.
  5. Compiler idiom recognition: Several functions contained the division-by-magic-number pattern (SMULH + ASR) for converting byte sizes to kilobytes. Without recognizing this idiom, the analyst might have wasted hours investigating a "mysterious multiplication."

Key takeaway: Every RE skill in this article — ELF sections, string cross-references, Ghidra workflow, compiler idiom recognition, and protection analysis — was essential to finding a critical vulnerability in shipping firmware.

HistoryTools
The Evolution of ARM Reverse Engineering Tools

ARM RE tools have evolved dramatically:

  • 1990s–2000s: IDA Pro was the only serious disassembler. ARM support was a paid add-on, and 64-bit AArch64 didn't exist yet. Most firmware RE was done on ARM7TDMI (Thumb/ARM32) using bare objdump and handwritten scripts.
  • 2010s: The smartphone explosion made ARM the most-analyzed architecture. Radare2 (open-source, 2006) added ARM64 support. Binary Ninja (2016) introduced the lifted IL concept, making cross-platform analysis practical. Frida (2014) enabled dynamic instrumentation without jailbreaking.
  • 2019: NSA released Ghidra as open-source — a game-changer. Its ARM64 decompiler and scripting API (Java/Python) made professional-grade RE free for everyone. Ghidra's headless mode enabled automated analysis of thousands of firmware images.
  • 2020s: AI-assisted RE tools (e.g., Hex-Rays' AI-decompiler hints, ChatGPT function naming) are beginning to automate the most tedious part: naming and annotating the 500+ stripped functions in a typical firmware binary.

Hands-On Exercises

Exercise 1Beginner
ELF Header Scavenger Hunt

Compile a simple C program for AArch64 and analyze its ELF structure:

  1. Write a C program with: a global variable, a string constant, a main() that calls printf, and a constructor function (__attribute__((constructor)))
  2. Compile: aarch64-linux-gnu-gcc -O2 -o hello hello.c
  3. Use readelf -h, readelf -S, readelf -s, readelf -d, readelf -r
  4. Answer: In which section is the string constant? Where is the global variable? What relocation type connects printf to the PLT? Where does the constructor pointer live?

Bonus: Strip the binary with strip hello and repeat — which information survived stripping?

Exercise 2Intermediate
Compiler Idiom Identification Challenge

Compile these C functions with -O2 and identify the idiom in the disassembly:

  1. int div7(int x) { return x / 7; } — find the magic constant and identify the division pattern
  2. void zero_buf(char buf[64]) { memset(buf, 0, 64); } — count how many STP XZR instructions the compiler generated
  3. int classify(int x) { switch(x) { case 0: ... case 9: ... } } — find the jump table in .rodata and decode the table entry format
  4. void vuln(char *s) { char buf[32]; strcpy(buf, s); } — compiled with -fstack-protector: find the canary load, check, and __stack_chk_fail call

Tool: Use objdump -d -M no-aliases for the most explicit instruction mnemonics.

Exercise 3Advanced
Ghidra Headless Analysis Script

Write a Ghidra Python script that automates initial triage of an ARM64 binary:

  1. Import the binary using analyzeHeadless with ARM:AARCH64:v8A processor
  2. List all functions with no name (starts with FUN_) that call sprintf, strcpy, or strcat (potential buffer overflow candidates)
  3. For each candidate, check if __stack_chk_guard is referenced in the same function (canary present?)
  4. Output a CSV: function address, function size, dangerous call, canary present (yes/no)

Expected output: A triage report that immediately highlights unprotected functions calling unsafe string operations — the most common vulnerability pattern in ARM firmware.

Reverse Engineering Analysis Worksheet

ARM RE Analysis Worksheet

Document your reverse engineering findings. Download as Word, Excel, or PDF.

Draft auto-saved

All data stays in your browser. Nothing is uploaded.

Conclusion & Next Steps

We covered ELF section anatomy for RE (PLT/GOT/RELA/dynsym), the full readelf -a / objdump -d / llvm-objdump command set, Ghidra ARM64 headless scripting and PAC gotchas, Binary Ninja IL hierarchy (LLIL → MLIL → HLIL), iOS FairPlay encryption and class-dump, Android NDK versioned symbol and JNI naming conventions, and the four most common compiler-generated pattern idioms: division by constant, small-memset, switch jump table, and stack canary. The IoT firmware case study demonstrated how these skills combine in real vulnerability research, and the exercises provide hands-on practice from ELF header analysis through automated Ghidra scripting.

Next in the Series

In Part 20: Building a Bare-Metal OS Kernel, we shift from analysis back to implementation: writing a minimal ARM64 kernel from scratch — bootloader stub, UART driver in assembly, trap vector table, slab-allocator-free memory manager, and a cooperative round-robin scheduler with context switch.

Technology