When to use / when not to use
- Use: target is a macOS/iOS Mach-O binary (executable/.dylib/.bundle/Framework), need to understand structure, load commands, entry point, code signing, dynamic library dependency chain
- Use: check for dylib injection (abnormal LC_LOAD_DYLIB entries), signing/notarization status, universal binary multi-architecture slices
- Don't use: PE (use [[re-format-pe]]), ELF (use [[re-format-elf]])
- Don't use: only need function logic (go straight to decompilation skill); iOS jailbreak environment issues belong to the [[re-mobile]] domain
Tool preparation
See the macOS branch of [[re-analyze/platform-tips]]—attach/debugging is restricted by SIP and TCC; static parsing is unaffected.
otool (bundled with Apple)
- macOS: Xcode Command Line Tools:
xcode-select --install - Verify:
otool --version - Linux alternative:
llvm-otool(apt install llvm/brew install llvm/pacman -S llvm), functional coverage of otool's commonly used subcommands
llvm-objdump (cross-platform)
- macOS:
brew install llvm;Linux:apt install llvm/dnf install llvm/pacman -S llvm - Verify:
llvm-objdump --version - Usage:
llvm-objdump --macho --private-headers sample(on Linux can directly parse Mach-O, output consistent with macOS)
jtool2 (deep Mach-O structure, macOS)
- macOS: clone from GitHub (intezer/jtool2) then
make, or download the release binary - Verify:
jtool2 -h sampleoutputs load commands
codesign / lipo (bundled with macOS)
- Verify:
codesign --version;lipo -info sample - Linux alternative:
llvm-lipo -info sample(bundled with llvm)
dyld_shared_cache_util(macOS)
- Bundled with the system (
/usr/bin/dyld_shared_cache_util); purpose: extract a single library when parsing system dylibs in the dyld shared cache
Operation steps
-
Initial survey (first confirm type and architecture):
file sample # 可执行文件 / dylib / 通用二进制 / 字节序 lipo -info sample # 通用二进制列出各架构片;单架构提示 "non-fat file" xxd -l 16 sample # 魔数: cffaedfe=64位小端, feedfacf=64位大端, cebafaed=32位小端, cafebabe=fatMagic numbers (bytes seen in the file):
cffa edfe(MH_MAGIC_64)、cefa edfe(MH_MAGIC 32-bit),cafe babe(FAT, followed by 4-byte architecture count). For universal binaries, firstlipo -thin <arch> -output piece sampleextract a single slice then parse. -
mach_header and load commands (core):
otool -h sample # magic/cputype/filetype/ncmds/flag 一行全出 otool -l sample | head -80 # 全部 load commandsThe 64-bit mach_header is a fixed 32 bytes (28 bytes for 32-bit): magic(4) + cputype(4) + cpusubtype(4) + filetype(4) + ncmds(4) + sizeofcmds(4) + flags(4) + reserved(4, 64-bit only). load commands are Mach-O's "section table + import table + entry point + signature" combined—iterate in
cmdsizeorder from start to end,ncmdscounts,sizeofcmdsis the total byte count, the two cross-check each other. filetype: 2=MH_EXECUTE 6=MH_DYLIB 8=MH_BUNDLE. For common LC values and field tables see [[layout]]. -
Segments and sections (__TEXT/__DATA/__LINKEDIT):
otool -l sample | grep -A8 'segname __TEXT' otool -l sample | grep -A8 'segname __DATA' llvm-objdump --macho --section-headers sampleRecords each segment's vmaddr/vmsize/fileoff;
__TEXT,__textis code,__DATA,__datais writable data, GOT is in__DATA_CONST,__got(read-only constant segment; in older binaries__DATA,__got);__LINKEDITisnormal segment with vmaddr/vmsize, usually mapped as a read-only region (visible in vmmap), storing link metadata such as symbol table/string table/relocations/signature—the specific way it exists is affected by dyld and the shared cache. -
Entry point (LC_MAIN / LC_UNIXTHREAD):
otool -l sample | grep -A6 LC_MAINentryoffis the offset of the entry relative to the file header; entry VA = __TEXT segment vmaddr + entryoff (on 64-bit, the __TEXT base address is usually 0x100000000). Older binaries use LC_UNIXTHREAD (the register state contains the entry point). An executable with neither LC_MAIN nor LC_UNIXTHREAD is malformed and should be investigated as a priority. -
dyld information (LC_DYLD_INFO_ONLY / export trie):
otool -l sample | grep -A9 LC_DYLD_INFO_ONLY llvm-objdump --macho --exports-trie sample # 导出符号(trie 解码)five groups of offset+size, 48 bytes total, all in LC_DYLD_INFO_ONLY: rebase (base address fixup points), bind (external symbol bindings, non-lazy), weak_bind (weak symbol bindings; weak symbol coalescing semantics are independent and must not be merged into bind), lazy_bind (lazy bindings), export (export trie); newer binaries use LC_DYLD_CHAINED_FIXUPS (chained fixups, default on iOS 13+). A dylib's exported symbols are in the trie,
nm -gUsame effect. -
Dynamic library dependency chain (injection detection):
otool -L sample # LC_LOAD_DYLIB 列表(依赖库 + install name) otool -l sample | grep -A4 LC_LOAD_DYLIBEach LC_LOAD_DYLIB: name (absolute path or @rpath/@executable_path relative) + timestamp/current_version.Malicious dylib persistence = adding a new LC_LOAD_DYLIB entry—inconsistency with the list at signing time is an important warning (see Pitfalls for details). Under two-level namespace, symbol lookup is by the (library, symbol) pair, and import resolution is strict.
-
Symbol table (nlist_64):
llvm-nm -m sample # 含段归属与 N_EXT 标记;undefined = 导入 llvm-nm -gU sample # 仅外部定义符号(导出) otool -l sample | grep -A3 LC_SYMTAB # symoff/nsyms/stroff/strsize 定位表Each nlist_64 entry is 16 bytes: n_strx (string table offset) + n_type (N_EXT/N_SECT etc.) + n_sect + n_desc + n_value. After stripping,
.symtabdisappears, leaving only LC_DYLD_INFO's export trie and dynamic binding information—the recovery approach is the same as for ELF ([[re-imports]] signature matching + string cross-references). -
Code signature check:
codesign -dv sample 2>&1 # 签名类型(adhoc / Apple Development / not signed) codesign --verify --deep sample # 完整性验证 jtool2 --sig sample | head -20 # LC_CODE_SIGNATURE 细节(CodeDirectory/哈希)adhoc: self-signed, can be modified and re-signed (codesign -f -s - sample)- Apple signature: modifying any byte invalidates it; with Library Validation enabled, injecting a dylib will fail
- No LC_CODE_SIGNATURE = unsigned (cannot run directly on iOS)
-
Manual parsing and verification: when load command output is abnormal / header fields are forged, use
xxd+ Pythonstructto parse the mach_header and each LC directly by offset (see [[examples]] for a minimal runnable example and byte samples); do not discard parsing failures as "corrupted files".
Cross-domain connections
- [[re-binary-core]]: workflow step 3, Mach-O target format parsing
- [[re-mobile]]: prerequisite for iOS App/jailbreak environment analysis (arm64 Mach-O main binary and dylibs)
- [[re-ios]] / [[re-macos]]: ecosystem linkage (App layer, signing and notarization, TCC/SIP restrictions)
- [[re-cracking]]: macOS cracking-type tasks (see step 8 of this skill for the re-signing process after patching)
- [[re-imports]]: analysis of the hijacking surface of dylib dependencies and exported symbols
- Dynamic debugging with [[re-lldb]] (see [[re-analyze/platform-tips]] for SIP/TCC restrictions)
Common pitfalls and traps
- Signature verification blocks debugging/injection: a modified binary fails codesign verification and cannot run—first
codesign -f -s -re-sign (only for self-signed/adhoc scenarios); with Library Validation enabled,DYLD_INSERT_LIBRARIESfails - iOS requires a jailbroken environment: debugging on a real device requires a developer certificate or jailbreak, and simulator processes are sandbox-restricted—static parsing can be done first, leaving the dynamic part to [[re-mobile]]
- LC_LOAD_DYLIB injection detection: malicious dylibs achieve persistence by adding an LC_LOAD_DYLIB—
otool -LA mismatch between the list and the list at signing time is an important alert - A universal binary contains multiple architecture slices: first
lipo -infoconfirm the architecture, thenlipo -thinextract it; do not parse the whole file; the offset of each slice in the fat header is an absolute file offset, so slice out a single slice by (offset, size) - The GOT location migrates with versions: the GOT of new artifacts is in
__DATA_CONST,__got(read-only, relocated once after loading); old artifacts have it in__DATA,__got(writable)—to find the GOT, firstotool -llook at the section name, do not assume - There is no standalone dylib file in the dyld shared cache: system libraries (libSystem.dylib, etc.) are actually inside the
/System/Library/dyld/cache; the file system only has stubs—to analyze system libraries, usedyld_shared_cache_util -extractto extract them - load command traversal misalignment: symptom—
otool -loutput becomes garbled/errors midway; cause—ncmds and sizeofcmds are forged or the cmdsize of some LC is abnormal; countermeasure—all LCs are at least 8 bytes (load_commandthe header itselfcmd+cmdsize), under 64-bit cmdsize must be a multiple of 8, under 32-bit a multiple of 4, then validate each specific cmd by its structure's minimum size and trailing section/strings, with the total not exceeding sizeofcmds - __LINKEDIT is a mapped segment, but its contents may be processed by dyld: it has its own vmaddr/vmsize, usually existing as a read-only mapping (
vmmapvisible); however, the link metadata of shared cache libraries resides in the shared cache, so finding the symbol table directly from process memory by file offset often does not match—to locate the symbol table, prioritize usingnm/dyldinfo/MachOView, etc., to parse according to LC_SYMTAB, rather than copying file offsets