Ciao, Habr! Oggi vorrei parlare di come si possano proteggere i processi dagli attacchi degli aggressori in macOS. Ad esempio, questo è utile per un antivirus o un sistema di backup, soprattutto alla luce del fatto che in macOS esistono diversi modi per "terminare" un processo. Di questo e dei metodi di protezione ne parlerò di seguito.
Il modo classico per "terminare" un processo
Il modo più conosciuto per "terminare" un processo è inviare un segnale SIGKILL al processo. Tramite bash è possibile eseguire i classici "kill -SIGKILL PID" o "pkill -9 NAME" per terminare. Il comando "kill" è conosciuto fin dai tempi di UNIX ed è disponibile non solo in macOS, ma anche in altri sistemi simili a UNIX.
Come nei sistemi simili a UNIX, macOS consente di intercettare qualsiasi segnale per un processo, tranne due: SIGKILL e SIGSTOP. In questo articolo ci concentreremo principalmente sul segnale SIGKILL, come segnale che provoca l'uccisione del processo.
Specificità di macOS
In macOS, la chiamata di sistema kill nel kernel XNU attiva la funzione psignal(SIGKILL,…). Proviamo a vedere quali altre azioni dell'utente nello userspace possono attivare la funzione psignal. Escluderemo le chiamate della funzione psignal nei meccanismi interni del kernel (anche se possono essere non banali, ma le lasceremo per un altro articolo 🙂 - verifica della firma, errori di memoria, gestione exit/terminate, violazioni della protezione dei file, ecc.).
Cominciamo la panoramica con la funzione e la relativa chiamata di sistema . È evidente che, oltre alla chiamata classica kill, esiste un approccio alternativo, specifico per il sistema operativo macOS e non presente in BSD. I principi di funzionamento di entrambe le chiamate di sistema sono similari. Rappresentano chiamate dirette alla funzione del kernel psignal. Notiamo anche che, prima di terminare un processo, viene eseguita una verifica "cansignal" - può il processo inviare un segnale a un altro processo, il sistema non consente a nessuna applicazione di terminare processi di sistema, ad esempio.
static int
terminate_with_payload_internal(struct proc *cur_proc, int target_pid, uint32_t reason_namespace,
uint64_t reason_code, user_addr_t payload, uint32_t payload_size,
user_addr_t reason_string, uint64_t reason_flags)
{
...
target_proc = proc_find(target_pid);
...
if (!cansignal(cur_proc, cur_cred, target_proc, SIGKILL)) {
proc_rele(target_proc);
return EPERM;
}
...
if (target_pid == cur_proc->p_pid) {
/*
* psignal_thread_with_reason() attenderà un SIGKILL sul thread specificato o
* restituirà se il thread e/o il task sono già in fase di terminazione. In ogni caso,
* il thread attuale non tornerà nello spazio utente.
* /
psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
} else {
psignal_with_reason(target_proc, SIGKILL, signal_reason);
}
...
}
launchd
Il modo standard per creare demoni all'avvio del sistema e controllarne il ciclo di vita è launchd. Vorrei sottolineare che i codici sorgente forniti si riferiscono a una versione precedente di launchctl, prima di macOS 10.10, e gli esempi di codice sono forniti solo a scopo illustrativo. La moderna launchctl invia segnali a launchd tramite XPC, e la logica di launchctl è stata trasferita in esso.
Vediamo come viene effettivamente fermata l'applicazione. Prima di inviare il segnale SIGTERM, si cerca di fermare l'applicazione utilizzando la chiamata di sistema "proc_terminate".
...
error = proc_terminate(j->p, &sig);
if (error) {
job_log(j, LOG_ERR | LOG_CONSOLE, "Impossibile terminare il lavoro: %d: %s", error, strerror(error));
job_log(j, LOG_NOTICE | LOG_CONSOLE, "Utilizzo dell'opzione di fallback per terminare il lavoro...");
error = kill2(j->p, SIGTERM);
if (error) {
job_log(j, LOG_ERR, "Impossibile inviare segnale al lavoro: %d: %s", error, strerror(error));
}
...
Sotto il cofano, proc_terminate, nonostante il suo nome, può inviare non solo psignal con SIGTERM, ma anche SIGKILL.
Uccisione indiretta — limitazione delle risorse
Un caso più interessante può essere visto in una diversa chiamata di sistema . L'uso standard di questa chiamata di sistema consiste nel porre limitazioni sulle risorse delle applicazioni, ad esempio per l'indicizzatore una limitazione sulla quota di tempo di CPU e memoria, in modo che il sistema non venga rallentato in modo significativo dalle operazioni di caching dei file. Se un'applicazione supera il limite delle risorse, come si può vedere dalla funzione proc_apply_resource_actions, al processo viene inviato il segnale SIGKILL.
Nonostante questa chiamata di sistema possa potenzialmente causare l'uccisione di un processo, il sistema non ha verificato adeguatamente i diritti del processo che invoca la chiamata di sistema. In realtà, la verifica , ma è sufficiente utilizzare l'alternativa del flag PROC_POLICY_ACTION_SET per aggirare questa condizione.
Da qui, se si “limita” la quota di utilizzo della CPU per l'applicazione (ad esempio, consentendo di eseguire solo 1 ns), si può terminare qualsiasi processo nel sistema. Così, un malware può terminare qualsiasi processo nel sistema, incluso quello dell'antivirus. Interessante è anche l'effetto che si ottiene terminando il processo con pid 1 (launchctl) — un kernel panic quando si tenta di elaborare il segnale SIGKILL 🙂

Come risolvere il problema?
Il modo più diretto per impedire l'uccisione di un processo è sostituire il puntatore alla funzione nella tabella delle chiamate di sistema. Sfortunatamente, questo metodo non è banale per molte ragioni.
In primo luogo, il simbolo che rappresenta la posizione di sysent in memoria non è solo un simbolo privato del kernel XNU, ma non può nemmeno essere trovato nei simboli del kernel. Sarà necessario utilizzare metodi euristici di ricerca, come la disassemblazione dinamica della funzione e la ricerca del puntatore al suo interno.
In secondo luogo, la struttura delle voci nella tabella dipende dai flag con cui è stato compilato il kernel. Se è stato dichiarato il flag CONFIG_REQUIRES_U32_MUNGING, la dimensione della struttura verrà modificata: verrà aggiunto un campo aggiuntivo. . È necessaria una verifica aggiuntiva riguardo al flag con cui è stato compilato il kernel, ad esempio confrontando i puntatori alle funzioni con quelli noti.
struct sysent {
/* tabella delle chiamate di sistema */
sy_call_t *sy_call; /* funzione di implementazione */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
sy_munge_t *sy_arg_munge32; /* munger degli argomenti delle chiamate di sistema per processi a 32 bit */
#endif
int32_t sy_return_type; /* tipi di ritorno delle chiamate di sistema */
int16_t sy_narg; /* numero di argomenti */
uint16_t sy_arg_bytes; /* Dimensione totale degli argomenti in byte per
* chiamate di sistema a 32 bit */
};
Fortunatamente, nelle versioni moderne di macOS, Apple fornisce una nuova API per lavorare con i processi. L'Endpoint Security API consente ai client di autorizzare molte richieste ad altri processi. Così, è possibile bloccare qualsiasi segnale ai processi, incluso il segnale SIGKILL, tramite l'API sopra menzionata.
#include <bsm/libbsm.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <unistd.h>
int main(int argc, const char * argv[]) {
es_client_t* cli = nullptr;
{
auto res = es_new_client(&cli, ^(es_client_t * client, const es_message_t * message) {
switch (message->event_type) {
case ES_EVENT_TYPE_AUTH_SIGNAL:
{
auto& msg = message->event.signal;
auto target = msg.target;
auto& token = target->audit_token;
auto pid = audit_token_to_pid(token);
printf("signal '%d' sent to pid '%d'n", msg.sig, pid);
es_respond_auth_result(client, message, pid == getpid() ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW, false);
}
break;
default:
break;
}
});
}
{
es_event_type_t evs[] = { ES_EVENT_TYPE_AUTH_SIGNAL };
es_subscribe(cli, evs, sizeof(evs) / sizeof(*evs));
}
printf("%dn", getpid());
sleep(60); // could be replaced with other waiting primitive
es_unsubscribe_all(cli);
es_delete_client(cli);
return 0;
}
Allo stesso modo, nel kernel è possibile registrare una MAC Policy, che fornisce un metodo di protezione dai segnali (policy proc_check_signal), ma l'API non è ufficialmente supportata.
Protezione dell'estensione del kernel
Oltre alla protezione dei processi nel sistema, è assolutamente necessaria anche la protezione dell'estensione del kernel stesso (kext). macOS fornisce ai desenvolvedores un framework per lo sviluppo di driver di dispositivi IOKit. Oltre a fornire strumenti per lavorare con i dispositivi, IOKit offre metodi per lo stacking dei driver tramite istanze di classi C++. L'applicazione nello userspace sarà in grado di "trovare" l'istanza registrata della classe per stabilire una connessione kernel-userspace.
Per rilevare il numero di istanze delle classi nel sistema esiste l'utilità ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Qualsiasi estensione del kernel che desideri registrarsi nello stack dei driver deve dichiarare una classe ereditata da IOService, ad esempio, my_kext_ioservice in questo caso. La connessione delle applicazioni personalizzate causa la creazione di una nuova istanza della classe, che è ereditata da IOUserClient, nell'esempio my_kext_iouserclient.
Quando si tenta di scaricare un driver dal sistema (comando kextunload), viene invocata la funzione virtuale "bool terminate(IOOptionBits options)". È sufficiente restituire false alla chiamata della funzione terminate in caso di tentativo di scaricamento per impedire kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Il caricamento non è consentito, restituzione false
return false;
}
return super::terminate(options);
}
Il flag IsUnloadAllowed può essere impostato da IOUserClient durante il caricamento. In caso di limitazione al caricamento, il comando kextunload restituirà il seguente output:
admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Impossibile rimuovere kext my.kext.test; i servizi non sono riusciti a terminare - 0xe00002c7.
Impossibile scaricare my.kext.test - (iokit/common) funzione non supportata.
È necessario fornire una protezione simile anche per IOUserClient. Le istanze delle classi possono essere scaricate tramite la funzione userspace IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Puoi restituire false alla chiamata del comando "terminate" fino a quando l'applicazione userspace non "muore", ovvero non viene chiamata la funzione "clientDied".
Protezione dei file
Per proteggere i file, è sufficiente utilizzare l'API Kauth, che consente di limitare l'accesso ai file. Apple fornisce agli sviluppatori notifiche su vari eventi nel proprio ambito; per noi sono importanti le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD. Limitare l'accesso ai file è più semplice tramite il percorso: utilizziamo l'API “vn_getpath” per ottenere il percorso del file e confrontiamo il prefisso del percorso. Si noti che per ottimizzare la ridenominazione dei percorsi delle cartelle con file, il sistema non autorizza l'accesso a ciascun file, ma solo alla cartella stessa che è stata rinominata. È necessario confrontare il percorso genitore e limitare KAUTH_VNODE_DELETE per essa.

