Threat Intelligence & APTs • macOS

macOS Malware Analysis - Part 1: Understanding Mach-O

Explore macOS Mach-O binary architecture, examining load commands, segments, and sections to build foundational reverse engineering skills.

If you are coming from Windows malware analysis, your muscle memory is tuned to Portable Executable (PE) headers, DOS stubs, import address tables (IAT), and Registry persistence.

When pivoting to macOS, that entire mental model changes. macOS does not use PEs or the Windows Registry. Instead, compiled executables run on the Mach-O (Mach Object) file format, interact with Apple’s dynamic linker (dyld), and leverage Mach kernel primitives and Darwin POSIX subsystems.

In this beginner-focused series, we won't get lost in abstract documentation. We are going to explore macOS internals hands-on by dissecting an actual malware sample from the wild: the second-stage payload from the RustBucket campaign (attributed to BlueNorOff).

1. Introduction & Lab Setup

Before touching live malware, ensure you are analyzing inside an isolated macOS virtual machine (such as a local VM with networking disabled).

Sample Details

▪Malware Family: RustBucket (Stage 2 Payload)
▪Dropped Name: .pd
▪SHA-256: 7887638bcafd57e2896c7c16698e927ce92fd7d409aae698d33cdca3ce8d25b8

Essential Toolkit

For this post, we will rely primarily on standard macOS command-line utilities bundled with Apple's Command Line Tools (xcode-select --install):

▪file: Quick inspection of architecture and file type.
▪otool: The standard object file displaying tool (Apple’s CLI inspector for Mach-O structures).
▪nm: Dumps symbol table entries.
▪strings: Scrapes printable strings.
How a macOS Binary Runs
Figure: How a macOS Binary Runs Click to zoom ↗

How a macOS Binary Runs

2. Mach-O Architecture & The Universal (Fat) Binary

On Windows, an executable target is strictly one architecture (a 64-bit PE cannot natively house both x86_64 and ARM64 code).

macOS solves multi-architecture support using Universal Binaries (historically called "Fat Binaries"). A Universal Binary is simply an archive wrapper that bundles multiple architecture-specific Mach-O slices into a single file.

macho fat binary structure
Figure: macho fat binary structure Click to zoom ↗

macho fat binary structure

When an analyst double-clicks an application, the macOS dynamic loader inspects the Fat Header:

▪On an Apple Silicon (M1/M2/M3/M4) Mac, it selects and maps the ARM64 slice.
▪On an Intel Mac (or under Rosetta 2), it executes the x86_64 slice.
▪Single-architecture binaries skip the Fat Header entirely and start immediately with the native Mach-O header.

Architecture Identification Cheat Sheet

Keep these magic bytes and CPU constants handy:

ArchitectureMagic Bytes (Hex)CPU Type Constantcputype Value
Fat Binary (32-bit offset)0xCAFEBABEFAT_MAGICN/A (Header container)
Fat Binary (64-bit offset)0xCAFEBABFFAT_MAGIC_64N/A (Header container)
Mach-O 64-bit (x86_64)0xFEEDFACFCPU_TYPE_X86_6416777223 (0x01000007)
Mach-O 64-bit (ARM64)0xFEEDFACFCPU_TYPE_ARM6416777228 (0x0100000C)

Hands-on Triage: Inspecting Our Specimen (.pd)

Let’s point our CLI tools at our RustBucket specimen to inspect its architecture.

Step 1: Check File Type and Slices

OBJECTIVEC
file .pd
file command output
Figure: file command output Click to zoom ↗

file command output

Breaking Down the Output

This single line tells us crucial details about the target:

▪Mach-O universal binary with 2 architectures: The payload contains two distinct compiled Mach-O slices packaged within a single file.
▪x86_64: Targets Intel-based Macs (or systems running under Rosetta 2).
▪arm64: Targets Apple Silicon (M1/M2/M3/M4 chips) natively.
▪Header Flags:
▪NOUNDEFS: No unresolved external references exist at compile time.
▪DYLDLINK: Informs the kernel to pass execution to the dynamic linker (dyld).
▪TWOLEVEL: Uses two-level namespace lookups (every imported symbol records both the symbol name and the specific library that provides it).
▪PIE: Position Independent Executable. The code can load at any randomized virtual memory address (ASLR).

