Jak chronić procesy i rozszerzenia jądra w macOS

Cześć, Habr! Dziś chciałbym porozmawiać o tym, jak można chronić procesy przed atakami złośliwych użytkowników w macOS. Jest to przydatne dla oprogramowania antywirusowego lub systemu kopii zapasowej, zwłaszcza w świetle faktu, że w macOS istnieje kilka sposobów na "zabicie" procesu. O tym oraz o metodach ochrony przeczytasz poniżej.

Jak chronić procesy i rozszerzenia jądra w macOS

Klasyczny sposób na "zabicie" procesu

Wszyscy znają sposób na "zabicie" procesu — wysłanie sygnału SIGKILL do procesu. Przez bash można wywołać standardowe „kill -SIGKILL PID” lub „pkill -9 NAZWA” w celu zabicia. Komenda „kill” znana jest już od czasów UNIX i dostępna nie tylko w macOS, ale i w innych systemach podobnych do UNIX.

Podobnie jak w systemach podobnych do UNIX, macOS pozwala przechwytywać wszelkie sygnały do procesu z wyjątkiem dwóch — SIGKILL i SIGSTOP. W tym artykule przede wszystkim omówimy sygnał SIGKILL, jako sygnał prowadzący do zabicia procesu.

Specyfika macOS

W macOS wywołanie systemowe kill w jądrze XNU wywołuje funkcję psignal(SIGKILL,…). Spróbujmy zobaczyć, jakie inne czynności użytkownika w userspace mogą wywołać funkcję psignal. Odrzucimy wywołania funkcji psignal w wewnętrznych mechanizmach jądra (chociaż mogą być złożone, zostawimy je na inny artykuł 🙂 — sprawdzanie podpisu, błędy pamięci, obsługa exit/terminate, naruszenie ochrony plików itp.

Zacznijmy przegląd od funkcji i odpowiedniego wywołania systemowego terminate_with_payload. Widać, że oprócz klasycznego wywołania kill 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ż zbliżone. Stanowią one bezpośrednie wywołania funkcji jądra psignal. Zwróćmy również uwagę, że przed zabiciem procesu wykonywana jest kontrola „cansignal” – czy proces może wysłać sygnał innemu procesowi, system nie pozwala żadnej aplikacji na zabijanie procesów systemowych na przykład.

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

Standardowym sposobem tworzenia demonów przy uruchamianiu systemu i kontrolowania ich czasu życia jest launchd. Zwracam uwagę, że podane źródła dotyczą starej wersji launchctl sprzed macOS 10.10, a przykłady kodu są podane w celach ilustracyjnych. Nowoczesny launchctl wysyła sygnały do launchd za pośrednictwem XPC, logika launchctl została w nim przeniesiona.

Zobaczmy, jak dokładnie odbywa się zatrzymanie aplikacji. Przed wysłaniem sygnału SIGTERM, aplikację próbuje się zatrzymać za pomocą wywołania systemowego „proc_terminate”.

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Nie można zakończyć zadania: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Użycie opcji awaryjnej do zakończenia zadania...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Nie można wysłać sygnału do zadania: %d: %s", error, strerror(error));
		} 
...

Pod maską proc_terminate, mimo swojej nazwy, może wysyłać nie tylko psignal z SIGTERM, ale także SIGKILL.

Pośrednie zabijanie — ograniczenia zasobów

Bardziej interesujący przypadek można zobaczyć w innym wywołaniu systemowym process_policy. Standardowym zastosowaniem tego wywołania systemowego są ograniczenia zasobów aplikacji, na przykład dla indeksatora ograniczenie na czas procesora i pamięć, aby system nie spowolnił się znacząco z powodu działań związanych z buforowaniem pliku. Jeśli aplikacja osiągnie limit zasobów, jak można zobaczyć w funkcji proc_apply_resource_actions, to proces otrzymuje sygnał SIGKILL.

Mimo że to wywołanie systemowe może potencjalnie powodować zabicie procesu, system nie sprawdzał odpowiednio uprawnień procesu, który wywołuje to wywołanie systemowe. W rzeczywistości sprawdzenie istniało, ale wystarczy użyć alternatywnego flagu PROC_POLICY_ACTION_SET, aby obejść ten warunek.

Jeśli ograniczysz kwotę użycia CPU przez aplikację (na przykład pozwalając na wykonanie tylko 1 ns), możesz zabić dowolny proces w systemie. W ten sposób złośliwe oprogramowanie może zabić każdy proces w systemie, w tym proces antywirusowy. Zadziwiający jest również efekt, który uzyskuje się przy zabijaniu procesu z pid 1 (launchctl) — kernel panic podczas próby przetworzenia sygnału SIGKILL 🙂