Uno svantaggio di questo approccio può essere la bassa prestazione quando aumenta il numero di prefissi. Affinché il confronto non sia pari a O(prefisso*lunghezza), dove prefisso è il numero di prefissi e lunghezza è la lunghezza della stringa, è possibile utilizzare un automa a stati finiti deterministico (DFA) costruito sui prefissi.
Prendiamo in considerazione il modo di costruire un DFA per questo insieme di prefissi. Iniziamo posizionando i cursori all'inizio di ogni prefisso. Se tutti i cursori puntano allo stesso simbolo, incrementiamo ogni cursore di un simbolo e annotiamo che la lunghezza della stringa identica è aumentata di uno. Se ci sono due cursori e i simboli sotto di essi sono diversi, dividiamo i cursori in gruppi in base al simbolo su cui puntano e ripetiamo l'algoritmo per ogni gruppo.
Nel primo caso (tutti i simboli sotto i cursori sono identici), otteniamo uno stato del DFA con solo una transizione tramite la stringa identica. Nel secondo caso, otteniamo una tabella di transizioni di dimensione 256 (numero di simboli e numero massimo di gruppi) agli stati successivi, ottenuti tramite una chiamata ricorsiva della funzione.
Consideriamo un esempio. Per l'insieme di prefissi (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) è possibile ottenere il seguente DFA. Nell'immagine sono indicati solo le transizioni che portano ad altri stati; le altre transizioni non saranno finali.

Durante il passaggio tra gli stati del DFA, ci possono essere 3 casi.
- È stato raggiunto uno stato finale: il percorso è protetto, limitando le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD.
- Non è stato raggiunto uno stato finale, ma il percorso “è terminato” (è stato raggiunto il terminatore zero) — il percorso è genitore, è necessario limitare KAUTH_VNODE_DELETE. Notiamo che se il vnode è una cartella, è necessario aggiungere alla fine ‘/’, altrimenti può avvenire una limitazione al file “/foor/bar/t”, il che è errato.
- Non è stato raggiunto uno stato finale, il percorso non è terminato. Nessuno dei prefissi corrisponde a questo, non introduciamo limitazioni.
Conclusione
L'obiettivo delle soluzioni di sicurezza in fase di sviluppo è aumentare il livello di sicurezza dell'utente e dei suoi dati. Da un lato, questo obiettivo è garantito dallo sviluppo del prodotto software Acronis, che chiude le vulnerabilità dove la stessa operativa è “debole”. Dall'altro lato, non si dovrebbe trascurare il potenziamento degli aspetti di sicurezza che possono essere migliorati anche a livello di OS, tanto più che la chiusura di tali vulnerabilità aumenta la nostra stessa resilienza come prodotto. La vulnerabilità è stata segnalata al Team di Sicurezza dei Prodotti Apple ed è stata corretta in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Tutto ciò può essere fatto solo se la tua utility è stata ufficialmente installata nel kernel. Vale a dire che per software esterno e indesiderato non ci sono tali scappatoie. Tuttavia, come vedi, anche per proteggere programmi legittimi, come antivirus e sistema di backup, è necessario lavorare sodo. Ma ora i nuovi prodotti Acronis per macOS avranno una protezione aggiuntiva contro il caricamento dal sistema.
Fonte: habr.com