Step 2: Peeking at the Universal (Fat) Header

Because our sample is a multi-architecture file, it doesn't start directly with raw assembly code or segment definitions. Instead, the file begins with a Fat Header—a small container directory that tells the macOS dynamic linker (dyld) where each architecture slice lives inside the file.

We can view the raw container breakdown using Apple's object file displaying tool, otool:

OBJECTIVEC
otool -f .pd
otool -f .pd output
Figure: otool -f .pd output Click to zoom ↗

otool -f .pd output

Deconstructing the Output

Every field here serves a specific purpose in macOS execution:

▪fat_magic 0xcafebabe: The classic magic bytes defining a universal binary with 32-bit architecture offsets. (If offsets exceed 4GB, you would encounter 0xcafebabf for 64-bit universal binaries).
▪nfat_arch 2: Informs the kernel that there are exactly two compiled architecture slices bundled inside.

Now look closely at the two architecture records:

FieldArchitecture 0 (x86_64)Architecture 1 (arm64)Why It Matters
cputype16777223 (0x01000007)16777228 (0x0100000C)Maps to CPU_TYPE_X86_64 and CPU_TYPE_ARM64.
cpusubtype3 (CPU_SUBTYPE_I386_ALL)0 (CPU_SUBTYPE_ARM64_ALL)Indicates standard processor capabilities without specialized instructions.
offset16384 (0x4000)98304 (0x18000)The exact byte offset in the file where each independent Mach-O binary begins.
size72480 bytes (~70.8 KB)82944 bytes (~81 KB)ARM64 instructions are fixed-width (4 bytes each), typically making the ARM64 slice slightly larger than the variable-length x86_64 slice.
align2^14 (16,384 bytes)2^14 (16,384 bytes)Memory page alignment.

Analyst Note on Alignment: Notice the align value is set to 2^14 (16 KB) for both slices. While Intel x86_64 systems traditionally use 4 KB memory pages, Apple Silicon hardware uses 16 KB pages. To ensure compatibility across all modern Mac hardware without paging faults, the compiler aligns both slices to 16 KB boundaries.

Step 3: Splitting the Universal Container with lipo

When performing static reverse engineering in tools like Ghidra, IDA Pro, or Hopper, loading a multi-architecture binary often prompts you to choose one specific architecture to decompile. On the command line, parsing commands against a Fat binary can clutter your output by printing duplicates for every single architecture slice.

To keep our analysis clean and controlled, we use lipo - macOS’s native utility for manipulating Universal Binaries—to carve each architecture into its own standalone Mach-O file.

OBJECTIVEC
lipo .pd -thin x86_64 -output pd_x86 && lipo .pd -thin arm64 -output pd_arm64 && file pd_x86 pd_arm64
splitting the fat binary using lipo command
Figure: splitting the fat binary using lipo command Click to zoom ↗

splitting the fat binary using lipo command

What Changed Under the Hood?

By "thinning" the binary:

▪The original 16 KB fat_header and padding are stripped away.
▪Each output file (pd_x86 and pd_arm64) now begins at byte 0x00 with its native 64-bit Mach-O header (0xFEEDFACF).
▪We now have two independent binaries that can be disassembled, debugged, or statically parsed side-by-side.

Step 4: Dissecting the Native Mach-O Headers

Now that both slices are separated, each file starts directly at offset 0x00 with a standard mach_header_64 structure (defined in <mach-o/loader.h>).

Let's inspect the headers of both slices side-by-side:

OBJECTIVEC
otool -h pd_x86 pd_arm64
otool output of both the binaries
Figure: otool output of both the binaries Click to zoom ↗

otool output of both the binaries

Decoding the Header Fields

Fieldpd_x86 Valuepd_arm64 ValueMeaning & Internals Detail
magic0xfeedfacf0xfeedfacfMH_MAGIC_64. Confirms both are 64-bit Mach-O binaries in native host byte order.
cputype16777223 (0x01000007)16777228 (0x0100000C)CPU_TYPE_X86_64 vs CPU_TYPE_ARM64.
cpusubtype30CPU_SUBTYPE_I386_ALL vs CPU_SUBTYPE_ARM64_ALL.
caps0x000x00Capability bits. On ARM64e systems, this field can hold pointer authentication (PAC) flags.
filetype22MH_EXECUTE. Tells dyld this is an executable binary with an entry point, not a shared library (MH_DYLIB = 6) or object bundle (MH_BUNDLE = 8).
ncmds2728The total count of Load Commands immediately following the header. Notice ARM64 has 28 vs. 27 on x86_64!
sizeofcmds3056 bytes2968 bytesThe total byte size occupied by all load command structures combined.
flags0x002000850x00200085Bitmask of runtime capabilities.

Demystifying the flags Bitmask (0x00200085)

The flags tell us how the macOS kernel and dyld will treat this binary in memory:

▪0x00000001 (MH_NOUNDEFS): The compiler resolved all internal symbols, no undefined references remain.
▪0x00000004 (MH_DYLDLINK): Binary uses the dynamic linker (dyld) rather than static linking.
▪0x00000080 (MH_TWOLEVEL): Uses two-level namespace symbol resolution (each import records both its symbol name and the specific library it belongs to, speeding up loading and preventing namespace collisions).
▪0x00200000 (MH_PIE): Position Independent Executable. ASLR (Address Space Layout Randomization) will randomize the base virtual address on every run.

For our next step, we need to inspect the Load Commands to see why ARM64 has 28 commands while x86_64 has 27, and how memory segments are mapped.

**Understanding Mach-O Headers**
Figure: **Understanding Mach-O Headers** Click to zoom ↗

Understanding Mach-O Headers

Step 5: The Execution Blueprint - Load Commands

If the Mach header is the identity badge of the binary, the Load Commands are the execution blueprint. Immediately following the header, they tell the kernel and the dynamic linker (dyld):

▪How to lay out segments in virtual memory
▪Where the main entry point is
▪Which shared system libraries (.dylib) must be mapped in
▪Where symbol tables and code signatures live

Let's list the first 15 load commands of the pd_x86 slice using otool -l:

OBJECTIVEC
otool -l pd_x86 | grep "cmd " | head -n 15
terminal output
Figure: terminal output Click to zoom ↗

terminal output

What These Commands Instruct the System to Do

▪LC_SEGMENT_64 (x4): Directs the kernel to map four primary memory regions into the virtual address space: __PAGEZERO, __TEXT, __DATA, and __LINKEDIT.
▪LC_DYLD_INFO_ONLY: Provides the compressed rebase, bind, weak-bind, lazy-bind, and export opcodes used by dyld during ASLR rebasing and symbol resolution.
▪LC_SYMTAB & LC_DYSYMTAB: Pointers to the static and dynamic symbol tables (function names, variable names, and relocation entries).
▪LC_LOAD_DYLINKER: Path to the dynamic linker responsible for bootstrapping this process (almost universally /usr/lib/dyld).
▪LC_UUID: A unique 128-bit build identifier matched with debug symbols (.dSYM).
▪LC_VERSION_MIN_MACOSX: The minimum macOS SDK version required to execute this slice.
▪LC_MAIN: The replacement for the legacy LC_UNIXTHREAD command. Specifies the offset to the program's initial entry point (main).
▪LC_LOAD_DYLIB: Dynamically linked libraries/frameworks required by the malware at runtime.

Now let's run the exact same command on the ARM64 slice to spot the architectural differences:

Step 6: Architectural Differences in Load Commands (x8664 vs. ARM64)

Now let's inspect the first 15 load commands of the pd_arm64 slice:

OBJECTIVEC
otool -l pd_arm64 | grep "cmd " | head -n 15
Screenshot 2026-09-03 at 9.22.55 PM.png
Figure: Screenshot 2026-09-03 at 9.22.55 PM.png Click to zoom ↗
Featurepd_x86 (Intel)pd_arm64 (Apple Silicon)What It Means for Malware Analysis
Segment Count4 segments5 segmentsARM64 splits constant data into its own dedicated segment (__DATA_CONST) alongside __DATA, strengthening page permission protections.
Version CommandLC_VERSION_MIN_MACOSXLC_BUILD_VERSIONLC_VERSION_MIN_MACOSX is the legacy load command. Apple Silicon uses LC_BUILD_VERSION, which specifies the target platform, minimum OS version, and the exact SDK version used to compile the binary.

Step 7: Anatomy of Mach-O Segments

