Skip to content

About

ARM64 → C-like decompiler for Mach-O and ELF

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

arm_decompiler

arm_decompiler

ARM64 → C-like decompiler for Mach-O (iOS / macOS) and ELF (Android .so / JNI).

Docs: pipeline · roadmap · parity plan · ELF structuring tests · Swift/Ghidra

Quick demos (in-repo)

Mach-O / IPA fixtures (iOS · macOS)

Uses the sibling apple-re crates (macho_core, ipa_vfs, apple_metadata) — same load path as apple-re decompile.

cargo run --example decompile_macho --
cargo run --example decompile_macho -- _while_sum
cargo run --example decompile_macho -- _cd_smoke_call   # ObjC [self hello:]
cargo run --example decompile_macho -- --list
cargo run --example decompile_macho -- --engine micro ./MyApp _main
cargo run --example decompile_macho -- --engine micro ./App.ipa _main
cargo run --example decompile_macho -- --framework Foo ./App.ipa -[Bar hello:]

Engine compare on the Mach-O fixture:

cargo run --example cmp_engines -- _add1

ELF fixtures (path + function name)

AArch64 Linux ELF built from the same C sources (testdata/decompiler_fixtures/ decompiler_fixtures.elf). Needs sibling [elf_core](https://github.com/androguard) beside this repo.

# Default: fixture ELF + symbol add1 (Legacy + Micro)
cargo run --example decompile_elf --

# Symbol only → uses the in-repo fixture ELF
cargo run --example decompile_elf -- while_sum
cargo run --example decompile_elf -- switch_sparse
cargo run --example decompile_elf -- --engine micro if_else

# Explicit ELF path + symbol (Android .so, your own binary, …)
cargo run --example decompile_elf -- \
  testdata/decompiler_fixtures/decompiler_fixtures.elf for_sum
cargo run --example decompile_elf -- \
  path/to/libdatastore_shared_counter.so \
  Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile

# List exported / function symbols
cargo run --example decompile_elf -- --list

Rebuild the ELF fixture (needs zig):

testdata/decompiler_fixtures/build_elf.sh

Engine compare helper (same args: ELF SYMBOL):

cargo run --example cmp_elf_engines -- \
  testdata/decompiler_fixtures/decompiler_fixtures.elf do_while_count

Example output

ELF fixture — add1 / while_sum / switch_sparse

cargo run --example decompile_elf -- --engine micro add1
int add1(int param_1) {
    int local_4;

    local_4 = param_1;
    return (local_4 + 1);
}
cargo run --example decompile_elf -- --engine micro while_sum
int while_sum(undefined8 param_1) {
    …
    while (local_8 < local_4) {
        local_c = (local_c + local_8);
        local_8 = (local_8 + 1);
    }
    return local_c;
}
cargo run --example decompile_elf -- --engine micro switch_sparse
int switch_sparse(undefined8 param_1) {
    …
    switch (local_8) {
        case 1:    local_4 = 0xb;  break;
        case 0xa:  local_4 = 0x16; break;
        case 0x64: local_4 = 0x21; break;
        default:   local_4 = 0;    break;
    }
    return local_4;
}

Mach-O — arithmetic (_add1)

int _add1(int param_1) {
    int local_4;

    local_4 = param_1;
    return (local_4 + 1);
}

Mach-O — loop (_while_sum)

int _while_sum(undefined8 param_1) {
    int local_4;
    int local_8;
    int local_c;

    local_4 = param_1;
    local_8 = 0;
    local_c = 0;
    while (local_8 < local_4) {
        local_c = (local_c + local_8);
        local_8 = (local_8 + 1);
    }
    return local_c;
}

Mach-O — ObjC message send (_cd_smoke_call)

undefined8 _cd_smoke_call(id self) {
    …
    x0 = [self hello:local_1c];
    local_20 = x0;
    return local_20;
}

Android JNI — error path with JNIEnv vtable

From androidx libdatastore_shared_counter.so (Micro engine):

jlong Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile(
    JNIEnv * env, jclass clazz, jint param_3)
{
    /* JNINativeInterface — see jni.h (*env)->FindClass / ThrowNew / … */

    x0 = _ZN9datastore12TruncateFileEi(param_3);
    if (w0 != 0) {
        x0 = _strerror();
        x0 = (*env)->FindClass(env, "java/io/IOException");
        if (x0 != 0) {
            x0 = (*env)->ThrowNew(env, x0, x20);
        }
    }
    return x0;
}

nativeCreateSharedCounter — same JNI fold, errno in x19, stack cookie:

jlong Java_androidx_datastore_core_NativeSharedCounter_nativeCreateSharedCounter(
    JNIEnv * env, jclass clazz, jint param_3)
{
    /* JNINativeInterface — see jni.h (*env)->FindClass / ThrowNew / … */

    undefined4 local_10;
    int local_38;
    int local_40;

    local_38 = local_10;
    local_40 = 0;
    x0 = _ZN9datastore19CreateSharedCounterEiPPv(param_3, sp);
    if (w0 != 0) {
        x0 = _strerror(x19);
        x0 = (*env)->FindClass(env, "java/io/IOException");
        if (x0 != 0) {
            x0 = (*env)->ThrowNew(env, x0, x21);
        }
    }
    x10 = *((x22 + 0x28));
    if (x10 != local_38) {
        x0 = __stack_chk_fail();
    } else {
        return x0;
    }
}

Rust API

use arm_decompiler::{decompile_macho_symbol, DecompilerOptions};

let out = decompile_macho_symbol(
    &std::fs::read("MyApp")?,
    "_main",
    &DecompilerOptions::default(),
)?;
println!("{}", out.source);

IPA or fat Mach-O (main executable / ARM64 slice). Optional framework inside the IPA:

use arm_decompiler::{decompile_apple_symbol, AppleLoadOptions, DecompilerOptions};

let ipa = std::fs::read("App.ipa")?;
let load = AppleLoadOptions {
    framework: Some("Foo".into()), // None = Payload/*.app main binary
    arch: None,                    // None = prefer arm64
};
let out = decompile_apple_symbol(&ipa, "_main", &load, &DecompilerOptions::default())?;
println!("{}", out.source);

ELF (you supply the function bytes + symbol map — see examples/cmp_elf_engines.rs):

use arm_decompiler::{collect_cstrings, decompile_elf_symbol, DecompilerOptions, Engine};

let mut opts = DecompilerOptions::default();
opts.engine = Engine::Micro;
opts.string_lits = /* VA → C string from PT_LOAD */;
let out = decompile_elf_symbol(code, base_va, "Java_…_nativeFoo", &symbols, &opts)?;

Apple-re CLI (full Mach-O / IPA / ObjC / Swift — same crates, extra commands):

cargo run -p apple_re_cli --manifest-path ../apple-re/Cargo.toml -- \
  decompile ./MyBinary -n _main

Python binding

[arm_decompiler-py/](arm_decompiler-py/) — PyO3 / maturin.

cd arm_decompiler-py
pip install maturin lief
maturin develop --release

Minimal API

from arm_decompiler import decompile

result = decompile(
    code,                    # raw ARM64 bytes of one function
    base_vaddr=0x1000,       # VA of first instruction
    name="sub_1000",
    symbols={0x2000: "malloc"},  # optional call targets
)
print(result["source"])

Android JNI .so (LIEF)

import lief
from arm_decompiler import decompile

elf = lief.parse("libdatastore_shared_counter.so")
name = "Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile"
sym = elf.get_symbol(name)
size = sym.size or 0x200
code = bytes(elf.get_content_from_virtual_address(sym.value, size))
symbols = {s.value: s.name for s in elf.symbols if s.value and s.name}

print(decompile(code, sym.value, name, symbols)["source"])

Batch every exported Java_*:

for sym in elf.exported_symbols:
    if not sym.name.startswith("Java_") or not sym.value:
        continue
    code = bytes(elf.get_content_from_virtual_address(sym.value, sym.size or 0x200))
    try:
        src = decompile(code, sym.value, sym.name, symbols)["source"]
    except ValueError as e:
        print(f"# skip {sym.name}: {e}")
        continue
    print(f"\n/* ==== {sym.name} @ {sym.value:#x} ==== */\n{src}")

Mach-O / IPA (apple-re)

from arm_decompiler import decompile_macho, list_macho_functions

image = open("MyApp", "rb").read()          # or App.ipa
print(list_macho_functions(image)[:8])
print(decompile_macho(image, "_main")["source"])
print(decompile_macho(image, "_main", framework="Foo")["source"])
import lief
from arm_decompiler import decompile

macho = lief.parse("MyApp")
sym = next(s for s in macho.symbols if s.name in ("_main", "main") or s.name.endswith("_main"))
va = sym.value
sect = next(s for s in macho.sections if s.virtual_address <= va < s.virtual_address + s.size)
off = va - sect.virtual_address
nxt = sorted(
    s.value for s in macho.symbols
    if s.value > va and sect.virtual_address <= s.value < sect.virtual_address + s.size
)
end = nxt[0] if nxt else sect.virtual_address + sect.size
code = bytes(sect.content[off : off + (end - va)])
symbols = {s.value: s.name for s in macho.symbols if s.value and s.name}
print(decompile(code, va, sym.name, symbols)["source"])

Dependencies

Crate Source
macho_core, apple_metadata, ipa_vfs sibling ../apple-re (path dep)
arm_disassembler androguard/arm_disassembler
elf_core (dev / ELF examples) local sibling until published

Build & test

cargo test
cargo test --test apple_image               # IPA wrap + Mach-O via apple-re
cargo test --test elf_structuring           # ELF control-flow / switch / calls
cargo test --test decbench_scoreboard -- --nocapture   # DecBench-style Union/S/T/R rates
cargo test --test decompiler_tests scoreboard -- --nocapture
cargo test --test decompiler_tests performance -- --nocapture

Rebuild fixtures (macOS + clang / swiftc):

cd testdata/decompiler_fixtures && ./build.sh
cd testdata/swift_fixtures && ./build.sh

About

ARM64 → C-like decompiler for Mach-O and ELF

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages