macOS Applications – Testing, Debugging, and Fuzzing

18 March 2024 8 minutes Author: Cyber Witcher

Various aspects of analyzing, debugging, and phasing macOS applications are covered. It covers static and dynamic analysis techniques, as well as various tools and techniques that can be used to investigate applications on macOS.

Static analysis

otool -L /bin/ls #List dynamically linked libraries
otool -tv /bin/ps #Decompile application


objdump -m --dylibs-used /bin/ls #List dynamically linked libraries
objdump -m -h /bin/ls # Get headers information
objdump -m --syms /bin/ls # Check if the symbol table exists to get function names
objdump -m --full-contents /bin/ls # Dump every section
objdump -d /bin/ls # Dissasemble the binary
objdump --disassemble-symbols=_hello --x86-asm-syntax=intel toolsdemo #Disassemble a function using intel flavour


The tool can be used as a replacement for codesign , otool and objdump and provides several additional features. Download it here or install it with brew.

# Install
brew install --cask jtool2

jtool2 -l /bin/ls # Get commands (headers)
jtool2 -L /bin/ls # Get libraries
jtool2 -S /bin/ls # Get symbol info
jtool2 -d /bin/ls # Dump binary
jtool2 -D /bin/ls # Decompile binary

# Get signature information
ARCH=x86_64 jtool2 --sig /System/Applications/

# Get MIG information
jtool2 -d __DATA.__const myipc_server | grep MIG

Code design / ldid

Codesign can be found on macOS and ldid on iOS

# Get signer
codesign -vv -d /bin/ls 2>&1 | grep -E "Authority|TeamIdentifier"

# Check if the app’s contents have been modified
codesign --verify --verbose /Applications/

# Get entitlements from the binary
codesign -d --entitlements :- /System/Applications/ # Check the TCC perms

# Check if the signature is valid
spctl --assess --verbose /Applications/

# Sign a binary
codesign -s <cert-name-keychain> toolsdemo

# Get signature info
ldid -h <binary>

# Get entitlements
ldid -e <binary>

# Change entilements
## /tmp/entl.xml is a XML file with the new entitlements to add
ldid -S/tmp/entl.xml <binary>


SuspiciousPackage is a useful tool for checking .pkg files (installers) and seeing what’s inside before installing. These installers have preinstall and postinstallbash scripts, which are commonly abused by malware authors to preserve their malware.


This tool allows you to mount Apple disk image ( .dmg ) files to test them before running:

hdiutil attach ~/Downloads/Firefox\ 58.0.2.dmg

It will be installed in /Volumes/Volumes


Note that programs written in Objective-C preserve their class declarations when compiled into Mach-O binaries. Such class declarations include name and type:
  • Class

  • Class methods

  • Class instance variables

You can get this information using class-dump :


Note that these names can be obfuscated to make reversing the binary difficult.

Function call

When a function is called in a binary that uses object-C, the compiled code will call objc_msgSend instead of calling that function. Which will call the last function:

Parameters expected by this function:

  • The first parameter ( self ) is “a pointer to an instance of the class that should receive the message.” Or more simply, it is the object on which the method is called. If the method is a class method, it will be an instance of the class object (as a whole), while for an instance method, self will point to the created instance of the class as the object.

  • The second parameter ( op ) is the “selector of the method that handles the message”. Again, simply put, this is just a method name.

  • The rest of the parameters are any values required by the (op) method.


With Swift binaries, because there is compatibility with Objective-C, you can sometimes extract declarations using the dump class, but not always.

Using the jtool -la or otool -l command line, you can find several sections that start with the __swift5 prefix:

jtool2 -l /Applications/
LC 00: LC_SEGMENT_64              Mem: 0x000000000-0x100000000    __PAGEZERO
LC 01: LC_SEGMENT_64              Mem: 0x100000000-0x100028000    __TEXT
    Mem: 0x100026630-0x100026d54        __TEXT.__swift5_typeref
    Mem: 0x100026d60-0x100027061        __TEXT.__swift5_reflstr
    Mem: 0x100027064-0x1000274cc        __TEXT.__swift5_fieldmd
    Mem: 0x1000274cc-0x100027608        __TEXT.__swift5_capture

You can find more information about the information stored in these sections in this blog post.

Also, Swift binaries can have symbols (for example, libraries must store symbols so that their functions can be called). Symbols usually contain function name and attr information in a messy way, so they are very useful, and there are demanglers that can get the original name:

# Ghidra plugin

# Swift cli
swift demangle

Packed binaries

  • Check for high entropy

  • Check the lines (almost no intelligible line, packed)

  • The UPX packager for MacOS creates a section called “__XHDR”

Dynamic analysis

Note that debugging SIP binaries needs to be disabled ( csrutil disable or csrutil enable –without debug ) or copy the binaries to a temporary folder and remove the signature codesign –remove-signature <binary-path> or enable debugging of the binary (you can use this script)

Note that in order to instrument system binaries (such as cloudconfigurationd) on macOS, SIP must be disabled (simply removing the signature will not work).

Unified journals

MacOS creates a lot of logs, which can be very useful when running a program trying to figure out what it’s doing.

Additionally, some logs include a <private> tag to hide some user or computer identifying information. However, a certificate can be installed to reveal this information. Follow the explanation here.


Left panel

On the left panel of the hopper you can see the symbols ( Labels ) of the binary file, the list of procedures and functions ( Proc ) and strings ( Str ). These are not all strings, but those defined in several parts of the Mac-O file (such as cstring or objc_methname ).

Middle panel

In the middle panel you can see the disassembled code. And you can see it in raw form, in graph form, in decompilation form and in binary form by clicking on the corresponding icon:

By right-clicking in a code object, you can see references to/from that object, or even change its name (this doesn’t work in decompiled pseudocode):

In addition, you can write python commands in the middle at the bottom.

Right panel

In the right panel, you can see interesting information such as the navigation history (so you know how you got to the current situation), the h call graph, where you can see all the functions that call this function , and all the functions that this function calls and information about local variables.


It allows users to access applications at an extremely low level and gives users the ability to monitor applications and even modify their execution flow. Dtrace uses probes that are placed throughout the kernel at places such as the start and end of system calls.

DTrace uses the dtrace_probe_create function to create a probe for each system call. These probes can be run at the entry and exit point of each system call. Interaction with DTrace is through /dev/dtrace, which is accessible only to the root user.

To enable Dtrace without completely disabling SIP security, you can execute in recovery mode:csrutil enable –without dtrace

You can also dtrace or dtruss the binaries you have compiled.

Available dtrace probes can be obtained using:

dtrace -l | head
   ID   PROVIDER            MODULE                          FUNCTION NAME
    1     dtrace                                                     BEGIN
    2     dtrace                                                     END
    3     dtrace                                                     ERROR
   43    profile                                                     profile-97
   44    profile                                                     profile-199

The probe name consists of four parts: provider, module, function, and name ( fbt:mach_kernel:ptrace:entry ). If you do not specify part of the name, Dtrace will use that part as a wildcard.

To configure DTrace to trigger probes and specify what actions to perform when they are triggered, we will need to use the D language.

A more detailed explanation and more examples can be found at


Run man -k dtrace to get a list of available DTrace scripts. example: sudo dtruss -n binary

In line

#Count the number of syscalls of each running process
sudo dtrace -n 'syscall:::entry {@[execname] = count()}'


/pid == $1/

#Log every syscall of a PID
sudo dtrace -s script.d 1234
    printf("%s(%s)", probefunc, copyinstr(arg0));
        printf("%s(%d)\n", probefunc, arg0);

#Log files opened and closed by a process
sudo dtrace -s b.d -c "cat /etc/hosts"
        printf("=%d\n", arg1);

#Log sys calls with values
sudo dtrace -s syscalls_info.d -c "cat /etc/hosts"


dtruss -c ls #Get syscalls of ls
dtruss -c -p 1000 #get syscalls of PID 1000


You can use it even if SIP is activated

ktrace trace -s -S -t c -c ls | grep "ls("

Continuation in the second part

Other related articles
Found an error?
If you find an error, take a screenshot and send it to the bot.