Kako zaščititi procese in razširitve jedra v sistemu macOS

Pozdravljeni, Habr! Danes bi rad govoril o tem, kako lahko zaščitite procese pred napadi napadalcev v macOS. To je na primer uporabno za protivirusni sistem ali sistem za varnostno kopiranje, še posebej, ker pod macOS obstaja več načinov za "ubijanje" procesa. Preberite o tem in načinih zaščite pod rezom.

Kako zaščititi procese in razširitve jedra v sistemu macOS

Klasičen način "ubijanja" procesa

Dobro znan način za "ubijanje" procesa je pošiljanje signala SIGKILL procesu. Skozi bash lahko pokličete standardni »kill -SIGKILL PID« ali »pkill -9 NAME« za uničenje. Ukaz »kill« je znan že iz časov UNIX-a in ni na voljo le v sistemu macOS, ampak tudi v drugih sistemih, podobnih UNIX-u.

Tako kot v sistemih, podobnih UNIX-u, vam macOS omogoča prestrezanje vseh signalov procesu razen dveh - SIGKILL in SIGSTOP. Ta članek se bo osredotočil predvsem na signal SIGKILL kot signal, ki povzroči zaustavitev procesa.

posebnosti macOS

V sistemu macOS sistemski klic kill v jedru XNU pokliče funkcijo psignal(SIGKILL,...). Poskusimo videti, katera druga uporabniška dejanja v uporabniškem prostoru lahko pokliče funkcija psignal. Izločimo klice funkcije psignal v notranjih mehanizmih jedra (čeprav morda niso trivialni, jih bomo pustili za drug članek 🙂 - preverjanje podpisa, napake v pomnilniku, obravnavanje izhoda/prekinitve, kršitve zaščite datotek itd. .

Začnimo pregled s funkcijo in pripadajočim sistemskim klicem zaključiti_s_koristnim. Vidi se, da poleg klasičnega kill klica obstaja alternativni pristop, ki je specifičen za operacijski sistem macOS in ga v BSD ni. Tudi principi delovanja obeh sistemskih klicev so podobni. So neposredni klici funkcije jedra psignal. Upoštevajte tudi, da se pred uničenjem procesa izvede preverjanje "cansignal" - ali lahko proces pošlje signal drugemu procesu; sistem na primer nobeni aplikaciji ne dovoli uničenja sistemskih procesov.

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

Standardni način za ustvarjanje demonov ob zagonu sistema in nadzor nad njihovo življenjsko dobo je launchd. Upoštevajte, da so viri za staro različico launchctl do macOS 10.10, primeri kode so na voljo za ilustracijo. Moderni launchctl pošilja signale launchd prek XPC, logika launchctl je bila premaknjena vanj.

Poglejmo, kako natančno se ustavijo aplikacije. Pred pošiljanjem signala SIGTERM se poskuša aplikacija zaustaviti s sistemskim klicem “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));
		} 
...
<>

Pod pokrovom lahko proc_terminate kljub svojemu imenu pošlje ne samo psignal s SIGTERM, ampak tudi SIGKILL.

Posredna usmrtitev - Omejitev virov

Bolj zanimiv primer lahko vidimo v drugem sistemskem klicu politika_procesa. Običajna uporaba tega sistemskega klica je omejevanje virov aplikacije, na primer za indekser za omejevanje CPE-ja in pomnilniških kvot, tako da sistem ni bistveno upočasnjen zaradi dejavnosti predpomnjenja datotek. Če je aplikacija dosegla svojo omejitev virov, kot je razvidno iz funkcije proc_apply_resource_actions, se procesu pošlje signal SIGKILL.

Čeprav bi ta sistemski klic potencialno lahko uničil proces, sistem ni ustrezno preveril pravic procesa, ki kliče sistemski klic. Pravzaprav preverjam obstajal, vendar je dovolj, da uporabite alternativno zastavico PROC_POLICY_ACTION_SET, da zaobidete ta pogoj.

Torej, če "omejite" kvoto uporabe procesorja aplikacije (na primer, dovolite samo 1 ns za izvajanje), potem lahko ubijete kateri koli proces v sistemu. Tako lahko zlonamerna programska oprema ubije kateri koli proces v sistemu, vključno s protivirusnim procesom. Zanimiv je tudi učinek, ki se pojavi pri ubijanju procesa s pid 1 (launchctl) - panika jedra pri poskusu obdelave signala SIGKILL :)

Kako zaščititi procese in razširitve jedra v sistemu macOS

Kako rešiti težavo?

Najpreprostejši način za preprečitev uničenja procesa je zamenjava funkcijskega kazalca v tabeli sistemskih klicev. Na žalost je ta metoda netrivialna iz več razlogov.

Prvič, simbol, ki nadzoruje pomnilniško lokacijo sysenta, ni samo zaseben za simbol jedra XNU, ampak ga ni mogoče najti v simbolih jedra. Uporabiti boste morali hevristične metode iskanja, kot je dinamično razstavljanje funkcije in iskanje kazalca v njej.

Drugič, struktura vnosov v tabeli je odvisna od zastavic, s katerimi je bilo prevedeno jedro. Če je deklarirana zastavica CONFIG_REQUIRES_U32_MUNGING, bo velikost strukture spremenjena – dodano bo dodatno polje sy_arg_munge32. Potrebno je opraviti dodatno preverjanje, da se ugotovi, s katero zastavico je bilo jedro prevedeno, ali pa preveriti funkcijske kazalce glede na znane.

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

Na srečo Apple v sodobnih različicah macOS ponuja nov API za delo s procesi. Endpoint Security API omogoča odjemalcem avtorizacijo številnih zahtev drugim procesom. Tako lahko z zgoraj omenjenim API-jem blokirate vse signale procesom, vključno s signalom 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;
}

Podobno je mogoče v jedru registrirati pravilnik MAC, ki zagotavlja metodo zaščite signala (pravilnik proc_check_signal), vendar API ni uradno podprt.

Zaščita razširitve jedra

Poleg zaščite procesov v sistemu je nujna tudi zaščita same razširitve jedra (kext). macOS ponuja razvijalcem ogrodje za preprost razvoj gonilnikov naprav IOKit. Poleg zagotavljanja orodij za delo z napravami IOKit ponuja metode za zlaganje gonilnikov z uporabo primerkov razredov C++. Aplikacija v uporabniškem prostoru bo lahko "našla" registrirani primerek razreda za vzpostavitev razmerja jedro-uporabniški prostor.

Za zaznavanje števila primerkov razreda v sistemu obstaja pripomoček ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Vsaka razširitev jedra, ki se želi registrirati v skladu gonilnikov, mora deklarirati razred, ki deduje od IOService, na primer my_kext_ioservice v tem primeru. Povezovanje uporabniških aplikacij povzroči ustvarjanje novega primerka razreda, ki deduje od IOUserClient, v primeru my_kext_iouserclient.

Ko poskušate odstraniti gonilnik iz sistema (ukaz kextunload), se pokliče navidezna funkcija »bool terminate(IOOptionBits options)«. Dovolj je, da vrnete false pri klicu za prekinitev, ko poskušate razbremeniti, da onemogočite kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Zastavico IsUnloadAllowed lahko med nalaganjem nastavi IOUserClient. Ko obstaja omejitev prenosa, bo ukaz kextunload vrnil naslednje rezultate:

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.

Podobno zaščito je treba narediti za IOUserClient. Primerke razredov je mogoče razložiti s funkcijo uporabniškega prostora IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Ko kličete ukaz »terminate«, lahko vrnete false, dokler aplikacija uporabniškega prostora ne »umre«, to pomeni, da funkcija »clientDied« ni poklicana.

Zaščita datotek

Za zaščito datotek zadostuje uporaba API-ja Kauth, ki omogoča omejitev dostopa do datotek. Apple razvijalcem zagotavlja obvestila o različnih dogodkih v obsegu, za nas so pomembne operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA in KAUTH_VNODE_DELETE_CHILD. Najlažji način za omejitev dostopa do datotek je po poti - uporabljamo API “vn_getpath”, da pridobimo pot do datoteke in primerjamo predpono poti. Upoštevajte, da za optimizacijo preimenovanja poti mape datotek sistem ne dovoli dostopa do vsake datoteke, ampak samo do same mape, ki je bila preimenovana. Potrebno je primerjati nadrejeno pot in zanjo omejiti KAUTH_VNODE_DELETE.

Kako zaščititi procese in razširitve jedra v sistemu macOS

Pomanjkljivost tega pristopa je lahko nizka zmogljivost, saj se število predpon povečuje. Da zagotovite, da primerjava ni enaka O(predpona*dolžina), kjer je predpona število predpon, dolžina dolžina niza, lahko uporabite deterministični končni avtomat (DFA), zgrajen s predponami.

Oglejmo si metodo za konstruiranje DFA za dani nabor predpon. Kazalce inicializiramo na začetku vsake predpone. Če vsi kazalci kažejo na isti znak, povečajte vsak kazalec za en znak in ne pozabite, da je dolžina iste vrstice večja za en znak. Če sta dva kurzorja z različnimi simboli, ju razdelite v skupine glede na simbol, na katerega kažeta, in ponovite algoritem za vsako skupino.

V prvem primeru (vsi znaki pod kazalci so enaki) dobimo stanje DFA, ki ima le en prehod vzdolž iste vrstice. V drugem primeru dobimo tabelo prehodov velikosti 256 (število znakov in maksimalno število skupin) v naslednja stanja, ki jih dobimo z rekurzivnim klicem funkcije.

Poglejmo si primer. Za nabor predpon (»/foo/bar/tmp/«, »/var/db/foo/«, »/foo/bar/aba/«, »foo/bar/aac/«) lahko dobite naslednje DFA. Slika prikazuje le prehode, ki vodijo v druga stanja; ostali prehodi ne bodo dokončni.

Kako zaščititi procese in razširitve jedra v sistemu macOS

Pri prehodu skozi stanja DKA so lahko 3 primeri.

  1. Končno stanje je doseženo - pot je zaščitena, omejujemo operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA in KAUTH_VNODE_DELETE_CHILD
  2. Končno stanje ni bilo doseženo, vendar se je pot “končala” (dosežen je bil ničelni terminator) - pot je nadrejena, potrebno je omejiti KAUTH_VNODE_DELETE. Upoštevajte, da če je vnode mapa, morate na koncu dodati '/', sicer jo lahko omeji na datoteko »/foor/bar/t«, kar ni pravilno.
  3. Končno stanje ni bilo doseženo, pot se ni končala. Nobena od predpon ne ustreza tej, ne uvajamo omejitev.

Zaključek

Cilj varnostnih rešitev, ki se razvijajo, je povečati stopnjo varnosti uporabnika in njegovih podatkov. Po eni strani je ta cilj dosežen z razvojem programskega izdelka Acronis, ki zapira tiste ranljivosti, kjer je sam operacijski sistem »šibek«. Po drugi strani pa ne smemo zanemariti krepitve tistih varnostnih vidikov, ki jih je mogoče izboljšati na strani OS, še posebej, ker zapiranje takšnih ranljivosti poveča našo lastno stabilnost kot izdelek. Ranljivost je bila prijavljena ekipi za varnost izdelkov Apple in je bila odpravljena v macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Kako zaščititi procese in razširitve jedra v sistemu macOS

Vse to je mogoče storiti le, če je bil vaš pripomoček uradno nameščen v jedro. To pomeni, da za zunanjo in neželeno programsko opremo ni takih vrzeli. Vendar, kot lahko vidite, tudi zaščita zakonitih programov, kot so protivirusni sistemi in sistemi za varnostno kopiranje, zahteva delo. Toda zdaj bodo novi izdelki Acronis za macOS imeli dodatno zaščito pred razkladanjem iz sistema.

Vir: www.habr.com

Dodaj komentar