Hoe processen en kernelextensies in macOS te beschermen

Hallo, Habr! Vandaag wil ik het hebben over hoe je processen kunt beschermen tegen aanvallen van kwaadwillenden in macOS. Dit is bijvoorbeeld nuttig voor antivirusprogramma's of back-upsystemen, vooral gezien het feit dat er in macOS verschillende manieren zijn om een proces te 'doden'. Lees verder voor meer informatie over dit onderwerp en beschermingstechnieken.

Hoe processen en kernelextensies in macOS te 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 standaardcommando's zoals 'kill -SIGKILL PID' of 'pkill -9 NAME' gebruiken om dit te doen. Het 'kill'-commando is al bekend sinds de tijd van UNIX en is beschikbaar, niet alleen in macOS, maar ook op andere UNIX-achtige systemen.

Net als in UNIX-achtige systemen, staat macOS het onderscheppen van alle signalen naar een proces toe, behalve twee: SIGKILL en SIGSTOP. In dit artikel zullen we ons voornamelijk richten op het SIGKILL-signaal, dat het doden van een proces in gang zet.

Specifiek voor macOS

In macOS roept de system call kill in de XNU-kernel de functie psignal(SIGKILL,…) aan. Laten we eens kijken welke andere acties van de gebruiker in de userspace deze functie psignal kunnen aanroepen. We zullen aanroepen van de psignal-functie in de interne mechanismen van de kernel uitsluiten (hoewel deze ook niet triviaal kunnen zijn, maar we laten die voor een ander artikel šŸ™‚ — signature checks, geheugenfouten, exit/terminate verwerking, schending van bestandsbescherming, enz.).

Laten we beginnen met een overzicht van de functie en de bijbehorende systeemaanroep terminate_with_payload. Het is duidelijk dat naast de klassieke kill-aanroep er een alternatieve aanpak bestaat die specifiek is voor het besturingssysteem macOS en niet voorkomt in BSD. De werkprincipes van beide systeemaanroepen zijn ook vergelijkbaar. Ze zijn directe aanroepen van de kernelfunctie psignal. Laten we ook opmerken dat vóór het doden van een proces er een controle 'cansignal' wordt uitgevoerd – kan het proces een signaal naar een ander proces sturen? Het systeem staat het niet toe dat een willekeurige applicatie systeemprocessen kan doden.

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() zal een SIGKILL naar de opgegeven thread versturen of
		 * terugkeren als de thread en/of taak al aan het beƫindigen is. Hoe dan ook, de
		 * huidige thread zal niet terugkeren naar de gebruikersruimte.
		 * /
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

launchd

De standaardmethode voor het creëren van daemonprocessen bij het opstarten van het systeem en het beheren van hun levensduur is launchd. Ik wil opmerken dat de bronbestanden betrekking hebben op een oude versie van launchctl vóór macOS 10.10, en de codevoorbeelden dienen ter illustratie. De moderne launchctl verstuurt signalen naar launchd via XPC, de logica van launchctl is daarin overgenomen.

Laten we bekijken hoe applicaties precies worden gestopt. Voordat het SIGTERM-signaal wordt verzonden, probeert het systeem de applicatie te stoppen met de systeemaanroep ā€œproc_terminateā€.

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Kon taak niet beƫindigen: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Fallback-optie gebruikt om taak te beƫindigen...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Kon signaal niet verzenden naar taak: %d: %s", error, strerror(error));
		} 
...

Ondanks zijn naam kan proc_terminate, onder de motorkap, niet alleen psignal met SIGTERM versturen, maar ook SIGKILL.

Indirecte beĆ«indiging — beperking op middelen

Een interessanter geval kan worden gezien in een andere systeemaanroep process_policy. Standaard gebruik van deze systeemaanroep is het beperken van de middelen van applicaties, bijvoorbeeld voor een indexeerprogramma de beperking op de quotum van de verwerkte tijd en het geheugen, zodat het systeem niet aanzienlijk vertraagd wordt door het cachen van bestanden. Als de applicatie de beperking op middelen bereikt, zoals te zien is in de functie proc_apply_resource_actions, wordt het proces een SIGKILL-signaal verzonden.

Hoewel deze systeemaanroep potentieel processen kan beƫindigen, heeft het systeem de rechten van het proces dat de systeemaanroep doet niet adequaat gecontroleerd. In feite bestond de controle voordien, maar het is voldoende om de alternatieve vlag PROC_POLICY_ACTION_SET te gebruiken om deze voorwaarde te omzeilen.

Als we hier de CPU-gebruikquota voor een toepassing ā€œbeperkenā€ (bijvoorbeeld alleen 1 ns toestaan), kunnen we elk proces in het systeem beĆ«indigen. Zo kan malware elk proces op het systeem beĆ«indigen, inclusief dat van antivirussoftware. Ook interessant is het effect bij het beĆ«indigen van het proces met pid 1 (launchctl) — kernel panic bij het proberen een SIGKILL-signaal te verwerken šŸ™‚

Hoe processen en kernelextensies in macOS te beschermen

Hoe het probleem op te lossen?

De meest directe manier om een proces te verbieden te worden beƫindigd, is door de functiepointer in de systeemaanroep-tabel te vervangen. Helaas is deze methode om verschillende redenen niet triviaal.

Ten eerste is het symbool dat verantwoordelijk is voor de locatie van sysent in het geheugen niet alleen een privaat symbool van de XNU-kernel, maar kan het ook niet worden gevonden in de symbolen van de kernel. We moeten heuristische zoekmethoden gebruiken, zoals dynamische disassemblage van de functie en het zoeken naar de pointer daarin.

Ten tweede hangt de structuur van de vermeldingen in de tabel af van de vlaggen waarmee de kernel is samengesteld. Als de vlag CONFIG_REQUIRES_U32_MUNGING is gedefinieerd, zal de grootte van de structuur veranderen — er wordt een extra veld toegevoegd. sy_arg_munge32. Er moet een extra controle worden uitgevoerd op de vlag waarmee de kernel is gecompileerd; als alternatief kunnen de pointer naar functies vergeleken worden met bekende.

struct sysent {         /* systeemaanroep tabel */
        sy_call_t       *sy_call;       /* uitvoerende functie */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* systeemaanroep-argumenten munger voor 32-bits proces */
#endif
        int32_t         sy_return_type; /* systeemaanroep-returntypes */
        int16_t         sy_narg;        /* aantal argumenten */
        uint16_t        sy_arg_bytes;   /* Totale grootte van argumenten in bytes voor
                                         * 32-bits systeemaanroepen
                                         */
};

Gelukkig biedt Apple in de moderne versies van macOS een nieuwe API voor procesbeheer. De Endpoint Security API stelt klanten in staat om veel verzoeken aan andere processen te autoriseren. Zo kunnen alle signalen naar processen worden geblokkeerd, inclusief het SIGKILL-signaal met behulp van de eerder genoemde 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 in de kernel een MAC-beleid worden geregistreerd, dat een beschermingsmethode tegen signalen biedt (policy proc_check_signal), maar deze API wordt niet officieel ondersteund.

Bescherming van de kernelextensie

Naast de bescherming van processen in het systeem is ook de bescherming van de kernelextensie (kext) noodzakelijk. macOS biedt ontwikkelaars een framework voor het gemakiger ontwikkelen van IOKit apparaatstuurprogramma's. Naast middelen voor het werken met apparaten, biedt IOKit methoden voor driver stacking via instanties van C++ klassen. Een applicatie in de userspace kan een geregistreerde klasseninstantie 'vinden' om verbinding te maken tussen de kernel en de userspace.

Voor het detecteren van het aantal klasseninstanties in het systeem is er de utiliteit ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Elke kernelextensie die zich wil registreren in de driver stack, moet een klasse definiƫren die is afgeleid van IOService, bijvoorbeeld my_kext_ioservice in dit geval. Het aansluiten van gebruikersapplicaties resulteert in het creƫren van een nieuwe instantie van een klasse, die is afgeleid van IOUserClient, in ons voorbeeld my_kext_iouserclient.

Bij het proberen om een driver uit het systeem te ontladen (commando kextunload) wordt de virtuele functie ā€œbool terminate(IOOptionBits options)ā€ aangeroepen. Het is voldoende om false terug te geven bij het aanroepen van de terminate functie bij een ontlaadpoging om kextunload te verbieden.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Ontladen is niet toegestaan, false retourneren
    return false;
  }

  return super::terminate(options);
}

De vlag IsUnloadAllowed kan door IOUserClient worden ingesteld tijdens de laadtijd. Bij ontlaadbeperkingen zal het commando kextunload de volgende uitvoer retourneren:

admin@admins-Mac drivermanager % sudo kextunload .\/test.kext
Wachtwoord:
(kernel) Kan kext my.kext.test niet verwijderen; diensten konden niet beƫindigd worden - 0xe00002c7.
Niet gelukt om my.kext.test te ontladen - (iokit/common) niet-ondersteunde functie.

Een soortgelijke bescherming moet ook voor IOUserClient worden uitgevoerd. Instanties van klassen kunnen worden ontladen via de userspace functie IOKitLib ā€œIOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);ā€. Men kan false retourneren bij het aanroepen van het commando ā€œterminateā€ totdat de userspace applicatie 'sterft', dat wil zeggen totdat de functie ā€œclientDiedā€ wordt aangeroepen.

