macOS Apps - Inspecting, debugging and Fuzzing
Last updated
Last updated
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
You can download disarm from here.
You can download jtool2 here or install it with brew
.
jtool is deprecated in favour of disarm
Codesign
can be found in macOS while ldid
can be found in iOS
SuspiciousPackage is a tool useful to inspect .pkg files (installers) and see what is inside before installing it.
These installers have preinstall
and postinstall
bash scripts that malware authors usually abuse to persist the malware.
This tool allows to mount Apple disk images (.dmg) files to inspect them before running anything:
It will be mounted in /Volumes
Check for high entropy
Check the strings (is there is almost no understandable string, packed)
The UPX packer for MacOS generates a section called "__XHDR"
Note that programs written in Objective-C retain their class declarations when compiled into Mach-O binaries. Such class declarations include the name and type of:
The interfaces defined
The interface methods
The interface instance variables
The protocols defined
Note that this names could be obfuscated to make the reversing of the binary more difficult.
When a function is called in a binary that uses objective-C, the compiled code instead of calling that function, it will call objc_msgSend
. Which will be calling the final function:
The params this function expects are:
The first parameter (self) is "a pointer that points to the instance of the class that is to receive the message". Or more simply put, it’s the object that the method is being invoked upon. If the method is a class method, this will be an instance of the class object (as a whole), whereas for an instance method, self will point to an instantiated instance of the class as an object.
The second parameter, (op), is "the selector of the method that handles the message". Again, more simply put, this is just the name of the method.
The remaining parameters are any values that are required by the method (op).
See how to get this info easily with lldb
in ARM64 in this page:
x64:
Argument
Register
(for) objc_msgSend
1st argument
rdi
self: object that the method is being invoked upon
2nd argument
rsi
op: name of the method
3rd argument
rdx
1st argument to the method
4th argument
rcx
2nd argument to the method
5th argument
r8
3rd argument to the method
6th argument
r9
4th argument to the method
7th+ argument
rsp+ (on the stack)
5th+ argument to the method
Dynadump is a tool to class-dump Objective-C binaries. The github specifies dylibs but this also works with executables.
At the time of the writing, this is currently the one that works the best.
class-dump is the original tool to generates declarations for the classes, categories and protocols in ObjetiveC formatted code.
It's old and unmaintained so it probably won't work properly.
iCDump is a modern and cross-platform Objective-C class dump. Compared to existing tools, iCDump can run independently from the Apple ecosystem and it exposes Python bindings.
With Swift binaries, since there is Objective-C compatibility, sometimes you can extract declarations using class-dump but not always.
With the jtool -l
or otool -l
command lines it's possible ti find several sections that start with __swift5
prefix:
You can find further information about the information stored in these section in this blog post.
Moreover, Swift binaries might have symbols (for example libraries need to store symbols so its functions can be called). The symbols usually have the info about the function name and attr in a ugly way, so they are very useful and there are "demanglers" that can get the original name:
Note that in order to debug binaries, SIP needs to be disabled (csrutil disable
or csrutil enable --without debug
) or to copy the binaries to a temporary folder and remove the signature with codesign --remove-signature <binary-path>
or allow the 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 (just removing the signature won't work).
macOS exposes some interesting APIs that give information about the processes:
proc_info
: This is the main one giving a lot of information about each process. You need to be root to get other processes information but you don't need special entitlements or mach ports.
libsysmon.dylib
: It allows to get information about processes via XPC exposed functions, however, it's needed to have the entitlement com.apple.sysmond.client
.
Stackshotting is a technique used to capture the state of the processes, including the call stacks of all running threads. This is particularly useful for debugging, performance analysis, and understanding the behavior of the system at a specific point in time. On iOS and macOS, stackshotting can be performed using several tools and methods like the tools sample
and spindump
.
This tool (/usr/bini/ysdiagnose
) basically collects a lot of information from your computer executing tens of different commands such as ps
, zprint
...
It must be run as root and the daemon /usr/libexec/sysdiagnosed
has very interesting entitlements such as com.apple.system-task-ports
and get-task-allow
.
Its plist is located in /System/Library/LaunchDaemons/com.apple.sysdiagnose.plist
which declares 3 MachServices:
com.apple.sysdiagnose.CacheDelete
: Deletes old archives in /var/rmp
com.apple.sysdiagnose.kernel.ipc
: Special port 23 (kernel)
com.apple.sysdiagnose.service.xpc
: User mode interface through Libsysdiagnose
Obj-C class. Three arguments in a dict can be passed (compress
, display
, run
)
MacOS generates a lot of logs that can be very useful when running an application trying to understand what is it doing.
Moreover, the are some logs that will contain the tag <private>
to hide some user or computer identifiable information. However, it's possible to install a certificate to disclose this information. Follow the explanations from here.
In the left panel of hopper it's possible to see the symbols (Labels) of the binary, the list of procedures and functions (Proc) and the strings (Str). Those aren't all the strings but the ones defined in several parts of the Mac-O file (like cstring or objc_methname
).
In the middle panel you can see the dissasembled code. And you can see it a raw disassemble, as graph, as decompiled and as binary by clicking on the respective icon:
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):
Moreover, in the middle down you can write python commands.
In the right panel you can see interesting information such as the navigation history (so you know how you arrived at the current situation), the call graph where you can see all the functions that call this function and all the functions that this function calls, and local variables information.
It allows users access to applications at an extremely low level and provides a way for users to trace programs and even change their execution flow. Dtrace uses probes which are placed throughout the kernel and are at locations such as the beginning and end of system calls.
DTrace uses the dtrace_probe_create
function to create a probe for each system call. These probes can be fired in the entry and exit point of each system call. The interaction with DTrace occur through /dev/dtrace which is only available for the root user.
To enable Dtrace without fully disabling SIP protection you could execute on recovery mode: csrutil enable --without dtrace
You can also dtrace
or dtruss
binaries that you have compiled.
The available probes of dtrace can be obtained with:
The probe name consists of four parts: the provider, module, function, and name (fbt:mach_kernel:ptrace:entry
). If you not specifies some part of the name, Dtrace will apply that part as a wildcard.
To configure DTrace to activate probes and to specify what actions to perform when they fire, we will need to use the D language.
A more detailed explanation and more examples can be found in https://illumos.org/books/dtrace/chp-intro.html
Run man -k dtrace
to list the DTrace scripts available. Example: sudo dtruss -n binary
In line
script
It's a kernel tracing facility. The documented codes can be found in /usr/share/misc/trace.codes
.
Tools like latency
, sc_usage
, fs_usage
and trace
use it internally.
To interface with kdebug
sysctl
is used over the kern.kdebug
namespace and the MIBs to use can be found in sys/sysctl.h
having the functions implemented in bsd/kern/kdebug.c
.
To interact with kdebug with a custom client these are usually the steps:
Remove existing settings with KERN_KDSETREMOVE
Set trace with KERN_KDSETBUF and KERN_KDSETUP
Use KERN_KDGETBUF to get number of buffer entries
Get the own client out of the trace with KERN_KDPINDEX
Enable tracing with KERN_KDENABLE
Read the buffer calling KERN_KDREADTR
To match each thread with its process call KERN_KDTHRMAP.
In order to get this information it's possible to use the Apple tool trace
or the custom tool kDebugView (kdv).
Note that Kdebug is only available for 1 costumer at a time. So only one k-debug powered tool can be executed at the same time.
The ktrace_*
APIs come from libktrace.dylib
which wrap those of Kdebug
. Then, a client can just call ktrace_session_create
and ktrace_events_[single/class]
to set callbacks on specific codes and then start it with ktrace_start
.
You can use this one even with SIP activated
You can use as clients the utility ktrace
:
Or tailspin
.
This is used to do a kernel level profiling and it's built using Kdebug
callouts.
Basically, the global variable kernel_debug_active
is checked and is set it calls kperf_kdebug_handler
withe Kdebug
code and address of the kernel frame calling. If the Kdebug
code matches one selected it gets the "actions" configured as a bitmap (check osfmk/kperf/action.h
for the options).
Kperf has a sysctl MIB table also: (as root) sysctl kperf
. These code can be found in osfmk/kperf/kperfbsd.c
.
Moreover, a subset of Kperfs functionality resides in kpc
, which provides information about machine performance counters.
ProcessMonitor is a very useful tool to check the process related actions a process is performing (for example, monitor which new processes a process is creating).
SpriteTree is a tool to prints the relations between processes.
You need to monitor your mac with a command like sudo eslogger fork exec rename create > cap.json
(the terminal launching this required FDA). And then you can load the json in this tool to view all the relations:
FileMonitor allows to monitor file events (such as creation, modifications, and deletions) providing detailed information about such events.
Crescendo is a GUI tool with the look and feel Windows users may know from Microsoft Sysinternal’s Procmon. This tool allows the recording of various event types to be started and stopped, allows for the filtering of these events by categories such as file, process, network, etc., and provides the functionality to save the events recorded in a json format.
Apple Instruments are part of Xcode’s Developer tools – used for monitoring application performance, identifying memory leaks and tracking filesystem activity.
Allows to follow actions performed by processes:
Taskexplorer is useful to see the libraries used by a binary, the files it's using and the network connections. It also checks the binary processes against virustotal and show information about the binary.
In this blog post you can find an example about how to debug a running daemon that used PT_DENY_ATTACH
to prevent debugging even if SIP was disabled.
lldb is the de facto tool for macOS binary debugging.
You can set intel flavour when using lldb creating a file called .lldbinit
in your home folder with the following line:
Inside lldb, dump a process with process save-core
(lldb) Command
Description
run (r)
Starting execution, which will continue unabated until a breakpoint is hit or the process terminates.
process launch --stop-at-entry
Strt execution stopping at the entry point
continue (c)
Continue execution of the debugged process.
nexti (n / ni)
Execute the next instruction. This command will skip over function calls.
stepi (s / si)
Execute the next instruction. Unlike the nexti command, this command will step into function calls.
finish (f)
Execute the rest of the instructions in the current function (“frame”) return and halt.
control + c
Pause execution. If the process has been run (r) or continued (c), this will cause the process to halt ...wherever it is currently executing.
breakpoint (b)
b main
#Any func called main
b <binname>`main
#Main func of the bin
b set -n main --shlib <lib_name>
#Main func of the indicated bin
breakpoint set -r '\[NSFileManager .*\]$'
#Any NSFileManager method
breakpoint set -r '\[NSFileManager contentsOfDirectoryAtPath:.*\]$'
break set -r . -s libobjc.A.dylib
# Break in all functions of that library
b -a 0x0000000100004bd9
br l
#Breakpoint list
br e/dis <num>
#Enable/Disable breakpoint
breakpoint delete <num>
help
help breakpoint #Get help of breakpoint command
help memory write #Get help to write into the memory
reg
x/s <reg/memory address>
Display the memory as a null-terminated string.
x/i <reg/memory address>
Display the memory as assembly instruction.
x/b <reg/memory address>
Display the memory as byte.
print object (po)
This will print the object referenced by the param
po $raw
{
dnsChanger = {
"affiliate" = "";
"blacklist_dns" = ();
Note that most of Apple’s Objective-C APIs or methods return objects, and thus should be displayed via the “print object” (po) command. If po doesn't produce a meaningful output use x/b
memory
memory read 0x000.... memory read $x0+0xf2a memory write 0x100600000 -s 4 0x41414141 #Write AAAA in that address memory write -f s $rip+0x11f+7 "AAAA" #Write AAAA in the addr
disassembly
dis #Disas current function
dis -n <funcname> #Disas func
dis -n <funcname> -b <basename> #Disas func dis -c 6 #Disas 6 lines dis -c 0x100003764 -e 0x100003768 # From one add until the other dis -p -c 4 # Start in current address disassembling
parray
parray 3 (char **)$x1 # Check array of 3 components in x1 reg
image dump sections
Print map of the current process memory
image dump symtab <library>
image dump symtab CoreNLP
#Get the address of all the symbols from CoreNLP
When calling the objc_sendMsg
function, the rsi register holds the name of the method as a null-terminated (“C”) string. To print the name via lldb do:
(lldb) x/s $rsi: 0x1000f1576: "startMiningWithPort:password:coreCount:slowMemory:currency:"
(lldb) print (char*)$rsi:
(char *) $1 = 0x00000001000f1576 "startMiningWithPort:password:coreCount:slowMemory:currency:"
(lldb) reg read $rsi: rsi = 0x00000001000f1576 "startMiningWithPort:password:coreCount:slowMemory:currency:"
The command sysctl hw.model
returns "Mac" when the host is a MacOS but something different when it's a VM.
Playing with the values of hw.logicalcpu
and hw.physicalcpu
some malwares try to detect if it's a VM.
Some malwares can also detect if the machine is VMware based on the MAC address (00:50:56).
It's also possible to find if a process is being debugged with a simple code such us:
if(P_TRACED == (info.kp_proc.p_flag & P_TRACED)){ //process being debugged }
It can also invoke the ptrace
system call with the PT_DENY_ATTACH
flag. This prevents a debugger from attaching and tracing.
You can check if the sysctl
or ptrace
function is being imported (but the malware could import it dynamically)
As noted in this writeup, “Defeating Anti-Debug Techniques: macOS ptrace variants” : “The message Process # exited with status = 45 (0x0000002d) is usually a tell-tale sign that the debug target is using PT_DENY_ATTACH”
Core dumps are created if:
kern.coredump
sysctl is set to 1 (by default)
If the process wasn't suid/sgid or kern.sugid_coredump
is 1 (by default is 0)
The AS_CORE
limit allows the operation. It's possible to suppress code dumps creation by calling ulimit -c 0
and re-enable them with ulimit -c unlimited
.
In those cases the core dumps is generated according to kern.corefile
sysctl and stored usually in /cores/core/.%P
.
ReportCrash analyzes crashing processes and saves a crash report to disk. A crash report contains information that can help a developer diagnose the cause of a crash.
For applications and other processes running in the per-user launchd context, ReportCrash runs as a LaunchAgent and saves crash reports in the user's ~/Library/Logs/DiagnosticReports/
For daemons, other processes running in the system launchd context and other privileged processes, ReportCrash runs as a LaunchDaemon and saves crash reports in the system's /Library/Logs/DiagnosticReports
If you are worried about crash reports being sent to Apple you can disable them. If not, crash reports can be useful to figure out how a server crashed.
While fuzzing in a MacOS it's important to not allow the Mac to sleep:
systemsetup -setsleep Never
pmset, System Preferences
If you are fuzzing via a SSH connection it's important to make sure the session isn't going to day. So change the sshd_config file with:
TCPKeepAlive Yes
ClientAliveInterval 0
ClientAliveCountMax 0
Checkout the following page to find out how you can find which app is responsible of handling the specified scheme or protocol:
macOS File Extension & URL scheme app handlersThis interesting to find processes that are managing network data:
Or use netstat
or lsof
Works for CLI tools
It "just works" with macOS GUI tools. Note some some macOS apps have some specific requirements like unique filenames, the right extension, need to read the files from the sandbox (~/Library/Containers/com.apple.Safari/Data
)...
Some examples:
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)