Kako zaštititi procese i ekstenzije kernela na macOS-u

Zdravo, Habr! Danas bih želio govoriti o tome kako možete zaštititi procese od napada napadača u macOS-u. Na primjer, ovo je korisno za antivirusni ili backup sistem, pogotovo jer pod macOS-om postoji nekoliko načina da se "ubije" proces. O tome i načinima zaštite pročitajte ispod.

Kako zaštititi procese i ekstenzije kernela na macOS-u

Klasičan način da se "ubije" proces

Dobro poznati način da se "ubije" proces je slanje SIGKILL signala procesu. Preko bash-a možete pozvati standardni “kill -SIGKILL PID” ili “pkill -9 NAME” za ubijanje. Komanda “kill” poznata je još od vremena UNIX-a i dostupna je ne samo na macOS-u, već i na drugim sistemima sličnim UNIX-u.

Baš kao u sistemima sličnim UNIX-u, macOS vam omogućava da presretnete sve signale procesu osim dva - SIGKILL i SIGSTOP. Ovaj članak će se prvenstveno fokusirati na signal SIGKILL kao signal koji uzrokuje da proces bude prekinut.

macOS specifičnosti

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

Započnimo pregled sa funkcijom i odgovarajućim sistemskim pozivom terminate_with_payload. Vidi se da pored klasičnog poziva kill postoji i alternativni pristup koji je specifičan za macOS operativni sistem i koji se ne nalazi u BSD-u. Principi rada oba sistemska poziva su također slični. Oni su direktni pozivi funkciji kernela psignal. Također imajte na umu da se prije ukidanja procesa vrši provjera "kansignala" - da li proces može poslati signal drugom procesu; sistem ne dozvoljava 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);
	}
...
}

launchd

Pokreće se standardni način kreiranja demona pri pokretanju sistema i kontrole njihovog životnog vijeka. Imajte na umu da su izvori za staru verziju launchctl do macOS 10.10, primjeri koda su dati u ilustrativne svrhe. Moderni launchctl šalje launchd signale preko XPC-a, logika launchctl je premještena na njega.

Pogledajmo kako se tačno aplikacije zaustavljaju. Prije slanja SIGTERM signala, aplikacija se pokušava zaustaviti korištenjem “proc_terminate” sistemskog poziva.

<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, uprkos svom imenu, može poslati ne samo psignal sa SIGTERM-om, već i SIGKILL.

Indirektno ubijanje - ograničenje resursa

Zanimljiviji slučaj se može vidjeti u drugom sistemskom pozivu process_policy. Uobičajena upotreba ovog sistemskog poziva je ograničavanje resursa aplikacije, kao što je indekser da ograniči CPU vrijeme i memorijske kvote tako da sistem ne bude značajno usporen aktivnostima keširanja datoteka. Ako je aplikacija dosegla ograničenje resursa, kao što se može vidjeti iz funkcije proc_apply_resource_actions, SIGKILL signal se šalje procesu.

Iako bi ovaj sistemski poziv potencijalno mogao ubiti proces, sistem nije na adekvatan način provjerio prava procesa koji poziva sistemski poziv. Zapravo provjeravam postojao, ali dovoljno je koristiti alternativnu zastavicu PROC_POLICY_ACTION_SET da zaobiđete ovaj uvjet.

Stoga, ako "ograničite" kvotu korištenja CPU-a aplikacije (na primjer, dozvolite samo 1 ns da se pokrene), tada možete ubiti bilo koji proces u sistemu. Dakle, zlonamjerni softver može ubiti bilo koji proces na sistemu, uključujući i antivirusni proces. Zanimljiv je i efekat koji se javlja kada se proces ubija sa pid 1 (launchctl) - kernel panika pri pokušaju obrade signala SIGKILL :)

Kako zaštititi procese i ekstenzije kernela na macOS-u

Kako riješiti problem?

Najjednostavniji način da spriječite da proces bude ubijen je da zamijenite pokazivač funkcije u tabeli sistemskih poziva. Nažalost, ova metoda nije trivijalna iz mnogo razloga.

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

Drugo, struktura unosa u tabeli zavisi od zastavica sa kojima je kernel kompajliran. Ako je deklarirana zastavica CONFIG_REQUIRES_U32_MUNGING, veličina strukture će biti promijenjena - bit će dodano dodatno polje sy_arg_munge32. Potrebno je izvršiti dodatnu provjeru kako bi se utvrdilo s kojom zastavicom je 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ćava klijentima da ovlaste mnoge zahtjeve drugim procesima. Dakle, 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, MAC politika se može registrovati u kernelu, koja obezbeđuje metod zaštite signala (policy proc_check_signal), ali API nije zvanično podržan.

Zaštita proširenja kernela

Osim zaštite procesa u sistemu, neophodna je i zaštita ekstenzije kernela (kext). macOS pruža okvir za programere da lako razviju IOKit drajvere uređaja. Pored obezbeđivanja alata za rad sa uređajima, IOKit obezbeđuje metode za slaganje drajvera koristeći instance C++ klasa. Aplikacija u korisničkom prostoru će moći da "pronađe" registrovanu instancu klase da uspostavi odnos kernel-korisnički prostor.

Da biste otkrili broj instanci klase u sistemu, postoji uslužni program ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Svaka ekstenzija kernela koja želi da se registruje sa stekom drajvera mora deklarisati klasu koja nasljeđuje od IOService, na primjer my_kext_ioservice u ovom slučaju Povezivanje korisničkih aplikacija uzrokuje kreiranje nove instance klase koja nasljeđuje od IOUserClient, u primjeru my_kext_iouserclient.

Kada pokušavate da skinete drajver iz sistema (komanda kextunload), poziva se virtuelna funkcija “bool terminate(IOOptionBits options)”. Dovoljno je vratiti false na poziv da se prekine kada pokušavate isprazniti 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 će vratiti 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 se mora uraditi za IOUserClient. Instance klasa se mogu isprazniti pomoću funkcije korisničkog prostora IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Možete vratiti false kada pozivate naredbu “terminate” sve dok aplikacija korisničkog prostora “umre”, to jest, funkcija “clientDied” nije pozvana.

Zaštita datoteka

Da biste zaštitili datoteke, dovoljno je koristiti Kauth API, koji vam omogućava da ograničite pristup datotekama. Apple programerima dostavlja 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 da ograničite pristup datotekama je putem putanje - koristimo API “vn_getpath” da dobijemo putanju do datoteke i uporedimo prefiks putanje. Imajte na umu da radi optimizacije preimenovanja putanja fascikle datoteka, sistem ne ovlašćuje pristup svakoj datoteci, već samo fascikli koja je preimenovana. Potrebno je uporediti roditeljski put i ograničiti KAUTH_VNODE_DELETE za njega.

Kako zaštititi procese i ekstenzije kernela na macOS-u

Nedostatak ovog pristupa može biti niske performanse kako se broj prefiksa povećava. Kako biste osigurali da poređenje nije jednako O(prefiks*dužina), gdje je prefiks broj prefiksa, a dužina je dužina niza, možete koristiti deterministički konačni automat (DFA) izgrađen od prefiksa.

Hajde da razmotrimo metodu za konstruisanje 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 dužina istog reda veća za jedan. Ako postoje dva kursora s različitim simbolima, podijelite kursore u grupe prema simbolu na koji pokazuju i ponovite algoritam za svaku grupu.

U prvom slučaju (svi znakovi ispod kursora su isti), dobijamo DFA stanje koje ima samo jedan prelaz duž iste linije. U drugom slučaju dobijamo tabelu prelaza veličine 256 (broj karaktera i maksimalan broj grupa) u naredna stanja dobijena rekurzivnim pozivanjem funkcije.

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

Kako zaštititi procese i ekstenzije kernela na macOS-u

Prilikom prolaska kroz DKA stanja mogu postojati 3 slučaja.

  1. Konačno stanje je dostignuto - put je zaštićen, ograničavamo operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD
  2. Završno stanje nije dostignuto, ali je putanja “završila” (dosegnut je nulti terminator) - putanja je roditelj, potrebno je ograničiti KAUTH_VNODE_DELETE. Imajte na umu da ako je vnode mapa, morate dodati '/' na kraju, inače ga može ograničiti na datoteku “/foor/bar/t”, što je netačno.
  3. Nije postignuto konačno stanje, put se nije završio. Nijedan od prefiksa ne odgovara ovom, ne uvodimo ograničenja.

zaključak

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

Kako zaštititi procese i ekstenzije kernela na macOS-u

Sve ovo se može učiniti samo ako je vaš uslužni program službeno instaliran u kernel. Odnosno, ne postoje takve rupe za eksterni i neželjeni softver. Međutim, kao što vidite, čak i zaštita legitimnih programa kao što su antivirusni i sigurnosni sistemi zahtijevaju rad. Ali sada će novi Acronis proizvodi za macOS imati dodatnu zaštitu od istovara iz sistema.

izvor: www.habr.com

Dodajte komentar