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 map

Everything 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":…

macho.parse: inputs flowing into the function and the resultpathdemocf fa ed fe (Mach-O 64) …macho.parseresult · everything macho knows (some keys shown)format"macho64"architecture"arm64"type"executable"is_pietrueneeded["/usr/lib/libSystem.B.dylib"]imports["_connect", "_execve", "_ptra…is_signedtrue

Real result: worked out by running this example with TB while the site was built.

parse_bytes(data)

returns map

Like 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":…

macho.parse_bytes: inputs flowing into the function and the resultdatabytes in memorycf fa ed fe …macho.parse_bytesresult · the same map as parse()format"macho64"architecture"arm64"type"executable"entry_hex"0x300"

Real result: worked out by running this example with TB while the site was built.

is_macho(path)

returns bool

True if the file starts like a Mach-O or universal binary.

Visualizehow macho.is_macho works

macho.is_macho("demo") gives true

macho.is_macho: inputs flowing into the function and the resultpathdemomacho.is_machoresult · booltruepathnotes.txtmacho.is_machoresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

segments(path)

returns list

The 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,…

macho.segments: inputs flowing into the function and the resultpathdemomacho.segmentsresult · __TEXT, __DATA ... with permissionsnameprotectionfile_size__PAGEZERO---0__TEXTr-x800__DATArw-32__LINKEDITr--317

Real result: worked out by running this example with TB while the site was built.

libraries(path)

returns list

The 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…

macho.libraries: inputs flowing into the function and the resultpathdemomacho.librariesresult · the dylibs it loadsnamekindcurrent_vers…/usr/lib/lib…load1.0

Real result: worked out by running this example with TB while the site was built.

imports(path)

returns list

Undefined external symbols: what it takes from libraries, like "_printf".

Visualizehow macho.imports works

macho.imports("demo") gives ["_connect", "_execve", "_ptrace", "_socket", "_system"]

macho.imports: inputs flowing into the function and the resultpathdemomacho.importsresult · functions it takes from libraries"_connect""_execve""_ptrace""_socket""_system"

Real result: worked out by running this example with TB while the site was built.

exports(path)

returns list

External symbols the file defines for others.

Visualizehow macho.exports works

macho.exports("libdemo.dylib") gives ["_plugin_init", "_plugin_run"]

macho.exports: inputs flowing into the function and the resultpathlibdemo.dylibmacho.exportsresult · what the library offers"_plugin_init""_plugin_run"pathdemomacho.exportsresult · what the library offers"_main"

Real result: worked out by running this example with TB while the site was built.

indicators(path_or_info)

returns list

Suspicious 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…

macho.indicators: inputs flowing into the function and the resultpath_or_infodemocheckssigned? W+X segments? encrypted?injection or exec imports?macho.indicatorsresult · suspicious things, most serious firstlevelnamemediumimports_anti_debuglowadhoc_signaturelowimports_executionlowimports_network

Real result: worked out by running this example with TB while the site was built.

summary(path)

returns text

A 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…

macho.summary: inputs flowing into the function and the resultpathdemomacho.summaryresult · one line for a reportmacho64 arm64 executable (PIE), 4 segments, needs 1 library, 5 imports, 1 exports,signed ad hoc

Real result: worked out by running this example with TB while the site was built.