Cum să protejăm procesele și extensiile nucleului în macOS

Bună, Habr! Astăzi aș dori să discut despre cum putem proteja procesele de atacurile infractorilor în macOS. De exemplu, acest lucru este util pentru un antivirus sau un sistem de backup, mai ales având în vedere că în macOS există mai multe modalități de a „omoara” un proces. Citiți despre asta și metodele de protecție în continuare.

Cum să protejăm procesele și extensiile nucleului în macOS

Metoda clasică de a „omoara” un proces

Metoda cunoscută de a „omoara” un proces este să trimiteți un semnal SIGKILL procesului. Prin bash, puteți apela comenzile standard „kill -SIGKILL PID” sau „pkill -9 NAME” pentru a-l elimina. Comanda „kill” este cunoscută încă din vremurile UNIX și este disponibilă nu doar în macOS, ci și în alte sisteme de tip UNIX.

La fel ca în sistemele UNIX-like, macOS permite interceptarea oricărui semnal către un proces, cu două excepții — SIGKILL și SIGSTOP. În această articolă, vom examina în principal semnalul SIGKILL, ca semnal care inițiază eliminarea procesului.

Specificațiile macOS

În macOS, apelul de sistem kill din nucleul XNU invocă funcția psignal(SIGKILL,…). Haideți să vedem ce alte acțiuni ale utilizatorului în userspace pot invoca funcția psignal. Vom exclude apelurile către funcția psignal din mecanismele interne ale nucleului (deși acestea pot fi neobișnuite, le vom lăsa pentru un alt articol 🙂 — verificarea semnăturii, erorile de memorie, gestionarea exit/terminate, încălcarea protecției fișierelor etc.).

Vom începe recenzia cu funcția și apelul de sistem corespunzător terminate_with_payload. Se observă că, pe lângă apelul clasic kill, există o abordare alternativă, care este specifică sistemului de operare macOS și nu întâlnită în BSD. Principiile de funcționare ale ambelor apeluri de sistem sunt, de asemenea, apropiate. Acestea reprezintă apeluri directe ale funcției nucleului psignal. De asemenea, observăm că înainte de a omorî un proces se efectuează o verificare „cansignal” – poate procesul să trimită un semnal către alt proces, sistemul nu permite oricărei aplicații să omoare procesele de sistem, de exemplu.

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() va părăsi un SIGKILL pe firul specificat sau
		 * va returna dacă firul și/sau sarcina sunt deja în proces de terminare. În orice caz, firul
		 * curent nu se va întoarce în utilizator.
		 * */
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

launchd

Metoda standard de a crea demoni la pornirea sistemului și de a controla timpul lor de viață este launchd. Voi sublinia că sursele prezentate sunt pentru o versiune veche de launchctl, înainte de macOS 10.10, exemplele de cod fiind oferite ca ilustrație. Launchctl-ul modern trimite semnalele launchd prin XPC, iar logica launchctl a fost mutată în acesta.

Să analizăm cum se opresc aplicațiile. Înainte de a trimite semnalul SIGTERM, aplicația este încercată să fie oprită prin apelul de sistem „proc_terminate”.

<launchctl src/core.c>
...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Nu am putut termina sarcina: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Folosind opțiunea de rezervă pentru a termina sarcina...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Nu am putut semnaliza sarcina: %d: %s", error, strerror(error));
		} 
...
<>

Sub capotă, proc_terminate, în ciuda numelui său, poate trimite nu doar psignal cu SIGTERM, ci și SIGKILL.

Uciderea indirectă — restricții privind resursele

O situație mai interesantă poate fi observată într-un alt apel de sistem process_policy. Utilizarea standard a acestui apel de sistem este restricționarea resurselor aplicațiilor, de exemplu, pentru un indexer restricționarea la cotele de timp de procesor și memorie, pentru a evita încetinirea semnificativă a sistemului datorită acțiunilor de stocare a fișierelor. Dacă aplicația a atins limita resurselor, așa cum se poate observa din funcția proc_apply_resource_actions, procesului i se trimite semnalul SIGKILL.

În ciuda faptului că acest apel de sistem poate produce potențial uciderea unui proces, sistemul nu verifica adecvat drepturile procesului care efectuează apelul de sistem. În realitate, verificarea exista, dar este suficient să folosești un flag alternativ PROC_POLICY_ACTION_SET pentru a o ocoli.

De aici, dacă „restricționăm” cota de utilizare a CPU-ului de către aplicație (de exemplu, permițându-i să funcționeze doar 1 ns), putem ucide orice proces din sistem. Astfel, un malware poate omorî orice proces din sistem, inclusiv procesul antivirus. De asemenea, merită menționat efectul care apare în urma uciderii procesului cu pid 1 (launchctl) — panică de nucleu în încercarea de a procesa semnalul SIGKILL 🙂

Cum să protejăm procesele și extensiile nucleului în macOS

Cum rezolvăm problema?

Cel mai direct mod de a interzice uciderea unui proces este de a înlocui indicatorul funcției din tabela apelurilor de sistem. Din păcate, această abordare este netrivială din multe puncte de vedere.

În primul rând, simbolul care răspunde de poziția sysent în memorie nu doar că este un simbol privat al nucleului XNU, dar nu poate fi găsit în simbolurile nucleului. Va trebui să utilizăm metode euristice de căutare, cum ar fi dezasmblarea dinamică a funcției și căutarea indicatorului în interiorul acesteia.

În al doilea rând, structura înregistrărilor din tabel depinde de flag-urile cu care a fost compilat nucleul. Dacă flag-ul CONFIG_REQUIRES_U32_MUNGING este declarat, atunci dimensiunea structurii va fi modificată — va fi adăugat un câmp suplimentar. sy_arg_munge32. Este necesar să se efectueze o verificare suplimentară a flag-ului cu care a fost compilat nucleul, ca opțiune comparând indicatorii funcțiilor cu cei cunoscuți.

struct sysent {         
        /* tabela apelurilor de sistem */
        sy_call_t       *sy_call;       /* funcția care implementează */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* munger pentru argumentele apelului de sistem pentru procesele de 32 de biți */
#endif
        int32_t         sy_return_type; /* tipurile de returnare ale apelului de sistem */
        int16_t         sy_narg;        /* numărul de argumente */
        uint16_t        sy_arg_bytes;   /* Dimensiunea totală a argumentelor în octeți pentru
                                         * apeluri de sistem de 32 de biți */
};

Din fericire, în versiunile moderne de macOS, Apple oferă un nou API pentru gestionarea proceselor. Endpoint Security API permite clienților să autorizeze multe solicitări către alte procese. Astfel, se pot bloca orice semnale către procese, inclusiv semnalul SIGKILL prin intermediul API-ului menționat anterior.

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

În mod similar, în nucleu poate fi înregistrată o Politică MAC, care oferă o metodă de protecție împotriva semnalelor (policy proc_check_signal), totuși API-ul nu este susținut oficial.

Protecția extensiei nucleului

Pe lângă protecția proceselor din sistem, este esențială și protecția extinderii nucleului în sine (kext). macOS oferă dezvoltatorilor un cadru pentru dezvoltarea ușoară a drivere-lor pentru dispozitive IOKit. În afară de furnizarea de instrumente pentru lucrul cu dispozitivele, IOKit asigură metode pentru stivuirea driverelor (driver stacking) prin intermediul instanțelor de clase C++. Aplicația din userspace va putea „găsi” o instanță de clasă înregistrată pentru a stabili o legătură între kernel și userspace.

Pentru a detecta numărul de instanțe de clase în sistem, există utilitarul ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Orice extensie a nucleului care dorește să se înregistreze în stiva driverelor trebuie să declare o clasă moștenită din IOService, de exemplu, my_kext_ioservice în acest caz. Conectarea aplicațiilor utilizator generează o nouă instanță a unei clase moștenite din IOUserClient, în exemplul my_kext_iouserclient.

Atunci când se încearcă descărcarea driverului din sistem (comanda kextunload), se apelează funcția virtuală „bool terminate(IOOptionBits options)”. Este suficient să returnezi false la apelul funcției terminate pentru a interzice descărcarea cu kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Descărcarea nu este permisă, returnând false
    return false;
  }

  return super::terminate(options);
}

Flagul IsUnloadAllowed poate fi setat de IOUserClient la încărcare. În cazul restricției la descărcare, comanda kextunload va returna următorul mesaj:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Parola:
(kernel) Nu pot elimina kext my.kext.test; serviciile nu au reușit să se termine - 0xe00002c7.
Eșec la descărcarea my.kext.test - (iokit/common) funcție nesuportată.

O protecție similară trebuie aplicată și pentru IOUserClient. Instanțele de clase pot fi descărcate cu ajutorul funcției din userspace IOKitLib „IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Se poate returna false la apelul comenzii „terminate” până când aplicația din userspace nu „moare”, adică până nu se apelează funcția „clientDied”.

Protecția fișierelor

Pentru protecția fișierelor, este suficient să utilizăm Kauth API, care permite restricționarea accesului la fișiere. Apple oferă dezvoltatorilor notificări despre diverse evenimente în scope, iar pentru noi sunt importante operațiile KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA și KAUTH_VNODE_DELETE_CHILD. Restricționarea accesului la fișiere este cel mai ușor de realizat prin intermediul căii — folosim API-ul “vn_getpath” pentru a obține calea către fișier și efectuăm comparația prefixului căii. Observăm că, pentru optimizarea redenumirii căilor folderelor cu fișiere, sistemul nu autorizează accesul la fiecare fișier, ci doar la folderul în sine, care a fost redenumit. Este necesar să facem comparația căii părinte și să restricționăm KAUTH_VNODE_DELETE pentru acesta.

Cum să protejăm procesele și extensiile nucleului în macOS

Un dezavantaj al acestei abordări poate fi performanța scăzută odată cu creșterea numărului de prefixe. Pentru a evita ca comparația să fie O(prefix*length), unde prefix este numărul de prefixe, iar length este lungimea șirului, putem folosi un automat finit determinist (DFA) construit pe baza prefixelor.

Să analizăm metoda de construire a DFA-ului pentru acest set de prefixe. Inițializăm cursori la începutul fiecărui prefix. Dacă toți cursori indică același simbol, atunci mărim fiecare cursor cu un simbol și reținem că lungimea șirului identic este mai mare cu unu. Dacă există doi cursori, simbolurile sub care se află sunt diferite, împărțim cursori în grupuri în funcție de simbolul pe care îl indică și repetăm algoritmul pentru fiecare grup.

În primul caz (toate simbolurile de sub cursori sunt identice) obținem un stadiu DFA, care are doar o singură tranziție pe un șir identic. În al doilea caz, obținem o tabelă de tranziții de dimensiuni 256 (numărul simbolurilor și numărul maxim de grupuri) către stările următoare, obținute prin apeluri recursive ale funcției.

Să luăm un exemplu. Pentru setul de prefixe (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) putem obține următorul DFA. În desen sunt indicate doar tranzițiile care duc către alte stări, celelalte tranziții nu vor fi considerată finale.

Cum să protejăm procesele și extensiile nucleului în macOS

În timpul parcurgerii stărilor DFA-ului, pot exista 3 cazuri.

  1. A fost atins stadiul final — calea este protejată, restricționăm operațiile KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA și KAUTH_VNODE_DELETE_CHILD.
  2. Starea finală nu a fost atinsă, dar calea „s-a terminat” (a fost atins un terminator zero) — calea este părinte, trebuie limitat KAUTH_VNODE_DELETE. Observăm că, dacă vnode este un director, trebuie să adăugăm la sfârșit ‘/’, altfel poate fi aplicată o limitare la fișierul “/foor/bar/t”, ceea ce este greșit.
  3. Starea finală nu a fost atinsă, calea nu s-a sfârșit. Niciunul dintre prefixe nu corespunde acestuia, nu introducem limitări.

Concluzie

Scopul soluțiilor de securitate dezvoltate este de a spori nivelul de securitate al utilizatorului și al datelor sale. Pe de o parte, acest scop este asigurat prin dezvoltarea produsului software Acronis, care închide acele vulnerabilități în care sistemul de operare este „slab”. Pe de altă parte, nu trebuie neglijate aspectele de securitate care pot fi îmbunătățite din partea OS, mai ales că închiderea unor astfel de vulnerabilități crește propria noastră rezistență ca produs. Vulnerabilitatea a fost raportată echipei de securitate a produselor Apple și a fost corectată în macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Cum să protejăm procesele și extensiile nucleului în macOS

Toate acestea pot fi realizate doar dacă utilitarul dvs. a fost instalat oficial în kernel. Cu alte cuvinte, pentru software-ul extern și nedorit nu există astfel de portițe. Cu toate acestea, după cum puteți vedea, chiar și pentru protecția programelor legitime, cum ar fi antivirusele și sistemele de backup, este nevoie de efort. Dar, acum, noile produse Acronis pentru macOS vor avea o protecție suplimentară împotriva descărcării din sistem.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster