macho
Read macOS and iOS executables (Mach-O, including universal binaries) without running them: architectures, type, load commands, segments and sections with entropy and permissions, the libraries it needs, imported and exported symbols, the code signature (identifier, team, certificates, entitlements), a hardening report and suspicious indicators. Pure Python; works on bytes in the playground.
import macho
Example
import binary
import macho
# A tiny Apple silicon program, built byte by byte: the Mach-O header, a
# __TEXT segment (readable and runnable), an LC_MAIN entry point and four
# instructions. It has no code signature, so macOS wouldn't run it.
let header = binary.pack("<IiiIIIII", 0xfeedfacf, 0x0100000c, 0, 2, 2, 96, 0x200085, 0)
let text = binary.pack(
"<II16sQQQQiiII",
0x19,
72,
b"__TEXT",
0x100000000,
0x4000,
0,
0x90,
5,
5,
0,
0,
)
let entry = binary.pack("<IIQQ", 0x80000028, 24, 0x80, 0)
let code = b"\x1f\x20\x03\xd5\x1f\x20\x03\xd5\x00\x00\x80\xd2\xc0\x03\x5f\xd6"
let head = binary.concat(header, text, entry)
let data = binary.concat(head, binary.zeros(0x80 - len(head)), code)
let info = macho.parse_bytes(data)
say(f"{info.format} {info.type} for {info.architecture}, entry {info.entry_hex}, PIE {info.is_pie}")
for seg in info.segments {
say(f" segment {seg.name}: {seg.file_size} bytes, {seg.protection}")
}
for hit in macho.indicators(info) {
say(f" [{hit.level}] {hit.detail}")
}
Functions
parse(path)
returns mapEverything macho knows about a file. Fails if the file isn't Mach-O. For a universal binary the fields describe its first architecture.
Fields
"format"macho32, macho64 or macho-fat."architecture"the CPU type, like "arm64" or "x64"."architectures", "slices"every architecture in a universal binary."type"executable, dylib, bundle, object...- "flags", "is_pie", "is_fat", "is_signed", "is_encrypted"
"load_commands"a list of {command, size}."segments"a list of maps with name, address, offset, sizes, protection and sections."sections"every section, named like "__TEXT,__text", with entropy."libraries"the dylibs it loads, each {name, kind, current_version}.- "needed", "rpaths", "dylib_id", "dylinker", "uuid"
- "entry_point", "entry_hex", "entry_section"
- "platform", "min_os", "sdk"
"imports", "exports"symbol names, like "_printf"."code_signature"nil, or {identifier, team_id, adhoc, hardened_runtime, hash_type, has_cms, signer, certificates, entitlements, verified, problem}. Nothing is checked: verified is always false."hardening"pie, nx_stack, nx_heap, canary, arc, signed, hardened_runtime and encrypted.- "entropy", "size", "truncated"
"limits"notes on what was skipped in a hostile file (over 1024 sections, or sections repeating the same bytes); entropy there is nil. Usually empty."indicators"a list of {level, name, detail} findings.
Visualizehow macho.parse works
macho.parse("demo") gives {"format": "macho64", "bits": 64, "endian": "little", "architecture": "arm64", "machine":…
Real result: worked out by running this example with TB while the site was built.
parse_bytes(data)
returns mapLike macho.parse() but for bytes already in memory.
Visualizehow macho.parse_bytes works
macho.parse_bytes(bin) gives {"format": "macho64", "bits": 64, "endian": "little", "architecture": "arm64", "machine":…
Real result: worked out by running this example with TB while the site was built.
is_macho(path)
returns boolTrue if the file starts like a Mach-O or universal binary.
Visualizehow macho.is_macho works
macho.is_macho("demo") gives truemacho.is_macho("notes.txt") gives false
Real result: worked out by running this example with TB while the site was built.
segments(path)
returns listThe segments (__TEXT, __DATA...) with their sections and permissions.
Visualizehow macho.segments works
macho.segments("demo") gives [{"name": "__PAGEZERO", "address": 0, "vm_size": 4294967296, "offset": 0, "file_size": 0,…
Real result: worked out by running this example with TB while the site was built.
libraries(path)
returns listThe dylibs it loads: [{"name", "kind", "current_version", "compatibility_version"}].
Visualizehow macho.libraries works
macho.libraries("demo") gives [{"name": "/usr/lib/libSystem.B.dylib", "kind": "load", "current_version": "1.0", "compat…
Real result: worked out by running this example with TB while the site was built.
imports(path)
returns listUndefined external symbols: what it takes from libraries, like "_printf".
Visualizehow macho.imports works
macho.imports("demo") gives ["_connect", "_execve", "_ptrace", "_socket", "_system"]
Real result: worked out by running this example with TB while the site was built.
exports(path)
returns listExternal symbols the file defines for others.
Visualizehow macho.exports works
macho.exports("libdemo.dylib") gives ["_plugin_init", "_plugin_run"]macho.exports("demo") gives ["_main"]
Real result: worked out by running this example with TB while the site was built.
indicators(path_or_info)
returns listSuspicious indicators for a file (or for the map macho.parse() gives back): [{"level": high/medium/low/info, "name", "detail"}].
Visualizehow macho.indicators works
macho.indicators("demo") gives [{"level": "medium", "name": "imports_anti_debug", "detail": "Imports anti debug function…
Real result: worked out by running this example with TB while the site was built.
summary(path)
returns textA short description, like "macho64 arm64 executable (PIE), 4 segments, needs 1 library, 2 imports, signed ad hoc".
Visualizehow macho.summary works
macho.summary("demo") gives "macho64 arm64 executable (PIE), 4 segments, needs 1 library, 5 imports, 1 exports, signe…
Real result: worked out by running this example with TB while the site was built.