Articles

A deep dive on the macOS binary structure

5 min read

The Mach-O (Mach Object) file format is the standard executable binary format used across all Apple operating systems, including macOS, iOS, iPadOS, watchOS, and tvOS. Despite its widespread use, Apple does not maintain any comprehensive public documentation of the Mach-O format. Let’s change that.

A single Mach-O binary file is divided into three primary sequential components: the header, load commands, and data. For the sake of simplicity, all of the examples below are based on the 64-bit Mach Object layout.

The Mach-O Header

Every Mach-O file contains a header which describes the file’s format.

struct mach_header_64 {
    std::uint32_t magic;        /* mach magic number identifier */
    cpu_type_t    cputype;      /* cpu specifier */
    cpu_subtype_t cpusubtype;   /* machine specifier */
    std::uint32_t filetype;     /* type of file */
    std::uint32_t ncmds;        /* number of load commands */
    std::uint32_t sizeofcmds;   /* total size of all the load commands */
    std::uint32_t flags;        /* flags */
    std::uint32_t reserved;     /* reserved */
};

All eight fields are 4 bytes wide, so the 64-bit header occupies exactly the first 32 bytes of the file. (The 32-bit mach_header is 28 bytes and has no reserved field.) Let’s go over the fields one by one.

  1. magic - a 32-bit constant that identifies the file as a Mach-O and encodes both its bitness and its byte order, <mach-o/loader.h> defines four:
#define MH_MAGIC     0xfeedface  /* 32-bit, host byte order  */
#define MH_CIGAM     0xcefaedfe  /* 32-bit, byte-swapped     */
#define MH_MAGIC_64  0xfeedfacf  /* 64-bit, host byte order  */
#define MH_CIGAM_64  0xcffaedfe  /* 64-bit, byte-swapped     */
  1. cputype - represents the file’s target architecture, e.g., arm64, x86_64.
  2. cpusubtype - which specific CPU the program is compiled for.
  3. filetype - the type of the file, e.g., MH_EXECUTE.
  4. ncmds - the number of load commands, placed right after the Mach-O header.
  5. sizeofcmds - the total size of all load commands in bytes.
  6. flags - represents optional feature flags.
  7. reserved - a trailing 4-byte field reserved for future use, currently unused.

Load Commands

Following the Mach-O header are the load command structures, which specify where specific data lives in the file, and its layout in virtual memory. This is where we find the data for function start addresses, segments, the symbol table, etc.

Load commands all begin with the following structure:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};
  1. cmd - the type of the load command. It is possible for multiple load commands to share the same structure, yet have a different type. The most notable types are LC_FUNCTION_STARTS, LC_SEGMENT_64, LC_SYMTAB, and LC_MAIN.
  2. cmdsize - the total size of the load command, including cmd and cmdsize.

Symbol Table

LC_SYMTAB represents our symbol table, which is a table that stores information about the symbols defined and referenced within the binary, such as functions, variables, and their corresponding addresses.

struct symtab_command {
    uint32_t cmd;       /* LC_SYMTAB */
    uint32_t cmdsize;   /* sizeof(struct symtab_command) */
    uint32_t symoff;    /* symbol table offset */
    uint32_t nsyms;     /* number of symbol table entries */
    uint32_t stroff;    /* string table offset */
    uint32_t strsize;   /* string table size */
};
  1. symoff - file offset where the symbol table begins. The entries at this location are nlist_64 structures.
  2. nsyms - number of entries in the symbol table.
  3. stroff - file offset where the string table begins. This contains the names referenced by symbols.
  4. strsize - size of the string table in bytes.

Segments

Segments are the most important and frequent load commands. They specify the range of bytes in a 64-bit Mach-O file that make up a segment. It is read right after cmd and cmdsize. This is also where sections are contained.

struct segment_command_64 {     /* for 64-bit architectures */
    uint32_t    cmd;            /* LC_SEGMENT_64 */
    uint32_t    cmdsize;        /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;         /* memory address of this segment */
    uint64_t    vmsize;         /* memory size of this segment */
    uint64_t    fileoff;        /* file offset of this segment */
    uint64_t    filesize;       /* amount to map from the file */
    vm_prot_t   maxprot;        /* maximum VM protection */
    vm_prot_t   initprot;       /* initial VM protection */
    uint32_t    nsects;         /* number of sections in segment */
    uint32_t    flags;          /* flags */
};
  1. segname - a fixed-length string holding the name of the segment, e.g., __TEXT. Names shorter than 16 bytes are null-padded, however a name occupying all 16 bytes does not require a null terminator
  2. vmaddr - virtual memory address where the segment is mapped.
  3. vmsize - the size of the segment once mapped.
  4. fileoff - file offset the bytes are read from.
  5. filesize - how many bytes are actually mapped from fileoff.
  6. maxprot - maximum virtual memory protections permitted for the segment
  7. initprot - protection at map time.
  8. nsects - how many section_64 structures follow the current segment.
  9. flags - segment flags

The most common segments include __PAGEZERO, __TEXT, __DATA.

__PAGEZERO

The __PAGEZERO segment is a special unmapped virtual memory region located at address 0x0, with no permissions. It’s commonly used to catch NULL pointers, since any attempt to read/write will trigger a memory access violation. This allows for a safe crashing method, instead of exposing silent corruption or exploitable behavior.

__TEXT

The __TEXT segment is arguably the most interesting segment, exposing the core information stored within a binary. It contains the __const, __cstring, and __text sections (not to be confused with __TEXT), followed by many more.

Sections

As mentioned before, file sections are found within segments, and contain vital information about the program, such as its instructions, strings, and more. This is where the application’s machine code lives. Every section shares the same structure, which looks like:

struct section_64 {         /* for 64-bit architectures */
    char sectname[16];      /* name of this section */
    char segname[16];       /* segment this section goes in */
    uint64_t addr;          /* memory address of this section */
    uint64_t size;          /* size in bytes of this section */
    uint32_t offset;        /* file offset of this section */
    uint32_t align;         /* section alignment (power of 2) */
    uint32_t reloff;        /* file offset of relocation entries */
    uint32_t nreloc;        /* number of relocation entries */
    uint32_t flags;         /* flags (section type and attributes) */
    uint32_t reserved1;     /* reserved (for offset or index) */
    uint32_t reserved2;     /* reserved (for count or sizeof) */
    uint32_t reserved3;     /* reserved */
};
  1. sectname - name of the section. e.g., __text, __data, __cstring.

  2. segname - name of the segment that contains this section. e.g., __TEXT, __DATA.

  3. addr - virtual memory address where the section begins when loaded into memory.

  4. size - size of the section in bytes.

  5. offset - file offset where the section’s data begins in the Mach-O file.

  6. align - required alignment of the section, represented as a power of two. For example, a value of 4 means an alignment of 2^4, or 16 bytes.

  7. reloff - file offset where the section’s relocation entries begin.

  8. nreloc - number of relocation entries associated with the section.

  9. flags - describes the section’s type and attributes, such as whether it contains instructions, constants, or other special data.

  10. reserved1 - section-specific value whose meaning depends on the section type. It may contain an offset or index.

  11. reserved2 - section-specific value whose meaning depends on the section type. It may contain a count or entry size.

  12. reserved3 - section-specific reserved value used by certain section types.

__text

The __text section is the most valuable in terms of information, as it contains the program’s executable machine code. We won’t be covering how to decode these instructions as architectures can expand very fast.

__cstring

The __cstring section can be very valuable for reverse engineering as it gives us a list of C-style strings embedded within the application.

These strings are stored as a sequence of characters, terminated by a null byte \0. They may contain error messages, file paths, function-related text, URLs, logs, and other information used by the program.

Parsing this section is relatively easy. We first read the section’s raw data using its offset section_64::offset and size section_64::size. We can iterate through the data until a null byte is encountered, which marks the end of the current string.

For every string, we can calculate both its virtual address and file offset:

// NOTE: string is a custom type, not std::string.

string.address = section.address + start;
string.file_offset = section.offset + start;

Here, start represents the string’s offset relative to the beginning of the __cstring section. Adding it to the section’s virtual address gives us the address where the string will appear in memory, while adding it to the section’s file offset gives us its location inside the Mach-O file.

We can therefore represent each parsed string using a simple structure containing its value, virtual address, and file offset. This becomes especially useful later when analyzing instructions inside __text, as references to addresses within __cstring can be resolved back to their corresponding strings.

References

  1. The Mach-O Format

  2. Notes on the Mach-O File Format