Come proteggere i processi e le estensioni del kernel su macOS

Ciao, Habr! Oggi vorrei parlare di come proteggere i processi dagli attacchi degli aggressori in macOS. Ciò è utile, ad esempio, per un antivirus o un sistema di backup, soprattutto perché su macOS esistono diversi modi per “uccidere” un processo. Leggi questo e i metodi di protezione sotto il taglio.

Come proteggere i processi e le estensioni del kernel su macOS

Il modo classico per “uccidere” un processo

Un modo ben noto per “uccidere” un processo è inviare un segnale SIGKILL al processo. Attraverso bash puoi chiamare lo standard “kill -SIGKILL PID” o “pkill -9 NAME” per kill. Il comando “kill” è noto fin dai tempi di UNIX ed è disponibile non solo su macOS, ma anche su altri sistemi simili a UNIX.

Proprio come nei sistemi simili a UNIX, macOS ti consente di intercettare qualsiasi segnale inviato a un processo tranne due: SIGKILL e SIGSTOP. Questo articolo si concentrerà principalmente sul segnale SIGKILL come segnale che provoca l'interruzione di un processo.

specifiche di macOS

Su macOS, la chiamata di sistema kill nel kernel XNU chiama la funzione psignal(SIGKILL,...). Proviamo a vedere quali altre azioni dell'utente nello userspace possono essere chiamate dalla funzione psignal. Eliminiamo le chiamate alla funzione psignal nei meccanismi interni del kernel (anche se potrebbero non essere banali, le lasceremo per un altro articolo 🙂 - verifica della firma, errori di memoria, gestione di uscita/terminazione, violazioni della protezione dei file, ecc. .

Iniziamo la recensione con la funzione e la corrispondente chiamata di sistema termina_con_carico utile. Si può vedere che oltre alla classica kill call esiste un approccio alternativo specifico per il sistema operativo macOS e non presente in BSD. Anche i principi operativi di entrambe le chiamate di sistema sono simili. Sono chiamate dirette alla funzione del kernel psignal. Si noti inoltre che prima di terminare un processo, viene eseguito un controllo "cansignal" - se il processo può inviare un segnale a un altro processo; il sistema non consente ad alcuna applicazione di terminare i 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() will pend a SIGKILL on the specified thread or
		 * return if the thread and/or task are already terminating. Either way, the
		 * current thread won't return to userspace.
		 */
		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 la durata è launchd. Tieni presente che i sorgenti si riferiscono alla vecchia versione di launchctl fino a macOS 10.10, gli esempi di codice sono forniti a scopo illustrativo. Il moderno launchctl invia segnali launchd tramite XPC, la logica launchctl è stata spostata su di esso.

Diamo un'occhiata a come vengono interrotte esattamente le applicazioni. Prima di inviare il segnale SIGTERM, si tenta di arrestare l'applicazione utilizzando la chiamata di sistema "proc_terminate".

<launchctl src/core.c>
...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Could not terminate job: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Using fallback option to terminate job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Could not signal job: %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 - Limite di risorse

Un caso più interessante può essere visto in un'altra chiamata di sistema process_policy. Un uso comune di questa chiamata di sistema è limitare le risorse dell'applicazione, ad esempio per un indicizzatore per limitare il tempo della CPU e le quote di memoria in modo che il sistema non venga rallentato in modo significativo dalle attività di memorizzazione nella cache dei file. Se un'applicazione ha raggiunto il limite delle risorse, come si può vedere dalla funzione proc_apply_resource_actions, viene inviato un segnale SIGKILL al processo.

Anche se questa chiamata di sistema potrebbe potenzialmente uccidere un processo, il sistema non ha controllato adeguatamente i diritti del processo che ha chiamato la chiamata di sistema. Effettivamente sto controllando ci, ma è sufficiente utilizzare il flag alternativo PROC_POLICY_ACTION_SET per bypassare questa condizione.

