Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

Tere, Habr! Täna soovin rääkida sellest, kuidas kaitsta protsesse küberkurjategijate rünnakute eest macOS-is. See on eriti oluline viirusetõrje või varundussüsteemi jaoks, arvestades, et macOS-is on mitmeid võimalusi protsessi "tapmiseks". Selle ja kaitsemeetodite kohta loe allpool.

Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

Klassikaline viis protsessi "tapmiseks"

Kõigile tuntud viis protsessi "tapmiseks" on saata SIGKILL signaal protsessile. Bash'i kaudu võib kasutada standardseid käske "kill -SIGKILL PID" või "pkill -9 NAME" tapmiseks. Käsk "kill" on tuntud juba UNIX-i aegadest ja on saadaval mitte ainult macOS-is, vaid ka teistes UNIX-taolistes süsteemides.

Nii nagu UNIX-taolistes süsteemides, võimaldab macOS salvestada kõik protsessile saadetud signaalid, välja arvatud kaks — SIGKILL ja SIGSTOP. Selles artiklis käsitletakse peamiselt SIGKILL signaali, mis põhjustab protsessi tapmise.

macOS-i spetsiifika

macOS-is kutsesüsteemi kill, XNU tuumas kutsub esile psignal(SIGKILL,…) funktsiooni. Vaadakem, milliseid veel tegevusi kasutaja userspace'is võib psignal funktsiooni kutsuda. Jätame välja psignal funktsiooni kutseid tuuma sisemiste mehhanismide kaudu (kuigi need võivad samuti olla mitte triviaalsete, kuid jätame need teise artikli jaoks 🙂 — allkirjakontroll, mäluhäired, exit/terminate töötlemine, failikaitse rikkumine jne).

Alustame ülevaadet funktsiooni ja vastava süsteemi kutsega terminate_with_payload. Näha on, et lisaks klassikalisele kill kutsele on olemas alternatiivne lähenemine, mis on spetsiifiline macOS operatsioonisüsteemile ja ei esine BSD-s. Mõlema süsteemi kutse tööpõhimõtted on samuti sarnased. Need on otsesed kutsed psignal tuumafunktsioonile. Samuti tasub märkida, et enne protsessi tapmist tehakse kontroll “cansignal” — kas protsess saab saata signaali teisele protsessile, süsteem ei lase ükskõik millisel rakendusel tappa süsteemiprotsesse näiteks.

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

Tavaline viis süsteemide käivitamiseks ja nende eluaja kontrollimiseks on launchd. Tahan rõhutada, et allpool toodud koodinäidised on vanema versiooni launchctl'i jaoks enne macOS 10.10, ja need on esitatud illustratiivse näitena. Kaasaegne launchctl saadab käsklusi launchd'le läbi XPC, ning launchctl'i loogika on sinna üle viidud.

Vaatame, kuidas rakenduste peatamine tegelikult toimub. Enne SIGTERM signaali saatmist üritatakse rakendust peatada süsteemikutsungi “proc_terminate” abil.

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

Kuigi proc_terminate on selline nimi, võib see saata mitte ainult psignale SIGTERM, vaid ka SIGKILL.

Kaudne tapmine — ressursipiirangud

Teise süsteemi süsteemikutsungi huvitavam juhtum process_policy. Selle süsteemikutsungi tavaline kasutus on rakenduste ressursipiirangud, näiteks indeksija protsessori aja ja mälu kvota piiramine, et süsteem ei aeglustuks oluliselt faili vahemälu toimingutest. Kui rakendus saavutab ressursipiirangu, nagu nähtav funktsioonis proc_apply_resource_actions, saadetakse protsessile SIGKILL signaal.

Kuigi see süsteemikutsung võib potentsiaalselt protsessi tappa, ei kontrollinud süsteem tõhusalt protsessi õigusi, mis süsteemikutsungit kutsus. Tegelikult kontroll oli olemas, kuid piisab alternatiivse flagi PROC_POLICY_ACTION_SET kasutamisest selle tingimuse möödaminekuks.

Siit, kui piirata CPU kasutuskvooti rakenduse poolt (näiteks lubada töötada ainult 1 ns), siis saab tappa ükskõik millise protsessi süsteemis. Nii võib pahatahtlik programm tappa ükskõik millise protsessi, sealhulgas ka viirusetõrje protsessi. Samuti on huvitav efekt, mis tekib protsessi tapmisel, mille pid on 1 (launchctl) — kernel panic, kui proovida töödelda signaali SIGKILL 🙂.

Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

Kuidas probleemi lahendada?

Otsem viis protsessi tapmise keelamiseks on asendada viitamine funktsioonile süsteemikutsungite tabelis. Kahjuks on see meetod paljuski keeruline erinevatel põhjustel.

Esiteks, sümbol, mis vastutab sysent'i asukoha eest mälu, ei ole mitte ainult XNU tuuma eraldi sümbol, vaid seda ei saa ka leida tuuma sümbolitest. Peab kasutama heuristika meetodeid otsimiseks, nagu dünaamiline disassembli funktsioon ja viitamise otsimine selles.

Teiseks, tabeli kirje struktuur sõltub lippudest, millega tuum on kokku pandud. Kui lipp CONFIG_REQUIRES_U32_MUNGING on deklareeritud, siis struktuuri suurust muudetakse – lisatakse täiendav väli. sy_arg_munge32. On oluline teha täiendav kontroll, millise lipuga tuum on kompileeritud, näiteks võrrelda funktsioonide silte tuntud väärtustega.

struct sysent {         /* süsteemi kõne tabel */
        sy_call_t       *sy_call;       /* teostav funktsioon */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* süsteemi kõne argumendi muutja 32-bitiste protsesside jaoks */
#endif
        int32_t         sy_return_type; /* süsteemi kõne tagastamise tüübid */
        int16_t         sy_narg;        /* argumendi arv */
        uint16_t        sy_arg_bytes;   /* Argumendi kogusumma baitides 32-bitiste süsteemi kõnede jaoks */
};

Õnneks pakub Apple kaasaegsetes macOS versioonides uut API-d, mis võimaldab töötada protsessidega. Endpoint Security API võimaldab klientidel autoriseerida paljusid päringuid teistele protsessidele. Nii saab blokeerida mis tahes signaalid protsessidele, sealhulgas signaali SIGKILL eelnevalt mainitud API abil.

#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;
}

Sarnaselt saab tuumas registreerida MAC-poliitika, mis pakub signaalide kaitse meetodit (poliitika proc_check_signal), kuid API-d ei toetata ametlikult.

Tuuma laienduse kaitse

Lisaks süsteemi protsesside kaitsmisele on hädavajalik kaitsta ka tuuma laiendust (kext). macOS pakub arendajatele raamistikku IOKit, mis lihtsustab seadmejuhtide arendamist. Lisaks seadmete käsitlemise vahenditele tagab IOKit ka draiverite ühendamise (driver stacking) meetodid C++ klasside eksemplaride abil. Kasutusrikkes rakendus suudab „leida” registreeritud klassi eksemplari kernel-userspace'i ühendamiseks.

Süsteemis klasside eksemplaride arvu avastamiseks on olemas utiliit ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Iga tuuma laiendus, mis soovib registreerida end draiverite kihis, peab kuulutama klassi, mis on pärinud IOService'ist, näiteks antud juhul my_kext_ioservice. Kasutajarakenduste ühendamine kutsub esile uue klassi eksemplari loomise, mis pärandub IOUserClient'ist, näites my_kext_iouserclient.

Kui proovite draiverit süsteemist eemaldada (käsk kextunload), kutsutakse virtuaalne funktsioon "bool terminate(IOOptionBits options)". Piisab, kui tagastada false terminate-funktsiooni kutsel, et keelata kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Eemaldamine ei ole lubatud, tagastame false
    return false;
  }

  return super::terminate(options);
}

Lipp IsUnloadAllowed võib olla määratud IOUserClienti laadimise ajal. Kui laadimine on piiratud, tagastab käsk kextunload järgmise väljundi:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Parool:
(kernel) Ei saa eemaldada kext my.kext.test; teenused ei suutnud lõpetada - 0xe00002c7.
Ei õnnestunud eemaldada my.kext.test - (iokit/common) toetamatud funktsioon.

Sarnane kaitse tuleb rakendada ka IOUserClientile. Klasside eksemplarid saab eemaldada userspace-funktsiooni IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);" abil. Saate tagastada false "terminate"-käskluse kutsel, kuni userspace-rakendus ei "sure", st kuni kutse "clientDied" ei toimu.

Failide kaitse

Failide kaitsmiseks piisab Kauth API kasutamisest, mis võimaldab piirata juurdepääsu failidele. Apple annab arendajatele teateid erinevate sündmuste kohta scopis; meie jaoks on olulised operatsioonid KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD. Failide juurdepääsu piiramine on kõige lihtsam tee alusel — kasutame API-d "vn_getpath" failitee saamiseks ja teostame tee prefiksi võrdlemise. Tuleb märkida, et süsteem ei autoriseeri ligipääsu iga failile folderitega, vaid ainult nimetatavale kaustale, mille nime on muudetud. Tuleb võrrelda vanemat teed ja piirata KAUTH_VNODE_DELETE selle puhul.

Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

Selle lähenemise miinuseks võib olla madal jõudlus, kui prefiksite arv suureneb. Et võrrelda ei oleks O(prefix*length), kus prefix on prefiksite arv ja length on stringi pikkus, võib kasutada prefiksitele põhinevat deterministlikku lõppautomaat (DFA).

Vaadakem, kuidas ehitada DKA antud eessõnade komplektile. Algatame kursorid igas eessõnas algusesse. Kui kõik kursorid osutavad samale sümbolile, siis suurendame iga kursori ühe sümboli võrra ja mäletame, et sama rea pikkus on suurem ühe võrra. Kui on olemas kaks kursorit, mille all olevad sümbolid erinevad, jagame kursorid rühmadesse sümboli järgi, millele nad osutavad, ja kordame algoritmi iga rühma jaoks.

Esimesel juhul (kui kõik kursorite all olevad sümbolid on samad) saame DKA oleku, millel on ainult üks üleminek sama rea järgi. Teisel juhul saame üleminekute tabeli suurusega 256 (sümbolite arv ja maksimaalne rühmade arv) järgmistele olekutele, mis saadakse funktsiooni rekursiivse väljakutse käigus.

Vaatleme näidet. Eessõnade komplekti jaoks ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") saab luua järgmise DKA. Joonisel on näidatud ainult üleminekud, mis viivad teistesse olekutesse, teised üleminekud ei ole lõppstaadiumid.

Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

DKA olekutes liikudes võib esineda 3 olukorda.

  1. Lõpufaasi on saavutatud — tee on kaitstud, piirates KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD operatsioone.
  2. Lõpufaasi ei ole saavutatud, kuid tee "lõppes" (null-terminaator saavutati) — tee on vanem, KAUTH_VNODE_DELETE tuleb piirata. Tõnote, et kui vnode on kaust, tuleb lõppu lisada ‘/’, vastasel juhul võib piirata faili “/foor/bar/t”, mis on vale.
  3. Lõpufaasi ei ole saavutatud, tee ei ole lõppenud. Ükski eelmäng ei vasta antud, piiranguid ei kehtestata.

Kokkuvõte

Arendatavatele turvalahendustele on seatud eesmärgiks kasutajate ja nende andmete turvalisuse taseme tõstmine. Esiteks saavutatakse see Acronise tarkvara arendamise kaudu, mis katab neid haavatavusi, kus operatsioonisüsteem ise on «nõrk». Teiseks ei tohiks unustada ka OS-poolsete turvalisuse aspektide tugevdamist, eriti kuna selliste haavatavuste sulgemine suurendab meie toote enda vastupidavust. Haavatavusest teatas Apple Product Security Team ning see parandati macOS 10.14.5 versioonis (https://support.apple.com/en-gb/HT210119).

Kuidas kaitsta protsesse ja kernelilaiendeid macOS-is

Seda saab teha ainult juhul, kui teie utiliit on ametlikult installitud tuumasse. See tähendab, et välise ja soovimatu tarkvara jaoks pole selliseid kõrvalteid. Kuid, nagu näete, tuleb isegi legitiimsete programmide, nagu viirusetõrje ja varundussüsteem, kaitsmiseks palju vaeva näha. Kuid nüüd saavad uued Acronise tooted macOS-i jaoks lisakaitse süsteemist väljakandmise eest.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster