Come proteggere i processi e le estensioni del kernel in macOS

Ciao, Habr! Oggi vorrei parlare di come proteggere i processi da attacchi malevoli su macOS. Questo è utile ad esempio per gli antivirus o i sistemi di backup, specialmente considerando che su macOS ci sono diversi modi per 'terminare' un processo. Leggi di più sui metodi di protezione qui sotto.

Come proteggere i processi e le estensioni del kernel in macOS

Metodo classico per 'terminare' un processo

Un metodo ben noto per 'terminare' un processo è inviare un segnale SIGKILL al processo. Tramite bash, puoi utilizzare i comandi standard 'kill -SIGKILL PID' o 'pkill -9 NAME' per terminare. Il comando 'kill' è conosciuto fin dai tempi di UNIX ed è disponibile non solo su macOS, ma anche su altri sistemi UNIX-like.

Così come nei sistemi UNIX-like, macOS consente di intercettare qualsiasi segnale per un processo, tranne due: SIGKILL e SIGSTOP. In questo articolo si parlerà principalmente del segnale SIGKILL, che provoca la terminazione del processo.

Specificità di macOS

In macOS, the system call kill in the XNU kernel invokes the psignal function(SIGKILL,…). Let's explore what other user actions in userspace can trigger the psignal function. We will exclude calls to the psignal function in the internal mechanisms of the kernel (although they may also be non-trivial, we will leave them for another article 🙂 — signature checks, memory errors, handling exit/terminate, file protection violations, etc.).

Let's start the review with the function and the corresponding system call terminate_with_payload. It is evident that besides the classic kill call, there is an alternative approach that is specific to the macOS operating system and does not occur in BSD. The principles of operation for both system calls are also similar. They directly invoke the kernel function psignal. We also note that before killing a process, a check is made to see if the process can send a signal to another process; the system does not allow any application to terminate system processes, for example.

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() sospende un SIGKILL sul thread specificato o
		 * restituisce se il thread e/o il task stanno già terminando. In entrambi i casi, il
		 * thread corrente non tornerà nello spazio degli utenti.
		 */
		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 sorgenti forniti sono per una vecchia versione di launchctl precedente a macOS 10.10, e gli esempi di codice sono forniti a scopo illustrativo. L'attuale launchctl invia segnali a launchd tramite XPC, mentre la logica di launchctl è stata trasferita in esso.

Esaminiamo come avviene esattamente l'arresto delle applicazioni. Prima di inviare il segnale SIGTERM, l'applicazione viene tentata di essere arrestata tramite la chiamata di sistema “proc_terminate”.

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Impossibile terminare il job: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Utilizzo dell'opzione di fallback per terminare il job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Impossibile segnalare il 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 - limitazione delle risorse

Un caso più interessante può essere visto in un'altra chiamata di sistema process_policy. L'uso standard di questa chiamata di sistema è la limitazione delle risorse delle applicazioni, ad esempio per un indicizzatore limitare il tempo di CPU e la memoria, affinché il sistema non venga rallentato in modo significativo dalle azioni di caching dei file. Se l'applicazione ha raggiunto 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 produrre l'uccisione di un processo, il sistema non ha controllato adeguatamente i diritti del processo che effettua la chiamata di sistema. In realtà, il controllo esisteva, ma è sufficiente utilizzare il flag alternativo PROC_POLICY_ACTION_SET per aggirare questa condizione.

Di conseguenza, se si "limita" la quota di utilizzo della CPU da parte di un'applicazione (ad esempio, consentendo l'esecuzione di solo 1 ns), allora si può terminare qualsiasi processo nel sistema. In questo modo, un malware può terminare qualsiasi processo nel sistema, incluso il processo antivirus. Interessante è anche l'effetto che si ottiene terminando il processo con pid 1 (launchctl) — kernel panic quando si tenta di gestire il segnale SIGKILL 🙂

Come proteggere i processi e le estensioni del kernel in macOS

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 corrisponde alla posizione di sysent in memoria non è solo un simbolo privato del kernel XNU, ma non può essere trovato nei simboli del kernel. Dovremo utilizzare metodi euristici di ricerca, come la disassemblaggio dinamico della funzione e la ricerca del puntatore in essa.

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. sy_arg_munge32. È necessario effettuare un'ulteriore verifica su quale flag è stato utilizzato per compilare il kernel, ad esempio confrontando i puntatori delle 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; /* modificatore 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'API di Endpoint Security consente ai client di autorizzare molte richieste ad altri processi. In questo modo, è possibile bloccare segnali a processi, incluso il segnale SIGKILL, utilizzando l'API sopracitata.

#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), tuttavia l'API non è ufficialmente supportata.

Protezione dell'estensione del kernel

Oltre a proteggere i processi nel sistema, è essenziale proteggere anche l'estensione del kernel (kext). macOS offre agli sviluppatori un framework per facilitare lo sviluppo dei driver di dispositivo IOKit. Oltre a fornire strumenti per interagire con i dispositivi, IOKit offre metodi per la sovrapposizione dei driver (driver stacking) attraverso istanze di classi C++. Un'applicazione nel userspace sarà in grado di "trovare" un'istanza di classe registrata per stabilire una connessione kernel-userspace.

Per scoprire il numero di istanze di classi nel sistema esiste un'utilità chiamata ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Qualsiasi estensione del kernel che desidera registrarsi nello stack dei driver deve dichiarare una classe ereditata da IOService, ad esempio, my_kext_ioservice in questo caso. La connessione di applicazioni utente provoca la creazione di una nuova istanza di classe, che è ereditata 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 alla chiamata della funzione terminate durante il tentativo di scarico per vietare kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    \/\/ Il download non è consentito, restituito false
    return false;
  }

  return super::terminate(options);
}

Il flag IsUnloadAllowed può essere impostato da IOUserClient durante il caricamento. Con un vincolo sul 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.

Una protezione analoga deve essere attuata 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);". Si può restituire false alla chiamata del comando "terminate" finché l'applicazione userspace non "muore", ossia fino a che non viene chiamata la funzione "clientDied".

Protezione dei file

Per proteggere i file, è sufficiente utilizzare il Kauth API, che consente di limitare l'accesso ai file. Apple fornisce agli sviluppatori notifiche su vari eventi nel contesto, e per noi sono rilevanti le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD. Limitare l'accesso ai file è più semplice utilizzando il percorso: usiamo l'API “vn_getpath” per ottenere il percorso del file e confrontiamo il prefisso del percorso. Si noti che, per ottimizzare la rinomina dei percorsi delle cartelle contenenti 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 esso.

Come proteggere i processi e le estensioni del kernel in macOS

Uno svantaggio di questo approccio può essere la bassa performance con un aumento del numero di prefissi. Per evitare che il confronto sia O(prefix*length), dove prefix è il numero di prefissi e length è la lunghezza della stringa, si può utilizzare un automa a stati finiti deterministico (DFA) costruito sui prefissi.

Consideriamo un metodo per costruire un DFA per questo insieme di prefissi. Iniziamo le corsie all'inizio di ciascun prefisso. Se tutte le corsie puntano allo stesso simbolo, incrementiamo ciascuna corsia di un simbolo e ricordiamo che la lunghezza della stringa identica aumenta di uno. Se ci sono due corsie con simboli diversi, dividiamo le corsie in gruppi in base al simbolo su cui puntano e ripetiamo l'algoritmo per ogni gruppo.

Nel primo caso (quando tutti i simboli sotto le corsie sono uguali), otteniamo uno stato del DFA che ha solo una transizione per la stringa identica. Nel secondo caso, otteniamo una tabella di transizioni di dimensioni 256 (numero di simboli e massimo numero di gruppi) per gli stati successivi, ottenuti mediante la chiamata ricorsiva della funzione.

Prendiamo un esempio. Per l'insieme di prefissi (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) possiamo ottenere il seguente DFA. Nella figura sono indicate solo le transizioni che portano ad altri stati; le altre transizioni non saranno finali.

Come proteggere i processi e le estensioni del kernel in macOS

Durante il passaggio attraverso gli stati del DFA possono verificarsi 3 casi.

  1. È stato raggiunto lo stato finale: il percorso è protetto, limitando le operazioni KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD.
  2. Non è stato raggiunto lo stato finale, ma il percorso è "terminato" (è stato raggiunto il terminatore zero): il percorso è genitore e KAUTH_VNODE_DELETE deve essere limitato. Si noti che se il vnode è una cartella, è necessario aggiungere ‘/’ alla fine; altrimenti, potrebbe essere limitato al file "/foor/bar/t", il che è errato.
  3. Non è stato raggiunto lo 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 quelle vulnerabilità in cui il sistema operativo è "debole". D'altro canto, non si dovrebbe trascurare il rafforzamento degli aspetti di sicurezza che possono essere migliorati a livello di OS, soprattutto considerando che la chiusura di tali vulnerabilità aumenta la nostra resilienza come prodotto. La vulnerabilità è stata segnalata dal team di sicurezza dei prodotti Apple ed è stata corretta in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Come proteggere i processi e le estensioni del kernel in macOS

Tutto ciò può essere fatto solo se la vostra utility è stata ufficialmente installata nel kernel. Questo significa che non ci sono scorciatoie per software esterni e indesiderati. Tuttavia, come potete vedere, anche per proteggere programmi legittimi come gli antivirus e i sistemi di backup bisogna lavorare. Ma ora i nuovi prodotti Acronis per macOS avranno una protezione aggiuntiva contro l'uscita dal sistema.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster