Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Ahoj Habr! Dnes by som chcel hovoriť o tom, ako môžete chrániť procesy pred útokmi útočníkov v systéme MacOS. Je to užitočné napríklad pre antivírusový alebo záložný systém, najmä preto, že v systéme MacOS existuje niekoľko spôsobov, ako „zabiť“ proces. Prečítajte si o tom a spôsoboch ochrany pod rezom.

Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Klasický spôsob, ako „zabiť“ proces

Dobre známy spôsob, ako „zabiť“ proces, je poslať do procesu signál SIGKILL. Prostredníctvom bash môžete zabiť štandardne „kill -SIGKILL PID“ alebo „pkill -9 NAME“. Príkaz „kill“ je známy už od čias UNIX a je dostupný nielen na macOS, ale aj na iných systémoch podobných UNIX.

Rovnako ako v systémoch podobných UNIX, macOS vám umožňuje zachytiť akékoľvek signály procesu okrem dvoch - SIGKILL a SIGSTOP. Tento článok sa primárne zameria na signál SIGKILL ako signál, ktorý spôsobí zabitie procesu.

špecifiká macOS

V systéme macOS systémové volanie kill v jadre XNU volá funkciu psignal(SIGKILL,...). Skúsme sa pozrieť, aké ďalšie akcie používateľa v používateľskom priestore možno volať pomocou funkcie psignal. Vylúčme volania funkcie psignal vo vnútorných mechanizmoch jadra (hoci môžu byť netriviálne, necháme si ich na iný článok 🙂 - overenie podpisu, chyby pamäte, spracovanie ukončenia/ukončenia, porušenie ochrany súborov atď. .

Začnime prehľad funkciou a príslušným systémovým volaním termin_with_payload. Je vidieť, že okrem klasického kill call existuje aj alternatívny prístup, ktorý je špecifický pre operačný systém macOS a v BSD sa nenachádza. Princípy fungovania oboch systémových volaní sú tiež podobné. Sú to priame volania funkcie jadra psignal. Všimnite si tiež, že pred zabitím procesu sa vykoná kontrola „kansignálu“ – či proces môže poslať signál inému procesu; systém napríklad neumožňuje žiadnej aplikácii zabíjať systémové procesy.

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

spustený

Spustí sa štandardný spôsob vytvárania démonov pri štarte systému a riadenia ich životnosti. Upozorňujeme, že zdroje sú pre starú verziu launchctl až po macOS 10.10, príklady kódu sú uvedené na ilustračné účely. Moderný launchctl posiela spúšťané signály cez XPC, logika launchctl bola presunutá do neho.

Pozrime sa, ako presne sa zastavujú aplikácie. Pred odoslaním signálu SIGTERM sa aplikácia pokúsi zastaviť pomocou systémového volania „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 kapotou proc_terminate, napriek svojmu názvu, dokáže vysielať nielen psignál so SIGTERM, ale aj SIGKILL.

Nepriame zabitie – limit zdrojov

Zaujímavejší prípad možno vidieť v inom systémovom volaní process_policy. Bežným používaním tohto systémového volania je obmedzenie prostriedkov aplikácie, napríklad indexer na obmedzenie času CPU a kvót pamäte, aby sa systém výrazne nespomalil aktivitami ukladania súborov do vyrovnávacej pamäte. Ak aplikácia dosiahne limit prostriedkov, ako je možné vidieť z funkcie proc_apply_resource_actions, do procesu sa odošle signál SIGKILL.

Hoci toto systémové volanie môže potenciálne zabiť proces, systém dostatočne neskontroloval práva procesu, ktorý volá systémové volanie. Vlastne kontrola existovali, ale na obídenie tejto podmienky stačí použiť alternatívny príznak PROC_POLICY_ACTION_SET.

Ak teda „obmedzíte“ kvótu využitia procesora aplikácie (napríklad povolíte spustenie iba 1 ns), môžete zabiť akýkoľvek proces v systéme. Malvér teda môže zabiť akýkoľvek proces v systéme vrátane antivírusového procesu. Zaujímavý je aj efekt, ktorý nastáva pri zabíjaní procesu pomocou pid 1 (launchctl) - panika jadra pri pokuse o spracovanie signálu SIGKILL :)

Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Ako vyriešiť problém?

Najjednoduchší spôsob, ako zabrániť zabitiu procesu, je nahradiť ukazovateľ funkcie v tabuľke systémových volaní. Bohužiaľ, táto metóda nie je triviálna z mnohých dôvodov.

Po prvé, symbol, ktorý riadi umiestnenie pamäte sysent, nie je len súkromný pre symbol jadra XNU, ale nemožno ho nájsť v symboloch jadra. Budete musieť použiť heuristické metódy vyhľadávania, ako je dynamická demontáž funkcie a hľadanie ukazovateľa v nej.

Po druhé, štruktúra záznamov v tabuľke závisí od príznakov, s ktorými bolo jadro kompilované. Ak je deklarovaný príznak CONFIG_REQUIRES_U32_MUNGING, zmení sa veľkosť štruktúry - pribudne ďalšie pole sy_arg_munge32. Je potrebné vykonať dodatočnú kontrolu, aby ste určili, s ktorým príznakom bolo jadro kompilované, alebo prípadne skontrolovať ukazovatele funkcií oproti známym.

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šťastie v moderných verziách macOS Apple poskytuje nové API pre prácu s procesmi. Endpoint Security API umožňuje klientom autorizovať mnohé požiadavky na iné procesy. Pomocou vyššie uvedeného API tak môžete blokovať akékoľvek signály do procesov, vrátane signálu 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;
}

Podobne je možné v jadre zaregistrovať MAC politiku, ktorá poskytuje metódu ochrany signálu (policy proc_check_signal), ale API nie je oficiálne podporované.

Ochrana rozšírenia jadra

Okrem ochrany procesov v systéme je potrebná aj ochrana samotného rozšírenia jadra (kext). macOS poskytuje vývojárom rámec na jednoduchý vývoj ovládačov zariadení IOKit. Okrem poskytovania nástrojov na prácu so zariadeniami poskytuje IOKit metódy pre stohovanie ovládačov pomocou inštancií tried C++. Aplikácia v užívateľskom priestore bude schopná „nájsť“ zaregistrovanú inštanciu triedy na vytvorenie vzťahu jadro-užívateľský priestor.

Na zistenie počtu inštancií tried v systéme existuje utilita ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Každé rozšírenie jadra, ktoré sa chce zaregistrovať do zásobníka ovládačov, musí deklarovať triedu, ktorá dedí od IOService, v tomto prípade napríklad my_kext_ioservice. Pripojenie používateľských aplikácií spôsobí vytvorenie novej inštancie triedy, ktorá sa dedí od IOUserClient, v príklade my_kext_iouserclient.

Pri pokuse o uvoľnenie ovládača zo systému (príkaz kextunload) sa zavolá virtuálna funkcia „bool terminate(IOOptionBits options)“. Na deaktiváciu kextunload stačí vrátiť false na ukončenie hovoru pri pokuse o uvoľnenie.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Príznak IsUnloadAllowed môže byť nastavený pomocou IOUserClient pri načítaní. Keď existuje limit sťahovania, príkaz kextunload vráti nasledujúci výstup:

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.

Podobná ochrana musí byť vykonaná pre IOUserClient. Inštancie tried je možné uvoľniť pomocou funkcie užívateľského priestoru IOKitLib „IOCatalogueTerminate(mach_port_t, príznak uint32_t, popis io_name_t);“. Pri volaní príkazu „terminate“ môžete vrátiť hodnotu false, kým aplikácia používateľského priestoru „nezomrie“, to znamená, že funkcia „clientDied“ nebude zavolaná.

Ochrana súborov

Na ochranu súborov stačí použiť Kauth API, ktoré umožňuje obmedziť prístup k súborom. Apple poskytuje vývojárom upozornenia na rôzne udalosti v rozsahu, pre nás sú dôležité operácie KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD. Najjednoduchší spôsob, ako obmedziť prístup k súborom, je cestou – na získanie cesty k súboru a porovnanie predpony cesty používame API „vn_getpath“. Upozorňujeme, že na optimalizáciu premenovania ciest priečinkov súborov systém neautorizuje prístup ku každému súboru, ale iba k samotnému priečinku, ktorý bol premenovaný. Je potrebné porovnať nadradenú cestu a obmedziť pre ňu KAUTH_VNODE_DELETE.

Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Nevýhodou tohto prístupu môže byť nízky výkon so zvyšujúcim sa počtom prefixov. Aby ste sa uistili, že porovnanie sa nerovná O (predpona*dĺžka), kde predpona je počet predpôn, dĺžka je dĺžka reťazca, môžete použiť deterministický konečný automat (DFA) zostavený z predpon.

Uvažujme o metóde konštrukcie DFA pre danú množinu predpôn. Kurzory inicializujeme na začiatku každého prefixu. Ak všetky kurzory ukazujú na rovnaký znak, zväčšite každý kurzor o jeden znak a nezabudnite, že dĺžka toho istého riadku je o jeden väčšia. Ak existujú dva kurzory s rôznymi symbolmi, rozdeľte kurzory do skupín podľa symbolu, na ktorý ukazujú, a zopakujte algoritmus pre každú skupinu.

V prvom prípade (všetky znaky pod kurzormi sú rovnaké) dostaneme stav DFA, ktorý má len jeden prechod pozdĺž tej istej čiary. V druhom prípade dostaneme tabuľku prechodov veľkosti 256 (počet znakov a maximálny počet skupín) do následných stavov získanú rekurzívnym volaním funkcie.

Pozrime sa na príklad. Pre sadu predpôn („/foo/bar/tmp/“, „/var/db/foo/“, „/foo/bar/aba/“, „foo/bar/aac/“) môžete získať nasledovné DFA. Na obrázku sú znázornené len prechody vedúce do iných stavov, ostatné prechody nebudú konečné.

Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Pri prechádzaní štátmi DKA môžu nastať 3 prípady.

  1. Dosiahnutý konečný stav - cesta je chránená, obmedzujeme operácie KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD
  2. Konečný stav nebol dosiahnutý, ale cesta „skončila“ (bol dosiahnutý nulový terminátor) - cesta je nadradená, je potrebné obmedziť KAUTH_VNODE_DELETE. Všimnite si, že ak je vnode priečinok, musíte na koniec pridať '/', inak to môže obmedziť na súbor „/foor/bar/t“, čo je nesprávne.
  3. Konečný stav sa nedosiahol, cesta neskončila. Žiadna z predpôn sa nezhoduje s touto, nezavádzame obmedzenia.

Záver

Cieľom vyvíjaných bezpečnostných riešení je zvýšiť úroveň bezpečnosti užívateľa a jeho dát. Na jednej strane je tento cieľ dosiahnutý vývojom softvérového produktu Acronis, ktorý uzatvára tie zraniteľnosti, pri ktorých je samotný operačný systém „slabý“. Na druhej strane by sme nemali zanedbávať posilnenie tých bezpečnostných aspektov, ktoré je možné zlepšiť na strane OS, najmä preto, že uzavretie takýchto zraniteľností zvyšuje našu vlastnú stabilitu ako produktu. Táto chyba zabezpečenia bola nahlásená tímu Apple Product Security a bola opravená v systéme macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Ako chrániť procesy a rozšírenia jadra v systéme MacOS

Toto všetko je možné vykonať iba vtedy, ak bol váš nástroj oficiálne nainštalovaný do jadra. To znamená, že neexistujú žiadne takéto medzery pre externý a nechcený softvér. Ako však vidíte, aj ochrana legitímnych programov, ako sú antivírusové a zálohovacie systémy, si vyžaduje prácu. Teraz však budú mať nové produkty Acronis pre macOS dodatočnú ochranu proti uvoľneniu zo systému.

Zdroj: hab.com

Pridať komentár