Mach-O file format for red team professionals - Part 3
Diving deep into the Header part of the Mach-O file format. Mach-O is the preferred file format on macOS.
Previously, we looked at the Mach-O file format at a high level. Now, lets descend a bit more into details of the Mach-O file format, starting with the Header part.
The Mach-O header is located at the beginning of a Mach-O file and provides essential information about the file type, architecture, and layout. It is represented by the struct mach_header
(32-bit) or struct mach_header_64
(64-bit) in C.
For a 32-bit Mach-O file, the header is defined as:
struct mach_header {
uint32_t magic; // Magic number to identify Mach-O
cpu_type_t cputype; // CPU architecture type
cpu_subtype_t cpusubtype; // CPU subtype
uint32_t filetype; // Type of file (e.g., executable, object, library) uint32_t ncmds; // Number of load commands
uint32_t sizeofcmds; // Size of all load commands in bytes
uint32_t flags; // Flags
};
For a 64-bit Mach-O file, the structure is slightly different:
struct mach_header_64 {
uint32_t magic; // Magic number to identify Mach-O
cpu_type_t cputype; // CPU architecture type
cpu_subtype_t cpusubtype; // CPU subtype
uint32_t filetype; // Type of file (e.g., executable, object, library) uint32_t ncmds; // Number of load commands
uint32_t sizeofcmds; // Size of all load commands in bytes
uint32_t flags; // Flags
uint32_t reserved; // Reserved field for alignment (only in 64-bit)
};
It begins with the magic field, a unique identifier that distinguishes between 32-bit and 64-bit Mach-O files, as well as endianness. Common values include 0xFEEDFACE
for 32-bit big-endian files, 0xCEFAEDFE
for 32-bit little-endian files, 0xFEEDFACF
for 64-bit big-endian files and 0xCFFAFEDFE
for 64-bit little-endian files. This allows operating systems and tools to correctly interpret the file format.
Next, the cputype and cpusubtype fields define the processor architecture for which the binary is intended. The cputype
field specifies the general architecture, such as CPU_TYPE_X86_64
for 64-bit Intel processors, CPU_TYPE_ARM64
for Appleās modern ARM processors, or CPU_TYPE_I386
for 32-bit Intel CPUs. The cpusubtype
provides additional granularity, identifying specific CPU variants or capabilities.
The filetype field determines the kind of Mach-O file, such as MH_EXECUTE
for an executable, MH_OBJECT
for a relocatable object file, MH_DYLIB
for a dynamic library, or MH_BUNDLE
for a loadable bundle. This classification helps the operating system handle the file appropriately.
The ncmds and sizeofcmds fields relate to the load commands, which define the memory layout and dependencies of the binary. ncmds
indicates the number of load commands present, while sizeofcmds
specifies their total size in bytes. Load commands store crucial information like segment mappings, shared library dependencies, and symbol tables.
The flags field contains various bitwise flags that affect execution and linking behavior. These flags indicate properties such as whether the binary uses the two-level namespace for symbols (MH_TWOLEVEL
), supports prebinding (MH_PREBOUND
), or restricts certain reexports (MH_NO_REEXPORTED_DYLIBS
).
For 64-bit Mach-O files, an additional reserved field is present for alignment purposes, ensuring proper memory layout. This field is not present in the 32-bit Mach-O header.
You can view the mach_header of a file by using otool with -hv flags:
otool -hv /bin/ls
The header for universal binaries (or fat binaries) includes an additional fat_header
structure followed by multiple fat_arch
structures. The definition of these structures is located in fat.h file.
fat_header
structure is defined as:
struct fat_header {
uint32_t magic; // Magic number (0xCAFEBABE)
uint32_t nfat_arch; // Number of architectures in the binary
};
It begins with a fat header, which provides an overview of the architectures included. The magic field in the fat header is always 0xCAFEBABE
, distinguishing it from a standard Mach-O file. In little-endian format, this value is reversed to 0xBEBAFECA
. On a big-endian host CPU, validation can be done using the constant FAT_MAGIC
, while on a little-endian host CPU, the constant FAT_CIGAM
should be used.
The nfat_arch field specifies the number of architectures contained within the file. Each architecture is represented by an entry in a table of fat_arch
structures, which describe the individual Mach-O binaries.
fat_arch
structure is defined as:
struct fat_arch {
cpu_type_t cputype; // CPU type (x86_64, ARM64, etc.)
cpu_subtype_t cpusubtype; // CPU subtype
uint32_t offset; // Offset to the Mach-O file for this architecture uint32_t size; // Size of the Mach-O file
uint32_t align; // Alignment (power of 2) }; struct fat_arch_64 { cpu_type_t cputype; // CPU type (x86_64, ARM64, etc.)
cpu_subtype_t cpusubtype; // CPU subtype
uint32_t offset; // Offset to the Mach-O file for this architecture uint32_t size; // Size of the Mach-O file
uint32_t align; // Alignment (power of 2)
uint32_t reserved; // Reserved
};
Within each fat_arch
structure, the cputype and cpusubtype fields define the target processor, similar to the fields in a standard Mach-O header. The offset field indicates the byte position within the fat binary where the Mach-O file for that architecture begins, while the size field specifies the length of that Mach-O file. This allows tools to extract and execute the correct architecture-specific binary as needed. Finally, the align field defines the memory alignment requirement for the architecture slice, typically expressed as a power of two (e.g., 2^12 for 4KB alignment).
You can view the fat_header
of a universal binary by using otool
with -f flag:
otool -f /bin/ls
Red Team Notes - The Mach-O header defines a fileās architecture, type, and structure, containing fields for CPU type, file type, load commands, and execution flags. A Fat Mach-O (universal binary) includes a fat header, allowing multiple architectures within a single file, identified by FAT_MAGIC for big-endian and FAT_CIGAM for little-endian systems. These headers enable macOS and iOS to handle different architectures efficiently.
Follow my journey of 100 Days of Red Team on WhatsApp or Discord.
References