Skip to main content

Mach-O Introduction

The executable format of an iOS binary is “Mach-O” (Mach Object), which is the format of choice for the XNU micro-kernel. Understanding how the binary is laid out, and how to obtain this information, will be crucial to testing mobile devices and their applications.

The diagram below is a breakdown of the sections of a Standard Mach-O file:

  • Header - contains the magic number ID, CPU type, filetype, flags, and number of load commands. Each of these start with “MH_” when viewing the file.

  • Load Commands - contains various information regarding what and how the binary loads additional data, such as dynamic libraries, encryption level, code signatures, etc. These will start with “LC_”, with pertinent details listed below.

  • Data - holds the Segment data which start with a “S_”, and contain things like addresses, flags, offsets, etc.

More detail on each of these are located in the loader.h header file. This file should be on your macOS system if you have Xcode, or you can find it on the Apple Open-Source page.

Xcode: /Library/Developer/CommandLineTools/SDKs/$sdkver/usr/include/mach-o/loader.h

GitHub: https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h

There are several tools available to review the Mach-O information included with the Xcode Command Line Tools. We will take a look at some of these tools below.

ToolDescription
otoolCommand line tool to display information about a Mach-O file
MachOViewGUI program to parse Mach-O files (including FAT files)
XMachOViewGUI program to parse Mach-O files - actively developed
lipoCommand line tool to slim a FAT or Universal file
objdumpCommand line tool similar to otool, provided by the LLVM org
stringsCommand line tool to dump readable data from a binary file
plutilCommand line tool to read/write Property List (PLIST) files
jtool/jtool2Command line tool(s) for interacting with Mach-O files
fileBuilt-in command to determine a file type

The most common way to view the load commands is by using the otool command with the -l flag. The loader.h file (listed above) contains a lot of valuable information for how to decode the load commands and their output. It is very well documented with comments and covers every load command available. It would be beneficial to review all of the load commands for various executables and use the loader.h file to get a better understanding of them. We will cover many throughout this guide, but we will not cover them all. At over 1600 lines of code and comments in that file, it could be its own book!

Mach-O executables can be FAT binaries. This means that there are multiple architectures included in the binary. When executed, it determines the architecture of the device and runs the appropriate code. For instance, Apple recently switched to ARM based computers. However, they still have several X86_64 based systems out there that need to maintain support. When compiling an application in Xcode, you can select it to include multiple architectures. So, a FAT file would contain an x86_64 section and an ARM64 section.

These FAT or Universal files can cause some confusion when analyzing them, so Apple includes a utility to slim the binary to one architecture called lipo. We will run lipo on some binaries later on since you will likely encounter these.

Just to confuse matters even more, when installing an iOS application from the Apple App Store, it will only deliver the architecture that you need for your device. This was more important when they supported a mixture of ARMv7 (32-bit) and ARMv8 (64-bit). However, the files were becoming too large to send all architectures down to a device and the device couldn’t use half the code. Apple doesn’t make anything easy.

In the unlikely event that a FAT executable is found, it is best to extract the appropriate architecture slice of the executable, and perform analysis on only that file. If you need to extract a specific architecture from the binary, you can use the lipo or jtool2 utilities.

  In the example below, the bof1 program is analyzed for multiple architectures:

% lipo -detailed_info bof1      
Fat header in: bof1
fat_magic 0xcafebabe
nfat_arch 2
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities 0x0
offset 16384
size 48128
align 2^14 (16384)
architecture arm64e
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64E
capabilities PTR_AUTH_VERSION USERSPACE 0
offset 65536
size 88816
align 2^14 (16384)

To extract the arm64e architecture:

% lipo bof1 -thin arm64e -output bof1.arm64
% lipo -detailed_info bof1.arm64
input file bof1.arm64 is not a fat file
Non-fat file: bof1.arm64 is architecture: arm64e