Ahoj, Habr! Dnes bych chtěl mluvit o tom, jak chránit procesy před útoky útočníků v macOS. Například to je užitečné pro antivirus nebo zálohovací systém, zvláště vzhledem k tomu, že v macOS existuje hned několik způsobů, jak "zabít" proces. O tom a metodách ochrany si přečtěte níže.
Klasický způsob "zabití" procesu
Všem známý způsob "zabít" proces – poslat signál SIGKILL procesu. Přes bash můžete zavolat standardní "kill -SIGKILL PID" nebo "pkill -9 NAME" pro jeho zabití. Příkaz "kill" je známý už od dob UNIXu a je dostupný nejen v macOS, ale i na dalších unixových systémech.
Stejně jako v unixových systémech, macOS umožňuje zachytit jakékoliv signály pro proces kromě dvou – SIGKILL a SIGSTOP. V tomto článku se především budeme zabývat signálem SIGKILL jako signálem, který způsobuje zabití procesu.
Specifika macOS
V macOS systémový volání kill v jádře XNU vyvolává funkci psignal(SIGKILL,…). Podívejme se, jaké další uživatelské akce v userspace mohou vyvolat funkci psignal. Odfiltrujme volání funkce psignal v interních mechanismech jádra (i když ta mohou být netriviální, ale necháme si je na jiný článek 🙂 – kontrola podpisu, chyby paměti, zpracování exit/terminate, porušení ochrany souborů atd.).
Začněme přehled funkcí a příslušného systémového volání . Je zřejmé, že kromě klasického volání kill existuje alternativní přístup, který je specifický pro operační systém macOS a nenajdete ho v BSD. Principy fungování obou systémových volání jsou také podobné. Představují přímé volání funkce jádra psignal. Také si povšimněme, že před zabitím procesu probíhá kontrola "cansignal" – zda může proces poslat signál jinému procesu, systém neumožňuje žá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) {
do {
* 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
Standardní způsob, jak vytvářet démony při spuštění systému a řídit jejich životnost, je launchd. Rád bych zdůraznil, že zdrojové kódy uvedené pro starou verzi launchctl před macOS 10.10 a příklady kódu byly uvedeny jako ilustrace. Moderní launchctl posílá signály launchd prostřednictvím XPC, logika launchctl byla přesunuta do něho.
Podíváme se, jak se vlastně provádí zastavení aplikací. Před odesláním signálu SIGTERM se aplikace snaží zastavit pomocí systémového volání “proc_terminate”.
...
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, navzdory svému názvu, může odesílat nejen psignal s SIGTERM, ale také SIGKILL.
Nepřímé zabití — omezení zdrojů
Zajímavější případ lze vidět v jiném systémovém volání . Standardní použití tohoto systémového volání — omezení zdrojů aplikací, například pro indexer omezení na kvótu procesorového času a paměti, aby se systém nezdůraznil z akcí kešování souboru. Pokud aplikace dosáhla omezení zdrojů, jak lze vidět z funkce proc_apply_resource_actions, je procesu odeslán signál SIGKILL.
Navzdory tomu, že toto systémové volání může potenciálně provádět zabití procesu, systém adekvátně nekontroloval práva procesu, který systémové volání vyvolal. Ve skutečnosti kontrola , ale stačí použít alternativní příznak PROC_POLICY_ACTION_SET k obchodu tohoto podmínku.
Pokud "omezíte" kvótu využívání CPU aplikací (například povolíte spuštění pouze 1 ns), můžete zabít jakýkoli proces v systému. Tak může malware zabít jakýkoli proces v systému, včetně procesu antiviru. Zajímavý je také efekt, který nastává při zabíjení procesu s pid 1 (launchctl) — kernel panic při pokusu o zpracování signálu SIGKILL 🙂

Jak vyřešit problém?
Nejjednodušší způsob, jak zakázat zabíjení procesu, je nahradit ukazatel na funkci v tabulce systémových volání. Bohužel, tento způsob je z mnoha důvodů netriviální.
Za prvé, symbol, který odpovídá za umístění sysent v paměti, je nejen privátním symbolem jádra XNU, ale nelze jej najít ani mezi symboly jádra. Bude nutné použít heuristické metody vyhledávání, například dynamickou disassemblaci funkce a hledání ukazatele v ní.
Za druhé, struktura záznamů v tabulce závisí na flagech, se kterými bylo jádro sestaveno. Pokud je vyhlášen flag CONFIG_REQUIRES_U32_MUNGING, velikost struktury se změní — přidá se další pole. . Je nutné provést další kontrolu na to, s jakým flagem bylo jádro zkompilováno, například porovnat ukazatele funkcí se známými.
struct sysent {
/* tabulka systémových volání */
sy_call_t *sy_call; /* implementující funkce */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
sy_munge_t *sy_arg_munge32; /* munger argumentů systémového volání pro 32-bitové procesy */
#endif
int32_t sy_return_type; /* typy návratů systémových volání */
int16_t sy_narg; /* počet argumentů */
uint16_t sy_arg_bytes; /* Celková velikost argumentů v bajtech pro
* 32-bitová systémová volání
*/
};
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ů k jiným procesům. Lze tak blokovat jakékoli signály k procesům, včetně signálu SIGKILL pomocí výše zmíněného API.
#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ádru zaregistrovat MAC Policy, která poskytuje metodu ochrany před signály (policy proc_check_signal), nicméně API není oficiálně podporováno.
Ochrana rozšíření jádra
Kromě ochrany procesů v systému je také nezbytná ochrana samotného jádra (kext). macOS poskytuje pro vývojáře rámec pro pohodlný vývoj ovladačů zařízení IOKit. Kromě poskytování prostředků pro práci se zařízeními IOKit zajišťuje také metody pro stohování ovladačů (driver stacking) pomocí instancí tříd C++. Aplikace v uživatelském prostoru může „najít“ registrovanou instanci třídy pro navázání vztahu mezi jádrem a uživatelským prostorem.
Pro zjištění počtu instancí tříd v systému existuje utilita ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Jakékoli rozšíření jádra, které si přeje registrovat se ve stohu ovladačů, musí deklarovat třídu odvozenou od IOService, například my_kext_ioservice v tomto případě. Připojení uživatelských aplikací vyvolává vytvoření nové instance třídy, která je odvozena od IOUserClient, v příkladu my_kext_iouserclient.
Při pokusu o odpojení ovladače ze systému (příkaz kextunload) se vyvolá virtuální funkce “bool terminate(IOOptionBits options)”. Stačí vrátit false při volání funkce terminate při pokusu o odpojení, aby se zakázal kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Odpojení není povoleno, vrací false
return false;
}
return super::terminate(options);
}
Příznak IsUnloadAllowed může být nastaven IOUserClient při načítání. Při omezení na načítání vrátí příkaz kextunload následující výstup:
admin@admins-Mac drivermanager % sudo kextunload .\/test.kext
Heslo:
(kernel) Nelze odstranit kext my.kext.test; služby se nepodařilo ukončit - 0xe00002c7.
Nepodařilo se odpojit my.kext.test - (iokit/common) nepodporovaná funkce.
Podobnou ochranu je nutné provést také pro IOUserClient. Instance tříd lze odpojit pomocí uživatelské funkce IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Může se vracet false při volání příkazu “terminate”, dokud uživatelská aplikace „neumře“, tedy dokud nebude vyvolána funkce “clientDied”.
Ochrana souborů
Pro ochranu souborů stačí využí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, a pro nás jsou důležité operace KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD. Omezit přístup k souborům je nejjednodušší podle cesty — použijeme API „vn_getpath“ pro získání cesty k souboru a provedeme porovnání prefixu cesty. Upozorňujeme, že pro optimalizaci přejmenování cest složek se systém neautorizuje přístup k každému souboru, ale pouze k samotné složce, kterou jsme přejmenovali. Je nutné provést porovnání rodičovské cesty a omezit KAUTH_VNODE_DELETE pro ni.

Nevýhodou tohoto přístupu může být nízký výkon při zvyšování počtu prefixů. Aby porovnání nebylo rovno O(prefix*length), kde prefix je počet prefixů a length je délka řetězce, lze použít deterministický konečný automat (DKA) postavený na prefixech.
Zvažme způsob konstrukce DKA pro danou množinu prefixů. Inicializujeme kurzory na začátek každého prefixu. Pokud všechny kurzory ukazují na stejný znak, zvýšíme každý kurzor o jeden znak a zapamatujeme si, že délka shodné řetězce je větší o jednotku. Pokud existují dva kurzory, pod kterými jsou různé znaky, rozdělíme kurzory do skupin podle znaku, na který ukazují, a opakujeme algoritmus pro každou skupinu.
V prvním případě (všechny znaky pod kurzory jsou shodné) získáváme stav DKA, který má pouze jeden přechod podle shodné řetězce. Ve druhém případě získáváme tabulku přechodů velikosti 256 (počet znaků a maximální počet skupin) do následujících stavů, které byly získány při rekurzivním volání funkce.
Uvažujme příklad. Pro sadu prefixů (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) lze získat následující DKA. Na obrázku jsou uvedeny pouze přechody vedoucí do jiných stavů, ostatní přechody nebudou konečnými.

Při procházení stavy DKA může nastat 3 případy.
- Bylo dosaženo konečného stavu — cesta je chráněná, omezujeme operace KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA a KAUTH_VNODE_DELETE_CHILD.
- Finální stav nebyl dosažen, ale cesta "skončila" (byl dosažen nulový terminátor) — cesta je rodičovská, je nutné omezit KAUTH_VNODE_DELETE. Všimněte si, že pokud je vnode složka, je třeba přidat na konec "/", jinak může být omezení aplikováno na soubor "/foor/bar/t", což je nesprávné.
- Finální stav nebyl dosažen, cesta neskončila. Žádný z prefixů neodpovídá danému, omezování nenastavujeme.
Závěr
Cílem vyvíjených bezpečnostních řešení je zvýšení úrovně bezpečnosti uživatele a jeho dat. Na jedné straně je tohoto cíle dosaženo vývojem produktu Acronis, který uzavírá ty zranitelnosti, kde je "slabá" samotná operaní systém. Na druhé straně by neměly být opomíjeny ani aspekty zabezpečení, které lze vylepšit na straně OS, zvlášť když uzavření takových zranitelností zvyšuje naši vlastní odolnost jako produktu. Zranitelnost byla nahlášena Apple Product Security Team a byla opravena v macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

To vše je možné provést pouze v případě, že vaše utilita byla oficiálně nainstalována do jádra. To znamená, že pro externí a nežádoucí software neexistují žádné skuliny. Nicméně, jak vidíte, i pro ochranu legitimních programů, jako jsou antiviry a zálohovací systémy, je zapotřebí se snažit. Ale za to nyní nové produkty Acronis pro macOS budou mít zvýšenou ochranu proti vysazení ze systému.
Zdroj: habr.com
