Hvordan beskytte prosesser og kjerneutvidelser på macOS

Hei, Habr! I dag vil jeg snakke om hvordan du kan beskytte prosesser mot angrep fra angripere i macOS. Dette er for eksempel nyttig for et antivirus- eller sikkerhetskopisystem, spesielt siden det under macOS er flere måter å "drepe" en prosess på. Les om dette og beskyttelsesmetoder under kuttet.

Hvordan beskytte prosesser og kjerneutvidelser på macOS

Den klassiske måten å "drepe" en prosess på

En velkjent måte å "drepe" en prosess på er å sende et SIGKILL-signal til prosessen. Gjennom bash kan du kalle standard "kill -SIGKILL PID" eller "pkill -9 NAME" for å drepe. "kill"-kommandoen har vært kjent siden UNIX-dagene og er tilgjengelig ikke bare på macOS, men også på andre UNIX-lignende systemer.

Akkurat som i UNIX-lignende systemer, lar macOS deg fange opp alle signaler til en prosess bortsett fra to - SIGKILL og SIGSTOP. Denne artikkelen vil først og fremst fokusere på SIGKILL-signalet som et signal som fører til at en prosess blir drept.

macOS-spesifikasjoner

På macOS kaller kill-systemkallet i XNU-kjernen funksjonen psignal(SIGKILL,...). La oss prøve å se hvilke andre brukerhandlinger i brukerområdet kan kalles av psignal-funksjonen. La oss luke ut kall til psignal-funksjonen i de interne mekanismene til kjernen (selv om de kan være ikke-trivielle, overlater vi dem til en annen artikkel 🙂 - signaturverifisering, minnefeil, exit/terminate-håndtering, brudd på filbeskyttelse osv. .

La oss starte gjennomgangen med funksjonen og den tilhørende systemkallingen terminate_with_payload. Det kan sees at i tillegg til den klassiske kill call, er det en alternativ tilnærming som er spesifikk for macOS-operativsystemet og ikke finnes i BSD. Driftsprinsippene for begge systemanrop er også like. De er direkte anrop til kjernefunksjonen psignal. Vær også oppmerksom på at før du dreper en prosess, utføres en "cansignal"-sjekk - om prosessen kan sende et signal til en annen prosess; systemet tillater for eksempel ingen applikasjoner å drepe systemprosesser.

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

Standardmåten for å lage demoner ved systemstart og kontrollere levetiden deres, er lansert. Vær oppmerksom på at kildene er for den gamle versjonen av launchctl opp til macOS 10.10, kodeeksempler er gitt for illustrative formål. Moderne launchctl sender lanserte signaler via XPC, launchctl-logikken er flyttet til den.

La oss se på nøyaktig hvordan applikasjoner stoppes. Før sending av SIGTERM-signalet, forsøkes applikasjonen å bli stoppet ved hjelp av "proc_terminate"-systemanropet.

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

Under panseret kan proc_terminate, til tross for navnet, sende ikke bare psignal med SIGTERM, men også SIGKILL.

Indirekte dreping - ressursgrense

En mer interessant sak kan sees i en annen systemsamtale process_policy. En vanlig bruk av dette systemkallet er å begrense applikasjonsressurser, for eksempel for en indekser for å begrense CPU-tid og minnekvoter, slik at systemet ikke bremses vesentlig av filbuffringsaktiviteter. Hvis en applikasjon har nådd sin ressursgrense, som man kan se fra funksjonen proc_apply_resource_actions, sendes et SIGKILL-signal til prosessen.

Selv om dette systemanropet potensielt kan drepe en prosess, sjekket ikke systemet tilstrekkelig rettighetene til prosessen som kaller systemkallet. Sjekker faktisk eksisterte, men det er nok å bruke det alternative flagget PROC_POLICY_ACTION_SET for å omgå denne betingelsen.

Derfor, hvis du "begrenser" applikasjonens CPU-brukskvote (for eksempel tillater bare 1 ns å kjøre), kan du drepe enhver prosess i systemet. Dermed kan skadelig programvare drepe enhver prosess på systemet, inkludert antivirusprosessen. Også interessant er effekten som oppstår når du dreper en prosess med pid 1 (launchctl) - kjernepanikk når du prøver å behandle SIGKILL-signalet :)

Hvordan beskytte prosesser og kjerneutvidelser på macOS

Hvordan løse problemet?

Den enkleste måten å forhindre at en prosess blir drept, er å erstatte funksjonspekeren i systemanropstabellen. Dessverre er denne metoden ikke-triviell av mange grunner.

For det første er symbolet som kontrollerer sysents minneplassering ikke bare privat for XNU-kjernesymbolet, men kan ikke finnes i kjernesymboler. Du må bruke heuristiske søkemetoder, som å dynamisk demontere funksjonen og søke etter en peker i den.

For det andre avhenger strukturen til oppføringene i tabellen av flaggene som kjernen ble kompilert med. Hvis CONFIG_REQUIRES_U32_MUNGING-flagget er deklarert, vil størrelsen på strukturen bli endret - et ekstra felt vil bli lagt til sy_arg_munge32. Det er nødvendig å utføre en ekstra sjekk for å finne ut hvilket flagg kjernen ble kompilert med, eller alternativt sjekke funksjonspekere mot kjente.

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

Heldigvis, i moderne versjoner av macOS, tilbyr Apple en ny API for å jobbe med prosesser. Endpoint Security API lar klienter autorisere mange forespørsler til andre prosesser. Dermed kan du blokkere alle signaler til prosesser, inkludert SIGKILL-signalet, ved å bruke ovennevnte 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;
}

På samme måte kan en MAC-policy registreres i kjernen, som gir en signalbeskyttelsesmetode (policy proc_check_signal), men API-en støttes ikke offisielt.

Beskyttelse for kjerneforlengelse

I tillegg til å beskytte prosesser i systemet, er det også nødvendig å beskytte selve kjerneutvidelsen (kext). macOS gir et rammeverk for utviklere for enkelt å utvikle IOKit-enhetsdrivere. I tillegg til å tilby verktøy for å jobbe med enheter, tilbyr IOKit metoder for driverstabling ved å bruke forekomster av C++-klasser. En applikasjon i brukerområdet vil kunne "finne" en registrert forekomst av klassen for å etablere en kjerne-brukerrom-relasjon.

For å oppdage antall klasseforekomster i systemet, er det ioclasscount-verktøyet.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Enhver kjerneutvidelse som ønsker å registrere seg med driverstabelen må deklarere en klasse som arver fra IOService, for eksempel my_kext_ioservice i dette tilfellet Koble til brukerapplikasjoner fører til at det opprettes en ny forekomst av klassen som arver fra IOUserClient, i eksemplet my_kext_iouserclient.

Når du prøver å laste ut en driver fra systemet (kextunload-kommando), kalles den virtuelle funksjonen "bool terminate(IOOptionBits options)". Det er nok å returnere false på samtalen for å avslutte når du prøver å losse for å deaktivere kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

IsUnloadAllowed-flagget kan settes av IOUserClient ved lasting. Når det er en nedlastingsgrense, vil kextunload-kommandoen returnere følgende utgang:

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.

Tilsvarende beskyttelse må gjøres for IOUserClient. Forekomster av klasser kan lastes ut ved å bruke IOKitLib-brukerromsfunksjonen "IOCatalogueTerminate(mach_port_t, uint32_t flagg, io_name_t description);". Du kan returnere false når du kaller «terminate»-kommandoen til brukerromsapplikasjonen «dør», det vil si at «clientDied»-funksjonen ikke kalles.

Filbeskyttelse

For å beskytte filer er det nok å bruke Kauth API, som lar deg begrense tilgangen til filer. Apple gir utviklere varsler om ulike hendelser i omfanget; for oss er operasjonene KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD viktige. Den enkleste måten å begrense tilgangen til filer på er via sti - vi bruker "vn_getpath" API for å få banen til filen og sammenligne baneprefikset. Merk at for å optimalisere omdøpningen av filmappebaner, godkjenner ikke systemet tilgang til hver fil, men bare til selve mappen som har fått nytt navn. Det er nødvendig å sammenligne den overordnede banen og begrense KAUTH_VNODE_DELETE for den.

Hvordan beskytte prosesser og kjerneutvidelser på macOS

Ulempen med denne tilnærmingen kan være lav ytelse ettersom antallet prefikser øker. For å sikre at sammenligningen ikke er lik O(prefiks*lengde), der prefiks er antall prefikser, lengde er lengden på strengen, kan du bruke en deterministisk endelig automat (DFA) bygget av prefikser.

La oss vurdere en metode for å konstruere en DFA for et gitt sett med prefikser. Vi initialiserer markørene i begynnelsen av hvert prefiks. Hvis alle markørene peker på samme tegn, øker du hver markør med ett tegn og husk at lengden på samme linje er én større. Hvis det er to markører med forskjellige symboler, del markørene inn i grupper i henhold til symbolet de peker på og gjenta algoritmen for hver gruppe.

I det første tilfellet (alle tegnene under markørene er de samme), får vi en DFA-tilstand som kun har én overgang langs samme linje. I det andre tilfellet får vi en tabell over overganger av størrelse 256 (antall tegn og maksimalt antall grupper) til påfølgende tilstander oppnådd ved å kalle funksjonen rekursivt.

La oss se på et eksempel. For et sett med prefikser ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kan du få følgende DFA. Figuren viser bare overganger som fører til andre stater; andre overganger vil ikke være endelige.

Hvordan beskytte prosesser og kjerneutvidelser på macOS

Når du går gjennom DKA-statene, kan det være 3 tilfeller.

  1. Den endelige tilstanden er nådd - banen er beskyttet, vi begrenser operasjonene KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD
  2. Den endelige tilstanden ble ikke nådd, men banen "sluttet" (nullterminatoren ble nådd) - banen er en forelder, det er nødvendig å begrense KAUTH_VNODE_DELETE. Merk at hvis vnode er en mappe, må du legge til en '/' på slutten, ellers kan det begrense den til filen "/foor/bar/t", som er feil.
  3. Den endelige tilstanden ble ikke nådd, veien tok ikke slutt. Ingen av prefiksene samsvarer med denne, vi innfører ingen begrensninger.

Konklusjon

Målet med sikkerhetsløsningene som utvikles er å øke sikkerhetsnivået til brukeren og hans data. På den ene siden oppnås dette målet ved utviklingen av Acronis-programvareproduktet, som lukker de sårbarhetene der selve operativsystemet er "svakt". På den annen side bør vi ikke overse å styrke de sikkerhetsaspektene som kan forbedres på OS-siden, spesielt siden lukking av slike sårbarheter øker vår egen stabilitet som produkt. Sårbarheten ble rapportert til Apple Product Security Team og er rettet i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Hvordan beskytte prosesser og kjerneutvidelser på macOS

Alt dette kan bare gjøres hvis verktøyet ditt er offisielt installert i kjernen. Det vil si at det ikke finnes slike smutthull for ekstern og uønsket programvare. Men som du kan se, krever selv å beskytte legitime programmer som antivirus- og sikkerhetskopieringssystemer arbeid. Men nå vil nye Acronis-produkter for macOS ha ekstra beskyttelse mot utlasting fra systemet.

Kilde: www.habr.com

Legg til en kommentar