Hoe u processen en kernelextensies op macOS kunt beschermen

Hallo, Habr! Vandaag wil ik het hebben over hoe je processen kunt beschermen tegen aanvallen van aanvallers in macOS. Dit is bijvoorbeeld handig voor een antivirus- of back-upsysteem, vooral omdat er onder macOS verschillende manieren zijn om een ​​proces te ‘doden’. Lees hierover en de beschermingsmethoden onder de rubriek.

Hoe u processen en kernelextensies op macOS kunt beschermen

De klassieke manier om een ​​proces te ‘doden’

Een bekende manier om een ​​proces te ‘doden’ is door een SIGKILL-signaal naar het proces te sturen. Via bash kun je de standaard “kill -SIGKILL PID” of “pkill -9 NAME” noemen om te doden. Het commando “kill” is bekend sinds de dagen van UNIX en is niet alleen beschikbaar op macOS, maar ook op andere UNIX-achtige systemen.

Net als bij UNIX-achtige systemen kun je met macOS alle signalen van een proces onderscheppen, behalve twee: SIGKILL en SIGSTOP. Dit artikel zal zich primair richten op het SIGKILL-signaal als signaal dat ervoor zorgt dat een proces wordt beëindigd.

macOS-specificaties

Op macOS roept de kill-systeemaanroep in de XNU-kernel de functie psignal(SIGKILL,...) aan. Laten we proberen te zien welke andere gebruikersacties in de gebruikersruimte kunnen worden aangeroepen door de psignal-functie. Laten we oproepen naar de psignal-functie in de interne mechanismen van de kernel verwijderen (hoewel ze misschien niet triviaal zijn, laten we ze over voor een ander artikel 🙂 - handtekeningverificatie, geheugenfouten, afhandeling bij afsluiten/beëindigen, schendingen van bestandsbescherming, enz. .

Laten we de beoordeling beginnen met de functie en de bijbehorende systeemaanroep beëindigen_met_payload. Het is duidelijk dat er naast de klassieke kill-oproep een alternatieve aanpak bestaat die specifiek is voor het macOS-besturingssysteem en niet voorkomt in BSD. De werkingsprincipes van beide systeemoproepen zijn ook vergelijkbaar. Het zijn directe oproepen naar de kernelfunctie psignal. Houd er ook rekening mee dat voordat een proces wordt beëindigd, er een “cansignal”-controle wordt uitgevoerd: of het proces een signaal naar een ander proces kan sturen; het systeem staat bijvoorbeeld geen enkele toepassing toe om systeemprocessen te beëindigen.

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

De standaardmanier om daemons te maken bij het opstarten van het systeem en hun levensduur te controleren is launchd. Houd er rekening mee dat de bronnen betrekking hebben op de oude versie van launchctl tot en met macOS 10.10. Er worden codevoorbeelden gegeven voor illustratieve doeleinden. Moderne launchctl verzendt launchd-signalen via XPC, de launchctl-logica is ernaartoe verplaatst.

Laten we eens kijken hoe applicaties precies worden gestopt. Voordat het SIGTERM-signaal wordt verzonden, wordt geprobeerd de applicatie te stoppen met behulp van de systeemaanroep “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));
		} 
...
<>

Onder de motorkap kan proc_terminate, ondanks zijn naam, niet alleen psignal verzenden met SIGTERM, maar ook met SIGKILL.

Indirecte moord - Grondstoflimiet

Een interessanter geval is te zien in een andere systeemoproep proces_beleid. Een algemeen gebruik van deze systeemaanroep is het beperken van applicatiebronnen, bijvoorbeeld voor een indexer om de CPU-tijd en geheugenquota te beperken, zodat het systeem niet aanzienlijk wordt vertraagd door activiteiten in de cache van bestanden. Als een applicatie zijn resourcelimiet heeft bereikt, zoals blijkt uit de functie proc_apply_resource_actions, wordt een SIGKILL-signaal naar het proces gestuurd.

Hoewel deze systeemaanroep mogelijk een proces zou kunnen beëindigen, heeft het systeem de rechten van het proces dat de systeemaanroep aanroept niet voldoende gecontroleerd. Eigenlijk controleren bestond, maar het is voldoende om de alternatieve vlag PROC_POLICY_ACTION_SET te gebruiken om deze voorwaarde te omzeilen.

Als u dus het CPU-gebruiksquotum van de toepassing ‘beperkt’ (bijvoorbeeld door slechts 1 ns toe te staan), kunt u elk proces in het systeem beëindigen. De malware kan dus elk proces op het systeem doden, inclusief het antivirusproces. Ook interessant is het effect dat optreedt bij het doden van een proces met pid 1 (launchctl) - kernelpaniek bij het verwerken van het SIGKILL-signaal :)

Hoe u processen en kernelextensies op macOS kunt beschermen

Hoe het probleem op te lossen?

De meest eenvoudige manier om te voorkomen dat een proces wordt beëindigd, is door de functieaanwijzer in de systeemaanroeptabel te vervangen. Helaas is deze methode om vele redenen niet triviaal.

Ten eerste is het symbool dat de geheugenlocatie van sysent controleert niet alleen privé voor het XNU-kernelsymbool, maar kan het ook niet in kernelsymbolen worden gevonden. U zult heuristische zoekmethoden moeten gebruiken, zoals het dynamisch demonteren van de functie en het zoeken naar een aanwijzer erin.

Ten tweede hangt de structuur van de vermeldingen in de tabel af van de vlaggen waarmee de kernel is gecompileerd. Als de vlag CONFIG_REQUIRES_U32_MUNGING wordt gedeclareerd, wordt de grootte van de structuur gewijzigd - er wordt een extra veld toegevoegd sy_arg_munge32. Het is noodzakelijk om een ​​extra controle uit te voeren om te bepalen met welke vlag de kernel is gecompileerd, of als alternatief functie-pointers te vergelijken met bekende pointers.

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

Gelukkig biedt Apple in moderne versies van macOS een nieuwe API voor het werken met processen. Met de Endpoint Security API kunnen clients veel verzoeken aan andere processen autoriseren. U kunt dus alle signalen voor processen blokkeren, inclusief het SIGKILL-signaal, met behulp van de bovengenoemde 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;
}

Op dezelfde manier kan een MAC-beleid in de kernel worden geregistreerd, wat een signaalbeschermingsmethode biedt (policy proc_check_signal), maar de API wordt niet officieel ondersteund.

Bescherming van kernelextensies

Naast het beschermen van processen in het systeem, is het ook noodzakelijk om de kernelextensie zelf (kext) te beschermen. macOS biedt ontwikkelaars een raamwerk waarmee ze eenvoudig IOKit-apparaatstuurprogramma's kunnen ontwikkelen. Naast het bieden van tools voor het werken met apparaten, biedt IOKit methoden voor het stapelen van stuurprogramma's met behulp van instanties van C++-klassen. Een applicatie in de gebruikersruimte kan een geregistreerd exemplaar van de klasse ‘vinden’ om een ​​kernel-gebruikersruimterelatie tot stand te brengen.

Om het aantal klasse-instanties in het systeem te detecteren, is er het hulpprogramma ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Elke kernelextensie die zich wil registreren bij de driverstack moet een klasse declareren die erft van IOService, bijvoorbeeld my_kext_ioservice in dit geval. Het verbinden van gebruikersapplicaties veroorzaakt de creatie van een nieuw exemplaar van de klasse die erft van IOUserClient, in het voorbeeld my_kext_iouserclient.

Wanneer u probeert een stuurprogramma uit het systeem te verwijderen (kextunload-opdracht), wordt de virtuele functie “bool terminatie (IOOptionBits-opties)” aangeroepen. Het is voldoende om false terug te geven bij de oproep om te beëindigen wanneer u probeert te lossen om kextunload uit te schakelen.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

De vlag IsUnloadAllowed kan tijdens het laden door de IOUserClient worden ingesteld. Als er een downloadlimiet is, retourneert het kextunload-commando de volgende uitvoer:

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.

Soortgelijke bescherming moet worden uitgevoerd voor IOUserClient. Instanties van klassen kunnen worden verwijderd met behulp van de IOKitLib-gebruikersruimtefunctie “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. U kunt false retourneren bij het aanroepen van de opdracht “terminate” totdat de gebruikersruimtetoepassing “sterft”, dat wil zeggen dat de functie “clientDied” niet wordt aangeroepen.

Bestandsbeveiliging

Om bestanden te beschermen volstaat het om de Kauth API te gebruiken, waarmee u de toegang tot bestanden kunt beperken. Apple biedt ontwikkelaars meldingen over verschillende gebeurtenissen in de scope; voor ons zijn de bewerkingen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA en KAUTH_VNODE_DELETE_CHILD belangrijk. De eenvoudigste manier om de toegang tot bestanden te beperken is via een pad. We gebruiken de “vn_getpath” API om het pad naar het bestand op te halen en het padvoorvoegsel te vergelijken. Houd er rekening mee dat om het hernoemen van bestandsmappaden te optimaliseren, het systeem geen toegang tot elk bestand autoriseert, maar alleen tot de map zelf waarvan de naam is gewijzigd. Het is noodzakelijk om het bovenliggende pad te vergelijken en KAUTH_VNODE_DELETE ervoor te beperken.

Hoe u processen en kernelextensies op macOS kunt beschermen

Het nadeel van deze aanpak kan de lage prestatie zijn naarmate het aantal voorvoegsels toeneemt. Om ervoor te zorgen dat de vergelijking niet gelijk is aan O(voorvoegsel*lengte), waarbij voorvoegsel het aantal voorvoegsels is en lengte de lengte van de string, kunt u een deterministische eindige automaat (DFA) gebruiken die is opgebouwd uit voorvoegsels.

Laten we een methode bekijken voor het construeren van een DFA voor een gegeven set voorvoegsels. We initialiseren de cursors aan het begin van elk voorvoegsel. Als alle cursors naar hetzelfde teken wijzen, verhoog dan elke cursor met één teken en onthoud dat de lengte van dezelfde regel één groter is. Als er twee cursors met verschillende symbolen zijn, verdeelt u de cursors in groepen op basis van het symbool waarnaar ze verwijzen en herhaalt u het algoritme voor elke groep.

In het eerste geval (alle tekens onder de cursors zijn hetzelfde) krijgen we een DFA-status die slechts één overgang langs dezelfde lijn heeft. In het tweede geval krijgen we een tabel met overgangen van grootte 256 (aantal tekens en maximaal aantal groepen) naar volgende toestanden, verkregen door de functie recursief aan te roepen.

Laten we eens kijken naar een voorbeeld. Voor een set voorvoegsels (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) kunt u het volgende krijgen DFA. De figuur toont alleen transities die naar andere toestanden leiden; andere transities zullen niet definitief zijn.

Hoe u processen en kernelextensies op macOS kunt beschermen

Wanneer u de DKA-staten doorloopt, kunnen er 3 gevallen zijn.

  1. De eindstatus is bereikt - het pad is beveiligd, we beperken de bewerkingen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA en KAUTH_VNODE_DELETE_CHILD
  2. De eindstatus is niet bereikt, maar het pad is "beëindigd" (de nulterminator is bereikt) - het pad is een ouder, het is noodzakelijk om KAUTH_VNODE_DELETE te beperken. Houd er rekening mee dat als vnode een map is, u aan het einde een '/' moet toevoegen, anders kan de map worden beperkt tot het bestand “/foor/bar/t”, wat onjuist is.
  3. De eindtoestand werd niet bereikt, het pad eindigde niet. Geen van de voorvoegsels komt overeen met deze, we introduceren geen beperkingen.

Conclusie

Het doel van de beveiligingsoplossingen die worden ontwikkeld is het verhogen van het beveiligingsniveau van de gebruiker en zijn gegevens. Enerzijds wordt dit doel bereikt door de ontwikkeling van het softwareproduct Acronis, dat de kwetsbaarheden dichtt waarin het besturingssysteem zelf “zwak” is. Aan de andere kant mogen we het versterken van de beveiligingsaspecten die aan de kant van het besturingssysteem kunnen worden verbeterd niet verwaarlozen, vooral omdat het dichten van dergelijke kwetsbaarheden onze eigen stabiliteit als product vergroot. De kwetsbaarheid is gemeld aan het Apple Product Security Team en is opgelost in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Hoe u processen en kernelextensies op macOS kunt beschermen

Dit alles kan alleen worden gedaan als uw hulpprogramma officieel in de kernel is geïnstalleerd. Dat wil zeggen dat dergelijke mazen in de wet niet bestaan ​​voor externe en ongewenste software. Zoals u kunt zien, vergt zelfs het beschermen van legitieme programma's zoals antivirus- en back-upsystemen echter werk. Maar nu zullen nieuwe Acronis-producten voor macOS extra bescherming bieden tegen verwijdering uit het systeem.

Bron: www.habr.com

Voeg een reactie