Jak chronić procesy i rozszerzenia jądra w macOS

Jak rozwiązać problem?

Najprostszym sposobem na zabronienie zabicia procesu jest podmiana wskaźnika na funkcję w tabeli wywołań systemowych. Niestety, metoda ta jest dość skomplikowana z wielu powodów.

Po pierwsze, symbol odpowiedzialny za położenie sysent w pamięci jest nie tylko prywatnym symbolem jądra XNU, ale także nie może być znaleziony w symbolach jądra. Należy zastosować heurystyczne metody wyszukiwania, na przykład dynamiczne dezasemblowanie funkcji i wyszukiwanie wskaźnika w niej.

Po drugie, struktura rekordów w tabeli zależy od flag, z którymi zostało zbudowane jądro. Jeśli zadeklarowano flagę CONFIG_REQUIRES_U32_MUNGING, to rozmiar struktury ulegnie zmianie — zostanie dodane dodatkowe pole. sy_arg_munge32. Należy przeprowadzić dodatkową kontrolę, z jaką flagą zostało skompilowane jądro, na przykład porównując wskaźniki na funkcje z tymi znanymi.

struct sysent {         
        /* tabela wywołań systemowych */
        sy_call_t       *sy_call;       /* funkcja implementująca */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* przetwarzane argumenty wywołania systemowego dla procesów 32-bitowych */
#endif
        int32_t         sy_return_type; /* typy zwracane przez wywołania systemowe */
        int16_t         sy_narg;        /* liczba argumentów */
        uint16_t        sy_arg_bytes;   /* Całkowity rozmiar argumentów w bajtach dla wywołań systemowych 32-bitowych */
};

Na szczęście w nowoczesnych wersjach macOS Apple udostępnia nowe API do pracy z procesami. Endpoint Security API umożliwia klientom autoryzację wielu zapytań do innych procesów. Można zablokować wszelkie sygnały do procesów, w tym sygnał SIGKILL, za pomocą wspomnianego powyżej 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 udostępnia metodę ochrony przed sygnałami (policy proc_check_signal), jednak API nie jest oficjalnie wspierane.

Ochrona rozszerzenia jądra

Oprócz ochrony procesów w systemie, niezbędna jest także ochrona samego rozszerzenia jądra (kext). macOS zapewnia programistom ramy do wygodnego tworzenia sterowników urządzeń IOKit. Oprócz dostarczania narzędzi do pracy z urządzeniami, IOKit zapewnia metody stakowania sterowników (driver stacking) za pomocą instancji klas C++. Aplikacja w przestrzeni użytkownika będzie mogła „znaleźć” zarejestrowaną instancję klasy w celu nawiązania połączenia między jądrem a przestrzenią użytkownika.

Aby wykryć liczbę instancji klas w systemie, istnieje narzędzie ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Każde rozszerzenie jądra, które chce zarejestrować się w stosie sterowników, musi zadeklarować klasę dziedziczącą z IOService, na przykład my_kext_ioservice w tym przypadku. Podłączenie aplikacji użytkownika powoduje utworzenie nowej instancji klasy, która dziedziczy po IOUserClient, w przykładzie my_kext_iouserclient.

Podczas próby usunięcia sterownika z systemu (polecenie kextunload) wywoływana jest wirtualna funkcja „bool terminate(IOOptionBits options)”. Wystarczy zwrócić false na wywołaniu funkcji terminate podczas próby usunięcia, aby zabronić kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Usunięcie nie jest dozwolone, zwracam false
    return false;
  }

  return super::terminate(options);
}

Flaga IsUnloadAllowed może być ustawiana przez IOUserClient podczas ładowania. Przy ograniczeniu na ładowanie polecenie kextunload zwróci następujący wynik:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Hasło:
(kernel) Nie można usunąć kext my.kext.test; usługi nie zakończyły działania - 0xe00002c7.
Nie udało się usunąć my.kext.test - (iokit/common) funkcja nieobsługiwana.

Podobną ochronę należy wprowadzić również dla IOUserClient. Instancje klas można usunąć za pomocą funkcji przestrzeni użytkownika IOKitLib „IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Można zwracać false na wywołaniu polecenia „terminate”, dopóki aplikacja użytkownika nie „umrze”, to znaczy nie zostanie wywołana funkcja „clientDied”.

Ochrona plików

Aby zabezpieczyć pliki, wystarczy użyć API Kauth, które pozwala ograniczać dostęp do plików. Apple dostarcza deweloperom powiadomienia o różnych zdarzeniach w zakresie, a dla nas ważne są operacje KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD. Najłatwiej ograniczać dostęp do plików na podstawie ścieżki — używamy API „vn_getpath” do uzyskania ścieżki do pliku i porównujemy prefiks ścieżki. Zauważmy, że w celu optymalizacji zmiany nazw ścieżek folderów z plikami, system nie autoryzuje dostępu do każdego pliku, lecz tylko do samego folderu, który został przemianowany. Należy porównywać rodzicielską ścieżkę i ograniczać KAUTH_VNODE_DELETE dla niej.

Jak chronić procesy i rozszerzenia jądra w macOS

Wadą tego podejścia może być niska wydajność przy wzroście liczby prefiksów. Aby porównanie nie było równe O(prefiks*length), gdzie prefiks to liczba prefiksów, a length to długość ciągu, można wykorzystać deterministyczny automat skończony (DKA) zbudowany na podstawie prefiksów.

Rozważmy sposób budowy DKA dla danego zestawu prefiksów. Inicjalizujemy kursory na początku każdego prefiksu. Jeśli wszystkie kursory wskazują na ten sam symbol, zwiększamy każdy kursor o jeden symbol i zapamiętujemy, że długość wspólnego ciągu jest większa o jednostkę. Jeśli istnieją dwa kursory, których symbole są różne, dzielimy kursory na grupy według symbolu, na który wskazują i powtarzamy algorytm dla każdej grupy.

W pierwszym przypadku (wszystkie symbole pod kursorami są takie same) uzyskujemy stan DKA, który ma tylko jedno przejście dla tego samego ciągu. W drugim przypadku uzyskujemy tabelę przejść o rozmiarze 256 (liczba symboli i maksymalna liczba grup) do kolejnych stanów, uzyskanych przy rekursywnym wywołaniu funkcji.

Rozważmy przykład. Dla zestawu prefiksów ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") można uzyskać następujący DKA. Na rysunku pokazano tylko przejścia prowadzące do innych stanów, inne przejścia nie będą końcowe.

Jak chronić procesy i rozszerzenia jądra w macOS

Podczas przechodzenia przez stany DKA mogą wystąpić 3 przypadki.

  1. Osiągnięto stan końcowy — ścieżka jest chroniona, ograniczamy operacje KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA i KAUTH_VNODE_DELETE_CHILD.
  2. Nie osiągnięto ostatecznego stanu, ale ścieżka „zakończyła się” (osiągnięto zero-terminator) — ścieżka jest rodzicielska, konieczne jest ograniczenie KAUTH_VNODE_DELETE. Zauważmy, że jeśli vnode jest folderem, należy dodać na końcu ‘/’, w przeciwnym razie może być wymuszone ograniczenie do pliku “/foor/bar/t”, co jest niepoprawne.
  3. Nie osiągnięto ostatecznego stanu, ścieżka się nie zakończyła. Żaden z prefiksów nie odpowiada podanym, nie wprowadzamy ograniczeń.

Podsumowanie

Celem opracowywanych rozwiązań zabezpieczeń jest zwiększenie poziomu bezpieczeństwa użytkownika i jego danych. Z jednej strony cel ten jest realizowany poprzez rozwój produktu Acronis, który zamyka te luki, gdzie sama system operacyjny jest „słaby”. Z drugiej strony nie należy zaniedbywać wzmocnienia tych aspektów bezpieczeństwa, które można poprawić po stronie systemu operacyjnego, tym bardziej że zamknięcie podobnych luk zwiększa naszą własną odporność jako produktu. Luka została zgłoszona przez zespół zabezpieczeń produktów Apple i została naprawiona w macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Jak chronić procesy i rozszerzenia jądra w macOS

To wszystko można zrobić tylko pod warunkiem, że Twoje narzędzie zostało oficjalnie zainstalowane w jądrze. Oznacza to, że dla zewnętrznego i niepożądanego oprogramowania nie ma takich luk. Jednak, jak widzisz, nawet dla ochrony legalnych programów, takich jak oprogramowanie antywirusowe i system kopii zapasowych, trzeba się napracować. Ale za to nowe produkty Acronis dla macOS będą miały dodatkową ochronę przed wyładowaniem z systemu.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster