Hur man skyddar processer och kärntillägg på macOS

Hej, Habr! Idag skulle jag vilja prata om hur du kan skydda processer från attacker från angripare i macOS. Detta är till exempel användbart för ett antivirus- eller säkerhetskopieringssystem, särskilt eftersom det under macOS finns flera sätt att "döda" en process. Läs om detta och skyddsmetoder under snittet.

Hur man skyddar processer och kärntillägg på macOS

Det klassiska sättet att "döda" en process

Ett välkänt sätt att "döda" en process är att skicka en SIGKILL-signal till processen. Genom bash kan du kalla standarden "kill -SIGKILL PID" eller "pkill -9 NAME" för att döda. Kommandot "kill" har varit känt sedan UNIX-dagarna och är tillgängligt inte bara på macOS utan även på andra UNIX-liknande system.

Precis som i UNIX-liknande system låter macOS dig fånga upp alla signaler till en process utom två - SIGKILL och SIGSTOP. Den här artikeln kommer i första hand att fokusera på SIGKILL-signalen som en signal som gör att en process dödas.

macOS-specifikationer

På macOS anropar kill-systemanropet i XNU-kärnan funktionen psignal(SIGKILL,...). Låt oss försöka se vilka andra användaråtgärder i användarutrymmet som kan anropas av psignal-funktionen. Låt oss sålla bort anrop till psignal-funktionen i kärnans interna mekanismer (även om de kan vara icke-triviala, lämnar vi dem till en annan artikel 🙂 - signaturverifiering, minnesfel, exit/terminate-hantering, filskyddsöverträdelser, etc. .

Låt oss börja granskningen med funktionen och motsvarande systemanrop avsluta_med_nyttolast. Det kan ses att utöver det klassiska kill-anropet finns det ett alternativt tillvägagångssätt som är specifikt för macOS-operativsystemet och som inte finns i BSD. Funktionsprinciperna för båda systemanropen är också likartade. De är direktanrop till kärnfunktionen psignal. Observera också att innan en process avbryts, utförs en "cansignal"-kontroll - om processen kan skicka en signal till en annan process; systemet tillåter till exempel inte någon applikation att döda systemprocesser.

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

lansera

Standardsättet att skapa demoner vid systemstart och kontrollera deras livslängd lanseras. Observera att källorna är för den gamla versionen av launchctl upp till macOS 10.10, kodexempel tillhandahålls i illustrativt syfte. Modern launchctl skickar lanserade signaler via XPC, launchctl logik har flyttats till den.

Låt oss titta på exakt hur applikationer stoppas. Innan SIGTERM-signalen skickas, försöker man stoppa applikationen med hjälp av systemanropet "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));
		} 
...
<>

Under huven kan proc_terminate, trots sitt namn, skicka inte bara psignal med SIGTERM, utan även SIGKILL.

Indirekt dödande - Resursgräns

Ett mer intressant fall kan ses i ett annat systemsamtal process_policy. En vanlig användning av detta systemanrop är att begränsa applikationsresurser, till exempel för en indexerare för att begränsa CPU-tid och minneskvoter så att systemet inte avsevärt saktas ner av filcache-aktiviteter. Om en applikation har nått sin resursgräns, som kan ses från funktionen proc_apply_resource_actions, skickas en SIGKILL-signal till processen.

Även om detta systemanrop potentiellt skulle kunna döda en process, kontrollerade systemet inte på ett adekvat sätt rättigheterna för processen som anropade systemanropet. Kollar faktiskt existerade, men det räcker att använda den alternativa flaggan PROC_POLICY_ACTION_SET för att kringgå detta villkor.

Därför, om du "begränsar" applikationens CPU-användningskvot (till exempel tillåter endast 1 ns att köras), kan du döda vilken process som helst i systemet. Således kan skadlig programvara döda alla processer på systemet, inklusive antivirusprocessen. Intressant är också effekten som uppstår när man dödar en process med pid 1 (launchctl) - kärnpanik när man försöker bearbeta SIGKILL-signalen :)

Hur man skyddar processer och kärntillägg på macOS

Hur löser man problemet?

Det enklaste sättet att förhindra att en process dödas är att ersätta funktionspekaren i systemanropstabellen. Tyvärr är denna metod icke-trivial av många skäl.

För det första är symbolen som styr sysents minnesplats inte bara privat för XNU-kärnsymbolen, utan kan inte hittas i kärnsymboler. Du måste använda heuristiska sökmetoder, som att dynamiskt demontera funktionen och söka efter en pekare i den.

För det andra beror strukturen på poster i tabellen på flaggorna som kärnan kompilerades med. Om CONFIG_REQUIRES_U32_MUNGING-flaggan deklareras kommer storleken på strukturen att ändras - ett ytterligare fält kommer att läggas till sy_arg_munge32. Det är nödvändigt att utföra en ytterligare kontroll för att fastställa vilken flagga kärnan kompilerades med, alternativt kontrollera funktionspekare mot kända.

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

Lyckligtvis, i moderna versioner av macOS, tillhandahåller Apple ett nytt API för att arbeta med processer. Endpoint Security API tillåter klienter att auktorisera många förfrågningar till andra processer. Således kan du blockera alla signaler till processer, inklusive SIGKILL-signalen, med hjälp av ovan nämnda 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å liknande sätt kan en MAC-policy registreras i kärnan, vilket ger en signalskyddsmetod (policy proc_check_signal), men API:et stöds inte officiellt.

