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

#!/bin/bash

# Check if an IPA file was provided
if [ -z "$1" ]; then
  echo "Usage: $0 <path_to_ipa_file>"
  exit 1
fi

IPA_FILE="$1"

# Check if the IPA file exists
if [ ! -f "$IPA_FILE" ]; then
  echo "[@] Error: IPA file not found!"
  exit 1
fi

# Get the app name from the IPA file
APP_NAME="$(basename ""$IPA_FILE"" .ipa)"
OUTPUT_DIR="$(dirname ""$IPA_FILE"" | xargs readlink -f)"

# Create output directory
OUTPUT_DIR="$OUTPUT_DIR/$APP_NAME"
mkdir -p "$OUTPUT_DIR"

# Unzip the IPA contents
UNZIP_DIR="$OUTPUT_DIR/_extracted"
echo "[*] Extracting IPA contents..."
mkdir -p "$UNZIP_DIR"
unzip -q "$IPA_FILE" -d "$UNZIP_DIR"

# Locate the .app directory
APP_PATH=$(find "$UNZIP_DIR" -name "*.app" -type d)

if [ -z "$APP_PATH" ]; then
  echo "[@] No .app found in $UNZIP_DIR, exiting..."
  exit 1
fi

BINARY="$APP_PATH/$(basename ""$APP_PATH"" .app)"

# Check if the binary exists (file without an extension in the .app folder)
if [ ! -f "$BINARY" ]; then
  echo "[@] No binary found in $APP_PATH, exiting..."
  exit 1
fi

# Create directories for class dumps
CLASS_DUMP_OUTPUT="$OUTPUT_DIR/class_dump"
SWIFT_DUMP_OUTPUT="$OUTPUT_DIR/swift_dump"
mkdir -p "$CLASS_DUMP_OUTPUT"
mkdir -p "$SWIFT_DUMP_OUTPUT"

# Dump Objective-C classes using class-dump
echo "[*] Dumping Objective-C classes for $APP_NAME..."
ipsw class-dump "$BINARY" --headers -o "$CLASS_DUMP_OUTPUT"

# Dump Swift classes using swift-dump
echo "[*] Dumping Swift classes for $APP_NAME..."
ipsw swift-dump "$BINARY" > "$SWIFT_DUMP_OUTPUT/$APP_NAME-mangled.txt"
ipsw swift-dump "$BINARY" --demangle > "$SWIFT_DUMP_OUTPUT/$APP_NAME-demangled.txt"

echo "[+] Decompilation completed for $APP_NAME"

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:

// Helper functions
function messageFromArray(arr) {
    var reversed = arr.reverse();
    var m = '';
    for (var i = 0; i < reversed.length; i++) {
        if (reversed[i] == 0) {
            break;
        }
        m += String.fromCharCode(reversed[i]);
    }
    return m;
}

function getMessage(x0, x1){
    var firstByte = x0.toString().slice(0,4);
    var message = '';
    if (firstByte == 0xf0) {
        // add 32 because of the header
        var loc = x1.add(32);
        message = Memory.readUtf8String(loc);
    } else {
        // small string, less than 16 bytes
        var firstArg = x0.toString().slice(2);
        var firstChars = [];

        // read bytes from x0 and convert them to int
        for (var i = 0; i < firstArg.length; i += 2) {
            var ch = parseInt(firstArg.slice(i, i+2), 16);
            firstChars.push(ch);
        }
        // convert those bytes to string
        var firstMessage = messageFromArray(firstChars);

        // we start reading from the second byte because in
        // maximum number of characters is 7 in x1 for Swift.String
        var secondArg = x1.toString().slice(4);
        var secondChars = [];
        // read bytes from x1 and convert them to int
        for (var i = 0; i < secondArg.length; i += 2){
            var ch = parseInt(secondArg.slice(i, i+2), 16);
            secondChars.push(ch);
        }

        // append the strings from both x0 and x1
        var secondMessage = messageFromArray(secondChars);

        message = firstMessage + secondMessage;
    }
    return message
}

// Hook methods
var myMethod = Module.findExportByName(null, "$s7DVIA_v210ObfuscatorC6reveal3keySSSays5UInt8VG_tF");

if (myMethod) {
    Interceptor.attach(myMethod, {
        onEnter: function (args) {
            console.log("Hooked Swift method: Obfuscator +reveal");
        },
        onLeave: function (retval) {
            var message = getMessage(this.context.x0, this.context.x1);
            console.log("Returned Swift value:", message, "(", retval, ")");
        }
    });
} else {
  console.log("Hooking Swift method failed!");

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?