Hallo, Habr! Heute möchte ich darĂŒber sprechen, wie man Prozesse in macOS vor Angriffen durch Eindringlinge schĂŒtzt. Dies ist beispielsweise nĂŒtzlich fĂŒr Antivirenprogramme oder Backup-Systeme, insbesondere im Hinblick darauf, dass es unter macOS mehrere Möglichkeiten gibt, einen Prozess "zu killen". DarĂŒber und ĂŒber die Schutzmethoden lesen Sie im Folgenden.
Klassischer Weg, um einen Prozess zu killen
Der allgemein bekannte Weg, einen Prozess zu killen, besteht darin, dem Prozess das SIGKILL-Signal zu senden. Ăber Bash kann man die Standardbefehle "kill -SIGKILL PID" oder "pkill -9 NAME" verwenden, um den Prozess zu beenden. Der Befehl "kill" ist seit den Tagen von UNIX bekannt und steht nicht nur unter macOS, sondern auch unter anderen UNIX-Ă€hnlichen Systemen zur VerfĂŒgung.
Ăhnlich wie in UNIX-Ă€hnlichen Systemen ermöglicht es macOS, alle Signale an einen Prozess abzufangen, mit Ausnahme von zwei: SIGKILL und SIGSTOP. In diesem Artikel wird in erster Linie das Signal SIGKILL betrachtet, da es den Prozessterminierung auslöst.
Spezifikationen von macOS
In macOS ruft der Systemaufruf kill im XNU-Kernel die Funktion psignal(SIGKILL, âŠ) auf. Lassen Sie uns prĂŒfen, welche anderen Benutzeraktionen im Userspace die Funktion psignal auslösen können. Wir werden Aufrufe der Funktion psignal in den internen Mechanismen des Kernels ausklammern (obwohl sie nicht trivial sein mögen, lassen wir sie fĂŒr einen anderen Artikel đ â SignaturprĂŒfung, Speicherfehler, Exit-/Terminate-Verarbeitung, VerstöĂe gegen den Dateischutz usw.).
Beginnen wir die Ăbersicht mit der Funktion und dem entsprechenden Systemaufruf . Es ist ersichtlich, dass es neben dem klassischen Aufruf kill einen alternativen Ansatz gibt, der spezifisch fĂŒr das Betriebssystem macOS ist und in BSD nicht vorkommt. Die Funktionsweise beider Systemaufrufe ist ebenfalls Ă€hnlich. Sie stellen direkte Aufrufe der Kernel-Funktion psignal dar. Zudem sei erwĂ€hnt, dass vor dem Töten eines Prozesses ĂŒberprĂŒft wird, ob "cansignal" â das bedeutet, ob ein Prozess einem anderen Prozess ein Signal senden kann; das System erlaubt es nicht, dass eine Anwendung Systemprozesse tötet.
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() wird ein SIGKILL an den angegebenen Thread senden oder
* zurĂŒckgeben, wenn der Thread und/oder die Aufgabe bereits beendet wird. So oder so,
* der aktuelle Thread wird nicht in den 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, um beim Systemstart Daemons zu erstellen und ihre Lebensdauer zu steuern, ist launchd. Ich möchte darauf hinweisen, dass die Quellcodes fĂŒr eine Ă€ltere Version von launchctl vor macOS 10.10 bereitgestellt werden, die Codebeispiele dienen nur zur Veranschaulichung. Das moderne launchctl sendet Signale an launchd ĂŒber XPC, die Logik von launchctl wurde dorthin ĂŒbertragen.
Schauen wir uns an, wie genau Anwendungen gestoppt werden. Vor dem Senden des Signals SIGTERM wird versucht, die Anwendung ĂŒber den Systemaufruf 'proc_terminate' zu stoppen.
...
error = proc_terminate(j->p, &sig);
if (error) {
job_log(j, LOG_ERR | LOG_CONSOLE, "Job konnte nicht beendet werden: %d: %s", error, strerror(error));
job_log(j, LOG_NOTICE | LOG_CONSOLE, "Verwendung der Fallback-Option zum Beenden des Jobs...");
error = kill2(j->p, SIGTERM);
if (error) {
job_log(j, LOG_ERR, "Signal konnte nicht an den Job gesendet werden: %d: %s", error, strerror(error));
}
...
Unter der Haube kann proc_terminate, trotz seines Namens, nicht nur psignal mit SIGTERM senden, sondern auch SIGKILL.
Indirekte Tötung â RessourcenbeschrĂ€nkung
Ein interessanterer Fall kann in einem anderen Systemaufruf gesehen werden . Die Standardverwendung dieses Systemaufrufs besteht darin, Ressourcen fĂŒr Anwendungen zu beschrĂ€nken, zum Beispiel fĂŒr den Indexierer eine Begrenzung der zulĂ€ssigen CPU- und SpeicherkapazitĂ€t, um sicherzustellen, dass das System durch Dateicaching nicht erheblich verlangsamt wird. Wenn die Anwendung die RessourcenbeschrĂ€nkung erreicht hat, kann man in der Funktion proc_apply_resource_actions sehen, wird das Signal SIGKILL an den Prozess gesendet.
Obwohl dieser Systemaufruf potenziell Prozesse beenden kann, hat das System die Berechtigungen des aufrufenden Prozesses nicht ausreichend geprĂŒft. TatsĂ€chlich gab es eine ĂberprĂŒfung, , aber es reicht aus, das alternative Flag PROC_POLICY_ACTION_SET zu verwenden, um diese Bedingung zu umgehen.
Von hier aus, wenn Sie die CPU-Nutzungsquote fĂŒr die Anwendung 'beschrĂ€nken' (zum Beispiel nur 1 ns zulassen), können Sie jeden Prozess im System beenden. So kann schadhafter Code jeden Prozess im System beenden, einschlieĂlich des Antivirenprozesses. Auch interessant ist der Effekt, der beim Beenden des Prozesses mit der PID 1 (launchctl) auftritt - ein Kernel-Panic beim Versuch, das SIGKILL-Signal zu verarbeiten đ

Wie löst man das Problem?
Der direkteste Weg, um das Beenden eines Prozesses zu verhindern, besteht darin, den Zeiger auf die Funktion in der Systemaufruf-Tabelle zu Ă€ndern. Leider ist dieser Ansatz aus mehreren 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 den Symbolen des Kernels gefunden werden. Daher mĂŒssen heuristische Suchmethoden verwendet werden, wie zum Beispiel 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, Ă€ndert sich die GröĂe der Struktur - ein zusĂ€tzliches Feld wird hinzugefĂŒgt. . Es ist notwendig, zusĂ€tzliche ĂberprĂŒfungen durchzufĂŒhren, um zu bestimmen, mit welchem Flag der Kernel kompiliert wurde, zum Beispiel indem man die Zeiger auf die Funktionen mit bekannten vergleicht.
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 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 SIGKILL-Signals, mithilfe der oben erwĂ€hnten 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;
}
Analog kann im Kernel eine MAC-Policy registriert werden, die eine Methode zum Schutz vor Signalen (policy proc_check_signal) bereitstellt. Diese API wird jedoch nicht offiziell unterstĂŒtzt.
Schutz der Kernel-Erweiterung
ZusĂ€tzlich zum Schutz der Prozesse im System ist auch der Schutz der Kernel-Erweiterung (kext) unbedingt erforderlich. macOS stellt Entwicklern ein Framework zur VerfĂŒgung, das die Entwicklung von IOKit-GerĂ€tetreibern erleichtert. Neben den Werkzeugen zum Umgang mit GerĂ€ten ermöglicht IOKit auch 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 von Klasseninstanzen im System gibt es das Dienstprogramm 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, in diesem 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 genĂŒgt, false im Aufruf der Funktion terminate zurĂŒckzugeben, um das 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 beim Laden durch IOUserClient gesetzt werden. Bei einer BeschrĂ€nkung auf das Entladen gibt der Befehl kextunload die folgende Ausgabe zurĂŒck:
admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Passwort:
(kernel) Konnte kext my.kext.test nicht entfernen; Dienste konnten nicht beendet werden - 0xe00002c7.
Konnte my.kext.test nicht entladen - (iokit/common) nicht unterstĂŒtzte Funktion.
Einen Ă€hnlichen Schutz muss auch fĂŒr IOUserClient erfolgen. Klasseninstanzen können mittels der Userspace-Funktion IOKitLib âIOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);â entladen werden. Man kann false im Aufruf des Befehls âterminateâ zurĂŒckgeben, solange die Userspace-Anwendung nicht âstirbtâ, das heiĂt, der Aufruf der Funktion âclientDiedâ nicht erfolgt.
Dateischutz
Zum Schutz von Dateien reicht es aus, die Kauth-API zu verwenden, 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 lĂ€sst sich am einfachsten ĂŒber den Pfad einschrĂ€nken â wir verwenden die API "vn_getpath", um den Pfad zur Datei zu ermitteln, und fĂŒhren einen Vergleich des PfadprĂ€fixes durch. Es ist zu beachten, dass das System zur Optimierung der Umbenennung von Ordnerpfaden mit Dateien nicht den Zugriff auf jede Datei autorisiert, sondern nur auf den Ordner, der umbenannt wurde. Es muss ein Vergleich des ĂŒbergeordneten Pfades erfolgen, und KAUTH_VNODE_DELETE muss fĂŒr diesen eingeschrĂ€nkt werden.

Ein Nachteil dieses Ansatzes kann die geringe Leistung bei steigender Anzahl von PrÀfixen sein. Damit der Vergleich nicht O(prefix*length) ergibt, wobei prefix die Anzahl der PrÀfixe und length die LÀnge der Zeichenkette ist, kann ein deterministischer endlicher Automat (DEA) genutzt werden, der auf den PrÀfixen basiert.
Betrachten wir, wie ein DEA fĂŒr dieses Set von PrĂ€fixen aufgebaut wird. Wir initialisieren Zeiger am Anfang jedes PrĂ€fixes. Wenn alle Zeiger auf dasselbe Symbol 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 zwei Zeiger existieren, deren Symbole unterschiedlich sind, teilen wir die Zeiger in Gruppen nach dem Symbol, auf das sie zeigen, und wiederholen den Algorithmus fĂŒr jede Gruppe.
Im ersten Fall (alle Symbole unter den Zeigern sind identisch) erhalten wir einen Zustand des DEA, der nur einen Ăbergang ĂŒber die identische Zeichenkette hat. Im zweiten Fall erhalten wir eine Ăbergangstabelle mit der GröĂe 256 (Anzahl der Symbole und maximale Anzahl der Gruppen) fĂŒr die folgenden ZustĂ€nde, die durch rekursive Funktionsaufrufe erhalten werden.
Betrachten wir ein Beispiel. FĂŒr das Set von PrĂ€fixen ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kann folgender DEA ermittelt werden. In der Abbildung sind nur die ĂbergĂ€nge dargestellt, die zu anderen ZustĂ€nden fĂŒhren; andere ĂbergĂ€nge werden kein Endzustand sein.

Beim Durchlaufen der ZustÀnde des DEA kann es drei FÀlle geben.
- Ein finales Zustand wurde erreicht â der Pfad ist geschĂŒtzt; wir schrĂ€nken die Operationen KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA und KAUTH_VNODE_DELETE_CHILD ein.
- Der finale Zustand wurde nicht erreicht, aber der Weg "endete" (der Null-Termin wurde erreicht) â der Weg ist ĂŒbergeordnet, es mĂŒssen EinschrĂ€nkungen fĂŒr KAUTH_VNODE_DELETE vorgenommen werden. Beachten Sie, dass, wenn das vnode ein Ordner ist, ein â/â am Ende hinzugefĂŒgt werden muss, andernfalls kann die EinschrĂ€nkung auf die Datei "/foor/bar/t" angewendet werden, was falsch ist.
- Der finale Zustand wurde nicht erreicht, der Weg ist nicht zu Ende. Keiner der PrĂ€fixe entspricht dem gegebenen, keine EinschrĂ€nkungen werden eingefĂŒhrt.
Fazit
Ziel der entwickelten Sicherheitslösungen ist es, das Sicherheitsniveau des Nutzers und seiner Daten zu erhöhen. Einerseits wird dieses Ziel durch die Entwicklung des Softwareprodukts Acronis erreicht, das die Schwachstellen schlieĂt, wo das Betriebssystem selbst "schwach" ist. Andererseits sollten auch die Sicherheitsaspekte nicht vernachlĂ€ssigt werden, die auf der Seite des Betriebssystems verbessert werden können, zumal das SchlieĂen solcher Schwachstellen unsere eigene WiderstandsfĂ€higkeit als Produkt erhöht. Die Schwachstelle wurde an das Apple Product Security Team gemeldet und in macOS 10.14.5 behoben (https://support.apple.com/en-gb/HT210119).

All dies kann nur dann durchgefĂŒhrt werden, wenn Ihr Dienstprogramm offiziell im Kern installiert wurde. Das heiĂt, fĂŒr externe und unerwĂŒnschte Software gibt es keine solchen Schlupflöcher. Allerdings, wie Sie sehen können, muss selbst der Schutz legitimer Programme wie Antiviren- und Backup-Systemen hart erarbeitet werden. Aber jetzt werden die neuen Produkte von Acronis fĂŒr macOS zusĂ€tzlichen Schutz vor dem Entladen aus dem System haben.
Quelle: habr.com
