So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

Hallo, Habr! Heute möchte ich darüber sprechen, wie Sie Prozesse in macOS vor Angriffen von Angreifern schützen können. Dies ist beispielsweise für ein Antiviren- oder Backup-System nützlich, insbesondere da es unter macOS mehrere Möglichkeiten gibt, einen Prozess zu „killen“. Lesen Sie mehr darüber und über Schutzmethoden unter dem Schnitt.

So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

Der klassische Weg, einen Prozess zu „killen“.

Eine bekannte Methode, einen Prozess zu „killen“, besteht darin, ein SIGKILL-Signal an den Prozess zu senden. Über Bash können Sie zum Töten den Standard „kill -SIGKILL PID“ oder „pkill -9 NAME“ aufrufen. Der Befehl „kill“ ist seit den Tagen von UNIX bekannt und nicht nur auf macOS, sondern auch auf anderen UNIX-ähnlichen Systemen verfügbar.

Genau wie in UNIX-ähnlichen Systemen können Sie mit macOS alle Signale an einen Prozess abfangen, mit Ausnahme von zwei – SIGKILL und SIGSTOP. Dieser Artikel konzentriert sich hauptsächlich auf das SIGKILL-Signal als ein Signal, das dazu führt, dass ein Prozess beendet wird.

macOS-Besonderheiten

Unter macOS ruft der Kill-Systemaufruf im XNU-Kernel die Funktion psignal(SIGKILL,...) auf. Versuchen wir herauszufinden, welche anderen Benutzeraktionen im Userspace von der psignal-Funktion aufgerufen werden können. Lassen Sie uns Aufrufe der psignal-Funktion in den internen Mechanismen des Kernels aussortieren (auch wenn sie nicht trivial sind, belassen wir sie für einen anderen Artikel 🙂 - Signaturüberprüfung, Speicherfehler, Exit-/Beendigungsbehandlung, Dateischutzverletzungen usw .

Beginnen wir die Überprüfung mit der Funktion und dem entsprechenden Systemaufruf Beenden_mit_Nutzlast. Es ist zu erkennen, dass es neben dem klassischen Kill-Call einen alternativen Ansatz gibt, der spezifisch für das Betriebssystem macOS ist und in BSD nicht zu finden ist. Auch die Funktionsprinzipien beider Systemaufrufe sind ähnlich. Es handelt sich um direkte Aufrufe der Kernelfunktion psignal. Beachten Sie außerdem, dass vor dem Beenden eines Prozesses eine „Cansignal“-Prüfung durchgeführt wird – ob der Prozess ein Signal an einen anderen Prozess senden kann; das System erlaubt beispielsweise keiner Anwendung, Systemprozesse zu beenden.

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

Die Standardmethode zum Erstellen von Daemons beim Systemstart und zum Steuern ihrer Lebensdauer ist launchd. Bitte beachten Sie, dass die Quellen für die alte Version von launchctl bis macOS 10.10 gelten, Codebeispiele dienen der Veranschaulichung. Modernes launchctl sendet launchd-Signale über XPC, die launchctl-Logik wurde dorthin verschoben.

Schauen wir uns an, wie genau Anwendungen gestoppt werden. Vor dem Senden des SIGTERM-Signals wird versucht, die Anwendung mit dem Systemaufruf „proc_terminate“ zu stoppen.

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

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

Indirekter Kill – Ressourcenlimit

Ein interessanterer Fall ist in einem anderen Systemaufruf zu sehen Prozessrichtlinie. Eine häufige Verwendung dieses Systemaufrufs besteht darin, Anwendungsressourcen zu begrenzen, z. B. damit ein Indexer die CPU-Zeit und Speicherkontingente begrenzt, damit das System durch Datei-Caching-Aktivitäten nicht wesentlich verlangsamt wird. Wenn eine Anwendung ihr Ressourcenlimit erreicht hat, wie aus der Funktion proc_apply_resource_actions ersichtlich ist, wird ein SIGKILL-Signal an den Prozess gesendet.

Obwohl dieser Systemaufruf möglicherweise einen Prozess beenden könnte, hat das System die Rechte des Prozesses, der den Systemaufruf aufruft, nicht ausreichend überprüft. Tatsächlich überprüfen existierte, aber es reicht aus, das alternative Flag PROC_POLICY_ACTION_SET zu verwenden, um diese Bedingung zu umgehen.

Wenn Sie also das CPU-Auslastungskontingent der Anwendung „begrenzen“ (z. B. die Ausführung nur 1 ns zulassen), können Sie jeden Prozess im System beenden. Somit kann die Malware jeden Prozess auf dem System beenden, einschließlich des Antivirenprozesses. Interessant ist auch der Effekt, der auftritt, wenn ein Prozess mit PID 1 (launchctl) beendet wird – Kernel-Panik beim Versuch, das SIGKILL-Signal zu verarbeiten :)

So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

Wie löse ich das Problem?

Der einfachste Weg, das Abbrechen eines Prozesses zu verhindern, besteht darin, den Funktionszeiger in der Systemaufruftabelle zu ersetzen. Leider ist diese Methode aus vielen Gründen nicht trivial.

Erstens ist das Symbol, das den Speicherort von sysent steuert, nicht nur privat für das XNU-Kernelsymbol, sondern kann auch nicht in Kernelsymbolen gefunden werden. Sie müssen heuristische Suchmethoden verwenden, z. B. die dynamische Zerlegung der Funktion und die Suche nach einem Zeiger 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 notwendig, eine zusätzliche Prüfung durchzuführen, um festzustellen, mit welchem ​​Flag der Kernel kompiliert wurde, oder alternativ die Funktionszeiger mit bekannten zu vergleichen.

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 stellt Apple in modernen Versionen von macOS eine neue API für die Arbeit mit Prozessen bereit. Mit der Endpoint Security API können Clients viele Anfragen an andere Prozesse autorisieren. Somit können Sie mithilfe der oben genannten API alle Signale an Prozesse, einschließlich des SIGKILL-Signals, blockieren.

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

Ebenso kann im Kernel eine MAC-Richtlinie registriert werden, die eine Signalschutzmethode bereitstellt (Richtlinie proc_check_signal), die API wird jedoch nicht offiziell unterstützt.

Schutz der Kernel-Erweiterung

Neben dem Schutz der Prozesse im System ist auch der Schutz der Kernel-Erweiterung selbst (kext) erforderlich. macOS bietet Entwicklern ein Framework zur einfachen Entwicklung von IOKit-Gerätetreibern. Neben der Bereitstellung von Tools für die Arbeit mit Geräten bietet IOKit auch Methoden für das Treiber-Stacking mithilfe von Instanzen von C++-Klassen. Eine Anwendung im Userspace kann eine registrierte Instanz der Klasse „finden“, um eine Kernel-Userspace-Beziehung herzustellen.

Um die Anzahl der Klasseninstanzen im System zu ermitteln, gibt es das Dienstprogramm ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Jede Kernel-Erweiterung, die sich beim Treiber-Stack registrieren möchte, muss eine Klasse deklarieren, die von IOService erbt, in diesem Fall beispielsweise my_kext_ioservice. Durch das Verbinden von Benutzeranwendungen wird eine neue Instanz der Klasse erstellt, die von IOUserClient erbt, in diesem Beispiel my_kext_iouserclient.

Beim Versuch, einen Treiber vom System zu entladen (Befehl kextunload), wird die virtuelle Funktion „boolterminate(IOOptionBits options)“ aufgerufen. Um kextunload zu deaktivieren, reicht es aus, beim Aufruf zum Beenden beim Entladeversuch „false“ zurückzugeben.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Das IsUnloadAllowed-Flag kann beim Laden vom IOUserClient gesetzt werden. Wenn es ein Download-Limit gibt, gibt der Befehl kextunload die folgende Ausgabe zurück:

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.

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

Dateischutz

Um Dateien zu schützen, reicht es aus, die Kauth-API zu verwenden, mit der Sie den Zugriff auf Dateien einschränken können. Apple stellt Entwicklern Benachrichtigungen über verschiedene Ereignisse im Bereich zur Verfügung; für uns sind die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD wichtig. Der einfachste Weg, den Zugriff auf Dateien einzuschränken, ist der Pfad – wir verwenden die API „vn_getpath“, um den Pfad zur Datei abzurufen und das Pfadpräfix zu vergleichen. Beachten Sie, dass das System zur Optimierung der Umbenennung von Dateiordnerpfaden nicht den Zugriff auf jede einzelne Datei, sondern nur auf den Ordner selbst, der umbenannt wurde, autorisiert. Es ist notwendig, den übergeordneten Pfad zu vergleichen und KAUTH_VNODE_DELETE dafür einzuschränken.

So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

Der Nachteil dieses Ansatzes kann eine geringe Leistung sein, wenn die Anzahl der Präfixe zunimmt. Um sicherzustellen, dass der Vergleich nicht gleich O (Präfix * Länge) ist, wobei Präfix die Anzahl der Präfixe und Länge die Länge der Zeichenfolge ist, können Sie einen durch Präfixe erstellten deterministischen endlichen Automaten (DFA) verwenden.

Betrachten wir eine Methode zum Erstellen eines DFA für einen bestimmten Satz von Präfixen. Wir initialisieren die Cursor am Anfang jedes Präfixes. Wenn alle Cursor auf dasselbe Zeichen zeigen, erhöhen Sie jeden Cursor um ein Zeichen und denken Sie daran, dass die Länge derselben Zeile um eins größer ist. Wenn zwei Cursor mit unterschiedlichen Symbolen vorhanden sind, teilen Sie die Cursor entsprechend dem Symbol, auf das sie zeigen, in Gruppen ein und wiederholen Sie den Algorithmus für jede Gruppe.

Im ersten Fall (alle Zeichen unter den Cursorn sind gleich) erhalten wir einen DFA-Status, der nur einen Übergang entlang derselben Zeile aufweist. Im zweiten Fall erhalten wir eine Tabelle mit Übergängen der Größe 256 (Anzahl der Zeichen und maximale Anzahl der Gruppen) zu nachfolgenden Zuständen, die wir durch rekursiven Aufruf der Funktion erhalten.

Schauen wir uns ein Beispiel an. Für eine Reihe von Präfixen („/foo/bar/tmp/“, „/var/db/foo/“, „/foo/bar/aba/“, „foo/bar/aac/“) können Sie Folgendes erhalten DFA. Die Abbildung zeigt nur Übergänge, die zu anderen Zuständen führen; andere Übergänge sind nicht endgültig.

So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

Bei der Durchreise durch die DKA-Staaten kann es zu 3 Fällen kommen.

  1. Der Endzustand ist erreicht – der Pfad ist geschützt, wir beschränken die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD
  2. Der Endzustand wurde nicht erreicht, aber der Pfad „endete“ (das Null-Terminator wurde erreicht) – der Pfad ist ein übergeordneter Pfad, es ist notwendig, KAUTH_VNODE_DELETE zu begrenzen. Beachten Sie, dass Sie, wenn vnode ein Ordner ist, am Ende ein „/“ hinzufügen müssen, da es sonst möglicherweise auf die Datei „/foor/bar/t“ beschränkt wird, was falsch ist.
  3. Der Endzustand wurde nicht erreicht, der Weg endete nicht. Keines der Präfixe stimmt mit diesem überein, wir führen keine Einschränkungen ein.

Abschluss

Ziel der entwickelten Sicherheitslösungen ist es, das Sicherheitsniveau des Benutzers und seiner Daten zu erhöhen. Dieses Ziel wird einerseits durch die Entwicklung des Softwareprodukts Acronis erreicht, das jene Schwachstellen schließt, bei denen das Betriebssystem selbst „schwach“ ist. Andererseits sollten wir die Stärkung der Sicherheitsaspekte, die auf der Seite des Betriebssystems verbessert werden können, nicht vernachlässigen, zumal das Schließen solcher Schwachstellen unsere eigene Stabilität als Produkt erhöht. Die Sicherheitslücke wurde dem Apple Product Security Team gemeldet und in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119) behoben.

So schützen Sie Prozesse und Kernel-Erweiterungen unter macOS

All dies ist nur möglich, wenn Ihr Dienstprogramm offiziell im Kernel installiert wurde. Das heißt, es gibt keine derartigen Schlupflöcher für externe und unerwünschte Software. Wie Sie jedoch sehen, erfordert selbst der Schutz legitimer Programme wie Antiviren- und Backup-Systeme Arbeit. Aber jetzt verfügen neue Acronis-Produkte für macOS über einen zusätzlichen Schutz gegen das Entladen vom System.

Source: habr.com

Kommentar hinzufügen