Jak chronić procesy i rozszerzenia jądra w systemie macOS

Witaj, Habro! Dzisiaj chciałbym porozmawiać o tym, jak można chronić procesy przed atakami atakujących w systemie macOS. Jest to przydatne na przykład w przypadku systemu antywirusowego lub systemu kopii zapasowych, zwłaszcza że w systemie macOS istnieje kilka sposobów „zabicia” procesu. Przeczytaj o tym i sposobach ochrony pod przecięciem.

Jak chronić procesy i rozszerzenia jądra w systemie macOS

Klasyczny sposób na „zabicie” procesu

Dobrze znanym sposobem „zabicia” procesu jest wysłanie do procesu sygnału SIGKILL. Za pomocą basha możesz wywołać standardowy „kill -SIGKILL PID” lub „pkill -9 NAME”, aby zabić. Polecenie „kill” jest znane od czasów UNIX-a i jest dostępne nie tylko na macOS, ale także na innych systemach UNIX-podobnych.

Podobnie jak w systemach typu UNIX, macOS umożliwia przechwytywanie dowolnych sygnałów do procesu z wyjątkiem dwóch - SIGKILL i SIGSTOP. W tym artykule skupimy się przede wszystkim na sygnale SIGKILL jako sygnale powodującym zakończenie procesu.

Specyfika systemu macOS

W systemie macOS wywołanie systemowe kill w jądrze XNU wywołuje funkcję psignal(SIGKILL,...). Spróbujmy zobaczyć, jakie inne akcje użytkownika w przestrzeni użytkownika można wywołać za pomocą funkcji psignal. Wyeliminujmy wywołania funkcji psignal w wewnętrznych mechanizmach jądra (choć mogą one być nietrywialne, ale zostawmy je na inny artykuł 🙂 - weryfikacja podpisu, błędy pamięci, obsługa wyjścia/zakończenia, naruszenia ochrony plików, itp.

Zacznijmy recenzję od funkcji i odpowiedniego wywołania systemowego zakończyć_z_ładunekem. Można zauważyć, że oprócz klasycznego wywołania „zabicia” istnieje alternatywne podejście, które jest specyficzne dla systemu operacyjnego macOS i nie występuje w BSD. Zasady działania obu wywołań systemowych są również podobne. Są to bezpośrednie wywołania funkcji jądra psignal. Należy również pamiętać, że przed zakończeniem procesu przeprowadzana jest kontrola „cansignal” – czy proces może wysłać sygnał do innego procesu; system nie pozwala na przykład żadnej aplikacji na zabijanie procesów systemowych.

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

uruchomiona

Uruchamiany jest standardowy sposób tworzenia demonów podczas uruchamiania systemu i kontrolowania ich czasu życia. Należy pamiętać, że źródła dotyczą starej wersji launchctl do macOS 10.10, przykłady kodu podano w celach ilustracyjnych. Nowoczesny launchctl wysyła sygnały launchd przez XPC, do niego przeniesiono logikę launchctl.

Przyjrzyjmy się, jak dokładnie aplikacje są zatrzymywane. Przed wysłaniem sygnału SIGTERM następuje próba zatrzymania aplikacji za pomocą wywołania systemowego „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 maską proc_terminate, pomimo swojej nazwy, może wysyłać nie tylko sygnał psygnałowy za pomocą SIGTERM, ale także SIGKILL.

Pośrednie zabójstwo – limit zasobów

Ciekawszy przypadek można zobaczyć w innym wywołaniu systemowym polityka_procesu. Typowym zastosowaniem tego wywołania systemowego jest ograniczenie zasobów aplikacji, na przykład w przypadku indeksatora w celu ograniczenia czasu procesora i przydziałów pamięci, tak aby system nie był znacząco spowalniany przez działania związane z buforowaniem plików. Jeśli aplikacja osiągnęła limit zasobów, jak widać z funkcji proc_apply_resource_actions, do procesu wysyłany jest sygnał SIGKILL.

Chociaż to wywołanie systemowe mogłoby potencjalnie zabić proces, system nie sprawdził odpowiednio uprawnień procesu wywołującego to wywołanie systemowe. Właściwie sprawdzam istniał, ale wystarczy użyć alternatywnej flagi PROC_POLICY_ACTION_SET, aby ominąć ten warunek.

Dlatego jeśli „ograniczysz” limit wykorzystania procesora aplikacji (na przykład zezwalając na działanie tylko przez 1 ns), możesz zabić dowolny proces w systemie. W ten sposób złośliwe oprogramowanie może zabić dowolny proces w systemie, w tym proces antywirusowy. Ciekawy jest również efekt, który pojawia się podczas zabijania procesu z pid 1 (launchctl) - panika jądra podczas próby przetworzenia sygnału SIGKILL :)

