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 = lief.MachO.parse("/bin/ls")

# Using a Path from pathlib
macho: lief.MachO.FatBinary = 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 = 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: lief.MachO.Binary = fat.at(0)

# Pick one based on the architecture
macho: lief.MachO.Binary = 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

macho: lief.MachO.FatBinary = lief.MachO.parse("my.macho", parser_config)

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

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

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:

macho = lief.MachO.parse("hello.bin")
lib: lief.MachO.DylibCommand = macho.find_library("libmylib.dylib")
lib.name = "/opt/hombrew/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:

macho = lief.MachO.parse("hello.bin")
rpath = lief.MachO.RPathCommand.create("/opt/hombrew/my_package")
macho.add(rpath)
  1. Then, we can change the library path of libmylib.dylib to include the RPath prefix:

lib: lief.MachO.DylibCommand = macho.find_library("libmylib.dylib")
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.