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

ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen đŸ”„ ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster