Mach-O


Introduction

Note

The Mach-O format defines FAT binaries, which can embed different architectures into a single file. always returns a , assuming that a non-FAT Mach-O can be represented as a containing a single architecture.
import lief

# Using filepath
macho: lief.MachO.FatBinary | None = lief.MachO.parse("/bin/ls")

# Using a Path from pathlib
macho: lief.MachO.FatBinary | None = lief.MachO.parse(
    pathlib.Path(r"C:\Users\test.macho")
)

# Using a io object
with open("/bin/ssh", "rb") as f:
    macho: lief.MachO.FatBinary | None = lief.MachO.parse(f)
fat: lief.MachO.FatBinary

# Iterate
for macho in fat:
    print(macho.entrypoint)
    print(len(macho.commands))

# Pick one at the specified index
macho = fat.at(0)

# Pick one based on the architecture
macho = fat.take(lief.MachO.Header.CPU_TYPE.ARM64)
macho: lief.MachO.FatBinary

macho.at(0).write("fit.macho")
macho.write("fat.macho")  # write-back the whole FAT binary
macho: lief.MachO.Binary

new_macho: bytes = macho.write_to_bytes()

Advanced Parsing/Writing

parser_config = lief.MachO.ParserConfig()
parser_config.parse_dyld_bindings = False

fat = lief.MachO.parse("my.macho", parser_config)
assert isinstance(fat, lief.MachO.FatBinary)

macho = fat.at(0)
assert isinstance(fat, lief.MachO.Binary)

builder_config = lief.MachO.Builder.config_t()
builder_config.linkedit = False

macho.write("new.macho", builder_config)

Dump Analysis

LIEF has the support to process Mach-O memory dump with . This function translates the file offsets referenced by the Mach-O structures into their location inside the dump, using the base address passed as the second parameter. As for the regular parser, it returns a :
# 0x11e32c000 is the (absolute) address at which the dump was mapped
fat = lief.MachO.parse_from_dump("module.dump", 0x11E32C000)
assert isinstance(fat, lief.MachO.FatBinary)

macho = fat.at(0)
assert isinstance(fat, lief.MachO.Binary)

for segment in macho.segments:
    print(segment.name, hex(segment.virtual_address))

Note

The second parameter must be the (absolute) virtual address at which the dump was mapped. It is used to convert the virtual addresses found in the Mach-O structures back into an offset within the dump.

Producing a dump with the runtime API

Such a dump can be produced from a live process thanks to the LIEF runtime and, more precisely, the Module API. captures the memory of a loaded module (from its imagebase over its virtual size):
# Find the module to dump in the current process
mod = lief.runtime.module_from_name("libsystem_c.dylib")
assert isinstance(mod, lief.runtime.osx.Module)

# Dump the module's memory into a file (the raw bytes are also returned) ...
data: bytes = mod.dump("module.dump")

# ... and parse it back using the same imagebase:
macho = lief.MachO.parse_from_dump(data, mod.imagebase)

RPath and Library Path Modification

Sometimes, we need to modify the Mach-O RPath commands or the (absolute) path of a linked library in an executable. When recompiling or linking the executable is not possible, LIEF can be used for these modifications.

For example, let’s consider a binary with the following dependencies:

$ otool -L hello.bin
hello:
      /Users/romain/dev/libmylib.dylib (compatibility version 0.0.0, current version 0.0.0)
      /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1700.255.0)
      /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2)

One can change the directory of libmylib.dylib with the following code:

fat = lief.MachO.parse("hello.bin")
assert isinstance(fat, lief.MachO.FatBinary)

macho = fat.at(0)
assert isinstance(fat, lief.MachO.Binary)

lib = macho.find_library("libmylib.dylib")
assert isinstance(lib, lief.MachO.DylibCommand)

lib.name = "/opt/homebrew/my_package/libmylib.dylib"

macho.write("hello_fixed.bin")

Note

It is worth mentioning that LIEF doesn’t impose restrictions on the length of modified library paths. LIEF manages all internal modifications to support both longer and shorter library paths.

This type of modification can be used in conjunction with the @rpath feature of Mach-O binaries:

fat = lief.MachO.parse("hello.bin")
assert isinstance(fat, lief.MachO.FatBinary)

macho = fat.at(0)
assert isinstance(fat, lief.MachO.Binary)

rpath = lief.MachO.RPathCommand.create("/opt/homebrew/my_package")
assert isinstance(rpath, lief.MachO.RPathCommand)

macho.add(rpath)
  1. Then, we can change the library path of libmylib.dylib to include the RPath prefix:

macho: lief.MachO.Binary

lib = macho.find_library("libmylib.dylib")
assert isinstance(lib, lief.MachO.DylibCommand)

lib.name = "@rpath/libmylib.dylib"

macho.write("hello_fixed.bin")

Objective-C Support

If a Mach-O binary is compiled from Objective-C sources, it may contain metadata represented by the object.
This metadata can help understand the underlying structures of the binary, and LIEF Extended provides support for accessing this information through .

For more details, you can check the Obj-C section.