Jak chronić procesy i rozszerzenia jądra w systemie macOS

Jak rozwiązać problem?

Najprostszym sposobem zapobiegania zabiciu procesu jest zastąpienie wskaźnika funkcji w tabeli wywołań systemowych. Niestety metoda ta nie jest trywialna z wielu powodów.

Po pierwsze, symbol kontrolujący lokalizację pamięci sysent jest nie tylko prywatny dla symbolu jądra XNU, ale nie można go znaleźć w symbolach jądra. Będziesz musiał użyć heurystycznych metod wyszukiwania, takich jak dynamiczna dezasemblacja funkcji i wyszukiwanie w niej wskaźnika.

Po drugie, struktura wpisów w tablicy zależy od flag, z jakimi skompilowano jądro. W przypadku zadeklarowania flagi CONFIG_REQUIRES_U32_MUNGING zmieni się rozmiar konstrukcji - zostanie dodane dodatkowe pole sy_arg_munge32. Należy dodatkowo sprawdzić, z jaką flagą zostało skompilowane jądro lub alternatywnie sprawdzić wskaźniki funkcji względem znanych.

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 szczęście we współczesnych wersjach systemu macOS Apple udostępnia nowe API do pracy z procesami. Interfejs API Endpoint Security umożliwia klientom autoryzację wielu żądań do innych procesów. Dzięki temu można zablokować dowolne sygnały do ​​procesów, w tym także sygnał SIGKILL, wykorzystując ww. 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;
}

Podobnie w jądrze można zarejestrować politykę MAC, która zapewnia metodę ochrony sygnału (policy proc_check_signal), ale API nie jest oficjalnie obsługiwane.

Ochrona rozszerzenia jądra

Oprócz ochrony procesów w systemie konieczna jest także ochrona samego rozszerzenia jądra (kext). macOS zapewnia programistom platformę umożliwiającą łatwe tworzenie sterowników urządzeń IOKit. Oprócz zapewnienia narzędzi do pracy z urządzeniami, IOKit udostępnia metody układania sterowników przy użyciu instancji klas C++. Aplikacja w przestrzeni użytkownika będzie w stanie „znaleźć” zarejestrowaną instancję klasy w celu ustanowienia relacji jądro-przestrzeń użytkownika.

Do wykrywania liczby instancji klas w systemie służy narzędzie ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Każde rozszerzenie jądra chcące zarejestrować się na stosie sterowników musi zadeklarować klasę dziedziczącą po IOService, np. w tym przypadku my_kext_ioservice.Podłączenie aplikacji użytkownika powoduje utworzenie nowej instancji klasy dziedziczącej po IOUserClient, na przykład my_kext_iouserclient.

Podczas próby wyładowania sterownika z systemu (polecenie kextunload) wywoływana jest funkcja wirtualna „bool termin(opcje IOOptionBits)”. Wystarczy zwrócić false przy próbie zakończenia wywołania, aby wyłączyć kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Flagę IsUnloadAllowed można ustawić przez IOUserClient podczas ładowania. Jeśli istnieje limit pobierania, polecenie kextunload zwróci następujące dane wyjściowe:

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ą ochronę należy zastosować dla IOUserClient. Instancje klas można rozładować za pomocą funkcji przestrzeni użytkownika IOKitLib „IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t opis);”. Możesz zwrócić wartość false podczas wywoływania polecenia „terminate”, dopóki aplikacja przestrzeni użytkownika „nie umrze”, to znaczy funkcja „clientDied” nie zostanie wywołana.

Ochrona plików

Aby chronić pliki, wystarczy skorzystać z API Kauth, które pozwala na ograniczenie dostępu do plików. Apple udostępnia programistom powiadomienia o różnych zdarzeniach z zakresu, dla nas ważne są operacje KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD. Najprostszym sposobem ograniczenia dostępu do plików jest ścieżka - używamy API „vn_getpath”, aby uzyskać ścieżkę do pliku i porównać przedrostek ścieżki. Należy pamiętać, że aby zoptymalizować zmianę nazw ścieżek folderów plików, system nie autoryzuje dostępu do każdego pliku, a jedynie do samego folderu, którego nazwa została zmieniona. Konieczne jest porównanie ścieżki nadrzędnej i ograniczenie dla niej KAUTH_VNODE_DELETE.

Jak chronić procesy i rozszerzenia jądra w systemie macOS

Wadą tego podejścia może być niska wydajność w miarę wzrostu liczby prefiksów. Aby mieć pewność, że porównanie nie będzie równe O(przedrostek*długość), gdzie przedrostek to liczba przedrostków, długość to długość ciągu znaków, można zastosować deterministyczny automat skończony (DFA) zbudowany z przedrostków.

Rozważmy metodę konstruowania DFA dla danego zestawu przedrostków. Inicjujemy kursory na początku każdego prefiksu. Jeśli wszystkie kursory wskazują na ten sam znak, to zwiększ każdy kursor o jeden znak i pamiętaj, że długość tej samej linii jest większa o jeden. Jeśli istnieją dwa kursory z różnymi symbolami, podziel kursory na grupy według symbolu, na który wskazują, i powtórz algorytm dla każdej grupy.

W pierwszym przypadku (wszystkie znaki pod kursorami są takie same) otrzymujemy stan DFA, który ma tylko jedno przejście wzdłuż tej samej linii. W drugim przypadku otrzymujemy tablicę przejść o rozmiarze 256 (liczba znaków i maksymalna liczba grup) do kolejnych stanów uzyskanych poprzez rekurencyjne wywołanie funkcji.

Spójrzmy na przykład. Dla zestawu przedrostków („/foo/bar/tmp/”, „/var/db/foo/”, „/foo/bar/aba/”, „foo/bar/aac/”) możesz uzyskać następujące informacje DFA. Na rysunku przedstawiono jedynie przejścia prowadzące do innych stanów, inne przejścia nie będą ostateczne.

Jak chronić procesy i rozszerzenia jądra w systemie macOS

Przeglądając stany DKA, mogą pojawić się 3 przypadki.

  1. Stan końcowy został osiągnięty - ścieżka jest zabezpieczona, ograniczamy operacje KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD
  2. Stan końcowy nie został osiągnięty, ale ścieżka „zakończyła się” (osiągnięto terminator zerowy) - ścieżka jest rodzicem, konieczne jest ograniczenie KAUTH_VNODE_DELETE. Pamiętaj, że jeśli vnode jest folderem, musisz dodać „/” na końcu, w przeciwnym razie może on ograniczyć się do pliku „/foor/bar/t”, co jest nieprawidłowe.
  3. Stan końcowy nie został osiągnięty, ścieżka się nie skończyła. Żaden z prefiksów nie pasuje do tego, nie wprowadzamy ograniczeń.

wniosek

Celem opracowywanych rozwiązań bezpieczeństwa jest podniesienie poziomu bezpieczeństwa użytkownika i jego danych. Z jednej strony cel ten osiąga się poprzez rozwój oprogramowania Acronis, które likwiduje luki tam, gdzie sam system operacyjny jest „słaby”. Z drugiej strony nie powinniśmy zaniedbywać wzmacniania tych aspektów bezpieczeństwa, które można poprawić po stronie systemu operacyjnego, zwłaszcza że zamknięcie takich luk zwiększa naszą własną stabilność jako produktu. Luka została zgłoszona zespołowi ds. bezpieczeństwa produktów Apple i została naprawiona w systemie macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Jak chronić procesy i rozszerzenia jądra w systemie macOS

Wszystko to można zrobić tylko wtedy, gdy twoje narzędzie zostało oficjalnie zainstalowane w jądrze. Oznacza to, że nie ma takich luk w przypadku zewnętrznego i niechcianego oprogramowania. Jak jednak widać, nawet ochrona legalnych programów, takich jak systemy antywirusowe i systemy tworzenia kopii zapasowych, wymaga pracy. Ale teraz nowe produkty Acronis dla macOS będą miały dodatkową ochronę przed wyładowaniem z systemu.

Źródło: www.habr.com

Dodaj komentarz