Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

Tere, Habr! Täna tahaksin rääkida sellest, kuidas saate MacOS-is protsesse kaitsta ründajate rünnakute eest. Näiteks on see kasulik viirusetõrje- või varusüsteemi jaoks, eriti kuna macOS-is on protsessi "tapmiseks" mitu võimalust. Lugege selle ja lõike all oleva kaitsemeetodite kohta.

Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

Klassikaline viis protsessi "tapmiseks".

Tuntud viis protsessi "tapmiseks" on protsessile SIGKILL-i signaali saatmine. Bashi kaudu saate tapmiseks kutsuda standardset "kill -SIGKILL PID" või "pkill -9 NAME". "Kill" käsk on tuntud UNIX-i aegadest peale ja see on saadaval mitte ainult macOS-is, vaid ka teistes UNIX-i sarnastes süsteemides.

Täpselt nagu UNIX-laadsetes süsteemides, võimaldab macOS teil pealt kuulata kõiki protsessi signaale, välja arvatud kaks – SIGKILL ja SIGSTOP. See artikkel keskendub peamiselt SIGKILL-signaalile kui signaalile, mis põhjustab protsessi surmamise.

macOS-i eripärad

MacOS-is kutsub XNU tuumas olev kill-süsteemikutse funktsiooni psignal (SIGKILL,...). Proovime näha, milliseid teisi kasutajatoiminguid kasutajaruumis saab psignal-funktsioon kutsuda. Rohime välja psignal-funktsiooni väljakutsed kerneli sisemistes mehhanismides (kuigi need ei pruugi olla triviaalsed, jätame need mõne teise artikli jaoks 🙂 - allkirja kontrollimine, mäluvead, väljumise/lõpetamise käsitlemine, failikaitse rikkumised jne .

Alustame ülevaadet funktsiooni ja vastava süsteemikutsega terminate_with_payload. Näha on, et lisaks klassikalisele kill callile on olemas alternatiivne lähenemine, mis on spetsiifiline macOS operatsioonisüsteemile ja mida BSD-s ei leidu. Mõlema süsteemikõne tööpõhimõtted on samuti sarnased. Need on otsekutsed kerneli funktsioonile psignal. Samuti pange tähele, et enne protsessi tapmist kontrollitakse "kanalisignaali" - kas protsess suudab teisele protsessile signaali saata; süsteem ei luba näiteks ühelgi rakendusel süsteemiprotsesse tappa.

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

käivitatud

Käivitatakse tavaline viis deemonite loomiseks süsteemi käivitamisel ja nende eluea juhtimiseks. Pange tähele, et allikad on mõeldud launchctl vana versiooni kohta kuni macOS 10.10, koodinäited on illustratiivsed. Kaasaegne launchctl saadab käivitatud signaale XPC kaudu, launchctl loogika on sinna viidud.

Vaatame, kuidas täpselt rakendused peatatakse. Enne SIGTERM-signaali saatmist proovitakse rakendust peatada, kasutades süsteemikutset “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));
		} 
...
<>

Kapoti all saab proc_terminate vaatamata oma nimele saata mitte ainult psignaali koos SIGTERMiga, vaid ka SIGKILLiga.

Kaudne tapmine – ressursipiirang

Huvitavamat juhtumit võib näha teises süsteemikutses protsessi_poliitika. Seda süsteemikutset kasutatakse tavaliselt rakendusressursside piiramiseks, näiteks indekseerija jaoks, et piirata protsessori aega ja mälukvoote, et failide vahemällu salvestamine ei aeglustaks süsteemi oluliselt. Kui rakendus on saavutanud oma ressursipiirangu, nagu on näha funktsioonist proc_apply_resource_actions, saadetakse protsessile SIGKILL-signaal.

Kuigi see süsteemikutse võib protsessi tappa, ei kontrollinud süsteem piisavalt süsteemikutset kutsuva protsessi õigusi. Tegelikult kontrollib olemas, kuid sellest tingimusest möödahiilimiseks piisab alternatiivse lipu PROC_POLICY_ACTION_SET kasutamisest.

