Translated ['src/macos-hardening/macos-security-and-privilege-escalation

This commit is contained in:
Translator 2025-07-23 10:11:20 +00:00
parent a66a2e25c7
commit 442c36ddd5

View File

@ -9,21 +9,21 @@
## 1. Thread Hijacking
Zunächst wird die **`task_threads()`**-Funktion auf dem Task-Port aufgerufen, um eine Thread-Liste vom Remote-Task zu erhalten. Ein Thread wird zum Hijacking ausgewählt. Dieser Ansatz weicht von herkömmlichen Code-Injektionsmethoden ab, da das Erstellen eines neuen Remote-Threads aufgrund der neuen Minderung, die `thread_create_running()` blockiert, verboten ist.
Zunächst wird die Funktion `task_threads()` auf dem Task-Port aufgerufen, um eine Thread-Liste vom Remote-Task zu erhalten. Ein Thread wird zum Hijacking ausgewählt. Dieser Ansatz weicht von herkömmlichen Code-Injection-Methoden ab, da das Erstellen eines neuen Remote-Threads aufgrund der Minderung, die `thread_create_running()` blockiert, verboten ist.
Um den Thread zu steuern, wird **`thread_suspend()`** aufgerufen, um seine Ausführung zu stoppen.
Um den Thread zu steuern, wird `thread_suspend()` aufgerufen, um seine Ausführung zu stoppen.
Die einzigen Operationen, die auf dem Remote-Thread erlaubt sind, beinhalten **Stoppen** und **Starten**, **Abrufen** und **Ändern** seiner Registerwerte. Remote-Funktionsaufrufe werden initiiert, indem die Register `x0` bis `x7` auf die **Argumente** gesetzt, **`pc`** auf die gewünschte Funktion konfiguriert und der Thread aktiviert wird. Um sicherzustellen, dass der Thread nach der Rückkehr nicht abstürzt, ist es notwendig, die Rückkehr zu erkennen.
Die einzigen Operationen, die auf dem Remote-Thread erlaubt sind, beinhalten **Stoppen** und **Starten** sowie **Abrufen**/**Ändern** seiner Registerwerte. Remote-Funktionsaufrufe werden initiiert, indem die Register `x0` bis `x7` auf die **Argumente** gesetzt, `pc` auf die gewünschte Funktion konfiguriert und der Thread fortgesetzt wird. Um sicherzustellen, dass der Thread nach der Rückkehr nicht abstürzt, ist es notwendig, die Rückkehr zu erkennen.
Eine Strategie besteht darin, einen **Ausnahmebehandler** für den Remote-Thread mit `thread_set_exception_ports()` zu registrieren, wobei das `lr`-Register vor dem Funktionsaufruf auf eine ungültige Adresse gesetzt wird. Dies löst eine Ausnahme nach der Funktionsausführung aus, die eine Nachricht an den Ausnahmeport sendet und eine Zustandsinspektion des Threads ermöglicht, um den Rückgabewert wiederherzustellen. Alternativ, wie im Triple-Fetch-Exploit von Ian Beer übernommen, wird `lr` so gesetzt, dass es unendlich schleift. Die Register des Threads werden dann kontinuierlich überwacht, bis **`pc` auf diese Anweisung zeigt**.
Eine Strategie besteht darin, einen **Ausnahmebehandler** für den Remote-Thread mit `thread_set_exception_ports()` zu registrieren und das Register `lr` vor dem Funktionsaufruf auf eine ungültige Adresse zu setzen. Dies löst nach der Funktionsausführung eine Ausnahme aus, die eine Nachricht an den Ausnahmeport sendet, wodurch eine Zustandsinspektion des Threads ermöglicht wird, um den Rückgabewert zu ermitteln. Alternativ, wie im *triple_fetch*-Exploit von Ian Beer übernommen, wird `lr` so gesetzt, dass es unendlich schleift; die Register des Threads werden dann kontinuierlich überwacht, bis `pc` auf diese Anweisung zeigt.
## 2. Mach ports for communication
Die nächste Phase besteht darin, Mach-Ports einzurichten, um die Kommunikation mit dem Remote-Thread zu erleichtern. Diese Ports sind entscheidend für den Transfer beliebiger Sende- und Empfangsrechte zwischen Tasks.
Die nächste Phase besteht darin, Mach-Ports einzurichten, um die Kommunikation mit dem Remote-Thread zu erleichtern. Diese Ports sind entscheidend für den Transfer beliebiger Send/Receive-Rechte zwischen Tasks.
Für die bidirektionale Kommunikation werden zwei Mach-Empfangsrechte erstellt: eines im lokalen und das andere im Remote-Task. Anschließend wird ein Senderecht für jeden Port an den entsprechenden Task übertragen, um den Nachrichtenaustausch zu ermöglichen.
Für die bidirektionale Kommunikation werden zwei Mach-Receive-Rechte erstellt: eines im lokalen und das andere im Remote-Task. Anschließend wird ein Senderecht für jeden Port an den entsprechenden Task übertragen, um den Nachrichtenaustausch zu ermöglichen.
Fokussiert auf den lokalen Port, wird das Empfangsrecht vom lokalen Task gehalten. Der Port wird mit `mach_port_allocate()` erstellt. Die Herausforderung besteht darin, ein Senderecht für diesen Port in den Remote-Task zu übertragen.
Fokussiert auf den lokalen Port, wird das Receive-Recht vom lokalen Task gehalten. Der Port wird mit `mach_port_allocate()` erstellt. Die Herausforderung besteht darin, ein Senderecht für diesen Port in den Remote-Task zu übertragen.
Eine Strategie besteht darin, `thread_set_special_port()` zu nutzen, um ein Senderecht für den lokalen Port im `THREAD_KERNEL_PORT` des Remote-Threads zu platzieren. Dann wird der Remote-Thread angewiesen, `mach_thread_self()` aufzurufen, um das Senderecht abzurufen.
@ -33,23 +33,23 @@ Der Abschluss dieser Schritte führt zur Einrichtung von Mach-Ports, die die Gru
## 3. Basic Memory Read/Write Primitives
In diesem Abschnitt liegt der Fokus auf der Nutzung des Execute-Primitivs, um grundlegende Speicher-Lese- und Schreibprimitive zu etablieren. Diese ersten Schritte sind entscheidend, um mehr Kontrolle über den Remote-Prozess zu erlangen, obwohl die Primitiven in diesem Stadium nicht viele Zwecke erfüllen werden. Bald werden sie auf fortgeschrittenere Versionen aktualisiert.
In diesem Abschnitt liegt der Fokus auf der Nutzung des Execute-Primitivs, um grundlegende Speicher-Lese-/Schreib-Primitiven zu etablieren. Diese ersten Schritte sind entscheidend, um mehr Kontrolle über den Remote-Prozess zu erlangen, obwohl die Primitiven in diesem Stadium nicht viele Zwecke erfüllen werden. Bald werden sie auf fortgeschrittenere Versionen aktualisiert.
### Memory Reading and Writing Using Execute Primitive
### Memory reading and writing using the execute primitive
Das Ziel ist es, Speicher zu lesen und zu schreiben, indem spezifische Funktionen verwendet werden. Zum Lesen von Speicher werden Funktionen verwendet, die einer folgenden Struktur ähneln:
Das Ziel ist es, Speicher zu lesen und zu schreiben, indem spezifische Funktionen verwendet werden. Für **Speicher lesen**:
```c
uint64_t read_func(uint64_t *address) {
return *address;
}
```
Und zum Schreiben in den Speicher werden Funktionen verwendet, die einer ähnlichen Struktur folgen:
Für **Speicher schreiben**:
```c
void write_func(uint64_t *address, uint64_t value) {
*address = value;
}
```
Diese Funktionen entsprechen den angegebenen Assemblierungsanweisungen:
Diese Funktionen entsprechen der folgenden Assemblierung:
```
_read_func:
ldr x0, [x0]
@ -60,102 +60,114 @@ ret
```
### Identifizierung geeigneter Funktionen
Ein Scan gängiger Bibliotheken hat geeignete Kandidaten für diese Operationen ergeben:
Ein Scan gängiger Bibliotheken ergab geeignete Kandidaten für diese Operationen:
1. **Speicher lesen:**
Die Funktion `property_getName()` aus der [Objective-C-Laufzeitbibliothek](https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-runtime-new.mm.auto.html) wird als geeignete Funktion zum Lesen von Speicher identifiziert. Die Funktion wird unten beschrieben:
1. **Speicher lesen — `property_getName()`** (libobjc):
```c
const char *property_getName(objc_property_t prop) {
return prop->name;
}
```
Diese Funktion wirkt effektiv wie die `read_func`, indem sie das erste Feld von `objc_property_t` zurückgibt.
2. **Speicher schreiben:**
Eine vorgefertigte Funktion zum Schreiben von Speicher zu finden, ist schwieriger. Die Funktion `_xpc_int64_set_value()` aus libxpc ist jedoch ein geeigneter Kandidat mit der folgenden Disassemblierung:
2. **Speichern von Speicher — `_xpc_int64_set_value()`** (libxpc):
```c
__xpc_int64_set_value:
str x1, [x0, #0x18]
ret
```
Um einen 64-Bit-Schreibvorgang an einer bestimmten Adresse durchzuführen, wird der Remote-Call wie folgt strukturiert:
Um einen 64-Bit-Schreibvorgang an einer beliebigen Adresse durchzuführen:
```c
_xpc_int64_set_value(address - 0x18, value)
_xpc_int64_set_value(address - 0x18, value);
```
Mit diesen Primitiven ist die Bühne für die Erstellung von gemeinsamem Speicher bereitet, was einen bedeutenden Fortschritt bei der Kontrolle des Remote-Prozesses darstellt.
## 4. Einrichtung des gemeinsamen Speichers
Das Ziel ist es, gemeinsamen Speicher zwischen lokalen und Remote-Aufgaben einzurichten, um den Datentransfer zu vereinfachen und das Aufrufen von Funktionen mit mehreren Argumenten zu erleichtern. Der Ansatz besteht darin, `libxpc` und seinen `OS_xpc_shmem` Objekttyp zu nutzen, der auf Mach-Speichereinträgen basiert.
Das Ziel ist es, gemeinsamen Speicher zwischen lokalen und Remote-Aufgaben einzurichten, um den Datentransfer zu vereinfachen und das Aufrufen von Funktionen mit mehreren Argumenten zu erleichtern. Der Ansatz nutzt `libxpc` und seinen `OS_xpc_shmem` Objekttyp, der auf Mach-Speichereinträgen basiert.
### Prozessübersicht:
### Prozessübersicht
1. **Speicherzuweisung**:
- Weisen Sie den Speicher für die gemeinsame Nutzung mit `mach_vm_allocate()` zu.
- Verwenden Sie `xpc_shmem_create()`, um ein `OS_xpc_shmem` Objekt für den zugewiesenen Speicherbereich zu erstellen. Diese Funktion verwaltet die Erstellung des Mach-Speichereintrags und speichert das Mach-Sende-Recht an Offset `0x18` des `OS_xpc_shmem` Objekts.
2. **Erstellung des gemeinsamen Speichers im Remote-Prozess**:
- Weisen Sie Speicher für das `OS_xpc_shmem` Objekt im Remote-Prozess mit einem Remote-Aufruf von `malloc()` zu.
- Kopieren Sie den Inhalt des lokalen `OS_xpc_shmem` Objekts in den Remote-Prozess. Diese erste Kopie wird jedoch falsche Mach-Speichereintragsnamen an Offset `0x18` haben.
3. **Korrektur des Mach-Speichereintrags**:
- Nutzen Sie die Methode `thread_set_special_port()`, um ein Sende-Recht für den Mach-Speichereintrag in die Remote-Aufgabe einzufügen.
- Korrigieren Sie das Mach-Speichereintragsfeld an Offset `0x18`, indem Sie es mit dem Namen des Remote-Speichereintrags überschreiben.
4. **Abschluss der Einrichtung des gemeinsamen Speichers**:
- Validieren Sie das Remote `OS_xpc_shmem` Objekt.
- Stellen Sie die gemeinsame Speicherzuordnung mit einem Remote-Aufruf von `xpc_shmem_remote()` her.
Durch das Befolgen dieser Schritte wird der gemeinsame Speicher zwischen den lokalen und Remote-Aufgaben effizient eingerichtet, was einfache Datenübertragungen und die Ausführung von Funktionen, die mehrere Argumente erfordern, ermöglicht.
## Zusätzliche Code-Snippets
Für die Speicherzuweisung und die Erstellung des gemeinsamen Speicherobjekts:
```c
mach_vm_allocate();
xpc_shmem_create();
```
Um das gemeinsam genutzte Speicherobjekt im Remote-Prozess zu erstellen und zu korrigieren:
```c
malloc(); // for allocating memory remotely
thread_set_special_port(); // for inserting send right
```
Denken Sie daran, die Details von Mach-Ports und Speicher-Eintragsnamen korrekt zu behandeln, um sicherzustellen, dass die gemeinsame Speicher-Einrichtung ordnungsgemäß funktioniert.
1. **Speicherzuweisung**
* Weisen Sie Speicher für die gemeinsame Nutzung mit `mach_vm_allocate()` zu.
* Verwenden Sie `xpc_shmem_create()`, um ein `OS_xpc_shmem` Objekt für den zugewiesenen Bereich zu erstellen.
2. **Erstellung des gemeinsamen Speichers im Remote-Prozess**
* Weisen Sie Speicher für das `OS_xpc_shmem` Objekt im Remote-Prozess (`remote_malloc`) zu.
* Kopieren Sie das lokale Template-Objekt; eine Anpassung des eingebetteten Mach-Sende-Rechts bei Offset `0x18` ist weiterhin erforderlich.
3. **Korrektur des Mach-Speichereintrags**
* Fügen Sie ein Sende-Recht mit `thread_set_special_port()` ein und überschreiben Sie das Feld `0x18` mit dem Namen des Remote-Eintrags.
4. **Abschluss**
* Validieren Sie das Remote-Objekt und mappen Sie es mit einem Remote-Aufruf von `xpc_shmem_remote()`.
## 5. Vollständige Kontrolle erreichen
Nach dem erfolgreichen Einrichten des gemeinsamen Speichers und dem Erlangen von beliebigen Ausführungsfähigkeiten haben wir im Wesentlichen die vollständige Kontrolle über den Zielprozess erlangt. Die Schlüssel-Funktionalitäten, die diese Kontrolle ermöglichen, sind:
Sobald willkürliche Ausführung und ein gemeinsamer Speicher-Backchannel verfügbar sind, besitzen Sie effektiv den Zielprozess:
1. **Beliebige Speicheroperationen**:
* **Willkürlicher Speicher R/W** — verwenden Sie `memcpy()` zwischen lokalen und gemeinsamen Regionen.
* **Funktionsaufrufe mit > 8 Argumenten** — platzieren Sie die zusätzlichen Argumente auf dem Stack gemäß der arm64 Aufrufkonvention.
* **Mach-Port-Übertragung** — übergeben Sie Rechte in Mach-Nachrichten über die etablierten Ports.
* **Dateideskriptor-Übertragung** — nutzen Sie Fileports (siehe *triple_fetch*).
- Führen Sie beliebige Speicherlesevorgänge durch, indem Sie `memcpy()` aufrufen, um Daten aus dem gemeinsamen Bereich zu kopieren.
- Führen Sie beliebige Schreibvorgänge im Speicher durch, indem Sie `memcpy()` verwenden, um Daten in den gemeinsamen Bereich zu übertragen.
All dies ist in der [`threadexec`](https://github.com/bazad/threadexec) Bibliothek für eine einfache Wiederverwendung verpackt.
2. **Behandlung von Funktionsaufrufen mit mehreren Argumenten**:
---
- Für Funktionen, die mehr als 8 Argumente erfordern, ordnen Sie die zusätzlichen Argumente auf dem Stack gemäß der Aufrufkonvention an.
## 6. Apple Silicon (arm64e) Nuancen
3. **Mach-Port-Übertragung**:
Auf Apple Silicon Geräten (arm64e) schützen **Pointer Authentication Codes (PAC)** alle Rückgabewerte und viele Funktionszeiger. Techniken zum Thread-Hijacking, die *vorhandenen Code wiederverwenden*, funktionieren weiterhin, da die ursprünglichen Werte in `lr`/`pc` bereits gültige PAC-Signaturen tragen. Probleme treten auf, wenn Sie versuchen, zu speicher, der vom Angreifer kontrolliert wird:
- Übertragen Sie Mach-Ports zwischen Aufgaben über Mach-Nachrichten über zuvor eingerichtete Ports.
1. Weisen Sie ausführbaren Speicher innerhalb des Ziels zu (remote `mach_vm_allocate` + `mprotect(PROT_EXEC)`).
2. Kopieren Sie Ihr Payload.
3. Signieren Sie den Zeiger im *Remote*-Prozess:
```c
uint64_t ptr = (uint64_t)payload;
ptr = ptrauth_sign_unauthenticated((void*)ptr, ptrauth_key_asia, 0);
```
4. Setze `pc = ptr` im gehijackten Thread-Zustand.
4. **Dateideskriptor-Übertragung**:
- Übertragen Sie Dateideskriptoren zwischen Prozessen mithilfe von Fileports, einer Technik, die von Ian Beer in `triple_fetch` hervorgehoben wird.
Alternativ bleibe PAC-konform, indem du vorhandene Gadgets/Funktionen verkettest (traditionelles ROP).
Diese umfassende Kontrolle ist in der [threadexec](https://github.com/bazad/threadexec) Bibliothek zusammengefasst, die eine detaillierte Implementierung und eine benutzerfreundliche API für die Interaktion mit dem Opferprozess bietet.
## 7. Erkennung & Härtung mit EndpointSecurity
## Wichtige Überlegungen:
Das **EndpointSecurity (ES)**-Framework gibt Kernelereignisse frei, die es Verteidigern ermöglichen, Thread-Injektionsversuche zu beobachten oder zu blockieren:
- Stellen Sie die ordnungsgemäße Verwendung von `memcpy()` für Speicher-Lese-/Schreiboperationen sicher, um die Systemstabilität und Datenintegrität zu gewährleisten.
- Befolgen Sie beim Übertragen von Mach-Ports oder Dateideskriptoren die richtigen Protokolle und gehen Sie verantwortungsbewusst mit Ressourcen um, um Lecks oder unbeabsichtigten Zugriff zu verhindern.
* `ES_EVENT_TYPE_AUTH_GET_TASK` wird ausgelöst, wenn ein Prozess den Port eines anderen Tasks anfordert (z. B. `task_for_pid()`).
* `ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE` wird ausgegeben, wann immer ein Thread in einem *anderen* Task erstellt wird.
* `ES_EVENT_TYPE_NOTIFY_THREAD_SET_STATE` (hinzugefügt in macOS 14 Sonoma) zeigt die Registermanipulation eines bestehenden Threads an.
Durch die Einhaltung dieser Richtlinien und die Nutzung der `threadexec` Bibliothek kann man Prozesse effizient verwalten und auf granularer Ebene interagieren, um die vollständige Kontrolle über den Zielprozess zu erreichen.
Minimaler Swift-Client, der Remote-Thread-Ereignisse ausgibt:
```swift
import EndpointSecurity
let client = try! ESClient(subscriptions: [.notifyRemoteThreadCreate]) {
(_, msg) in
if let evt = msg.remoteThreadCreate {
print("[ALERT] remote thread in pid \(evt.target.pid) by pid \(evt.thread.pid)")
}
}
RunLoop.main.run()
```
Abfragen mit **osquery** ≥ 5.8:
```sql
SELECT target_pid, source_pid, target_path
FROM es_process_events
WHERE event_type = 'REMOTE_THREAD_CREATE';
```
### Überlegungen zur gehärteten Laufzeit
Die Verteilung Ihrer Anwendung **ohne** das `com.apple.security.get-task-allow` Entitlement verhindert, dass Angreifer ohne Root-Rechte auf ihren Task-Port zugreifen können. Der System Integrity Protection (SIP) blockiert weiterhin den Zugriff auf viele Apple-Binärdateien, aber Drittanbieter-Software muss sich ausdrücklich abmelden.
## 8. Neueste öffentliche Werkzeuge (2023-2025)
| Werkzeug | Jahr | Anmerkungen |
|---------|------|-------------|
| [`task_vaccine`](https://github.com/rodionovd/task_vaccine) | 2023 | Kompakte PoC, die das PAC-bewusste Thread-Hijacking auf Ventura/Sonoma demonstriert |
| `remote_thread_es` | 2024 | EndpointSecurity-Helfer, der von mehreren EDR-Anbietern verwendet wird, um `REMOTE_THREAD_CREATE`-Ereignisse anzuzeigen |
> Das Lesen des Quellcodes dieser Projekte ist nützlich, um die in macOS 13/14 eingeführten API-Änderungen zu verstehen und um die Kompatibilität zwischen Intel ↔ Apple Silicon aufrechtzuerhalten.
## Referenzen
- [https://bazad.github.io/2018/10/bypassing-platform-binary-task-threads/](https://bazad.github.io/2018/10/bypassing-platform-binary-task-threads/)
- [https://github.com/rodionovd/task_vaccine](https://github.com/rodionovd/task_vaccine)
- [https://developer.apple.com/documentation/endpointsecurity/es_event_type_notify_remote_thread_create](https://developer.apple.com/documentation/endpointsecurity/es_event_type_notify_remote_thread_create)
{{#include ../../../../banners/hacktricks-training.md}}