In Mach-O, a Segment is a high-level container that the kernel maps into virtual memory with a specific page protection (rwx). Inside segments reside individual Sections, which hold the actual instructions, constants, and pointers.

Let's inspect the segments declared in each slice:

OBJECTIVEC
otool -l pd_x86 | grep -E "(cmd LC_SEGMENT_64|segname)"
otool -l pd_arm64 | grep -E "(cmd LC_SEGMENT_64|segname)"
otool -l pd_x86 | grep -E "(cmd LC_SEGMENT_64|segname)”
Figure: otool -l pd_x86 | grep -E "(cmd LC_SEGMENT_64|segname)” Click to zoom ↗

otool -l pd_x86 | grep -E "(cmd LC_SEGMENT_64|segname)”

otool -l pd_arm64 | grep -E "(cmd LC_SEGMENT_64|segname)”
Figure: otool -l pd_arm64 | grep -E "(cmd LC_SEGMENT_64|segname)” Click to zoom ↗

otool -l pd_arm64 | grep -E "(cmd LC_SEGMENT_64|segname)”

OBJECTIVEC
[pd_x86]
  cmd LC_SEGMENT_64 -> segname __PAGEZERO
  cmd LC_SEGMENT_64 -> segname __TEXT        (11 sections)
  cmd LC_SEGMENT_64 -> segname __DATA        (12 sections)
  cmd LC_SEGMENT_64 -> segname __LINKEDIT

[pd_arm64]
  cmd LC_SEGMENT_64 -> segname __PAGEZERO
  cmd LC_SEGMENT_64 -> segname __TEXT        (11 sections)
  cmd LC_SEGMENT_64 -> segname __DATA_CONST  (3 sections)
  cmd LC_SEGMENT_64 -> segname __DATA        (5 sections)
  cmd LC_SEGMENT_64 -> segname __LINKEDIT

What Each Segment Does Under the Hood

SegmentPermissionsPurpose
__PAGEZERO--- (None)A non-executable, non-readable guard block starting at virtual address 0x0. Catches NULL pointer dereferences by immediately triggering an EXC_BAD_ACCESS crash if the malware tries to read/write to 0x0.
__TEXTr-x (Read / Exec)Contains the compiled machine instructions and immutable data (like string literals). It is read-only to prevent self-modifying code.
__DATA_CONSTr-- (Read-only)ARM64 / Modern macOS only. Holds pointers and structures initialized at load time by dyld and then immediately locked down to read-only before execution starts.
__DATArw- (Read / Write)Mutable global variables, static storage, and lazy symbol pointers that are modified at runtime.
__LINKEDITr-- (Read-only)Raw dynamic link metadata, including export tables, symbol tables, code signatures, and relocation info used by dyld.

Step 8: Diving into PAGEZERO & Virtual Memory Protection

To understand how macOS protects memory at the binary level, let’s inspect the fields of the very first segment command:

OBJECTIVEC
otool -l pd_arm64 | grep -A 8 "segname __PAGEZERO"
otool -l pd_arm64 | grep -A 8 "segname __PAGEZERO”
Figure: otool -l pd_arm64 | grep -A 8 "segname __PAGEZERO” Click to zoom ↗

otool -l pd_arm64 | grep -A 8 "segname __PAGEZERO”

Decoding the Guard Page

▪vmaddr 0x0000000000000000 & vmsize 0x0000000100000000:

The segment begins at virtual address 0x0 and extends for 0x100000000 bytes (exactly 4 GB in size). This 4 GB region covers the entire 32-bit lower address range.

▪fileoff 0 & filesize 0:

It consumes zero bytes on disk. It is purely a virtual memory allocation made by the kernel at runtime.

▪initprot 0x0 & maxprot 0x0:

Initial and maximum page permissions are both 0 (--, no read, no write, no execute).

Why This Matters for Malware Analysis:

If a developer or malware author introduces a NULL pointer dereference (or tries to access memory via a corrupted 32-bit integer offset inside this 4 GB window), the CPU immediately faults with EXC_BAD_ACCESS (SIGSEGV) rather than allowing an exploit to hijack execution via NULL page spraying.

Step 9: Inside TEXT & Calculating the Entry Point

Now let's inspect the __TEXT segment and the load command that points to execution: LC_MAIN.

BASH
otool -l pd_arm64 | grep -A 8 "segname __TEXT" | head -n 9
otool -l pd_arm64 | grep -A 4 "cmd LC_MAIN"
output
Figure: output Click to zoom ↗

output

BASH
segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x0000000000004000
  fileoff 0
 filesize 16384
  maxprot 0x00000005
 initprot 0x00000005
   nsects 11
    flags 0x0
---
       cmd LC_MAIN
   cmdsize 24
  entryoff 9892
 stacksize 0

Deconstructing TEXT Memory Setup

▪vmaddr 0x0000000100000000: As expected, __TEXT begins immediately where __PAGEZERO ends (at the 4 GB mark, 0x100000000).
▪initprot 0x00000005: In octal/hex permissions, 0x5 corresponds to Read + Execute (4 + 1 = 5, r-x). It is non-writable.
▪filesize 16384: Exactly 16 KB (aligned to Apple Silicon page size).

****

Demystifying LCMAIN & The Entry Point

On legacy Mach-O binaries, execution entry was governed by LC_UNIXTHREAD, which explicitly supplied CPU register states (rip or pc). Modern macOS uses LC_MAIN:

▪entryoff 9892: This is a raw relative byte offset from the start of the binary image to the first instruction of main().
▪Calculating the Virtual Memory Entry Point:
BASH
    VM Base + entryoff = 0x100000000 + 9892 (0x26A4) = 0x1000026A4

When you open pd_arm64 in a disassembler like Ghidra or IDA Pro, this address (0x1000026A4) is precisely where the decompilation flow starts.

Step 10: Mapping Dynamic Dependencies (otool -L)

Before any malware code executes, the dynamic linker (dyld) walks the LC_LOAD_DYLIB commands to map shared libraries into the process address space.

Checking linked libraries is the macOS equivalent of scanning the Import Directory on a Windows PE. It immediately answers two fundamental questions:

1.What programming language runtime was this compiled with?
2.Which macOS subsystems and frameworks does it interact with?

Let's inspect the dynamic libraries loaded by pd_arm64:

BASH
otool -L pd_arm64
Screenshot 2026-09-03 at 9.48.27 PM.png
Figure: Screenshot 2026-09-03 at 9.48.27 PM.png Click to zoom ↗
BASH
pd_arm64:
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1953.255.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)
    /usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 5.7.1)
    /usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
    /usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 0.0.0)
    /usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 17.0.0)
    /usr/lib/swift/libswiftIOKit.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
    /usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 6.0.0, weak)
    /usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 6.0.0, weak)
    /usr/lib/swift/libswiftFoundation.dylib (compatibility version 1.0.0, current version 1.0.0)

Triage Analysis: What Does This Tell Us?

▪Written in Swift: The prevalence of /usr/lib/swift/libswift*.dylib indicates this payload is written in Swift rather than pure C or Objective-C. When reversing in Ghidra or IDA Pro later, function names will follow Swift name mangling conventions ($s...).
▪High-Level OS Interactions (Foundation.framework): Foundation provides classes like FileManager, URLSession, and Process. This points to standard file dropping, HTTP network requests, or launching subprocesses.
▪Low-Level Subsystem Access (libswiftIOKit.dylib & libswiftXPC.dylib):
▪IOKit: Often leveraged by malware for hardware profiling, gathering serial numbers, or detecting virtualized/sandbox environments.
▪XPC: macOS’s native Inter-Process Communication (IPC) framework, often used to communicate with system daemons or escalate privileges.
▪libSystem.B.dylib: The core Darwin umbrella library containing libc, libdispatch, POSIX calls, and Mach messaging primitives.

Step 11: Targeted Triage with cstring

Running strings blindly against an entire binary often returns hundreds of lines of noise, mangled symbol references, and linker metadata.

In Mach-O, real ASCII string literals embedded by the programmer sit in a dedicated section: __cstring inside the __TEXT segment. We can dump just this section with its exact virtual memory offsets using otool:

BASH
otool -v -s __TEXT __cstring pd_arm64 | head -n 35
strings
Figure: strings Click to zoom ↗

strings

BASH
pd_arm64:
Contents of (__TEXT,__cstring) section
0000000100003d70  mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
0000000100003db0  v32@?0@"NSData"8@"NSURLResponse"16@"NSError"24
0000000100003ddf  
0000000100003de0  HTTP Request Failed 
0000000100003df5  wb
0000000100003df8  v8@?0
0000000100003dfe  swift_getFunctionTypeMetadataGlobalActor
0000000100003e27  swift_getFunctionTypeMetadataGlobalActorStandalone

Triaging the Indicators

Even without decompiling a single line of assembly, this snippet reveals critical operational behaviors:

▪Fake User-Agent Header:
BASH
    mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)

This is an ancient Windows XP / Internet Explorer 8 User-Agent string. A macOS binary running natively on Apple Silicon using a legacy Windows XP User-Agent is an immediate red flag—designed to blend in with legacy network traffic or evade naive macOS detections.

▪Network Callback Signatures:
BASH
    v32@?0@"NSData"8@"NSURLResponse"16@"NSError"24

This is an Objective-C / Swift type encoding string representing a block completion handler signature: (void)(NSData *data, NSURLResponse *response, NSError *error). It confirms that the binary uses Foundation's URLSession dataTaskWithRequest:completionHandler: for command-and-control (C2) communication.

▪Error Logging & Disk Operations:
▪HTTP Request Failed: Standard error logging following an unsuccessful beacon attempt.
▪wb: Standard POSIX file mode for "write binary," commonly passed to fopen() when writing an incoming payload directly to disk.

Step 12: Code Signing & Gatekeeper Checks (codesign)

Every modern binary executing on Apple Silicon must have at least a baseline cryptographic signature. If an ARM64 binary completely lacks a code signature, the Mach kernel aborts execution immediately with a crash (SIGKILL / CODESIGNING).

To see how the author signed this binary, we invoke Apple’s native verification tool, codesign:

BASH
codesign -dv --verbose=4 pd_arm64
codesign output
Figure: codesign output Click to zoom ↗

codesign output

Reading the Security Attributes

▪Signature=adhoc & flags=0x2(adhoc):

The binary is ad-hoc signed (signed locally with a pseudo-identity rather than an Apple Developer Certificate).

▪Why? Apple Silicon requires some signature to execute without kernel rejection. Ad-hoc signing fulfills this minimal CPU architecture requirement without registering a paid Apple Developer account.
▪TeamIdentifier=not set:

There is no Apple Developer Team ID tied to this specimen.

▪CDHash=90089ad92e2eb0688a8ea060a752e18128539253:

The Code Directory Hash (CDHash) is one of the most critical IoCs in macOS threat hunting. It is a cryptographic hash of the binary's code directory pages. macOS security subsystems (like Endpoint Security Framework, Endpoint Detection and Response tools, and XProtect) track binaries by CDHash rather than just the full file SHA-256.

Gatekeeper Impact: Because the binary lacks a valid Developer ID signature and Apple Notarization ticket, if a user downloads this file via a web browser or email client, macOS applies the com.apple.quarantine extended attribute (xattr). Gatekeeper will immediately block execution with a warning dialog.

To bypass this, campaigns like RustBucket rely on an initial first-stage dropper or AppleScript droplet that writes the .pd payload directly to disk via command-line utilities (like curl or shell scripts), intentionally omitting the quarantine attribute.

Static Triage Cheat Sheet

When a new macOS specimen lands on your triage desk, this command chain gives you full architectural context within 60 seconds:

BASH
# 1. Identify container type and architectures
file sample

# 2. Inspect Fat container offsets (if Universal)
otool -f sample

# 3. Thin down to isolated architectures
lipo sample -thin arm64 -output sample_arm64

# 4. View Mach-O header flags and capabilities
otool -h sample_arm64

# 5. Extract virtual memory layouts and the entry point
otool -l sample_arm64 | grep -E "(cmd LC_MAIN|entryoff|segname)"

# 6. Map linked runtime frameworks and system libraries
otool -L sample_arm64

# 7. Dump literal ASCII strings from the code section
otool -v -s __TEXT __cstring sample_arm64

# 8. Check code signing identity and CDHash
codesign -dv --verbose=4 sample_arm64

Coming Up in Part 2: Disassembly & Dynamic Behavior

Now that we understand the basics of how a macOS binary is structured and runs, it’s time to see what happens inside the code.

In Part 2, we’ll take a closer look at the RustBucket sample and follow its behavior step by step.

See you in Part 2!

Copied