Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Hei, Habr! Tänään haluaisin puhua siitä, kuinka voit suojata prosesseja hyökkääjien hyökkäyksiltä macOS:ssä. Tämä on hyödyllistä esimerkiksi virustorjunta- tai varmuuskopiojärjestelmälle, varsinkin kun macOS:ssä on useita tapoja "tappaa" prosessi. Lue tästä ja suojausmenetelmistä leikkauksen alla.

Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Klassinen tapa "tappaa" prosessi

Tunnettu tapa "tappaa" prosessi on lähettää SIGKILL-signaali prosessille. Bashin kautta voit kutsua tavallista "kill -SIGKILL PID" tai "pkill -9 NAME" tappaaksesi. "Kill"-komento on tunnettu UNIXin ajoista lähtien, ja se on saatavilla macOS:n lisäksi myös muissa UNIX-tyyppisissä järjestelmissä.

Aivan kuten UNIX-tyyppisissä järjestelmissä, macOS antaa sinun siepata kaikki signaalit prosessiin paitsi kaksi - SIGKILL ja SIGSTOP. Tämä artikkeli keskittyy ensisijaisesti SIGKILL-signaaliin signaalina, joka aiheuttaa prosessin lopettamisen.

macOS:n tiedot

MacOS:ssa XNU-ytimen kill-järjestelmäkutsu kutsuu psignal(SIGKILL,...)-funktiota. Yritetään nähdä, mitä muita käyttäjän toimintoja käyttäjätilassa psignal-funktio voi kutsua. Karsitaan pois kutsut psignal-funktiolle ytimen sisäisistä mekanismeista (vaikka ne saattavat olla ei-triviaaleja, jätämme ne toiseen artikkeliin 🙂 - allekirjoituksen varmistus, muistivirheet, poistumis-/päätekäsittely, tiedostojen suojausrikkomukset jne. .

Aloitetaan tarkastelu funktiolla ja vastaavalla järjestelmäkutsulla terminate_with_payload. Voidaan nähdä, että klassisen tappamiskutsun lisäksi on olemassa vaihtoehtoinen lähestymistapa, joka on macOS-käyttöjärjestelmäkohtainen ja jota ei löydy BSD:stä. Molempien järjestelmäkutsujen toimintaperiaatteet ovat myös samanlaiset. Ne ovat suoria kutsuja ytimen funktiolle psignal. Huomaa myös, että ennen prosessin lopettamista suoritetaan "cansignal"-tarkistus - voiko prosessi lähettää signaalin toiselle prosessille; järjestelmä ei esimerkiksi salli minkään sovelluksen tappaa järjestelmäprosesseja.

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äynnistetty

Vakiotapa luoda demoneja järjestelmän käynnistyksen yhteydessä ja hallita niiden käyttöikää on käynnistetty. Huomaa, että lähteet ovat vanhasta launchctl-versiosta macOS 10.10 asti, koodiesimerkit ovat havainnollistavia. Moderni launchctl lähettää laukaisusignaaleja XPC:n kautta, launchctl-logiikka on siirretty siihen.

Katsotaanpa, kuinka tarkalleen sovellukset pysäytetään. Ennen SIGTERM-signaalin lähettämistä sovellus yritetään pysäyttää "proc_terminate" järjestelmäkutsulla.

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

Konepellin alla oleva proc_terminate voi nimestään huolimatta lähettää paitsi p-signaalin SIGTERMillä, myös SIGKILLillä.

Epäsuora tappaminen - Resurssirajoitus

Mielenkiintoisempi tapaus voidaan nähdä toisessa järjestelmäkutsussa prosessin_käytäntö. Tämän järjestelmäkutsun yleinen käyttötarkoitus on rajoittaa sovellusresursseja, kuten indeksoija rajoittaa suorittimen aikaa ja muistikiintiöitä, jotta tiedostojen välimuistitoiminnot eivät merkittävästi hidasta järjestelmää. Jos sovellus on saavuttanut resurssirajansa, kuten proc_apply_resource_actions-funktiosta voidaan nähdä, prosessiin lähetetään SIGKILL-signaali.

Vaikka tämä järjestelmäkutsu voisi mahdollisesti tappaa prosessin, järjestelmä ei tarkistanut riittävästi järjestelmäkutsua kutsuvan prosessin oikeuksia. Oikeastaan ​​tarkistamassa olemassa, mutta tämän ehdon ohittamiseksi riittää käyttää vaihtoehtoista lippua PROC_POLICY_ACTION_SET.

Näin ollen, jos "rajoitat" sovelluksen suorittimen käyttökiintiötä (esimerkiksi sallit vain 1 ns:n suorittaa), voit tappaa minkä tahansa prosessin järjestelmässä. Näin ollen haittaohjelma voi tappaa minkä tahansa prosessin järjestelmässä, mukaan lukien virustentorjuntaprosessin. Mielenkiintoinen on myös se vaikutus, joka syntyy, kun prosessi lopetetaan pid 1:llä (launchctl) - ytimen paniikki yritettäessä käsitellä SIGKILL-signaalia :)

Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Kuinka ratkaista ongelma?

