Sådan beskytter du processer og kerneudvidelser på macOS

Hej, Habr! I dag vil jeg gerne tale om, hvordan du kan beskytte processer mod angreb fra angribere i macOS. For eksempel er dette nyttigt til et antivirus- eller backupsystem, især da der under macOS er flere måder at "dræbe" en proces på. Læs om dette og beskyttelsesmetoder under snittet.

Sådan beskytter du processer og kerneudvidelser på macOS

Den klassiske måde at "dræbe" en proces

En velkendt måde at "dræbe" en proces på er at sende et SIGKILL-signal til processen. Gennem bash kan du kalde standarden "kill -SIGKILL PID" eller "pkill -9 NAME" for at dræbe. "kill"-kommandoen har været kendt siden UNIX-dagene og er tilgængelig ikke kun på macOS, men også på andre UNIX-lignende systemer.

Ligesom i UNIX-lignende systemer giver macOS dig mulighed for at opsnappe alle signaler til en proces undtagen to - SIGKILL og SIGSTOP. Denne artikel vil primært fokusere på SIGKILL-signalet som et signal, der får en proces til at blive dræbt.

macOS-specifikationer

På macOS kalder kill-systemkaldet i XNU-kernen funktionen psignal(SIGKILL,...). Lad os prøve at se, hvilke andre brugerhandlinger i brugerområdet kan kaldes af psignal-funktionen. Lad os luge ud opkald til psignal-funktionen i kernens interne mekanismer (selvom de kan være ikke-trivielle, lader vi dem stå til en anden artikel 🙂 - signaturbekræftelse, hukommelsesfejl, exit/terminate-håndtering, filbeskyttelsesovertrædelser osv. .

Lad os starte gennemgangen med funktionen og det tilsvarende systemkald terminate_with_payload. Det kan ses, at der ud over det klassiske kill-kald er en alternativ tilgang, der er specifik for macOS-operativsystemet og ikke findes i BSD. Driftsprincipperne for begge systemopkald er også ens. De er direkte kald til kernefunktionen psignal. Bemærk også, at før en proces dræbes, udføres en "cansignal"-kontrol - om processen kan sende et signal til en anden proces; systemet tillader ikke nogen applikation at dræbe systemprocesser, for eksempel.

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

lanceret

Standardmåden til at skabe dæmoner ved systemstart og kontrollere deres levetid er lanceret. Bemærk venligst, at kilderne er til den gamle version af launchctl op til macOS 10.10, kodeeksempler er givet til illustrative formål. Moderne launchctl sender lancerede signaler via XPC, launchctl logik er blevet flyttet til den.

Lad os se på, hvordan applikationer præcist stoppes. Før SIGTERM-signalet sendes, forsøges applikationen at blive stoppet ved hjælp af "proc_terminate"-systemkaldet.

<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 motorhjelmen kan proc_terminate, på trods af sit navn, sende ikke kun psignal med SIGTERM, men også SIGKILL.

Indirekte dræb - Ressourcegrænse

En mere interessant sag kan ses i et andet systemkald proces_politik. En almindelig brug af dette systemkald er at begrænse applikationsressourcer, såsom for en indekser til at begrænse CPU-tid og hukommelseskvoter, så systemet ikke bremses væsentligt af filcache-aktiviteter. Hvis en applikation har nået sin ressourcegrænse, som det kan ses fra funktionen proc_apply_resource_actions, sendes et SIGKILL-signal til processen.

Selvom dette systemkald potentielt kunne dræbe en proces, kontrollerede systemet ikke tilstrækkeligt rettighederne til den proces, der kalder systemkaldet. Tjekker faktisk eksisterede, men det er nok at bruge det alternative flag PROC_POLICY_ACTION_SET for at omgå denne betingelse.

Derfor, hvis du "begrænser" applikationens CPU-brugskvote (for eksempel tillader kun 1 ns at køre), så kan du dræbe enhver proces i systemet. Således kan malwaren dræbe enhver proces på systemet, inklusive antivirusprocessen. Også interessant er effekten, der opstår, når en proces dræbes med pid 1 (launchctl) - kernepanik, når man forsøger at behandle SIGKILL-signalet :)

Sådan beskytter du processer og kerneudvidelser på macOS

Hvordan løses problemet?

Den mest ligetil måde at forhindre en proces i at blive dræbt er at erstatte funktionsmarkøren i systemkaldstabellen. Desværre er denne metode ikke-triviel af mange grunde.

For det første er symbolet, der styrer sysents hukommelsesplacering, ikke kun privat for XNU-kernesymbolet, men kan ikke findes i kernesymboler. Du bliver nødt til at bruge heuristiske søgemetoder, såsom dynamisk adskillelse af funktionen og søgning efter en pointer i den.

For det andet afhænger strukturen af ​​indtastninger i tabellen af ​​de flag, som kernen blev kompileret med. Hvis CONFIG_REQUIRES_U32_MUNGING-flaget erklæres, vil størrelsen af ​​strukturen blive ændret - et ekstra felt vil blive tilføjet sy_arg_munge32. Det er nødvendigt at udføre en yderligere kontrol for at afgøre, hvilket flag kernen var kompileret med, eller alternativt kontrollere funktionspointere mod kendte.

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 leverer Apple i moderne versioner af macOS en ny API til at arbejde med processer. Endpoint Security API giver klienter mulighed for at godkende mange anmodninger til andre processer. Således kan du blokere alle signaler til processer, inklusive SIGKILL-signalet, ved hjælp af ovennævnte 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åde kan en MAC-politik registreres i kernen, som giver en signalbeskyttelsesmetode (policy proc_check_signal), men API'et er ikke officielt understøttet.

Kerneludvidelsesbeskyttelse

Udover at beskytte processer i systemet er det også nødvendigt at beskytte selve kerneudvidelsen (kext). macOS giver en ramme for udviklere til nemt at udvikle IOKit-enhedsdrivere. Ud over at levere værktøjer til at arbejde med enheder, giver IOKit metoder til driverstabling ved hjælp af forekomster af C++-klasser. En applikation i brugerområdet vil være i stand til at "finde" en registreret forekomst af klassen for at etablere et kerne-brugerområde-forhold.

For at detektere antallet af klasseforekomster i systemet er der hjælpeprogrammet ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Enhver kerneudvidelse, der ønsker at registrere sig med driverstakken, skal deklarere en klasse, der arver fra IOService, for eksempel my_kext_ioservice i dette tilfælde. Tilslutning af brugerapplikationer forårsager oprettelsen af ​​en ny instans af klassen, der arver fra IOUserClient, i eksemplet my_kext_iouserclient.

Når du forsøger at fjerne en driver fra systemet (kextunload-kommando), kaldes den virtuelle funktion "bool terminate(IOOptionBits options)". Det er nok at returnere falsk på opkaldet for at afslutte, når du prøver at losse for at deaktivere kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

IsUnloadAllowed-flaget kan indstilles af IOUserClient ved indlæsning. Når der er en downloadgrænse, vil kommandoen kextunload returnere følgende output:

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 skal udføres for IOUserClient. Forekomster af klasser kan fjernes ved hjælp af IOKitLib-brugerrumsfunktionen "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Du kan returnere false, når du kalder kommandoen "terminate", indtil userspace-applikationen "dør", det vil sige, at "clientDied"-funktionen ikke kaldes.

Filbeskyttelse

For at beskytte filer er det nok at bruge Kauth API, som giver dig mulighed for at begrænse adgangen til filer. Apple giver udviklere meddelelser om forskellige hændelser i omfanget; for os er operationerne KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD vigtige. Den nemmeste måde at begrænse adgangen til filer på er via sti - vi bruger "vn_getpath" API til at få stien til filen og sammenligne stipræfikset. Bemærk, at for at optimere omdøbningen af ​​filmappestier, tillader systemet ikke adgang til hver fil, men kun til selve mappen, der er blevet omdøbt. Det er nødvendigt at sammenligne den overordnede sti og begrænse KAUTH_VNODE_DELETE for den.

Sådan beskytter du processer og kerneudvidelser på macOS

Ulempen ved denne fremgangsmåde kan være lav ydeevne, efterhånden som antallet af præfikser stiger. For at sikre at sammenligningen ikke er lig med O(præfiks*længde), hvor præfiks er antallet af præfikser, længde er længden af ​​strengen, kan du bruge en deterministisk finit automat (DFA) bygget af præfikser.

Lad os overveje en metode til at konstruere en DFA for et givet sæt præfikser. Vi initialiserer markørerne i begyndelsen af ​​hvert præfiks. Hvis alle markører peger på det samme tegn, skal du øge hver markør med ét tegn og huske, at længden af ​​den samme linje er én længere. Hvis der er to markører med forskellige symboler, opdel markørerne i grupper efter det symbol, de peger på, og gentag algoritmen for hver gruppe.

I det første tilfælde (alle tegnene under markørerne er de samme), får vi en DFA-tilstand, der kun har én overgang langs den samme linje. I det andet tilfælde får vi en tabel over overgange af størrelse 256 (antal tegn og maksimalt antal grupper) til efterfølgende tilstande opnået ved rekursivt at kalde funktionen.

Lad os se på et eksempel. For et sæt præfikser ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kan du få følgende DFA. Figuren viser kun overgange, der fører til andre stater; andre overgange vil ikke være endelige.

Sådan beskytter du processer og kerneudvidelser på macOS

Ved gennemgang af DKA-staterne kan der være tale om 3 tilfælde.

  1. Den endelige tilstand er nået - stien er beskyttet, vi begrænser operationerne KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD
  2. Den endelige tilstand blev ikke nået, men stien "sluttede" (nul-terminatoren blev nået) - stien er en forælder, det er nødvendigt at begrænse KAUTH_VNODE_DELETE. Bemærk, at hvis vnode er en mappe, skal du tilføje et '/' i slutningen, ellers kan det begrænse det til filen "/foor/bar/t", hvilket er forkert.
  3. Den endelige tilstand blev ikke nået, vejen sluttede ikke. Ingen af ​​præfikserne matcher denne, vi indfører ikke begrænsninger.

Konklusion

Målet med de sikkerhedsløsninger, der udvikles, er at øge sikkerhedsniveauet for brugeren og dennes data. På den ene side opnås dette mål ved udviklingen af ​​Acronis-softwareproduktet, som lukker de sårbarheder, hvor selve operativsystemet er "svagt". På den anden side bør vi ikke forsømme at styrke de sikkerhedsaspekter, der kan forbedres på OS-siden, især da lukning af sådanne sårbarheder øger vores egen stabilitet som et produkt. Sårbarheden blev rapporteret til Apple Product Security Team og er blevet rettet i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Sådan beskytter du processer og kerneudvidelser på macOS

Alt dette kan kun gøres, hvis dit hjælpeprogram er blevet officielt installeret i kernen. Det vil sige, at der ikke er sådanne smuthuller for ekstern og uønsket software. Men som du kan se, kræver selv beskyttelse af legitime programmer såsom antivirus- og sikkerhedskopieringssystemer arbejde. Men nu vil nye Acronis-produkter til macOS have yderligere beskyttelse mod aflæsning fra systemet.

Kilde: www.habr.com

Tilføj en kommentar