Bestandsbescherming

Om bestanden te beschermen, is het voldoende om de Kauth API te gebruiken, die toegang tot bestanden kan beperken. Apple biedt ontwikkelaars notificaties over verschillende gebeurtenissen in de scope; voor ons zijn de operaties KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA en KAUTH_VNODE_DELETE_CHILD belangrijk. Toegang tot bestanden kan het beste op basis van het pad worden beperkt — we gebruiken de API ā€œvn_getpathā€ om het pad naar het bestand te verkrijgen en vergelijken het padprefix. Merk op dat het systeem, om het hernoemen van mappen met bestanden te optimaliseren, niet elke bestandstoegang autoriseert, maar alleen toegang tot de map die is hernoemd. We moeten het bovenliggende pad vergelijken en KAUTH_VNODE_DELETE daarvoor beperken.

Hoe processen en kernelextensies in macOS te beschermen

Een nadeel van deze aanpak kan de lage prestaties zijn bij een toenemend aantal prefixen. Om ervoor te zorgen dat de vergelijking niet gelijk is aan O(prefix*length), waarbij prefix het aantal prefixen is en length de lengte van de string, kan een deterministische eindige automaat (DEA) worden gebruikt, gebouwd op basis van de prefixen.

Laten we de manier bekijken om een DEA te construeren voor deze set prefixen. We initialiseren cursors aan het begin van elke prefix. Als alle cursors op hetzelfde teken wijzen, verhogen we elke cursor met ƩƩn teken en onthouden we dat de lengte van de gelijke string met ƩƩn is toegenomen. Als er twee cursors zijn met verschillende symbolen, splitsen we de cursors in groepen op basis van het teken waarop ze wijzen en herhalen we het algoritme voor elke groep.

In het eerste geval (wanneer alle symbolen onder de cursors gelijk zijn) krijgen we een DEA-toestand die slechts ƩƩn overgang heeft voor de gelijke string. In het tweede geval krijgen we een overgangstabel van 256 (aantal symbolen en maximaal aantal groepen) naar de volgende toestanden, verkregen bij een recursieve functieaanroep.

Laten we een voorbeeld bekijken. Voor de set prefixen (ā€œ/foo/bar/tmp/ā€, ā€œ/var/db/foo/ā€, ā€œ/foo/bar/aba/ā€, ā€œfoo/bar/aac/ā€) kan de volgende DEA worden verkregen. In de afbeelding worden alleen de overgangen weergegeven die naar andere toestanden leiden; andere overgangen zijn geen eindige.

Hoe processen en kernelextensies in macOS te beschermen

Tijdens het doorgaan door de DEA-toestanden kunnen er 3 gevallen zijn.

  1. Het eindtoestand is bereikt — het pad is beveiligd, we beperken de operaties KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA en KAUTH_VNODE_DELETE_CHILD.
  2. De finale status is niet bereikt, maar het pad ā€˜eindigde’ (er werd een null-terminator bereikt) — het pad is parent, KAUTH_VNODE_DELETE moet worden beperkt. Opmerkelijk is dat als de vnode een map is, er een ā€˜/’ aan het einde moet worden toegevoegd, anders kan er een beperking plaatsvinden naar het bestand ā€˜/foor/bar/t’, wat onjuist is.
  3. De finale status is niet bereikt, het pad is niet geƫindigd. Geen van de prefixen komt overeen met wat er is gegeven, we voeren geen beperkingen in.

Conclusie

Het doel van de ontwikkelde beveiligingsoplossingen is het verhogen van het beveiligingsniveau voor de gebruiker en zijn gegevens. Enerzijds wordt dit doel bereikt door de ontwikkeling van de Acronis-software die de kwetsbaarheden afdekt waar het besturingssysteem zelf ā€˜zwak’ is. Anderzijds moet ook de verbetering van die beveiligingsaspecten die aan de OS-kant verbeterd kunnen worden niet worden verwaarloosd, vooral omdat het dichten van dergelijke kwetsbaarheden onze eigen weerbaarheid als product verhoogt. De kwetsbaarheid werd gerapporteerd aan het Apple Product Security Team en is verholpen in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Hoe processen en kernelextensies in macOS te beschermen

Dit kan alleen worden gedaan als uw hulpprogramma officieel in de kernel is geĆÆnstalleerd. Dit betekent dat er geen achterdeurtjes zijn voor externe en ongewenste software. Echter, zoals u ziet, zelfs voor de bescherming van legitieme programma's, zoals antivirus en back-upsysteem, moet er moeite worden gedaan. Maar nu zullen de nieuwe Acronis-producten voor macOS extra bescherming hebben tegen uitloading uit het systeem.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster