Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

Hallo, Habr! Heute möchte ich darüber sprechen, wie man Prozesse in macOS vor Angriffen von Hackern schützen kann. Dies ist beispielsweise nützlich für Antivirensoftware oder Backup-Systeme, vor allem angesichts der Tatsache, dass es unter macOS mehrere Möglichkeiten gibt, einen Prozess "zu beenden". Lesen Sie mehr dazu und zu den Schutzmethoden im Folgenden.

Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

Klassischer Weg, um einen Prozess zu beenden

Der allgemein bekannte Weg, um einen Prozess zu beenden, ist das Senden eines SIGKILL-Signals an den Prozess. Dies kann über bash mit den Befehlen "kill -SIGKILL PID" oder "pkill -9 NAME" zum Beenden des Prozesses aufgerufen werden. Der Befehl "kill" ist seit den Anfängen von UNIX bekannt und in macOS sowie anderen UNIX-ähnlichen Systemen verfügbar.

Wie auch in UNIX-ähnlichen Systemen ermöglicht es macOS, alle Signale an einen Prozess abzufangen, mit zwei Ausnahmen — SIGKILL und SIGSTOP. In diesem Artikel liegt der Fokus hauptsächlich auf dem Signal SIGKILL, da es das Signal ist, das den Prozess beendet.

Spezifika von macOS

In macOS löst der Systemaufruf kill im XNU-Kernel die Funktion psignal(SIGKILL, …) aus. Lassen Sie uns untersuchen, welche weiteren Benutzeraktionen im Userspace die Funktion psignal auslösen können. Wir werden Aufrufe der Funktion psignal in den internen Mechanismen des Kernels ausscheiden (obwohl auch diese nicht trivial sein könnten, lassen wir sie für einen anderen Artikel 🙂 – Überprüfung der Signatur, Speicherfehler, Verarbeitung von Exit/Terminate, Verletzungen des Dateischutzes usw.).

Beginnen wir mit einem Überblick über die Funktion und den entsprechenden Systemaufruf. terminate_with_payload. Es ist ersichtlich, dass neben dem klassischen Aufruf kill ein alternativer Ansatz existiert, der spezifisch für das Betriebssystem macOS ist und in BSD nicht vorkommt. Die Prinzipien der beiden Systemaufrufe sind ebenfalls ähnlich. Sie stellen direkte Aufrufe der Kernfunktion psignal dar. Zudem sollten wir beachten, dass vor dem Beenden des Prozesses die „cansignal“-Überprüfung erfolgt – ob der Prozess einem anderen Prozess ein Signal senden kann. Das System erlaubt es keinem Anwendung, Systemprozesse abzutöten.

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() setzt ein SIGKILL auf den angegebenen Thread oder
		 * gibt zurück, wenn der Thread und/oder die Aufgabe bereits beendet werden. In jedem Fall wird der
		 * aktuelle Thread nicht zu Benutzerraum zurückkehren.
		 */
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

launchd

Der Standardweg zum Erstellen von Daemons beim Systemstart und zur Kontrolle ihrer Lebensdauer ist launchd. Ich möchte darauf hinweisen, dass die Quellcodes für eine ältere Version von launchctl vor macOS 10.10 angegeben sind; die Codebeispiele dienen lediglich zur Veranschaulichung. Das moderne launchctl sendet Signale an launchd über XPC, die Logik von launchctl wurde in dieses System übertragen.

Betrachten wir, wie die Beendigung von Anwendungen tatsächlich erfolgt. Bevor das SIGTERM-Signal gesendet wird, wird versucht, die Anwendung mit dem Systemaufruf „proc_terminate“ zu stoppen.

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Konnte den Job nicht beenden: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Fallback-Option zur Beendigung des Jobs wird verwendet...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Konnte dem Job kein Signal senden: %d: %s", error, strerror(error));
		} 
...

Trotz seines Namens kann proc_terminate unter der Haube nicht nur psignal mit SIGTERM senden, sondern auch SIGKILL.

Indirekte Tötung — Ressourcenbeschränkung

Ein interessanterer Fall ist in einem anderen Systemaufruf zu sehen process_policy. Die Standardnutzung dieses Systemaufrufs sind Einschränkungen der Ressourcen von Anwendungen, zum Beispiel für den Indexer eine Begrenzung der Quoten für CPU-Zeit und Speicher, um zu verhindern, dass das System erheblich durch Datei-Caching verlangsamt wird. Wenn eine Anwendung die Ressourcenbeschränkung erreicht hat, wie aus der Funktion proc_apply_resource_actions ersichtlich ist, wird dem Prozess das Signal SIGKILL gesendet.

Obwohl dieser Systemaufruf potenziell die Tötung eines Prozesses durchführen kann, hat das System nicht angemessen die Rechte des Prozesses überprüft, der den Systemaufruf tätigt. Tatsächlich gab es eine Überprüfung existiert., aber es reicht aus, das alternative Flag PROC_POLICY_ACTION_SET zu verwenden, um diese Bedingung zu umgehen.

Damit kann, wenn die CPU-Nutzung des Prozesses (zum Beispiel die Ausführung von nur 1 ns) „eingeschränkt“ wird, jeder Prozess im System beendet werden. Ein Angreifer könnte somit jeden Prozess im System, einschließlich des Antivirenprogramms, beenden. Auch der Effekt, der beim Töten des Prozesses mit pid 1 (launchctl) auftritt, ist interessant — ein Kernel-Panik bei dem Versuch, das SIGKILL-Signal zu verarbeiten 🙂.

Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

Wie löst man das Problem?

Der direkteste Weg, das Töten eines Prozesses zu verhindern, besteht darin, den Funktionszeiger in der Systemaufruftabelle zu ersetzen. Leider ist dieser Ansatz aus vielen Gründen nicht trivial.

Erstens ist das Symbol, das für die Position von sysent im Speicher verantwortlich ist, nicht nur ein privates Symbol des XNU-Kernels, sondern kann auch in den Kernel-Symbolen nicht gefunden werden. Es müssen heuristische Suchmethoden verwendet werden, wie beispielsweise das dynamische Disassemblieren der Funktion und das Suchen des Zeigers darin.

Zweitens hängt die Struktur der Einträge in der Tabelle von den Flags ab, mit denen der Kernel kompiliert wurde. Wenn das FLAG CONFIG_REQUIRES_U32_MUNGING deklariert ist, wird die Größe der Struktur geändert – ein zusätzliches Feld wird hinzugefügt. sy_arg_munge32. Es ist erforderlich, eine zusätzliche Überprüfung durchzuführen, mit welchem Flag der Kernel kompiliert wurde, zum Beispiel durch den Abgleich der Funktionszeiger mit bekannten.

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
                                         */
};

Glücklicherweise bietet Apple in den modernen Versionen von macOS eine neue API zur Arbeit mit Prozessen an. Die Endpoint Security API ermöglicht es Clients, viele Anfragen an andere Prozesse zu autorisieren. So können alle Signale an Prozesse, einschließlich des Signals SIGKILL, mit der oben genannten API blockiert werden.

#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;
}

Ähnlich kann im Kernel eine MAC-Policy registriert werden, die eine Schutzmethode gegen Signale (Policy proc_check_signal) bereitstellt, allerdings wird die API nicht offiziell unterstützt.

Schutz des Kernel-Extensions

Neben dem Prozessschutz im System ist auch der Schutz der Kernel-Erweiterung (kext) zwingend erforderlich. macOS bietet Entwicklern ein Framework für eine bequeme Entwicklung von IOKit-Gerätetreibern. Zusätzlich zu den Werkzeugen für die Geräteverwaltung ermöglicht IOKit Methoden zum Stapeln von Treibern (Driver Stacking) mittels Instanzen von C++-Klassen. Eine Anwendung im Userspace kann eine registrierte Klasseninstanz „finden“, um eine Verbindung zwischen Kernel und Userspace herzustellen.

Zur Ermittlung der Anzahl der Klasseninstanzen im System gibt es das Tool ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Jede Kernel-Erweiterung, die sich im Treiberstapel registrieren möchte, muss eine Klasse deklarieren, die von IOService abgeleitet ist, zum Beispiel my_kext_ioservice in diesem Fall. Die Verbindung von Benutzeranwendungen führt zur Erstellung einer neuen Klasseninstanz, die von IOUserClient abgeleitet ist, im Beispiel my_kext_iouserclient.

Beim Versuch, den Treiber aus dem System zu entladen (Befehl kextunload) wird die virtuelle Funktion „bool terminate(IOOptionBits options)“ aufgerufen. Es reicht aus, false beim Aufruf der Funktion terminate zurückzugeben, um kextunload zu verhindern.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Entladen ist nicht erlaubt, false zurückgeben
    return false;
  }

  return super::terminate(options);
}

Das Flag IsUnloadAllowed kann bei der Ladezeit von IOUserClient gesetzt werden. Bei einer Entladebeschränkung gibt der Befehl kextunload die folgende Ausgabe zurück:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Passwort:
(kernel) Kann kext my.kext.test nicht entfernen; Dienste konnten nicht beendet werden - 0xe00002c7.
Entladen von my.kext.test fehlgeschlagen - (iokit/common) nicht unterstützte Funktion.

Ein ähnlicher Schutz muss auch für IOUserClient durchgeführt werden. Instanzen von Klassen können mit der Userspace-Funktion IOKitLib „IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);“ entladen werden. Es kann false beim Aufruf des Befehls „terminate“ zurückgegeben werden, solange die Userspace-Anwendung nicht „stirbt“, d.h. solange die Funktion „clientDied“ nicht aufgerufen wird.

Dateischutz

Zum Schutz von Dateien genügt die Verwendung der Kauth API, die den Zugriff auf Dateien einschränkt. Apple bietet Entwicklern Benachrichtigungen über verschiedene Ereignisse im Scope an; für uns sind die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD von Bedeutung. Der Zugriff auf Dateien kann am einfachsten über den Pfad eingeschränkt werden – wir verwenden die API „vn_getpath“, um den Pfad zur Datei zu erhalten und vergleichen das Präfix des Pfades. Es sei bemerkt, dass zur Optimierung der Umbenennung von Ordnerpfaden mit Dateien das System nicht den Zugriff auf jede Datei autorisiert, sondern nur auf den Ordner, der umbenannt wurde. Es ist erforderlich, den übergeordneten Pfad zu vergleichen und KAUTH_VNODE_DELETE für ihn zu beschränken.

Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

Ein Nachteil dieses Ansatzes kann die geringe Leistung bei zunehmender Zahl von Präfixen sein. Um zu verhindern, dass der Vergleich O(prefix*length) wird, wobei prefix die Anzahl der Präfixe und length die Länge des Strings ist, kann ein deterministischer endlicher Automat (DEA) verwendet werden, der aus den Präfixen aufgebaut ist.

Betrachten wir, wie ein DFA für diese Menge von Präfixen aufgebaut wird. Wir initialisieren die Zeiger am Anfang jedes Präfixes. Wenn alle Zeiger auf dasselbe Zeichen zeigen, erhöhen wir jeden Zeiger um ein Zeichen und merken uns, dass die Länge der identischen Zeichenkette um eins größer ist. Wenn es zwei Zeiger gibt, deren Zeichen unterschiedlich sind, teilen wir die Zeiger in Gruppen nach dem Zeichen, auf das sie zeigen, und wiederholen den Algorithmus für jede Gruppe.

Im ersten Fall (wenn alle Zeichen unter den Zeigern identisch sind) erhalten wir einen Zustand des DFA, der nur einen Übergang über die identische Zeichenkette hat. Im zweiten Fall erhalten wir eine Übergangstabelle von der Größe 256 (Anzahl der Zeichen und maximale Anzahl der Gruppen) in die folgenden Zustände, die durch den rekursiven Aufruf der Funktion erzielt werden.

Betrachten wir ein Beispiel. Für die Menge der Präfixe ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kann der folgende DFA erstellt werden. Im Bild sind nur die Übergänge zu anderen Zuständen angegeben; andere Übergänge werden nicht als endgültig betrachtet.

Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

Beim Durchlaufen der Zustände des DFA kann es 3 Fälle geben.

  1. Der endgültige Zustand wurde erreicht — der Pfad ist gesichert, Operationen wie KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD sind eingeschränkt.
  2. Der endgültige Zustand wurde nicht erreicht, aber der Pfad "endete" (Null-Terminator erreicht) — der Pfad ist der übergeordnete, es müssen Einschränkungen für KAUTH_VNODE_DELETE gelten. Beachten Sie, dass wenn vnode ein Ordner ist, am Ende ‘/’ hinzugefügt werden muss, andernfalls könnte eine Einschränkung auf die Datei “/foor/bar/t” angewendet werden, was nicht korrekt ist.
  3. Der endgültige Zustand wurde nicht erreicht, der Pfad ist nicht abgeschlossen. Keiner der Präfixe stimmt überein, daher werden keine Einschränkungen eingeführt.

Fazit

Ziel der entwickelten Sicherheitslösungen ist es, die Sicherheit der Benutzer und ihrer Daten zu erhöhen. Einerseits wird dieses Ziel durch die Entwicklung des Softwareprodukts Acronis erreicht, das die Schwachstellen schließt, wo die Betriebssysteme anfällig sind. Andererseits sollten auch die Sicherheitsaspekte nicht vernachlässigt werden, die auf der OS-Seite verbessert werden können, besonders da das Schließen solcher Schwachstellen unsere eigene Widerstandsfähigkeit als Produkt erhöht. Eine Schwachstelle wurde dem Apple Product Security Team gemeldet und wurde in macOS 10.14.5 behoben (https://support.apple.com/en-gb/HT210119).

Wie man Prozesse und Kernel-Erweiterungen in macOS schützt

All dies kann nur erfolgen, wenn Ihr Tool offiziell im Kernel installiert wurde. Das bedeutet, dass es für externe und unerwünschte Software keine solchen Schlupflöcher gibt. Wie Sie sehen können, erfordert es jedoch auch Mühe, legitime Programme wie Antiviren- und Backup-Systeme zu schützen. Aber jetzt werden die neuen Produkte von Acronis für macOS über zusätzlichen Schutz gegen das Herunterladen aus dem System verfügen.

Quelle: habr.com

Erwerben Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster