IOS Reverse Engineering

Prerequisites

  • DVIA-v2 - download the IPA file

  • Ghidra - install in your host or a VM

  • Basic knowledge about reverse engineering and arm64 assembly To learn more about arm64 (AARCH64 / armv8a) assembly follow our Userland trainings. To learn the basics there are a few good resources you can find online for example:

Decompiling the App

# IPSW Injstall
brew install blacktop/tap/ipsw
ipsw --help

# Install Swift
MacOS -> just install xcode
sudo apt install -y curl
curl -L https://swiftlygo.xyz/install.sh | bash
sudo swiftlygo install latest
swift --help

# Extract the IPA File
unzip ./DVIA-v2.ipa

# Locate the App Binary
./Payload/DVIA-v2.app/DVIA-v2

# Dumping Objective-C Classes Using class-dump
ipsw class-dump ./Payload/DVIA-v2.app/DVIA-v2 --headers -o ./class_dump

# Dumping Swift Classes Using swift-dump
ipsw swift-dump ./Payload/DVIA-v2.app/DVIA-v2 > ./swift_dump_mangled.txt
ipsw swift-dump ./Payload/DVIA-v2.app/DVIA-v2 --demangle > ./swift_dump_demangled.txt

Automation for Decompiling

Analyzing Decompiled Output from an IPA File

1 โ€” Goals and workflows

  1. Goal: turn raw decompiled symbols into an understanding of app behaviour, attack surface, and risky logic (network, auth, crypto, IPC, privileged checks).

  2. High-level workflow:

    • Static reconnaissance: read Objective-C headers and demangled Swift symbols to map structure and likely behaviour.

    • Prioritization: mark classes/methods that touch secrets, I/O, network, system APIs, or platform checks.

    • Deeper static analysis: open interesting routines in a disassembler/decompiler to inspect control flow and data handling.

    • Dynamic analysis: instrument or run the app to observe real behaviour, confirm hypotheses, and gather run-time data.

    • Iterate: refine targets and repeat.


2 โ€” Reading Objective-C headers (class-dump output) โ€” what to look for

Focus on structure, not perfect semantics.

Essentials:

  • Class names & inheritance: identify controllers, managers, clients (e.g., *Manager, *Client, *Controller, *Handler). These often centralize logic.

  • Protocols & delegates: they reveal callback flows and event pathways.

  • Properties & instance variables: note fields typed as NSString, NSData, NSDictionary, NSUserDefaults, Keychain wrappers โ€” potential secret storage.

  • Method signatures: methods named with login, authenticate, fetch, send, encrypt, decrypt, verify, jailbreak, root, entitlement deserve higher priority.

  • I/O and parsing methods: anything reading/writing files, parsing JSON/XML, serializing/deserializing data.

  • Network call wrappers: methods accepting URLs, forming requests, or using NSURLSession/CFNetwork are high-value.

How to triage:

  • Tag each class as Informational / Sensitive / High-priority.

  • Build a quick map: UI โ†’ Controller โ†’ Manager โ†’ Network/Storage so you can trace data flow.


3 โ€” Inspecting Swift symbols (swift-dump) โ€” how to extract meaning

Swift lacks header files; symbols give hints.

What to extract:

  • Class/struct names and fields: names often describe responsibilities (UserStore, NetworkClient, CryptoService).

  • Method names and argument types: demangled methods show actions (loginButtonTapped, fetchData, encryptData).

  • Accessors and computed properties: getter/setter symbols indicate where state is read and mutated.

  • Static/utility functions: often hold config, constants, or helper logic.

Inferences:

  • Map UI fields (text fields, buttons) to backend calls by name similarity.

  • Note methods that hint at platform checks (e.g., isJailbroken, hasRootedFilesystem) or obfuscation helpers (e.g., reveal, obfuscate).


4 โ€” From symbols to hypotheses: what to test

Form simple, testable hypotheses from static evidence:

  • โ€œThis class reads credentials from a text field then calls NetworkClient.sendAuth; check if credentials are sent in plaintext.โ€

  • โ€œThis routine calls KeychainWrapper.save(token:); check whether token protection uses proper attributes.โ€

  • โ€œA JailbreakChecker or Obfuscator is referenced; check what values it returns at runtime and where theyโ€™re consumed.โ€

Write each hypothesis as: (Where) โ†’ (What) โ†’ (How to confirm).


5 โ€” Deeper static analysis (disassembler / decompiler)

When a method looks important, move to a Disassembler (Ghidra, IDA, Hopper):

What to inspect:

  • Control flow: branches that gate privileged paths or feature flags.

  • Constants & embedded data: strings, URLs, magic numbers, salts.

  • Opaque calls / indirections: observe calls where pointers or function tables are used โ€” these often represent vtables, callbacks, or obfuscated logic.

  • Crypto usage: calls into CommonCrypto/CryptoKit or custom routines; identify inputs/outputs.

Renaming: rename meaningless locals/temps to meaningful names (e.g., obfuscatorPtr) to help reasoning.

Limit yourself to understanding logic: identify where sensitive decisions are made and which inputs affect them.


6 โ€” Runtime/dynamic analysis (general approach) (frida)

Purpose: confirm static hypotheses and observe live values.

General steps (tool-agnostic):

  • Run the app in a controlled environment (emulator/device under test) and exercise the functionality of interest.

  • Log or observe: capture network traffic, filesystem access, API calls, and key return values to see real data shapes.

  • Probe interfaces: call methods that appear to return flags (e.g., isDeviceSecure) and record outputs for different environments.

  • Trace data flow: observe how input (user text, files, system state) propagates to sinks (network, storage, system APIs).

Be careful: do not run instrumentation on production systems or without authorization.

Example: To analyze the reveal method, we can hook them using the following Frida script:


7 โ€” Instrumentation tips (non-code, conceptual)

If you use dynamic instrumentation frameworks, keep these high-level strategies in mind:

  • Hook high-value functions, not everything: focus on functions that return strings, booleans, or perform I/O.

  • Capture arguments and return values: this tells you what data the app is acting on and what it expects.

  • Watch for heap vs. inline storage: short strings may be stored in registers/stack, longer ones on the heapโ€”know your toolโ€™s string-reading limits.

  • Alter outputs carefully for testing hypotheses: e.g., replace a returned flag with the opposite and observe behavior โ€” but only in controlled, authorized testing.

Again: this is methodology; do not treat this as an executable script.


8 โ€” Common patterns that indicate risk

Hardcoded secrets: strings that look like API keys, tokens, or salts.

  • Insecure network usage: plain HTTP endpoints or custom, unauthenticated protocols.

  • Poor storage of secrets: sensitive data in files, NSUserDefaults, or unprotected SQLite.

  • Weak crypto wrappers: custom or non-standard crypto functions that reimplement known algorithms.

  • Device/environment checks: explicit jailbreak/root checks, emulation checks, or integrity checks โ€” these can affect testing strategy and explain conditional behaviour.


9 โ€” Practical checklist (quick)

Patching an app with Ghidra

1. Extract the binary from the app

DVIA-V2 (.ipa renamed to .zip) - Payload - DVIA-v2.app -> DVIA-v2

2. Load the binary in Ghidra

  • Open Ghidra., create a new project and import the DVIA-v2 binary, via file -> batch import:

  • Double-click on the file name after the batch import finished.

  • Click "Yes" on the question regarding analyze now in Ghidra, and "Check Decompiler Parameter ID" next to the default Analysis options, and click on "Analyze":


3. Patch the binary in Ghidra

  • Search in the "Symbol Tree" for a function with the name "isJailbroken".

  • Find the local_11 variable in the return statement of the decompiled 'isJailbroken' function,

    which has a signature like: byte JailbreakDetection::isJailbroken(undefined8 param_1,undefined8 param_2).

  • Right click on the matching line in the assembly code "and w0, w8, #01" -> Patch instruction

  • Change "and w0, w8, #01" to "mov w0, #0x0". This will result in the function (isJailbroken) always returning 0 (w0 is the register containing function return values and will always get the value 0x0 now via the MOV instruction) instead of the bitwise compare (AND instruction), depending on the function logic.

  • Save the modified binary via file -> export program, and pick "Original File":

  • Replace the original DVIA-V2 binary with the patched DVIA-V2 binary in the Payload folder.

  • Zip the modified "Payload" as new IPA, for exaple with zip: zip -r DVIA-V2-patched.ipa Payload/

Last updated

Was this helpful?