Kako zaštititi procese i proširenja kernela na macOS-u

Pozdrav, Habr! Danas bih želio razgovarati o tome kako možete zaštititi procese od napada napadača u macOS-u. Na primjer, ovo je korisno za antivirusni ili backup sustav, pogotovo jer pod macOS-om postoji nekoliko načina da se "ubije" proces. Pročitajte o ovome i metodama zaštite ispod reza.

Kako zaštititi procese i proširenja kernela na macOS-u

Klasičan način "ubijanja" procesa

Dobro poznati način "ubijanja" procesa je slanje SIGKILL signala procesu. Kroz bash možete pozvati standardni "kill -SIGKILL PID" ili "pkill -9 NAME" za ubijanje. Naredba “kill” poznata je još od dana UNIX-a i dostupna je ne samo na macOS-u, već i na drugim sustavima sličnim UNIX-u.

Baš kao u sustavima sličnim UNIX-u, macOS vam omogućuje presretanje bilo kojeg signala procesu osim dva - SIGKILL i SIGSTOP. Ovaj članak će se prvenstveno fokusirati na SIGKILL signal kao signal koji uzrokuje gašenje procesa.

specifičnosti macOS-a

Na macOS-u, sistemski poziv kill u jezgri XNU poziva funkciju psignal(SIGKILL,...). Pokušajmo vidjeti koje druge korisničke radnje u korisničkom prostoru može pozvati funkcija psignal. Uklonimo pozive funkcije psignal u internim mehanizmima kernela (iako možda nisu trivijalni, ostavit ćemo ih za drugi članak 🙂 - provjera potpisa, pogreške u memoriji, rukovanje izlazom/završetkom, kršenja zaštite datoteka itd. .

Započnimo pregled s funkcijom i odgovarajućim pozivom sustava prekinuti_s_korisnim teretom. Vidljivo je da osim klasičnog kill call-a postoji i alternativni pristup koji je specifičan za macOS operativni sustav i nema ga u BSD-u. Načela rada obaju sistemskih poziva također su slična. Oni su izravni pozivi funkciji jezgre psignal. Također imajte na umu da se prije ukidanja procesa provodi provjera "cansignal" - može li proces poslati signal drugom procesu; sustav ne dopušta nijednoj aplikaciji da ubije sistemske procese, na primjer.

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);
	}
...
}

lansirand

Standardni način za stvaranje demona pri pokretanju sustava i kontrolu njihovog trajanja je launchd. Imajte na umu da su izvori za staru verziju launchctl-a do macOS 10.10, primjeri koda navedeni su u ilustrativne svrhe. Moderni launchctl šalje launchd signale preko XPC-a, launchctl logika je premještena na njega.

Pogledajmo kako se točno zaustavljaju aplikacije. Prije slanja signala SIGTERM, aplikacija se pokušava zaustaviti pomoću sistemskog poziva “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));
		} 
...
<>

Ispod haube, proc_terminate, unatoč svom nazivu, može poslati ne samo psignal sa SIGTERM, već i SIGKILL.

Indirektno ubijanje - ograničenje resursa

Zanimljiviji slučaj može se vidjeti u drugom pozivu sustava politika_procesa. Uobičajena upotreba ovog sistemskog poziva je ograničavanje resursa aplikacije, kao što je indeksiranje za ograničavanje CPU vremena i memorijskih kvota tako da sustav nije značajno usporen aktivnostima predmemorije datoteka. Ako je aplikacija dosegla ograničenje resursa, kao što se može vidjeti iz funkcije proc_apply_resource_actions, procesu se šalje signal SIGKILL.

Iako bi ovaj sistemski poziv potencijalno mogao ubiti proces, sustav nije adekvatno provjerio prava procesa koji poziva sistemski poziv. Zapravo provjeravam postojao, ali dovoljno je upotrijebiti alternativnu zastavu PROC_POLICY_ACTION_SET da biste zaobišli ovaj uvjet.

Stoga, ako "ograničite" kvotu upotrebe procesora aplikacije (na primjer, dopustite samo 1 ns da se pokrene), tada možete ubiti bilo koji proces u sustavu. Stoga zlonamjerni softver može ubiti bilo koji proces u sustavu, uključujući antivirusni proces. Zanimljiv je i efekt koji se javlja kada se ubije proces s pid 1 (launchctl) - kernel panika kada pokušava obraditi SIGKILL signal :)

Kako zaštititi procese i proširenja kernela na macOS-u

Kako riješiti problem?

Najjednostavniji način za sprječavanje prekida procesa je zamjena pokazivača funkcije u tablici sistemskih poziva. Nažalost, ova metoda nije trivijalna iz mnogo razloga.

Prvo, simbol koji kontrolira memorijsku lokaciju sysenta nije samo privatan za simbol XNU kernela, već se ne može pronaći u simbolima kernela. Morat ćete koristiti heurističke metode pretraživanja, kao što je dinamičko rastavljanje funkcije i traženje pokazivača u njoj.

Drugo, struktura unosa u tablici ovisi o oznakama s kojima je kernel kompajliran. Ako je zastavica CONFIG_REQUIRES_U32_MUNGING deklarirana, veličina strukture će se promijeniti - dodatno polje će biti dodano sy_arg_munge32. Potrebno je izvršiti dodatnu provjeru kako bi se utvrdilo s kojom je zastavom kernel kompajliran ili alternativno provjeriti pokazivače funkcija u odnosu na poznate.

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
                                         */
};

