Keypoints
- macOS uses a hybrid XNU kernel (Mach + FreeBSD) and exposes Mach IPC and task/port primitives used for low-level process control and memory operations.
- System protections like SIP and Hardened Runtime (and entitlements such as com.apple.security.cs.allow-dyld-environment-variables) restrict techniques like task_for_pid and DYLD-based injection on protected binaries.
- Host and user discovery are done via Mach calls and POSIX APIs (host_info(), sysctlbyname(), uname(), getpwuid()), and the article includes C examples for collecting and sending this data to a C2 over sockets.
- Two injection methods are demonstrated: environment-variable/library preload (DYLD_INSERT_LIBRARIES) for non‑hardened binaries, and Mach-based process/shellcode injection using task_for_pid, mach_vm_allocate/write/protect, and thread creation.
- Shellcode for macOS (Mach-O x86_64) can be assembled with NASM, linked with ld, extracted via objdump, embedded into a __TEXT,__text section, and injected/executed in a remote task.
- Basic persistence techniques covered include user LaunchAgents, system LaunchDaemons, emond rules, and shell startup files (.zshenv) to run payloads at login or shell start.
- Demonstrations include self-copying to /usr/bin or ~/Library, daemonizing via fork/setsid to hide processes, and a minimal C2 server example that accepts and prints client data.
MITRE Techniques
- [T1055] Process Injection – Injecting code into remote processes via DYLD preload and Mach memory/thread APIs (‘it will load any dylibs specified in this variable before the program loads, essentially injecting a dylib into the application’)
- [T1547] Boot or Logon Autostart Execution – Persistence using LaunchAgents/LaunchDaemons and emond rules (‘Launch agents and launch daemons are by far the most prevalent methods of persistence’)
- [T1082] System Information Discovery – Gathering host details with host_info(), sysctlbyname(), and uname() (‘host_info()… tells us stuff like how many CPUs there are, both maximum and available, the physical and logical CPUs, memory size’)
- [T1033] System Owner/User Discovery – Collecting user and group info via POSIX APIs like getpwuid() and getgrgid() (‘we typically access user-related data through standard POSIX interfaces like getpwuid()’)
- [T1071] Application Layer Protocol – Command-and-control over sockets used to send collected host data to a C2 server (‘we use socket… send() for sending data to the Command & Control server’)
- [T1564] Hide Artifacts – Simple process hiding/daemonization via fork(), exit parent, and setsid() to detach from the terminal (‘hide_process()… hides the child process from user interaction and prevents it from receiving signals from the terminal’)
Indicators of Compromise
- [File path] Persistence / self‑copy locations – /usr/bin/MALWARE_NAME, ~/Library/MALWARE_NAME (used for copying a payload to system or user directories)
- [Executable / binary] System/utility targets and LOLBins – /usr/sbin/system_profiler, /sbin/emond, /bin/zsh -c (used for information gathering, event-driven persistence, and command execution)
- [IP address] C2/client example – 10.0.0.1 (shown in server output as an example client connection)
- [Entitlement keys] App entitlements referenced – com.apple.security.network.client, com.apple.security.cs.allow-dyld-environment-variables, com.apple.security.get-task-allow (mentioned as relevant to injection/hardened runtime behavior)
- [Shellcode bytes] Injected payload sample – e.g., sequence beginning with 0xeb 0x1e 0x5e … (embedded ‘Hello World’ shellcode used to validate injection/execution)
The article’s technical procedure condensed:
Paragraph 1:
Interaction with macOS at a low level relies on Mach and BSD primitives: obtain host info via host_info()/host_kernel_version()/host_page_size(), fetch kernel/state variables via sysctlbyname(), and read user info with getpwuid()/getgrgid(). The demo collects these values and transmits them over a socket to a simple C2 server (socket, send, recv), and includes a self-copy routine that attempts to write the binary to /usr/bin/ or falls back to ~/Library/, with process daemonization achieved by fork(), parent exit, and setsid().
Paragraph 2:
For code injection the article shows two approaches. First, DYLD_INSERT_LIBRARIES preload: compile a dylib with a constructor function that runs before main, set DYLD_INSERT_LIBRARIES for a non‑hardened binary (or for binaries with the proper entitlements) and observe remote code execution. Second, Mach-based injection: use task_for_pid() (subject to entitlements/SIP restrictions) or mach_task_self() for local operations, allocate remote memory with mach_vm_allocate(), write shellcode with mach_vm_write(), set permissions via vm_protect, and create/prepare a remote thread (thread_create / thread_set_state / thread_resume) pointing RIP/RSP to the injected region; note the need to promote/initialize pthread structures (e.g., _pthread_create_from_mach_thread) when executing in user-space to avoid thread/TLS issues.
Paragraph 3:
Building and testing shellcode involves assembling x86_64 Mach‑O code (NASM), linking with ld (-lSystem, -syslibroot), extracting bytes via objdump or a small parser, and embedding the byte sequence into a C variable placed in __TEXT,__text for validation before remote injection. For persistence and stealth the article documents common userland techniques (LaunchAgents/LaunchDaemons plist examples, emond rules, shell startup files like .zshenv) and stresses platform defenses—SIP, Hardened Runtime, and entitlements—which constrain techniques such as DYLD injection and task_for_pid unless appropriate permissions are present.
Read more: https://0xf00sec.github.io/2024/03/09/MacOs-X.html