Yksinkertaisin tapa estää prosessin lopettaminen on korvata toimintoosoitin järjestelmäkutsutaulukossa. Valitettavasti tämä menetelmä ei ole triviaali monista syistä.

Ensinnäkin symboli, joka ohjaa sysentin muistipaikkaa, ei ole vain yksityinen XNU-ytimen symbolille, vaan sitä ei löydy ytimen symboleista. Sinun on käytettävä heuristisia hakumenetelmiä, kuten funktion dynaaminen purkaminen ja osoittimen etsiminen siitä.

Toiseksi taulukon merkintöjen rakenne riippuu lipuista, joilla ydin on käännetty. Jos CONFIG_REQUIRES_U32_MUNGING-lippu ilmoitetaan, rakenteen koko muuttuu - lisäkenttä lisätään sy_arg_munge32. On tarpeen suorittaa lisätarkistus sen määrittämiseksi, millä lipulla ydin on käännetty, tai vaihtoehtoisesti vertaa toimintoosoittimia tunnettuihin.

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

Onneksi nykyaikaisissa macOS-versioissa Apple tarjoaa uuden API:n prosessien kanssa työskentelyyn. Endpoint Security API:n avulla asiakkaat voivat valtuuttaa monia pyyntöjä muihin prosesseihin. Siten voit estää kaikki signaalit prosesseille, mukaan lukien SIGKILL-signaali, käyttämällä yllä mainittua API:ta.

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

Vastaavasti ytimeen voidaan rekisteröidä MAC-käytäntö, joka tarjoaa signaalinsuojausmenetelmän (policy proc_check_signal), mutta API:ta ei tueta virallisesti.

Ytimen laajennussuojaus

Järjestelmän prosessien suojaamisen lisäksi itse ytimen laajennuksen (kext) suojaaminen on myös välttämätöntä. macOS tarjoaa kehittäjille kehyksen IOKit-laiteajurien kehittämiseen helposti. Sen lisäksi, että IOKit tarjoaa työkaluja laitteiden kanssa työskentelemiseen, se tarjoaa menetelmiä ajurien pinoamiseen käyttämällä C++-luokkien esiintymiä. Käyttäjätilassa oleva sovellus pystyy "löytämään" luokan rekisteröidyn esiintymän muodostaakseen ytimen ja käyttäjätilan suhteen.

Järjestelmän luokkaesiintymien määrän havaitsemiseksi on ioclasscount-apuohjelma.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Jokaisen ytimen laajennuksen, joka haluaa rekisteröityä ajuripinoon, on ilmoitettava luokka, joka perii IOServicesta, esimerkiksi tässä tapauksessa my_kext_ioservice. Käyttäjäsovellusten yhdistäminen aiheuttaa luokan uuden ilmentymän luomisen, joka perii IOUserClientistä, esimerkissä my_kext_iouserclient.

Kun ajuria yritetään purkaa järjestelmästä (kextunload-komento), kutsutaan virtuaalifunktiota "bool terminate (IOoptionBits options)". Riittää, kun palauttaa false puhelun lopettamiseksi yritettäessä purkaa kextunloadin poistamiseksi käytöstä.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

IOUserClient voi asettaa IsUnloadAllowed-lipun latauksen aikana. Kun latausraja on olemassa, kextunload-komento palauttaa seuraavan tulosteen:

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.

Samanlainen suojaus on tehtävä IOUserClientille. Luokkien ilmentymiä voidaan purkaa IOKitLib-käyttäjätilan funktiolla "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Voit palauttaa false, kun kutsut "terminate"-komentoa, kunnes käyttäjätilasovellus "kuolee", eli "clientDied"-funktiota ei kutsuta.

Tiedostojen suojaus

Tiedostojen suojaamiseksi riittää käyttää Kauthin API:ta, jonka avulla voit rajoittaa tiedostoihin pääsyä. Apple tarjoaa kehittäjille ilmoituksia erilaisista alueen tapahtumista; meille toiminnot KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD ovat tärkeitä. Helpoin tapa rajoittaa tiedostoihin pääsyä on polku - käytämme “vn_getpath” API:ta tiedoston polun hakemiseen ja polun etuliitteen vertailuun. Huomaa, että tiedostokansiopolkujen uudelleennimeämisen optimoimiseksi järjestelmä ei valtuuta pääsyä jokaiseen tiedostoon, vaan vain itse uudelleennimettyyn kansioon. Pääpolkua on verrattava ja sille on rajoitettava KAUTH_VNODE_DELETE.

Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Tämän lähestymistavan haittana voi olla alhainen suorituskyky etuliitteiden määrän kasvaessa. Varmistaaksesi, että vertailu ei ole yhtä suuri kuin O (etuliite*pituus), jossa etuliite on etuliitteiden lukumäärä, pituus on merkkijonon pituus, voit käyttää etuliitteistä rakennettua determinististä äärellistä automaattia (DFA).

Tarkastellaan menetelmää DFA:n muodostamiseksi tietylle etuliitejoukolle. Alustamme osoittimet jokaisen etuliitteen alkuun. Jos kaikki osoittimet osoittavat samaa merkkiä, lisää kutakin kohdistinta yhdellä merkillä ja muista, että saman rivin pituus on yhdellä suurempi. Jos on kaksi kohdistinta eri symboleilla, jaa kursorit ryhmiin symbolin mukaan, johon ne osoittavat, ja toista algoritmi jokaiselle ryhmälle.

Ensimmäisessä tapauksessa (kaikki kohdistimien alla olevat merkit ovat samat) saadaan DFA-tila, jossa on vain yksi siirtymä samalla rivillä. Toisessa tapauksessa saamme taulukon koon 256 siirtymistä (merkkien määrä ja ryhmien enimmäismäärä) seuraaviin tiloihin, jotka saadaan kutsumalla funktiota rekursiivisesti.

Katsotaanpa esimerkkiä. Etuliitteiden joukolle ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") saat seuraavat tiedot DFA. Kuvassa on vain siirtymät, jotka johtavat muihin tiloihin, muut siirtymät eivät ole lopullisia.

Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Kun käydään läpi DKA-tilat, tapauksia voi olla 3.

  1. Lopullinen tila on saavutettu - polku on suojattu, rajoitamme toimintoja KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ja KAUTH_VNODE_DELETE_CHILD
  2. Lopullista tilaa ei saavutettu, mutta polku "päättyi" (nollapääte saavutettiin) - polku on vanhempi, on tarpeen rajoittaa KAUTH_VNODE_DELETE. Huomaa, että jos vnode on kansio, sinun on lisättävä '/' loppuun, muuten se voi rajoittaa sen tiedostoon “/foor/bar/t”, mikä on virheellinen.
  3. Lopullista tilaa ei saavutettu, polku ei päättynyt. Mikään etuliite ei vastaa tätä, emme aseta rajoituksia.

Johtopäätös

Kehitettävien tietoturvaratkaisujen tavoitteena on nostaa käyttäjän ja hänen tietojensa turvallisuustasoa. Toisaalta tämä tavoite saavutetaan kehittämällä Acronis-ohjelmistotuotetta, joka sulkee ne haavoittuvuudet, joissa käyttöjärjestelmä itsessään on "heikko". Toisaalta emme saa laiminlyödä niiden turvallisuusnäkökohtien vahvistamista, joita voidaan parantaa käyttöjärjestelmäpuolella, varsinkin kun tällaisten haavoittuvuuksien sulkeminen lisää omaa vakautta tuotteena. Haavoittuvuudesta ilmoitettiin Applen tuoteturvatiimille, ja se on korjattu macOS 10.14.5:ssä (https://support.apple.com/en-gb/HT210119).

Kuinka suojata prosesseja ja ytimen laajennuksia macOS:ssä

Kaikki tämä voidaan tehdä vain, jos apuohjelmasi on virallisesti asennettu ytimeen. Eli ulkoisille ja ei-toivotuille ohjelmistoille ei ole olemassa tällaisia ​​porsaanreikiä. Kuten näette, jopa laillisten ohjelmien, kuten virustorjunta- ja varmuuskopiointijärjestelmien, suojaaminen vaatii työtä. Mutta nyt uusilla Acronis-tuotteilla macOS:lle on lisäsuojaus järjestelmästä purkamista vastaan.

Lähde: will.com

Lisää kommentti