You cannot exploit what you cannot read. Before any of the bypasses later in this series (the XPC privilege escalations, the sandbox escapes, the TCC tricks) there is a long stretch of unglamorous work: pulling a binary apart, reading its signature and entitlements, finding the one function that matters, and watching what it does to the system at runtime. The tooling for that work on macOS is its own small dialect, and fluency in it is the difference between an afternoon and a week.

This post is a tour of that toolkit: codesign for signing and entitlements, objdump and jtool2 for Mach-O structure, Hopper for disassembly and decompilation, LLDB for live debugging, and DTrace for tracing a process’s every syscall without touching its binary. The previous post laid out the terrain: Mach-O, code signing, the dyld shared cache. This one is about surveying it on a running system.

codesign: The Entitlements Are the Real Story

codesign ships with macOS and does double duty: it signs binaries and it inspects existing signatures. Inspection is what matters first. A quick -dv dumps the basics:

# filename: codesign-dv.txt
% codesign -dv /System/Applications/Automator.app
Executable=/System/Applications/Automator.app/Contents/MacOS/Automator
Identifier=com.apple.Automator
Format=app bundle with Mach-O universal (x86_64 arm64e)
CodeDirectory v=20100 size=3780 flags=0x0(none) hashes=111+5 location=embedded
Signature size=4658
TeamIdentifier=not set

The Identifier is the bundle ID. The flags field carries code-signing flags that change how the OS treats the binary. Add a second v (-dvv) and you also get the signing authority chain: Software Signing, Apple Code Signing Certification Authority, Apple Root CA for an Apple binary.

But the part you will spend the most time staring at is the entitlements. Entitlements are a plist baked into the signature that grant a binary specific capabilities, and on macOS they are frequently the whole ballgame. Dump them with -d --entitlements:- (the : strips the binary header, the - writes to stdout):

# filename: automator-entitlements.txt
% codesign -d --entitlements :- /System/Applications/Automator.app
    <key>com.apple.private.automator.securityHost</key>            <true/>
    <key>com.apple.private.cs.automator-plugins</key>              <true/>
    <key>com.apple.private.tcc.allow</key>
    <array>
        <string>kTCCServiceAppleEvents</string>
        <string>kTCCServiceAddressBook</string>
        <string>kTCCServiceCalendar</string>
        <string>kTCCServicePhotos</string>
    </array>
    <key>com.apple.private.xprotect</key>                          <true/>

Read that carefully, because it is a preview of half this series. com.apple.private.tcc.allow means this binary is pre-authorized to touch your Contacts, Calendar, and Photos without ever prompting: the TCC system that normally gates that access simply waves Automator through. The com.apple.private.* entitlements are Apple-only, granted by Apple’s signature. When a later post hunts for a TCC bypass, the strategy is precisely this: find an Apple binary carrying a private entitlement and find a way to make it act on your behalf. The entitlement dump is where you go shopping.

codesign -s <identity> <binary> signs a binary. On your own research machine an ad-hoc or self-signed identity is enough to make a binary runnable for testing. The asymmetry of macOS: reading signatures is trivial, forging Apple’s is not.

objdump and jtool2: Mach-O Without the Mystery

To see a Mach-O’s skeleton, objdump (from the Xcode command-line tools) with -m parses it as Mach-O. The most useful starting query is the dependency list: every LC_LOAD_DYLIB:

# filename: objdump-dylibs.txt
% objdump -m --dylibs-used toolsdemo
/usr/lib/libSystem.B.dylib

That list is a hijacking map. The dylib-injection post in this series reads exactly this output to find a library a target loads but does not pin down, then drops a malicious one in its place. objdump -m -h prints section headers, --syms the symbol table, and --disassemble-functions=_hello -x86-asm-syntax=intel disassembles a single function in the Intel syntax that most people find easier than the default AT&T:

# filename: objdump-disas.txt
% objdump --disassemble-functions=_hello -x86-asm-syntax=intel toolsdemo

jtool2, Jonathan Levin’s tool, overlaps with otool, codesign, and objdump but adds a few things and organizes the output differently. jtool2 -l lists load commands and segments, -L lists linked dylibs, -S dumps symbols, and --sig shows code-signature detail including the computed and stored CDHash: the hash the system actually checks:

# filename: jtool2-sig.txt
% ARCH=x86_64 jtool2 --sig toolsdemo
        CDHash:  460b3d4416780b826236a8d21b7e941d31b2af60 (computed)

Two practical gotchas worth knowing before they cost you an hour: jtool2 has a long-standing bug that makes it report code-signing flags as “none” regardless, and its disassembler does not handle Intel binaries: fall back to objdump for x86_64 disassembly. Tools on macOS are sharp but chipped; knowing where the chips are is part of the craft.

Hopper: Disassembly You Can Actually Read

For anything beyond a single function, you want a real disassembler. Hopper is the pragmatic choice on macOS (cheaper than IDA, more native than Ghidra) and its headline feature is a decompiler that turns a function’s assembly into C-like pseudocode. For Objective-C-heavy macOS binaries, where every call is an objc_msgSend with a selector, the pseudocode view is what makes the logic legible.

Open a binary, let Hopper finish its analysis pass, and you get six panels: the disassembly, the control-flow graph, the procedure list, strings, and so on. The workflow that matters for this series is navigation: following a cross-reference from a string like "unlock" back to the code that uses it, then flipping to pseudocode to read the surrounding logic. Hopper also resolves external C function calls and lets you save your annotated work to a .hop file, which you will want once a target gets large. Hopper can drive a debugger too, but for live work LLDB is the sharper instrument.

LLDB: Driving a Live Process

LLDB is the system debugger. The first thing to internalize is not a command: it is a restriction. macOS will not let you attach to an arbitrary signed process. A binary is debuggable only if it carries the com.apple.security.get-task-allow entitlement; to debug one that does not, your debugger needs com.apple.security.cs.debugger, and SIP further constrains what you can touch. Your own unsigned test binaries debug freely; Apple’s do not. That single rule shapes how a lot of macOS research actually proceeds.

With that out of the way, the core loop. Set Intel syntax once, create a target, break, run:

# filename: lldb-session.txt
(lldb) settings set target.x86-disassembly-flavor intel
(lldb) target create "toolsdemo"
(lldb) breakpoint set -name main
(lldb) run
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1

Breakpoints come in flavors: by symbol (b toolsdemomain), by name scoped to a module (breakpoint set -n main -s toolsdemo), or by raw address (breakpoint set -a 0x100003e00`). Once stopped, you inspect and rewrite machine state directly:

# filename: lldb-memory.txt
(lldb) register read rip                       ; read one register
(lldb) memory read $rsi                        ; dereference a register
(lldb) memory write -f s $rip+0x11f+7 "Aloha World!"   ; patch a string in memory
(lldb) register write rax 42                   ; change a register

That last pair is the point. LLDB is not just an observer; it is an editor for a live process. You can rewrite a string, flip a return value, redirect control flow, and watch the program continue under your altered reality. Half of validating an exploit hypothesis is exactly this: stop at the check, change the value the check reads, confirm the program does what you predicted.

DTrace: Tracing Without Touching the Binary

DTrace is the most powerful tool in this set and the one people reach for last. It instruments a running system (syscalls, library calls, kernel functions) without modifying or even restarting the target. You write a tiny script: a probe (what to watch), an optional predicate (a filter), and an action.

Watch every syscall a specific program makes:

// filename: trace-syscalls.d
syscall:::entry
/execname == "toolsdemo"/
{
    printf("%s called %s\n", execname, probefunc);
}

syscall:::entry fires on entry to any syscall; the predicate /execname == "toolsdemo"/ restricts it to our target; execname and probefunc are built-in variables for the process name and the probed function. Narrow the probe to filter further: syscall::write*:entry catches only write-family calls, and you can print the buffer being written:

// filename: trace-writes.d
syscall::write*:entry
/execname == "toolsdemo"/
{
    printf("%s wrote: %s\n", execname, copyinstr(arg1));
}

And DTrace aggregates. To rank which syscalls a program leans on, accumulate counts keyed by function name:

// filename: count-syscalls.d
syscall:::entry
/execname == "toolsdemo"/
{
    @[probefunc] = count();
}

On exit DTrace prints a sorted table. This is how you fingerprint an unknown binary’s behavior in seconds (what it opens, what it writes, which privileged calls it makes) and it is how the shellcode post verifies that hand-written assembly is issuing exactly the syscalls intended. Because DTrace sees the whole system, it does need elevated privileges, and SIP restricts probing of protected processes. Used within those limits it is unmatched for understanding behavior you cannot get from static analysis alone.

Why This Matters

These six tools are not interchangeable; they answer different questions. codesign tells you what a binary is allowed to do, and on macOS the entitlement list often hands you the exploit plan outright. objdump and jtool2 expose the Mach-O structure and the dylib dependencies an injection attack feeds on. Hopper turns a stripped binary into readable logic. LLDB lets you stop a process mid-check and rewrite the value it is about to read. DTrace shows you, system-wide, what a program actually does rather than what it claims to.

Every later post in this series assumes you can pick the right one without thinking about it. When the XPC post says “we dump the service’s entitlements and find it does not verify the caller,” that is codesign plus Hopper. When the dylib post says “the target loads this library from a writable path,” that is objdump --dylibs-used. When the shellcode post says “we confirmed the syscalls fire in order,” that is DTrace. The tools are the vocabulary; the rest of the series is the sentences. Next we put them to use building something offensive from scratch: macOS shellcode.

References

  1. Levin, J. jtool / jtool2. newosxbook.com.
  2. LLVM Project. The LLDB Debugger. lldb.llvm.org.
  3. Apple. DTrace and Instruments. Apple Developer Documentation.