Seega, kui piirate rakenduse CPU kasutuskvooti (näiteks lubate töötada ainult 1 ns), võite süsteemis kõik protsessid tappa. Seega võib pahavara tappa kõik süsteemis olevad protsessid, sealhulgas viirusetõrjeprotsessid. Huvitav on ka efekt, mis tekib protsessi tapmisel pid 1-ga (launchctl) - kerneli paanika, kui proovite töödelda SIGKILL-signaali :)

Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

Kuidas probleemi lahendada?

Kõige lihtsam viis protsessi surmamise vältimiseks on asendada funktsioonikursor süsteemikutsete tabelis. Kahjuks pole see meetod mitmel põhjusel triviaalne.

Esiteks, sümbol, mis kontrollib sysenti mälu asukohta, ei ole mitte ainult XNU tuuma sümbolile privaatne, vaid seda ei leia ka kerneli sümbolites. Peate kasutama heuristlikke otsingumeetodeid, näiteks funktsiooni dünaamiliselt lahti võtma ja sellest kursorit otsima.

Teiseks sõltub tabeli kirjete struktuur lippudest, millega kernel kompileeriti. Kui on deklareeritud lipp CONFIG_REQUIRES_U32_MUNGING, muudetakse struktuuri suurust – lisatakse täiendav väli sy_arg_munge32. On vaja läbi viia täiendav kontroll, et teha kindlaks, millise lipuga kernel kompileeriti, või teise võimalusena kontrollida funktsiooninäitajaid teadaolevatega.

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

Õnneks pakub Apple macOS-i kaasaegsetes versioonides protsessidega töötamiseks uut API-d. Endpoint Security API võimaldab klientidel volitada palju taotlusi muude protsesside jaoks. Seega saate ülalmainitud API abil blokeerida protsessidele kõik signaalid, sealhulgas signaali SIGKILL.

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

Samamoodi saab tuumas registreerida MAC-poliitika, mis pakub signaalikaitsemeetodit (policy proc_check_signal), kuid API-d ametlikult ei toetata.

Kerneli laienduse kaitse

Lisaks protsesside kaitsmisele süsteemis on vajalik ka tuuma laienduse enda (kext) kaitsmine. macOS pakub arendajatele raamistikku IOKiti seadmedraiverite hõlpsaks arendamiseks. Lisaks seadmetega töötamise tööriistade pakkumisele pakub IOKit ka draiverite virnastamise meetodeid, kasutades C++ klasside eksemplare. Kasutajaruumis olev rakendus suudab "leida" klassi registreeritud eksemplari, et luua kerneli-kasutajaruumi suhe.

Klassijuhtumite arvu tuvastamiseks süsteemis on ioclasscount utiliit.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Kõik kerneli laiendused, mis soovivad registreerida draiveri virna, peavad deklareerima klassi, mis pärib IOService'ist, näiteks minu_kext_ioservice. Kasutajarakenduste ühendamine põhjustab klassi uue eksemplari loomise, mis pärib IOUserClient'ilt, näites my_kext_iouserclient.

Kui proovite draiverit süsteemist maha laadida (kextunload käsk), kutsutakse välja virtuaalne funktsioon "bool terminate (IOOptionBits options)". Kextunloadi keelamiseks piisab, kui tagastada kõne lõpetamiseks vale.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

IOUserClient saab laadimisel määrata lipu IsUnloadAllowed. Kui allalaadimise limiit on piiratud, tagastab käsk kextunload järgmise väljundi:

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.

Sarnane kaitse tuleb teha ka IOUserClienti jaoks. Klasside eksemplare saab maha laadida IOKitLib kasutajaruumi funktsiooni "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);" abil. Käsu "terminate" kutsumisel saate tagastada vale väärtuse, kuni kasutajaruumi rakendus "sureb", see tähendab, et funktsiooni "clientDied" ei kutsuta.

Failide kaitse