Pertanto, se si "limita" la quota di utilizzo della CPU dell'applicazione (ad esempio, consentendo l'esecuzione di solo 1 ns), è possibile interrompere qualsiasi processo nel sistema. Pertanto, il malware può uccidere qualsiasi processo del sistema, incluso il processo antivirus. Interessante è anche l'effetto che si verifica quando si termina un processo con pid 1 (launchctl) - panico del kernel quando si tenta di elaborare il segnale SIGKILL :)

Come proteggere i processi e le estensioni del kernel su macOS

Come risolvere il problema?

Il modo più semplice per evitare che un processo venga terminato è sostituire il puntatore alla funzione nella tabella delle chiamate di sistema. Sfortunatamente, questo metodo non è banale per molte ragioni.

Innanzitutto, il simbolo che controlla la posizione della memoria di sysent non solo è privato del simbolo del kernel XNU, ma non può essere trovato nei simboli del kernel. Dovrai utilizzare metodi di ricerca euristici, come disassemblare dinamicamente la funzione e cercare un puntatore al suo interno.

In secondo luogo, la struttura delle voci nella tabella dipende dai flag con cui è stato compilato il kernel. Se viene dichiarato il flag CONFIG_REQUIRES_U32_MUNGING, la dimensione della struttura verrà modificata - verrà aggiunto un campo aggiuntivo sy_arg_munge32. È necessario effettuare un controllo aggiuntivo per determinare con quale flag è stato compilato il kernel o, in alternativa, controllare i puntatori a funzione rispetto a quelli conosciuti.

struct sysent {         /* system call table */
        sy_call_t       *sy_call;       /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
        int32_t         sy_return_type; /* system call return types */
        int16_t         sy_narg;        /* number of args */
        uint16_t        sy_arg_bytes;   /* Total size of arguments in bytes for
                                         * 32-bit system calls
                                         */
};

Fortunatamente, nelle versioni moderne di macOS, Apple fornisce una nuova API per lavorare con i processi. L'API Endpoint Security consente ai client di autorizzare molte richieste ad altri processi. Pertanto, puoi bloccare qualsiasi segnale ai processi, incluso il segnale SIGKILL, utilizzando 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 può essere registrata una policy MAC, che fornisce un metodo di protezione del segnale (policy proc_check_signal), ma l'API non è ufficialmente supportata.

Protezione dell'estensione del kernel

Oltre a proteggere i processi nel sistema, è necessaria anche la protezione dell'estensione del kernel stessa (kext). macOS fornisce un framework per gli sviluppatori per sviluppare facilmente driver di dispositivo IOKit. Oltre a fornire strumenti per lavorare con i dispositivi, IOKit fornisce metodi per lo stacking dei driver utilizzando istanze di classi C++. Un'applicazione nello spazio utente sarà in grado di "trovare" un'istanza registrata della classe per stabilire una relazione kernel-spazio utente.

Per rilevare il numero di istanze di classe nel sistema, esiste l'utilità ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Qualsiasi estensione del kernel che desideri registrarsi con lo stack dei driver deve dichiarare una classe che eredita da IOService, ad esempio my_kext_ioservice in questo caso. La connessione delle applicazioni utente provoca la creazione di una nuova istanza della classe che eredita da IOUserClient, nell'esempio my_kext_iouserclient.

Quando si tenta di scaricare un driver dal sistema (comando kextunload), viene chiamata la funzione virtuale "bool terminate (IOOptionBits options)". È sufficiente restituire false sulla chiamata da terminare quando si tenta di scaricare per disabilitare kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Unload is not allowed, returning false
    return false;
  }

  return super::terminate(options);
}

Il flag IsUnloadAllowed può essere impostato da IOUserClient durante il caricamento. Quando esiste un limite di download, il comando kextunload restituirà il seguente output:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Can't remove kext my.kext.test; services failed to terminate - 0xe00002c7.
Failed to unload my.kext.test - (iokit/common) unsupported function.

Una protezione simile deve essere eseguita per IOUserClient. Le istanze delle classi possono essere scaricate utilizzando la funzione dello spazio utente IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. È possibile restituire false quando si chiama il comando "terminate" fino a quando l'applicazione in spazio utente "muore", ovvero la funzione "clientDied" non viene chiamata.

Protezione 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 nell'ambito; per noi sono importanti le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD. Il modo più semplice per limitare l'accesso ai file è tramite percorso: utilizziamo l'API "vn_getpath" per ottenere il percorso del file e confrontare il prefisso del percorso. Si noti che per ottimizzare la ridenominazione dei percorsi delle cartelle dei file, il sistema non autorizza l'accesso a ciascun file, ma solo alla cartella stessa che è stata rinominata. È necessario confrontare il percorso principale e limitare KAUTH_VNODE_DELETE per esso.

Come proteggere i processi e le estensioni del kernel su macOS

Lo svantaggio di questo approccio potrebbe essere una prestazione ridotta all'aumentare del numero di prefissi. Per garantire che il confronto non sia uguale a O(prefisso*lunghezza), dove prefisso è il numero di prefissi, lunghezza è la lunghezza della stringa, è possibile utilizzare un automa deterministico a finiti (DFA) costruito dai prefissi.

Consideriamo un metodo per costruire un DFA per un dato insieme di prefissi. Inizializziamo i cursori all'inizio di ciascun prefisso. Se tutti i cursori puntano allo stesso carattere, aumenta ciascun cursore di un carattere e ricorda che la lunghezza della stessa riga è maggiore di uno. Se sono presenti due cursori con simboli diversi, dividere i cursori in gruppi in base al simbolo a cui puntano e ripetere l'algoritmo per ciascun gruppo.

Nel primo caso (tutti i caratteri sotto i cursori sono uguali), otteniamo uno stato DFA che ha una sola transizione lungo la stessa linea. Nel secondo caso, otteniamo una tabella di transizioni di dimensione 256 (numero di caratteri e numero massimo di gruppi) a stati successivi ottenuti chiamando ricorsivamente la funzione.

Diamo un'occhiata a un esempio. Per un insieme di prefissi (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) è possibile ottenere quanto segue DFAE. La figura mostra solo le transizioni che portano ad altri stati; le altre transizioni non saranno definitive.

Come proteggere i processi e le estensioni del kernel su macOS

Quando si attraversano gli stati DKA, potrebbero esserci 3 casi.

  1. Lo stato finale è stato raggiunto: il percorso è protetto, limitiamo le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD
  2. Lo stato finale non è stato raggiunto, ma il percorso è “terminato” (è stato raggiunto il terminatore nullo): il percorso è genitore, è necessario limitare KAUTH_VNODE_DELETE. Tieni presente che se vnode è una cartella, devi aggiungere un '/' alla fine, altrimenti potrebbe limitarlo al file “/foor/bar/t”, che non è corretto.
  3. Lo stato finale non è stato raggiunto, il percorso non è finito. Nessuno dei prefissi corrisponde a questo, non introduciamo restrizioni.

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 viene raggiunto attraverso lo sviluppo del prodotto software Acronis, che chiude le vulnerabilità in cui il sistema operativo stesso è “debole”. D’altra parte, non dovremmo trascurare il rafforzamento degli aspetti di sicurezza che possono essere migliorati dal lato del sistema operativo, soprattutto perché l’eliminazione di tali vulnerabilità aumenta la nostra stessa stabilità come prodotto. La vulnerabilità è stata segnalata al team di sicurezza del prodotto Apple ed è stata risolta in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Come proteggere i processi e le estensioni del kernel su macOS

Tutto ciò può essere fatto solo se la tua utility è stata ufficialmente installata nel kernel. Cioè, non esistono scappatoie per software esterni e indesiderati. Tuttavia, come puoi vedere, anche la protezione di programmi legittimi come antivirus e sistemi di backup richiede lavoro. Ma ora i nuovi prodotti Acronis per macOS avranno una protezione aggiuntiva contro lo scaricamento dal sistema.

Fonte: habr.com

Aggiungi un commento