Srećom, u modernim verzijama macOS-a Apple nudi novi API za rad s procesima. Endpoint Security API omogućuje klijentima da autoriziraju mnoge zahtjeve drugim procesima. Stoga možete blokirati sve signale procesima, uključujući signal SIGKILL, koristeći gore spomenuti API.

#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, MAC politika se može registrirati u kernelu, koja pruža metodu zaštite signala (pravilo proc_check_signal), ali API nije službeno podržan.

Zaštita proširenja kernela

Osim zaštite procesa u sustavu, neophodna je i zaštita same kernel ekstenzije (kext). macOS pruža okvir za programere za jednostavan razvoj IOKit upravljačkih programa uređaja. Uz pružanje alata za rad s uređajima, IOKit pruža metode za slaganje upravljačkih programa pomoću instanci C++ klasa. Aplikacija u korisničkom prostoru moći će "pronaći" registriranu instancu klase kako bi uspostavila odnos kernel-korisnički prostor.

Za otkrivanje broja instanci klase u sustavu postoji uslužni program ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

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

Prilikom pokušaja uklanjanja upravljačkog programa iz sustava (kextunload naredba), poziva se virtualna funkcija “bool terminate(IOOptionBits options)”. Dovoljno je vratiti false na poziv za prekid prilikom pokušaja pražnjenja da biste onemogućili kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Oznaku IsUnloadAllowed može postaviti IOUserClient prilikom učitavanja. Kada postoji ograničenje preuzimanja, naredba kextunload vratit će sljedeći izlaz:

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.

Slična zaštita mora se napraviti za IOUserClient. Instance klasa mogu se iskrcati pomoću funkcije korisničkog prostora IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Možete vratiti false prilikom pozivanja naredbe “terminate” dok aplikacija korisničkog prostora ne “umre”, odnosno dok se ne pozove funkcija “clientDied”.

Zaštita datoteka

Za zaštitu datoteka dovoljno je koristiti Kauth API koji omogućuje ograničavanje pristupa datotekama. Apple pruža programerima obavijesti o raznim događajima u opsegu; za nas su važne operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD. Najlakši način za ograničavanje pristupa datotekama je put - koristimo API “vn_getpath” da dobijemo put do datoteke i usporedimo prefiks puta. Imajte na umu da radi optimizacije preimenovanja putova mapa datoteka, sustav ne autorizira pristup svakoj datoteci, već samo samoj mapi koja je preimenovana. Potrebno je usporediti nadređenu stazu i ograničiti KAUTH_VNODE_DELETE za nju.

Kako zaštititi procese i proširenja kernela na macOS-u

Nedostatak ovog pristupa može biti niska izvedba kako se broj prefiksa povećava. Kako biste osigurali da usporedba nije jednaka O(prefiks*duljina), gdje je prefiks broj prefiksa, duljina je duljina niza, možete koristiti deterministički konačni automat (DFA) izgrađen od prefiksa.

Razmotrimo metodu za konstruiranje DFA za dati skup prefiksa. Inicijaliziramo kursore na početku svakog prefiksa. Ako svi kursori pokazuju na isti znak, povećajte svaki kursor za jedan znak i zapamtite da je duljina istog retka veća za jedan. Ako postoje dva kursora s različitim simbolima, podijelite kursore u skupine prema simbolu na koji pokazuju i ponovite algoritam za svaku grupu.

U prvom slučaju (svi znakovi ispod kursora su isti) dobivamo DFA stanje koje ima samo jedan prijelaz duž iste linije. U drugom slučaju dobivamo tablicu prijelaza veličine 256 (broj znakova i maksimalan broj grupa) u naknadna stanja dobivena rekurzivnim pozivom funkcije.

Pogledajmo primjer. Za skup prefiksa (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) možete dobiti sljedeće DFA. Slika prikazuje samo prijelaze koji vode u druga stanja; drugi prijelazi neće biti konačni.

Kako zaštititi procese i proširenja kernela na macOS-u

Kada prolazite kroz DKA države, mogu postojati 3 slučaja.

  1. Završno stanje je postignuto - put je zaštićen, 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” (dostignut je nulti terminator) - put je roditelj, potrebno je ograničiti KAUTH_VNODE_DELETE. Imajte na umu da ako je vnode mapa, morate dodati '/' na kraju, inače bi se mogla ograničiti na datoteku “/foor/bar/t”, što je netočno.
  3. Konačno stanje nije postignuto, put nije završio. Nijedan od prefiksa ne odgovara ovom, ne uvodimo ograničenja.

Zaključak

Cilj sigurnosnih rješenja koja se razvijaju je povećati razinu sigurnosti korisnika i njegovih podataka. S jedne strane, ovaj cilj je postignut razvojem softverskog proizvoda Acronis, koji zatvara one ranjivosti gdje je sam operativni sustav “slab”. S druge strane, ne bismo smjeli zanemariti jačanje onih sigurnosnih aspekata koji se mogu poboljšati na strani OS-a, pogotovo jer zatvaranje takvih ranjivosti povećava vlastitu stabilnost kao proizvoda. Ranjivost je prijavljena Appleovom timu za sigurnost proizvoda i popravljena je u macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Kako zaštititi procese i proširenja kernela na macOS-u

Sve ovo možete učiniti samo ako je vaš uslužni program službeno instaliran u kernel. Odnosno, nema takvih rupa za vanjski i neželjeni softver. Međutim, kao što vidite, čak i zaštita legitimnih programa kao što su antivirusni i sigurnosni sustavi zahtijevaju rad. Ali sada će novi Acronis proizvodi za macOS imati dodatnu zaštitu od pražnjenja iz sustava.

Izvor: www.habr.com

Dodajte komentar