Kako zaštititi procese i proširenja jezgre u macOS-u

Pozdrav, Habr! Danas bih razgovarao o tome kako zaštititi procese od napada zlonamjernika na macOS-u. Na primjer, to je korisno za antivirusne programe ili sustave za izradu sigurnosnih kopija, pogotovo u svjetlu činjenice da u macOS-u postoji nekoliko načina kako "ubiti" proces. O tome i o metodama zaštite pročitajte u nastavku.

Kako zaštititi procese i proširenja jezgre u macOS-u

Klasičan način "ubijanja" procesa

Svima poznati način "ubijanja" procesa je slanje SIGKILL signala procesu. Kroz bash možete pozvati standardne "kill -SIGKILL PID" ili "pkill -9 NAME" za ubijanje. Komanda "kill" poznata je još od vremena UNIX-a i dostupna je ne samo u macOS-u, nego i na drugim UNIX-like sustavima.

Također, kao i na UNIX-like sustavima, macOS omogućuje presretanje svih signala prema procesu osim dva — SIGKILL i SIGSTOP. U ovom članku prvenstveno će se razmatrati SIGNAL SIGKILL, kao signal koji uzrokuje ubijanje procesa.

Specifičnost macOS-a

U macOS-u, sustavni poziv kill u XNU kernelu poziva funkciju psignal(SIGKILL,…). Pokušajmo vidjeti koji još korisnički postupci mogu izazvati pozivanje funkcije psignal. Isključit ćemo pozive funkcije psignal unutar mehanizama jezgre (iako oni mogu biti složeni, ostavit ćemo ih za drugi članak 🙂 — provjera potpisa, greške u memoriji, obrada exit/terminate, kršenje zaštite datoteka itd.).

Započnimo pregled s funkcijom i odgovarajućim sustavnim pozivom terminate_with_payload. Vidljivo je da, osim klasičnog poziva kill, postoji alternativni pristup specifičan za operacijski sustav macOS, koji se ne susreće u BSD-u. Principi rada oba sustavna poziva također su slični. Oni predstavljaju izravne pozive funkciji jezgre psignal. Također, obratimo pažnju na to da se prije ubijanja procesa provodi provjera "cansignal" – može li proces poslati signal drugom procesu, sustav ne dopušta nikakvoj aplikaciji da ubija sustavne procese.

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() će postaviti SIGKILL na određenu nit ili
		 * će se vratiti ako je nit i/ili zadatak već u postupku završavanja. U svakom slučaju,
		 * trenutna nit se neće vratiti u korisnički prostor.
		 *
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

launchd

Standardni način kreiranja demona prilikom pokretanja sistema i kontroliranja njihovog vremena trajanja je launchd. Napominjem da su izvorni kodovi u prikazu starije verzije launchctl pre macOS-a 10.10, a primeri koda su dati kao ilustracija. Savremeni launchctl šalje signale launchd-u putem XPC-a, a logika launchctl je prebačena na njega.

Pogledajmo kako se zapravo zaustavljaju aplikacije. Pre slanja signala SIGTERM, aplikacija se pokušava zaustaviti pomoću sistemskog poziva "proc_terminate".

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Nije moguće prekinuti posao: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Koristim alternativnu opciju za prekinuti posao...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Nije moguće poslati signal poslu: %d: %s", error, strerror(error));
		}
...

Ispod haube, proc_terminate, uprkos svom nazivu, može slati ne samo psignal sa SIGTERM, već i SIGKILL.

Neizravno ubijanje — ograničenje na resurse

Zanimljiviji slučaj može se videti u drugom sistemskom pozivu process_policy. Standardna upotreba ovog sistemskog poziva — ograničavanje resursa aplikacija, na primer, za indeksator ograničenje na kvotu procesorskog vremena i memorije, kako se sistem ne bi značajno usporio usled radnji keširanja datoteka. Ako aplikacija dostigne ograničenje resursa, kao što se može videti iz funkcije proc_apply_resource_actions, procesu se šalje signal SIGKILL.

Iako ovaj sistemski poziv može potencijalno izvršiti ubijanje procesa, sistem nije adekvatno proveravao prava procesa koji poziva sistemski poziv. U stvarnosti provera je postojala, ali dovoljna je alternativna zastavica PROC_POLICY_ACTION_SET za zaobilaženje ovog uslova.

