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

Hallo Habr! Heute möchte ich darüber sprechen, wie Sie Prozesse vor Eindringlingen in macOS schützen können. Dies ist beispielsweise für ein Antiviren- oder Sicherungssystem nützlich, insbesondere angesichts der Tatsache, dass es unter macOS verschiedene Möglichkeiten gibt, den Prozess abzubrechen. Lesen Sie darüber und über Schutzmethoden unter einer Katze.

Bild

Der klassische Weg, einen Prozess zu beenden


Der bekannte Weg, einen Prozess zu "töten", besteht darin, ein Signal über einen SIGKILL-Prozess zu senden. Durch Bash können Sie den Standard "kill -SIGKILL PID" oder "pkill -9 NAME" zum Töten aufrufen. Der Befehl kill ist seit UNIX bekannt und steht nicht nur unter macOS, sondern auch auf anderen UNIX-ähnlichen Systemen zur Verfügung.

Wie in UNIX-ähnlichen Systemen können Sie mit macOS alle Signale für den Prozess abfangen, mit Ausnahme von zwei - SIGKILL und SIGSTOP. In diesem Artikel wird das SIGKILL-Signal in erster Linie als Signal betrachtet, das zum Abbruch eines Prozesses führt.

MacOS-Besonderheiten


Unter macOS ruft der Kill-Systemaufruf im XNU-Kernel die Psignal-Funktion (SIGKILL, ...) auf. Versuchen wir herauszufinden, welche anderen Benutzeraktionen im Benutzerbereich die psignal-Funktion aufrufen können. Wir eliminieren Aufrufe der psignal-Funktion in den internen Mechanismen des Kernels (obwohl sie möglicherweise nicht trivial sind, lassen wir sie für einen anderen Artikel :) - Signaturüberprüfung, Speicherfehler, Beenden / Beenden der Verarbeitung, Verletzung des Dateischutzes usw.

Wir beginnen die Übersicht mit der Funktion und dem entsprechenden Systemaufruf terminate_with_payload. Es ist ersichtlich, dass es zusätzlich zum klassischen Kill-Aufruf einen alternativen Ansatz gibt, der spezifisch für das MacOS-Betriebssystem ist und in BSD nicht zu finden ist. Die Funktionsprinzipien beider Systemaufrufe sind ebenfalls eng. Sie sind direkte Aufrufe der Psignal-Kernelfunktion. Beachten Sie auch, dass vor dem Beenden eines Prozesses eine "Cansignal" -Prüfung durchgeführt wird. Ob der Prozess ein Signal an einen anderen Prozess senden kann, lässt das System beispielsweise keine Anwendung zum Beenden von Systemprozessen zu.

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 Dämonen beim Systemstart und zum Steuern ihrer Lebensdauer ist das Starten. Ich möchte darauf hinweisen, dass der Quellcode für die alte Version von launchctl vor macOS 10.10 ist. Codebeispiele werden zur Veranschaulichung angegeben. Das moderne launchctl sendet launchd-Signale über XPC, die Logik von launchctl wird darauf übertragen.

Betrachten wir, wie die Anwendung gestoppt wird. Vor dem Senden eines SIGTERM-Signals versuchen sie, die Anwendung mithilfe des Systemaufrufs 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 ein Signal mit SIGTERM senden, sondern auch SIGKILL.

Indirekte Tötung - Ressourcenlimit


Ein interessanterer Fall ist in einem anderen Systemaufruf process_policy zu sehen . Die Standardverwendung dieses Systemaufrufs sind Anwendungsressourcenbeschränkungen. Beispielsweise gibt es für den Indexer eine Begrenzung der Prozessorzeit und des Speicherkontingents, sodass das System aufgrund von Datei-Caching-Aktionen nicht wesentlich langsamer wird. Wenn die Anwendung das Ressourcenlimit erreicht hat, wie aus der Funktion proc_apply_resource_actions ersichtlich, wird das 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 verursacht hat, nicht angemessen überprüft. In der Tat, eine Prüfung bestanden , aber es ist genug , um die alternative Flagge PROC_POLICY_ACTION_SET zu umgehen diese Bedingung zu verwenden.

Wenn Sie also das CPU-Nutzungskontingent durch die Anwendung „begrenzen“ (z. B. nur 1 ns ausführen lassen), können Sie jeden Prozess im System beenden. Die Malware kann also 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, ein SIGKILL-Signal zu verarbeiten :)

Bild

Wie löse ich das Problem?


Der einfachste Weg, um zu verhindern, dass ein Prozess abgebrochen wird, besteht darin, den Funktionszeiger in der Systemaufruftabelle zu ersetzen. Leider ist diese Methode 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 nicht in Kernelsymbolen gefunden werden. Sie müssen heuristische Suchmethoden verwenden, z. B. die dynamische Zerlegung einer 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 erstellt wurde. Wenn das Flag CONFIG_REQUIRES_U32_MUNGING deklariert ist, wird die Größe der Struktur geändert - ein zusätzliches Feld sy_arg_munge32 wird hinzugefügt. Es ist erforderlich, eine zusätzliche Überprüfung des Flags durchzuführen, mit dem der Kernel kompiliert wurde. Optional können Zeiger auf Funktionen mit bekannten verglichen werden.

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 modernen Versionen von macOS eine neue API für die Arbeit mit Prozessen. Mit der Endpoint Security-API können Clients viele Anforderungen für andere Prozesse autorisieren. Sie können also alle Signale für Prozesse blockieren, einschließlich des SIGKILL-Signals, indem Sie die oben genannte API verwenden.

#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("%d\n", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

Ebenso können Sie die MAC-Richtlinie im Kernel registrieren, der eine Signalschutzmethode (Richtlinie proc_check_signal) bereitstellt, die API wird jedoch nicht offiziell unterstützt.

Kernel-Erweiterungsschutz


Neben dem Schutz von Prozessen im System ist auch der Schutz der Kernel-Erweiterung selbst (kext) erforderlich. macOS bietet Entwicklern ein Framework zur bequemen Entwicklung von IOKit-Gerätetreibern. IOKit bietet nicht nur Tools für die Arbeit mit Geräten, sondern auch Treiberstapelmethoden mit Instanzen von C ++ - Klassen. Eine Anwendung im Userspace kann eine registrierte Instanz der Klasse "finden", um eine Kernel-Userspace-Verbindung herzustellen.

Um die Anzahl der Klasseninstanzen im System zu ermitteln, ist das Dienstprogramm ioclasscount vorhanden.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Jede Kernel-Erweiterung, die sich auf dem Treiberstapel registrieren möchte, muss eine von IOService geerbte Klasse deklarieren, z. B. my_kext_ioservice. Durch das Verbinden von Benutzeranwendungen wird eine neue Instanz der Klasse erstellt, die von IOUserClient erbt, im Beispiel my_kext_iouserclient.

Beim Versuch, den Treiber vom System zu entladen (Befehl kextunload), wird die virtuelle Funktion "bool terminate (IOOptionBits options)" aufgerufen. Es reicht aus, beim Aufruf der Terminate-Funktion false zurückzugeben, wenn versucht wird, kextunload zu deaktivieren.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}


Das IsUnloadAllowed-Flag kann von IOUserClient beim Booten gesetzt werden. Wenn das Laden begrenzt ist, 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. Klasseninstanzen können mit der Userspace-Funktion IOKitLib "IOCatalogueTerminate (mach_port_t, Flag uint32_t, Beschreibung io_name_t);" entladen werden. Sie können bei einem Aufruf des Befehls "Beenden" false zurückgeben, bis der Benutzerbereich der Anwendung beendet ist, dh die Funktion clientDied wird nicht aufgerufen.

Dateischutz


Zum Schutz von Dateien reicht es aus, die Kauth-API zu verwenden, mit der Sie den Zugriff auf Dateien einschränken können. Apple informiert Entwickler über verschiedene Ereignisse im Bereich. Die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD sind für uns wichtig. Der einfachste Weg, den Zugriff auf Dateien einzuschränken, besteht darin, mithilfe der API „vn_getpath“ den Pfad zur Datei abzurufen und das Pfadpräfix zu vergleichen. Beachten Sie, dass das System nicht den Zugriff auf jede Datei autorisiert, sondern nur auf den Ordner selbst, der umbenannt wurde, um die Umbenennung der Ordnerpfade mit Dateien zu optimieren. Es ist notwendig, den übergeordneten Pfad zu vergleichen und KAUTH_VNODE_DELETE dafür einzuschränken.

Bild

Der Nachteil dieses Ansatzes kann eine geringe Leistung mit zunehmender Anzahl von Präfixen sein. Damit 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 eine deterministische Finite-State-Machine (DFA) verwenden, die aus Präfixen besteht.

Überlegen Sie, wie Sie einen DFA für diesen Satz von Präfixen erstellen können. Wir initialisieren die Cursor am Anfang jedes Präfixes. Wenn alle Cursor auf dasselbe Zeichen zeigen, erhöhen wir jeden Cursor um ein Zeichen und denken daran, dass die Länge derselben Zeile um eins größer ist. Wenn sich zwei Cursor mit unterschiedlichen Symbolen befinden, teilen wir die Cursor durch das Symbol, auf das sie zeigen, in Gruppen ein und wiederholen den Algorithmus für jede Gruppe.

Im ersten Fall (alle Zeichen unter den Cursorn sind gleich) erhalten wir den DFA-Status, der nur einen Übergang in derselben Zeile enthält. Im zweiten Fall erhalten wir eine Übergangstabelle der Größe 256 (Anzahl der Zeichen und maximale Anzahl der Gruppen) in den nachfolgenden Zuständen, die durch rekursives Aufrufen der Funktion erhalten werden.

Betrachten Sie ein Beispiel. Für eine Reihe von Präfixen ("/ foo / bar / tmp /", "/ var / db / foo /", "/ foo / bar / aba /", "foo / bar / aac /") können Sie den folgenden DFA erhalten. Die Abbildung zeigt nur Übergänge, die zu anderen Zuständen führen. Andere Übergänge sind nicht endgültig.

Bild

Beim Durchlaufen der DKA-Staaten kann es 3 Fälle geben.

  1. Der Endzustand wurde erreicht - der Pfad ist geschützt, wir beschränken die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD
  2. , “” ( -) — , KAUTH_VNODE_DELETE. , vnode , ‘/’, “/foor/bar/t”, .
  3. , . , .


Ziel der entwickelten Sicherheitslösungen ist es, das Sicherheitsniveau des Benutzers und seiner Daten zu erhöhen. Dieses Ziel wird zum einen durch die Entwicklung eines Acronis-Softwareprodukts sichergestellt, das Schwachstellen abdeckt, bei denen das Betriebssystem selbst „schwach“ ist. Andererseits sollten wir die Verbesserung der Sicherheitsaspekte, die auf der Betriebssystemseite verbessert werden können, nicht vernachlässigen, zumal das Schließen solcher Schwachstellen unsere eigene Stabilität als Produkt erhöht. Die Sicherheitsanfälligkeit wurde vom Apple Product Security Team gemeldet und in macOS 10.14.5 (https://support.apple.com/en-gb/HT210119) behoben.

Bild

All dies ist nur möglich, wenn Ihr Dienstprogramm offiziell im Kernel installiert wurde. Das heißt, es gibt keine solchen Lücken für externe und unerwünschte Software. Wie Sie sehen, müssen Sie jedoch hart arbeiten, um legitime Programme wie Antiviren- und Sicherungssysteme zu schützen. Jetzt bieten neue Acronis-Produkte für macOS zusätzlichen Schutz vor dem Entladen aus dem System.

All Articles