Jak chránit procesy a rozšíření jádra v macOS

Dobrý den, Habr! Dnes bych chtěl mluvit o tom, jak můžete chránit procesy před útoky útočníků v macOS. To je užitečné například pro antivirový nebo zálohovací systém, zejména proto, že v systému macOS existuje několik způsobů, jak „zabít“ proces. Přečtěte si o tom a způsobech ochrany pod řezem.

Jak chránit procesy a rozšíření jádra v macOS

Klasický způsob, jak „zabít“ proces

Dobře známý způsob, jak „zabít“ proces, je poslat do procesu signál SIGKILL. Prostřednictvím bash můžete zabíjet standardním „kill -SIGKILL PID“ nebo „pkill -9 NAME“. Příkaz „kill“ je znám již od dob UNIXu a je dostupný nejen na macOS, ale i na jiných systémech podobných UNIXu.

Stejně jako v systémech podobných UNIXu vám macOS umožňuje zachytit jakékoli signály procesu kromě dvou - SIGKILL a SIGSTOP. Tento článek se primárně zaměří na signál SIGKILL jako signál, který způsobí zabití procesu.

specifika macOS

V macOS volá systémové volání kill v jádře XNU funkci psignal(SIGKILL,...). Zkusme se podívat, jaké další akce uživatele v uživatelském prostoru lze volat funkcí psignal. Vyřaďme volání funkce psignal v interních mechanismech jádra (ačkoli mohou být netriviální, necháme je na jiný článek 🙂 - ověřování podpisu, chyby paměti, manipulace s ukončením/ukončením, porušení ochrany souborů atd. .

Začněme recenzi funkcí a odpovídajícím systémovým voláním termin_with_payload. Je vidět, že kromě klasického kill call existuje alternativní přístup, který je specifický pro operační systém macOS a v BSD se nenachází. Principy fungování obou systémových volání jsou také podobné. Jsou to přímá volání funkce jádra psignal. Všimněte si také, že před zabitím procesu je provedena kontrola „kansignálu“ – zda ​​proces může poslat signál jinému procesu, systém například nedovolí žádné aplikaci zabíjet 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);
	}
...
}

launchd

Spustí se standardní způsob vytváření démonů při startu systému a řízení jejich životnosti. Upozorňujeme, že zdroje jsou pro starou verzi launchctl až do macOS 10.10, příklady kódu jsou uvedeny pro ilustrativní účely. Moderní launchctl posílá launchd signály přes XPC, logika launchctl byla přesunuta do něj.

Podívejme se, jak přesně se aplikace zastavují. Před odesláním signálu SIGTERM se aplikace pokusí zastavit pomocí systémového volání „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 může proc_terminate, navzdory svému názvu, vysílat nejen psignál pomocí SIGTERM, ale také SIGKILL.

Nepřímé zabíjení – limit zdrojů

Zajímavější případ lze vidět v jiném systémovém volání process_policy. Běžným použitím tohoto systémového volání je omezení aplikačních prostředků, například indexátor k omezení času procesoru a kvót paměti, aby systém nebyl výrazně zpomalován aktivitami ukládání souborů do mezipaměti. Pokud aplikace dosáhla limitu prostředků, jak je vidět z funkce proc_apply_resource_actions, je do procesu odeslán signál SIGKILL.

Ačkoli toto systémové volání mohlo potenciálně zabít proces, systém dostatečně nezkontroloval práva procesu volajícího systémové volání. Vlastně kontrola existoval, ale k obejití této podmínky stačí použít alternativní příznak PROC_POLICY_ACTION_SET.

Pokud tedy „omezíte“ kvótu využití procesoru aplikace (například povolíte spuštění pouze 1 ns), můžete zabít jakýkoli proces v systému. Malware tedy může zabít jakýkoli proces v systému, včetně antivirového procesu. Zajímavý je také efekt, který nastává při zabíjení procesu pomocí pid 1 (launchctl) - panika jádra při pokusu o zpracování signálu SIGKILL :)

Jak chránit procesy a rozšíření jádra v macOS

Jak problém vyřešit?

Nejpřímější způsob, jak zabránit zabití procesu, je nahradit ukazatel funkce v tabulce systémových volání. Bohužel je tato metoda z mnoha důvodů netriviální.

Za prvé, symbol, který řídí umístění paměti sysent, není soukromý pouze pro symbol jádra XNU, ale nelze jej nalézt v symbolech jádra. Budete muset použít heuristické metody vyhledávání, jako je dynamické rozebrání funkce a hledání ukazatele v ní.

Za druhé, struktura položek v tabulce závisí na příznacích, se kterými bylo jádro zkompilováno. Pokud je deklarován příznak CONFIG_REQUIRES_U32_MUNGING, velikost struktury se změní - přidá se další pole sy_arg_munge32. Je nutné provést dodatečnou kontrolu, abychom zjistili, se kterým příznakem bylo jádro zkompilováno, nebo alternativně zkontrolovat ukazatele funkcí proti známým.

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štěstí v moderních verzích macOS Apple poskytuje nové API pro práci s procesy. Endpoint Security API umožňuje klientům autorizovat mnoho požadavků na jiné procesy. Pomocí výše uvedeného API tak můžete blokovat jakékoli signály do procesů, včetně 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;
}

Podobně lze v jádře zaregistrovat MAC Policy, která poskytuje metodu ochrany signálu (policy proc_check_signal), ale API není oficiálně podporováno.

Ochrana rozšíření jádra

Kromě ochrany procesů v systému je nezbytná i ochrana samotného rozšíření jádra (kext). macOS poskytuje vývojářům rámec pro snadný vývoj ovladačů zařízení IOKit. Kromě poskytování nástrojů pro práci se zařízeními poskytuje IOKit metody pro stohování ovladačů pomocí instancí tříd C++. Aplikace v uživatelském prostoru bude moci „najít“ registrovanou instanci třídy a vytvořit tak vztah jádro-uživatelský prostor.

Pro zjištění počtu instancí tříd v systému existuje obslužný program ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Jakékoli rozšíření jádra, které se chce zaregistrovat do zásobníku ovladačů, musí deklarovat třídu, která dědí od IOService, v tomto případě například my_kext_ioservice. Připojení uživatelských aplikací způsobí vytvoření nové instance třídy, která dědí od IOUserClient, v příkladu my_kext_iouserclient.

Při pokusu o uvolnění ovladače ze systému (příkaz kextunload) se zavolá virtuální funkce „bool terminate(IOOptionBits options)“. K deaktivaci kextunload stačí vrátit false při ukončení volání při pokusu o uvolnění.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Příznak IsUnloadAllowed může být nastaven pomocí IOUserClient při načítání. Pokud existuje limit stahování, příkaz kextunload vrátí následující 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í být provedena pro IOUserClient. Instance tříd lze uvolnit pomocí funkce uživatelského prostoru IOKitLib „IOCatalogueTerminate(mach_port_t, příznak uint32_t, popis io_name_t);“. Při volání příkazu „terminate“ můžete vracet false, dokud uživatelská aplikace „neumře“, to znamená, že funkce „clientDied“ není volána.

Ochrana souborů

K ochraně souborů stačí použít Kauth API, které umožňuje omezit přístup k souborům. Apple poskytuje vývojářům upozornění na různé události v rozsahu, pro nás jsou důležité operace KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD. Nejjednodušší způsob, jak omezit přístup k souborům, je pomocí cesty – k získání cesty k souboru a porovnání předpony cesty používáme API „vn_getpath“. Všimněte si, že pro optimalizaci přejmenování cest složek souborů systém neautorizuje přístup ke každému souboru, ale pouze k samotné složce, která byla přejmenována. Je nutné porovnat nadřazenou cestu a omezit pro ni KAUTH_VNODE_DELETE.

Jak chránit procesy a rozšíření jádra v macOS

Nevýhodou tohoto přístupu může být nízký výkon s rostoucím počtem prefixů. Chcete-li zajistit, aby se porovnání nerovnalo O(prefix*délka), kde prefix je počet prefixů, délka je délka řetězce, můžete použít deterministický konečný automat (DFA) sestavený z prefixů.

Zvažme metodu pro konstrukci DFA pro danou sadu prefixů. Kurzory inicializujeme na začátku každého prefixu. Pokud všechny kurzory ukazují na stejný znak, zvyšte každý kurzor o jeden znak a nezapomeňte, že délka stejného řádku je o jeden větší. Pokud existují dva kurzory s různými symboly, rozdělte kurzory do skupin podle symbolu, na který ukazují, a opakujte algoritmus pro každou skupinu.

V prvním případě (všechny znaky pod kurzory jsou stejné) dostaneme stav DFA, který má pouze jeden přechod podél stejné čáry. V druhém případě dostaneme tabulku přechodů o velikosti 256 (počet znaků a maximální počet skupin) do následných stavů získanou rekurzivním voláním funkce.

Podívejme se na příklad. Pro sadu předpon („/foo/bar/tmp/“, „/var/db/foo/“, „/foo/bar/aba/“, „foo/bar/aac/“) můžete získat následující DFA. Obrázek ukazuje pouze přechody vedoucí do jiných stavů, ostatní přechody nebudou konečné.

Jak chránit procesy a rozšíření jádra v macOS

Při průchodu státy DKA mohou nastat 3 případy.

  1. Bylo dosaženo konečného stavu - cesta je chráněna, omezujeme operace KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD
  2. Nebylo dosaženo konečného stavu, ale cesta „skončila“ (byl dosažen nulový terminátor) - cesta je rodič, je nutné omezit KAUTH_VNODE_DELETE. Všimněte si, že pokud je vnode složka, musíte na konec přidat '/', jinak to může omezit na soubor „/foor/bar/t“, což je nesprávné.
  3. Nebylo dosaženo konečného stavu, cesta nekončila. Žádná z předpon neodpovídá této, nezavádíme omezení.

Závěr

Cílem vyvíjených bezpečnostních řešení je zvýšit úroveň bezpečnosti uživatele a jeho dat. Na jedné straně je tohoto cíle dosaženo vývojem softwarového produktu Acronis, který uzavírá ty zranitelnosti, kde je samotný operační systém „slabý“. Na druhou stranu bychom neměli zanedbávat posílení těch bezpečnostních aspektů, které lze zlepšit na straně operačního systému, zejména proto, že uzavření takových zranitelností zvyšuje naši vlastní stabilitu jako produktu. Tato chyba zabezpečení byla nahlášena týmu zabezpečení produktů Apple a byla opravena v macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Jak chránit procesy a rozšíření jádra v macOS

To vše lze provést pouze v případě, že byl váš nástroj oficiálně nainstalován do jádra. To znamená, že neexistují žádné takové mezery pro externí a nežádoucí software. Jak však vidíte, i ochrana legitimních programů, jako jsou antiviry a zálohovací systémy, vyžaduje práci. Nyní ale budou mít nové produkty Acronis pro macOS další ochranu proti vyjmutí ze systému.

Zdroj: www.habr.com

Přidat komentář