Ako se ovdje "ograniči" kvota korištenja CPU-a aplikacijom (primjerice, omogućiti da se izvršava samo 1 ns), moguće je ubiti bilo koji proces u sustavu. Na taj način, zlonamjerni program može ubiti bilo koji proces u sustavu, uključujući i proces antivirusnog programa. Također je zanimljiv efekt koji se ostvaruje prilikom ubijanja procesa s pid 1 (launchctl) — kernel panic prilikom pokušaja obrade signala SIGKILL 🙂

Kako zaštititi procese i proširenja jezgre u macOS-u

Kako riješiti problem?

Najizravniji način da se zabraniti ubijanje procesa je zamjena pokazivača na funkciju u tablici sustavnih poziva. Nažalost, ova metoda nije trivijalna iz mnogih razloga.

Prvo, simbol koji odgovara za poziciju sysent u memoriji nije samo privatni simbol XNU jezgre, već ga nije moguće pronaći u simbolima jezgre. Morat ćemo koristiti heurističke metode traženja, poput dinamičkog disasembliiranja funkcije i pretraživanja pokazivača u njoj.

Drugo, struktura zapisa u tablici ovisi o flagovima s kojima je jezgra sastavljena. Ako je deklariran flag CONFIG_REQUIRES_U32_MUNGING, tada će veličina strukture biti promijenjena — dodano je dodatno polje. sy_arg_munge32. Potrebno je provesti dodatnu provjeru na tome s kojim je flagom jezgra kompilirana, kao alternativa uporediti pokazivače na funkcije s poznatim.

struct sysent {         
        /* tablica sistemskih poziva */
        sy_call_t       *sy_call;       /* implementirajuća funkcija */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* korisnici argumenata sistemskih poziva za 32-bitne procese */
#endif
        int32_t         sy_return_type; /* tipovi povratka sistema poziva */
        int16_t         sy_narg;        /* broj argumenata */
        uint16_t        sy_arg_bytes;   /* Ukupna veličina argumenata u bajtovima za 32-bitne sistemske pozive */
};

Srećom, u modernim verzijama macOS-a, Apple pruža novo API za rad s procesima. Endpoint Security API omogućava korisnicima autoriziranje mnogih zahtjeva prema drugim procesima. Na taj način, moguće je blokirati sve signale prema procesima, uključujući signal SIGKILL pomoću ranije spomenutog API-ja.

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

Slično tome, u jezgri se može registrirati MAC Policy, koja pruža metodu zaštite od signala (policy proc_check_signal), no API se službeno ne podržava.

Zaštita proširenja jezgre

Osim zaštite procesa u sustavu, nužna je i zaštita samog proširenja jezgre (kext). macOS pruža programerima okvir za jednostavni razvoj IOKit uređajnih upravljača. Osim donošenja sredstava za rad s uređajima, IOKit osigurava metode za stakiranje upravljača (driver stacking) putem instanci C++ klasa. Aplikacija u userspace-u će moći “pronaći” registriranu instancu klase kako bi uspostavila vezu između kernela i userspace-a.

Za otkrivanje broja instanci klasa u sustavu postoji alat ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Svako proširenje jezgre koje želi registrirati se u stog upravljača mora izjaviti klasu naslijeđenu od IOService, na primjer, my_kext_ioservice u ovom slučaju. Povezivanje korisničkih aplikacija uzrokuje stvaranje nove instance klase koja nasljeđuje IOUserClient, u primjeru my_kext_iouserclient.

Prilikom pokušaja isključivanja upravljača iz sustava (komanda kextunload) poziva se virtualna funkcija “bool terminate(IOOptionBits options)”. Dovoljno je vratiti false na pozivu funkcije terminate prilikom pokušaja isključivanja kako bi se zabranio kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Isključenje nije dozvoljeno, vraćanje false
    return false;
  }

  return super::terminate(options);
}

Zastavica IsUnloadAllowed može biti postavljena od strane IOUserClient tijekom učitavanja. Ako je isključenje ograničeno, komanda kextunload će vratiti sljedeći izlaz:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Lozinka:
(kernel) Ne mogu ukloniti kext my.kext.test; usluge nisu uspjele prekinuti - 0xe00002c7.
Neuspješno isključivanje my.kext.test - (iokit/common) nepodržana funkcija.