Failide kaitsmiseks piisab Kauthi API kasutamisest, mis võimaldab piirata juurdepääsu failidele. Apple annab arendajatele märguandeid erinevate rakendusalasse kuuluvate sündmuste kohta, meie jaoks on olulised toimingud KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD. Lihtsaim viis failidele juurdepääsu piiramiseks on tee – me kasutame faili tee leidmiseks ja tee prefiksi võrdlemiseks API-t “vn_getpath”. Pange tähele, et failide kaustateede ümbernimetamise optimeerimiseks ei luba süsteem juurdepääsu igale failile, vaid ainult ümbernimetatud kaustale endale. On vaja võrrelda vanemateed ja piirata selle jaoks KAUTH_VNODE_DELETE.

Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

Selle lähenemisviisi puuduseks võib olla madal jõudlus, kuna eesliidete arv suureneb. Tagamaks, et võrdlus ei oleks võrdne O-ga (eesliide*pikkus), kus prefiks on eesliidete arv, pikkus on stringi pikkus, võite kasutada eesliidete abil ehitatud deterministlikku lõplikku automaati (DFA).

Vaatleme meetodit DFA koostamiseks antud eesliidete komplekti jaoks. Initsialiseerime kursorid iga prefiksi alguses. Kui kõik kursorid osutavad samale märgile, suurendage iga kursorit ühe märgi võrra ja pidage meeles, et sama rea ​​pikkus on ühe võrra suurem. Kui on kaks erineva sümboliga kursorit, jagage kursorid rühmadesse vastavalt sümbolile, millele nad osutavad, ja korrake iga rühma jaoks algoritmi.

Esimesel juhul (kõik kursorite all olevad märgid on ühesugused) saame DFA oleku, millel on ainult üks üleminek samal real. Teisel juhul saame tabeli 256 suuruse üleminekute kohta (märkide arv ja maksimaalne rühmade arv) järgmistesse olekutesse, mis saadakse funktsiooni rekursiivsel väljakutsumisel.

Vaatame näidet. Eesliidete komplekti (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) jaoks saate järgmise DFA. Joonisel on näidatud ainult üleminekud, mis viivad teistesse olekutesse, muud üleminekud ei ole lõplikud.

Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

DKA olekuid läbides võib esineda 3 juhtu.

  1. Lõppseisund on saavutatud – tee on kaitstud, piirame toiminguid KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD
  2. Lõppolekusse ei jõutud, kuid tee "lõpetatud" (jõuti nullterminaatorini) - tee on vanem, vaja on piirata KAUTH_VNODE_DELETE. Pange tähele, et kui vnode on kaust, peate selle lõppu lisama '/', vastasel juhul võib see piirduda failiga "/foor/bar/t", mis on vale.
  3. Lõppseisu ei jõutud, tee ei lõppenud. Ükski eesliide ei vasta sellele, me ei kehtesta piiranguid.

Järeldus

Arendatavate turvalahenduste eesmärk on tõsta kasutaja ja tema andmete turvalisuse taset. Ühest küljest saavutatakse see eesmärk tarkvaratoote Acronise arendamisega, mis sulgeb need haavatavused, kus operatsioonisüsteem ise on “nõrk”. Teisest küljest ei tohiks me unustada nende turvaaspektide tugevdamist, mida saab OS-i poolel parandada, eriti kuna selliste haavatavuste sulgemine suurendab meie enda kui toote stabiilsust. Haavatavusest teatati Apple'i tooteturbe meeskonnale ja see parandati operatsioonisüsteemis macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Kuidas kaitsta protsesse ja kerneli laiendusi MacOS-is

Seda kõike saab teha ainult siis, kui teie utiliit on kernelisse ametlikult installitud. See tähendab, et välise ja soovimatu tarkvara jaoks pole selliseid lünki. Kuid nagu näete, nõuab isegi seaduslike programmide, näiteks viirusetõrje- ja varundussüsteemide kaitsmine tööd. Kuid nüüd on uutel MacOS-i Acronise toodetel täiendav kaitse süsteemist mahalaadimise eest.

Allikas: www.habr.com

Lisa kommentaar