Kärnförlängningsskydd

Förutom att skydda processer i systemet är det också nödvändigt att skydda själva kärntillägget (kext). macOS tillhandahåller ett ramverk för utvecklare att enkelt utveckla IOKit-drivrutiner. Förutom att tillhandahålla verktyg för att arbeta med enheter, tillhandahåller IOKit metoder för drivrutinsstapling med instanser av C++-klasser. En applikation i användarutrymmet kommer att kunna "hitta" en registrerad instans av klassen för att upprätta ett förhållande mellan kärnan och användarutrymmet.

För att upptäcka antalet klassinstanser i systemet finns verktyget ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Alla kärntillägg som vill registrera sig med drivrutinsstacken måste deklarera en klass som ärver från IOService, till exempel my_kext_ioservice i detta fall.Anslutning av användarapplikationer skapar en ny instans av klassen som ärver från IOUserClient, i exemplet my_kext_iouserclient.

När du försöker ladda bort en drivrutin från systemet (kextunload-kommando), anropas den virtuella funktionen "bool terminate(IOOptionBits options)". Det räcker med att returnera falskt på samtalet för att avsluta när man försöker lossa för att inaktivera kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Flaggan IsUnloadAllowed kan ställas in av IOUserClient vid laddning. När det finns en nedladdningsgräns kommer kommandot kextunload att returnera följande utdata:

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.

Liknande skydd måste göras för IOUserClient. Förekomster av klasser kan laddas ur med hjälp av IOKitLib-användarutrymmesfunktionen "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Du kan returnera false när du anropar kommandot "terminate" tills användarutrymmesapplikationen "dör", det vill säga funktionen "clientDied" anropas inte.

Filskydd

För att skydda filer räcker det att använda Kauth API, som låter dig begränsa åtkomsten till filer. Apple förser utvecklare med aviseringar om olika händelser i omfattningen; för oss är operationerna KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD viktiga. Det enklaste sättet att begränsa åtkomst till filer är genom sökväg - vi använder "vn_getpath" API för att få sökvägen till filen och jämföra sökvägsprefixet. Observera att för att optimera byte av sökvägar för filmappar, tillåter systemet inte åtkomst till varje fil, utan endast till själva mappen som har bytt namn. Det är nödvändigt att jämföra den överordnade sökvägen och begränsa KAUTH_VNODE_DELETE för den.

Hur man skyddar processer och kärntillägg på macOS

Nackdelen med detta tillvägagångssätt kan vara låg prestanda när antalet prefix ökar. För att säkerställa att jämförelsen inte är lika med O(prefix*längd), där prefix är antalet prefix, längd är längden på strängen, kan du använda en deterministisk finit automat (DFA) byggd av prefix.

Låt oss överväga en metod för att konstruera en DFA för en given uppsättning prefix. Vi initierar markörerna i början av varje prefix. Om alla markörer pekar på samma tecken, öka sedan varje markör med ett tecken och kom ihåg att längden på samma rad är en längre. Om det finns två markörer med olika symboler, dela in markörerna i grupper enligt symbolen de pekar på och upprepa algoritmen för varje grupp.

I det första fallet (alla tecken under markörerna är samma) får vi ett DFA-tillstånd som bara har en övergång längs samma linje. I det andra fallet får vi en tabell över övergångar av storlek 256 (antal tecken och maximalt antal grupper) till efterföljande tillstånd som erhålls genom att rekursivt anropa funktionen.

Låt oss titta på ett exempel. För en uppsättning prefix ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kan du få följande DFA. Bilden visar endast övergångar som leder till andra stater, andra övergångar kommer inte att vara slutgiltiga.

Hur man skyddar processer och kärntillägg på macOS

När man går igenom DKA-staterna kan det finnas 3 fall.

  1. Sluttillståndet har nåtts - sökvägen är skyddad, vi begränsar operationerna KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD
  2. Det slutliga tillståndet nåddes inte, men sökvägen "slutade" (nollterminatorn nåddes) - sökvägen är en förälder, det är nödvändigt att begränsa KAUTH_VNODE_DELETE. Observera att om vnode är en mapp måste du lägga till en '/' i slutet, annars kan det begränsa den till filen "/foor/bar/t", vilket är felaktigt.
  3. Det slutliga tillståndet nåddes inte, vägen tog inte slut. Inget av prefixen matchar detta, vi inför inga begränsningar.

Slutsats

Målet med säkerhetslösningarna som utvecklas är att öka säkerhetsnivån för användaren och dennes data. Å ena sidan uppnås detta mål genom utvecklingen av Acronis mjukvaruprodukt, som stänger de sårbarheter där själva operativsystemet är "svagt". Å andra sidan bör vi inte försumma att stärka de säkerhetsaspekter som kan förbättras på OS-sidan, särskilt eftersom att stänga sådana sårbarheter ökar vår egen stabilitet som produkt. Sårbarheten rapporterades till Apple Product Security Team och har åtgärdats i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Hur man skyddar processer och kärntillägg på macOS

Allt detta kan bara göras om ditt verktyg har officiellt installerats i kärnan. Det vill säga, det finns inga sådana kryphål för extern och oönskad programvara. Men som du kan se kräver även att skydda legitima program som antivirus- och säkerhetskopieringssystem arbete. Men nu kommer nya Acronis-produkter för macOS att ha ytterligare skydd mot avlastning från systemet.

Källa: will.com

Lägg en kommentar