Sličnu zaštitu potrebno je provesti i za IOUserClient. Instance klasa mogu se isključiti pomoću userspace funkcije IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Može se vratiti false na pozivu komande “terminate” dok korisnička aplikacija ne “umre”, tj. dok ne dođe do poziva funkcije “clientDied”.

Zaštita datoteka

Za zaštitu datoteka dovoljno je koristiti Kauth API, koji omogućava ograničavanje pristupa datotekama. Apple pruža programerima obavijesti o raznim događajima unutar scope-a, a za nas su važne operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD. Ograničavanje pristupa datotekama najjednostavnije je po putu — koristimo API “vn_getpath” za dobivanje puta do datoteke i obavljamo usporedbu prefiksa puta. Primijetimo da, radi optimizacije preimenovanja putanja mapa s datotekama, sustav ne autorizira pristup svakoj datoteci, već samo samoj mapi koja je preimenovana. Potrebno je provesti usporedbu roditeljskog puta i ograničiti KAUTH_VNODE_DELETE za nju.

Kako zaštititi procese i proširenja jezgre u macOS-u

Nedostatak ovog pristupa može biti niska učinkovitost s porastom broja prefiksa. Da bi usporedba bila manje od O(prefix*length), gdje je prefix — broj prefiksa, a length — duljina niza, možemo koristiti deterministički konačni automat (DKA) izgrađen na osnovu prefiksa.

Razmotrimo način izgradnje DKA za ovaj skup prefiksa. Inicijaliziramo kursor na početak svakog prefiksa. Ako svi kursori pokazuju na isti simbol, povećavamo svaki kursor za jedan simbol i bilježimo da je duljina istog niza veća za jedan. Ako postoji dva kursora, simbole ispod kojih su različiti, dijelimo kursore u grupe prema simbolu na koji pokazuju i ponavljamo algoritam za svaku grupu.

U prvom slučaju (svi simboli ispod kursora su isti) dobivamo stanje DKA koje ima samo jedan prijelaz po istom nizu. U drugom slučaju, dobivamo tablicu prijelaza veličine 256 (broj simbola i maksimalan broj grupa) u sljedeća stanja koja se dobivaju rekurzivnim pozivom funkcije.

Razmotrimo primjer. Za skup prefiksa (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) možemo dobiti sljedeći DKA. Na slici su prikazani samo prijelazi koji vode u druga stanja, drugi prijelazi ne će biti završni.

Kako zaštititi procese i proširenja jezgre u macOS-u

Pri prolasku kroz stanja DKA mogu se javiti 3 slučaja.

  1. Postignuto je završno stanje — putanja je zaštićena, ograničavamo operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD.
  2. Nije postignuto konačno stanje, ali je put "završio" (postignut je nulti terminator) — put je roditeljski, potrebno je ograničiti KAUTH_VNODE_DELETE. Napominjemo da, ako je vnode direktorij, treba dodati ‘/’ na kraj, inače može doći do ograničenja na datoteku "/foor/bar/t", što nije ispravno.
  3. Nije postignuto konačno stanje, put nije završio. Nijedan od prefiksa se ne poklapa sa ovim, ne postavljamo ograničenja.

Zaključak

Cilj razvijanih sigurnosnih rešenja je povećanje nivoa sigurnosti korisnika i njegovih podataka. S jedne strane, ovaj cilj se ostvaruje razvojem softverskog proizvoda Acronis, koji zatvara one ranjivosti gde je sama operativna sistema "slaba". S druge strane, ne treba zanemariti ni jačanje onih aspekata sigurnosti koje možemo poboljšati na strani OS, pogotovo što zatvaranje takvih ranjivosti povećava našu vlastitu otpornost kao proizvoda. Ranjivost je prijavljena Apple Product Security Team-u i ispravljena je u macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Kako zaštititi procese i proširenja jezgre u macOS-u

Sve to se može učiniti samo ako je vaša alatka zvanično instalirana u jezgru. Drugim rečima, za eksterni i neželjeni softver nema takvih rupa. Međutim, kao što vidite, čak i za zaštitu legitimnih programa, poput antivirusnog softvera i sistema za pravljenje rezervnih kopija, potrebno je dosta truda. No, sada će novi Acronis proizvodi za macOS imati dodatnu zaštitu od izražavanja iz sistema.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster