macOS IPC - Inter Process Communication
Last updated
Last updated
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE) Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Mach używa zadań jako najmniejszej jednostki do dzielenia zasobów, a każde zadanie może zawierać wiele wątków. Te zadania i wątki są mapowane 1:1 na procesy i wątki POSIX.
Komunikacja między zadaniami odbywa się za pomocą Mach Inter-Process Communication (IPC), wykorzystując jednokierunkowe kanały komunikacyjne. Wiadomości są przesyłane między portami, które działają jak kolejki wiadomości zarządzane przez jądro.
Port jest podstawowym elementem Mach IPC. Może być używany do wysyłania wiadomości i ich odbierania.
Każdy proces ma tabelę IPC, w której można znaleźć porty mach procesu. Nazwa portu mach to tak naprawdę liczba (wskaźnik do obiektu jądra).
Proces może również wysłać nazwę portu z pewnymi prawami do innego zadania, a jądro sprawi, że ten wpis w tabeli IPC innego zadania się pojawi.
Prawa portu, które definiują, jakie operacje zadanie może wykonać, są kluczowe dla tej komunikacji. Możliwe prawa portu to (definicje stąd):
Prawo odbioru, które pozwala na odbieranie wiadomości wysyłanych do portu. Porty Mach są kolejkami MPSC (wielu producentów, jeden konsument), co oznacza, że w całym systemie może być tylko jedno prawo odbioru dla każdego portu (w przeciwieństwie do rur, gdzie wiele procesów może posiadać deskryptory plików do końca odczytu jednej rury).
Zadanie z prawem odbioru może odbierać wiadomości i tworzyć prawa wysyłania, co pozwala mu na wysyłanie wiadomości. Początkowo tylko własne zadanie ma prawo odbioru nad swoim portem.
Jeśli właściciel prawa odbioru umiera lub je zabija, prawo wysyłania staje się bezużyteczne (martwa nazwa).
Prawo wysyłania, które pozwala na wysyłanie wiadomości do portu.
Prawo wysyłania może być klonowane, więc zadanie posiadające prawo wysyłania może sklonować to prawo i przyznać je trzeciemu zadaniu.
Należy zauważyć, że prawa portu mogą być również przekazywane przez wiadomości Mac.
Prawo wysyłania raz, które pozwala na wysłanie jednej wiadomości do portu, a następnie znika.
To prawo nie może być klonowane, ale może być przenoszone.
Prawo zestawu portów, które oznacza zestaw portów zamiast pojedynczego portu. Usunięcie wiadomości z zestawu portów usuwa wiadomość z jednego z portów, które zawiera. Zestawy portów mogą być używane do nasłuchiwania na kilku portach jednocześnie, podobnie jak select
/poll
/epoll
/kqueue
w Unixie.
Martwa nazwa, która nie jest rzeczywistym prawem portu, ale jedynie miejscem. Gdy port zostaje zniszczony, wszystkie istniejące prawa portu do portu zamieniają się w martwe nazwy.
Zadania mogą przekazywać prawa WYSYŁANIA innym, umożliwiając im wysyłanie wiadomości z powrotem. Prawa WYSYŁANIA mogą być również klonowane, więc zadanie może zduplikować i przekazać prawo trzeciemu zadaniu. To, w połączeniu z pośrednim procesem znanym jako serwer bootstrap, umożliwia skuteczną komunikację między zadaniami.
Porty plików pozwalają na enkapsulację deskryptorów plików w portach Mac (używając praw portu Mach). Możliwe jest utworzenie fileport
z danego FD za pomocą fileport_makeport
i utworzenie FD z fileportu za pomocą fileport_makefd
.
Jak wspomniano wcześniej, możliwe jest wysyłanie praw za pomocą wiadomości Mach, jednak nie można wysłać prawa bez już posiadania prawa do wysłania wiadomości Mach. Jak więc nawiązywana jest pierwsza komunikacja?
W tym celu zaangażowany jest serwer bootstrap (launchd w mac), ponieważ każdy może uzyskać prawo WYSYŁANIA do serwera bootstrap, możliwe jest poproszenie go o prawo do wysłania wiadomości do innego procesu:
Zadanie A tworzy nowy port, uzyskując prawo ODBIORU nad nim.
Zadanie A, będąc posiadaczem prawa ODBIORU, generuje prawo WYSYŁANIA dla portu.
Zadanie A nawiązuje połączenie z serwerem bootstrap i wysyła mu prawo WYSYŁANIA dla portu, który wygenerowało na początku.
Pamiętaj, że każdy może uzyskać prawo WYSYŁANIA do serwera bootstrap.
Zadanie A wysyła wiadomość bootstrap_register
do serwera bootstrap, aby powiązać dany port z nazwą taką jak com.apple.taska
Zadanie B wchodzi w interakcję z serwerem bootstrap, aby wykonać bootstrap lookup dla nazwy usługi (bootstrap_lookup
). Aby serwer bootstrap mógł odpowiedzieć, zadanie B wyśle mu prawo WYSYŁANIA do portu, który wcześniej stworzyło w wiadomości lookup. Jeśli lookup zakończy się sukcesem, serwer duplikuje prawo WYSYŁANIA otrzymane od Zadania A i przekazuje je do Zadania B.
Pamiętaj, że każdy może uzyskać prawo WYSYŁANIA do serwera bootstrap.
Z tym prawem WYSYŁANIA, Zadanie B jest w stanie wysłać wiadomość do Zadania A.
Dla komunikacji dwukierunkowej zazwyczaj zadanie B generuje nowy port z prawem ODBIORU i prawem WYSYŁANIA, a następnie przekazuje prawo WYSYŁANIA do Zadania A, aby mogło wysyłać wiadomości do ZADANIA B (komunikacja dwukierunkowa).
Serwer bootstrap nie może uwierzytelnić nazwy usługi, którą zadanie twierdzi, że posiada. Oznacza to, że zadanie może potencjalnie podszywać się pod dowolne zadanie systemowe, na przykład fałszywie twierdząc, że ma nazwę usługi autoryzacji i następnie zatwierdzając każdą prośbę.
Następnie Apple przechowuje nazwy usług dostarczanych przez system w zabezpieczonych plikach konfiguracyjnych, znajdujących się w katalogach chronionych przez SIP: /System/Library/LaunchDaemons
i /System/Library/LaunchAgents
. Obok każdej nazwy usługi, przechowywana jest również powiązana binarka. Serwer bootstrap utworzy i zachowa prawo ODBIORU dla każdej z tych nazw usług.
Dla tych zdefiniowanych usług, proces lookup różni się nieco. Gdy nazwa usługi jest wyszukiwana, launchd uruchamia usługę dynamicznie. Nowy przepływ pracy wygląda następująco:
Zadanie B inicjuje bootstrap lookup dla nazwy usługi.
launchd sprawdza, czy zadanie działa, a jeśli nie, uruchamia je.
Zadanie A (usługa) wykonuje bootstrap check-in (bootstrap_check_in()
). Tutaj serwer bootstrap tworzy prawo WYSYŁANIA, zachowuje je i przekazuje prawo ODBIORU do Zadania A.
launchd duplikuje prawo WYSYŁANIA i wysyła je do Zadania B.
Zadanie B generuje nowy port z prawem ODBIORU i prawem WYSYŁANIA, a następnie przekazuje prawo WYSYŁANIA do Zadania A (usługa), aby mogło wysyłać wiadomości do ZADANIA B (komunikacja dwukierunkowa).
Jednak ten proces dotyczy tylko zdefiniowanych zadań systemowych. Zadania nie-systemowe nadal działają zgodnie z opisem pierwotnym, co może potencjalnie umożliwić podszywanie się.
Dlatego launchd nigdy nie powinien się zawieszać, bo cały system się zawiesi.
Znajdź więcej informacji tutaj
Funkcja mach_msg
, zasadniczo wywołanie systemowe, jest wykorzystywana do wysyłania i odbierania wiadomości Mach. Funkcja wymaga, aby wiadomość do wysłania była pierwszym argumentem. Ta wiadomość musi zaczynać się od struktury mach_msg_header_t
, a następnie zawierać rzeczywistą treść wiadomości. Struktura jest zdefiniowana w następujący sposób:
Procesy posiadające prawo odbioru mogą odbierać wiadomości na porcie Mach. Z kolei nadający otrzymują prawo wysyłania lub prawo wysyłania-jednorazowego. Prawo wysyłania-jednorazowego jest przeznaczone wyłącznie do wysyłania pojedynczej wiadomości, po czym staje się nieważne.
Początkowe pole msgh_bits
jest bitmapą:
Pierwszy bit (najbardziej znaczący) jest używany do wskazania, że wiadomość jest złożona (więcej na ten temat poniżej)
i 4. bit są używane przez jądro
5 najmniej znaczących bitów 2. bajtu może być używane dla voucher: inny typ portu do wysyłania kombinacji klucz/wartość.
5 najmniej znaczących bitów 3. bajtu może być używane dla portu lokalnego
5 najmniej znaczących bitów 4. bajtu może być używane dla portu zdalnego
Typy, które mogą być określone w voucherze, portach lokalnych i zdalnych to (z mach/message.h):
Na przykład, MACH_MSG_TYPE_MAKE_SEND_ONCE
może być użyty do wskazania, że prawo do wysyłania raz powinno być wyprowadzone i przekazane dla tego portu. Może być również określone MACH_PORT_NULL
, aby zapobiec możliwości odpowiedzi przez odbiorcę.
Aby osiągnąć łatwą komunikację dwukierunkową, proces może określić port mach w nagłówku wiadomości mach zwanym portem odpowiedzi (msgh_local_port
), gdzie odbiorca wiadomości może wysłać odpowiedź na tę wiadomość.
Zauważ, że ten rodzaj komunikacji dwukierunkowej jest używany w wiadomościach XPC, które oczekują odpowiedzi (xpc_connection_send_message_with_reply
i xpc_connection_send_message_with_reply_sync
). Ale zazwyczaj tworzone są różne porty, jak wcześniej wyjaśniono, aby stworzyć komunikację dwukierunkową.
Inne pola nagłówka wiadomości to:
msgh_size
: rozmiar całego pakietu.
msgh_remote_port
: port, na który ta wiadomość jest wysyłana.
msgh_voucher_port
: vouchery mach.
msgh_id
: ID tej wiadomości, które jest interpretowane przez odbiorcę.
Zauważ, że wiadomości mach są wysyłane przez mach port
, który jest kanałem komunikacyjnym z jednym odbiorcą i wieloma nadawcami wbudowanym w jądro mach. Wiele procesów może wysyłać wiadomości do portu mach, ale w danym momencie tylko jeden proces może z niego odczytać.
Wiadomości są następnie formowane przez nagłówek mach_msg_header_t
, po którym następuje treść i trailer (jeśli jest obecny) i może on przyznać pozwolenie na odpowiedź. W tych przypadkach jądro musi tylko przekazać wiadomość z jednego zadania do drugiego.
Trailer to informacja dodana do wiadomości przez jądro (nie może być ustawiona przez użytkownika), która może być żądana przy odbiorze wiadomości z flagami MACH_RCV_TRAILER_<trailer_opt>
(istnieje różna informacja, która może być żądana).
Jednak istnieją inne, bardziej złożone wiadomości, takie jak te przekazujące dodatkowe prawa do portów lub dzielące pamięć, gdzie jądro również musi wysłać te obiekty do odbiorcy. W tych przypadkach najbardziej znaczący bit nagłówka msgh_bits
jest ustawiony.
Możliwe deskryptory do przekazania są zdefiniowane w mach/message.h
:
W 32 bitach wszystkie deskryptory mają 12B, a typ deskryptora znajduje się w 11. miejscu. W 64 bitach rozmiary się różnią.
Jądro skopiuje deskryptory z jednego zadania do drugiego, ale najpierw tworząc kopię w pamięci jądra. Ta technika, znana jako "Feng Shui", była nadużywana w kilku exploitach, aby jądro skopiowało dane w swojej pamięci, co pozwala procesowi wysyłać deskryptory do samego siebie. Następnie proces może odbierać wiadomości (jądro je zwolni).
Możliwe jest również wysłanie praw portu do podatnego procesu, a prawa portu po prostu pojawią się w procesie (nawet jeśli nie obsługuje ich).
Zauważ, że porty są powiązane z przestrzenią nazw zadania, więc aby utworzyć lub wyszukać port, przestrzeń nazw zadania jest również zapytana (więcej w mach/mach_port.h
):
mach_port_allocate
| mach_port_construct
: Utwórz port.
mach_port_allocate
może również utworzyć zbiór portów: prawo odbioru dla grupy portów. Kiedy wiadomość jest odbierana, wskazuje, z którego portu pochodzi.
mach_port_allocate_name
: Zmień nazwę portu (domyślnie 32-bitowa liczba całkowita)
mach_port_names
: Pobierz nazwy portów z docelowego
mach_port_type
: Uzyskaj prawa zadania do nazwy
mach_port_rename
: Zmień nazwę portu (jak dup2 dla FD)
mach_port_allocate
: Przydziel nowy RECEIVE, PORT_SET lub DEAD_NAME
mach_port_insert_right
: Utwórz nowe prawo w porcie, w którym masz RECEIVE
mach_port_...
mach_msg
| mach_msg_overwrite
: Funkcje używane do wysyłania i odbierania wiadomości mach. Wersja nadpisująca pozwala określić inny bufor do odbioru wiadomości (inna wersja po prostu go ponownie wykorzysta).
Ponieważ funkcje mach_msg
i mach_msg_overwrite
są używane do wysyłania i odbierania wiadomości, ustawienie punktu przerwania na nich pozwoli na inspekcję wysyłanych i odbieranych wiadomości.
Na przykład rozpocznij debugowanie dowolnej aplikacji, którą możesz debugować, ponieważ załaduje libSystem.B
, która użyje tej funkcji.
Aby uzyskać argumenty mach_msg
, sprawdź rejestry. Oto argumenty (z mach/message.h):
Pobierz wartości z rejestrów:
Sprawdź nagłówek wiadomości, sprawdzając pierwszy argument:
Ten typ mach_msg_bits_t
jest bardzo powszechny, aby umożliwić odpowiedź.
nazwa to domyślna nazwa przypisana do portu (sprawdź, jak wzrasta w pierwszych 3 bajtach). ipc-object
to zaburzony unikalny identyfikator portu.
Zauważ również, jak porty z tylko prawem send
identyfikują właściciela (nazwa portu + pid).
Zauważ także użycie +
do wskazania innych zadań połączonych z tym samym portem.
Możliwe jest również użycie procesxp, aby zobaczyć również zarejestrowane nazwy usług (z wyłączonym SIP z powodu potrzeby com.apple.system-task-port
):
Możesz zainstalować to narzędzie w iOS, pobierając je z http://newosxbook.com/tools/binpack64-256.tar.gz
Zauważ, jak nadawca alokuje port, tworzy prawo do wysyłania dla nazwy org.darlinghq.example
i wysyła je do serwera bootstrap, podczas gdy nadawca prosił o prawo do wysyłania tej nazwy i użył go do wysłania wiadomości.
Istnieją specjalne porty, które pozwalają na wykonywanie pewnych wrażliwych działań lub uzyskiwanie dostępu do pewnych wrażliwych danych, jeśli zadania mają uprawnienia SEND do nich. Czyni to te porty bardzo interesującymi z perspektywy atakującego, nie tylko ze względu na możliwości, ale także dlatego, że możliwe jest dzielenie się uprawnieniami SEND między zadaniami.
Te porty są reprezentowane przez numer.
Prawa SEND można uzyskać, wywołując host_get_special_port
, a prawa RECEIVE wywołując host_set_special_port
. Jednak oba wywołania wymagają portu host_priv
, do którego dostęp ma tylko root. Co więcej, w przeszłości root mógł wywołać host_set_special_port
i przejąć dowolny port, co pozwalało na przykład na ominięcie podpisów kodu poprzez przejęcie HOST_KEXTD_PORT
(SIP teraz temu zapobiega).
Są one podzielone na 2 grupy: pierwsze 7 portów jest własnością jądra, a są to 1 HOST_PORT
, 2 HOST_PRIV_PORT
, 3 HOST_IO_MASTER_PORT
, a 7 to HOST_MAX_SPECIAL_KERNEL_PORT
.
Porty zaczynające się od numeru 8 są własnością demonów systemowych i można je znaleźć zadeklarowane w host_special_ports.h
.
Port hosta: Jeśli proces ma uprawnienia SEND do tego portu, może uzyskać informacje o systemie, wywołując jego rutyny, takie jak:
host_processor_info
: Uzyskaj informacje o procesorze
host_info
: Uzyskaj informacje o hoście
host_virtual_physical_table_info
: Tabela stron wirtualnych/fizycznych (wymaga MACH_VMDEBUG)
host_statistics
: Uzyskaj statystyki hosta
mach_memory_info
: Uzyskaj układ pamięci jądra
Port Priv hosta: Proces z prawem SEND do tego portu może wykonywać uprzywilejowane działania, takie jak wyświetlanie danych rozruchowych lub próba załadowania rozszerzenia jądra. Proces musi być rootem, aby uzyskać to uprawnienie.
Co więcej, aby wywołać API kext_request
, konieczne jest posiadanie innych uprawnień com.apple.private.kext*
, które są przyznawane tylko binarkom Apple.
Inne rutyny, które można wywołać, to:
host_get_boot_info
: Uzyskaj machine_boot_info()
host_priv_statistics
: Uzyskaj uprzywilejowane statystyki
vm_allocate_cpm
: Przydziel kontygentową pamięć fizyczną
host_processors
: Wyślij prawo do procesorów hosta
mach_vm_wire
: Uczyń pamięć rezydentną
Ponieważ root ma dostęp do tego uprawnienia, może wywołać host_set_[special/exception]_port[s]
, aby przejąć specjalne lub wyjątkowe porty hosta.
Możliwe jest zobaczenie wszystkich specjalnych portów hosta poprzez uruchomienie:
Te porty są zarezerwowane dla dobrze znanych usług. Można je uzyskać/ustawić, wywołując task_[get/set]_special_port
. Można je znaleźć w task_special_ports.h
:
From here:
TASK_KERNEL_PORT[task-self send right]: Port używany do kontrolowania tego zadania. Używany do wysyłania wiadomości, które wpływają na zadanie. To jest port zwracany przez mach_task_self (patrz poniżej Task Ports).
TASK_BOOTSTRAP_PORT[bootstrap send right]: Port bootstrap zadania. Używany do wysyłania wiadomości z prośbą o zwrot innych portów usług systemowych.
TASK_HOST_NAME_PORT[host-self send right]: Port używany do żądania informacji o zawierającym hoście. To jest port zwracany przez mach_host_self.
TASK_WIRED_LEDGER_PORT[ledger send right]: Port wskazujący źródło, z którego to zadanie pobiera swoją pamięć jądra.
TASK_PAGED_LEDGER_PORT[ledger send right]: Port wskazujący źródło, z którego to zadanie pobiera swoją domyślną pamięć zarządzaną.
Początkowo Mach nie miał "procesów", miał "zadania", które były uważane za bardziej kontener wątków. Gdy Mach został połączony z BSD, każde zadanie było skorelowane z procesem BSD. Dlatego każdy proces BSD ma szczegóły, których potrzebuje, aby być procesem, a każde zadanie Mach ma również swoje wewnętrzne działanie (z wyjątkiem nieistniejącego pid 0, który jest kernel_task
).
Istnieją dwie bardzo interesujące funkcje związane z tym:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Uzyskaj prawo SEND dla portu zadania związane z określonym pid
i przekaż je do wskazanego target_task_port
(który zazwyczaj jest zadaniem wywołującym, które użyło mach_task_self()
, ale może być portem SEND w innym zadaniu).
pid_for_task(task, &pid)
: Mając prawo SEND do zadania, znajdź, do którego PID to zadanie jest związane.
Aby wykonać działania w ramach zadania, zadanie potrzebowało prawa SEND
do siebie, wywołując mach_task_self()
(co używa task_self_trap
(28)). Z tym uprawnieniem zadanie może wykonać kilka działań, takich jak:
task_threads
: Uzyskaj prawo SEND do wszystkich portów zadań wątków zadania
task_info
: Uzyskaj informacje o zadaniu
task_suspend/resume
: Wstrzymaj lub wznowić zadanie
task_[get/set]_special_port
thread_create
: Utwórz wątek
task_[get/set]_state
: Kontroluj stan zadania
i więcej można znaleźć w mach/task.h
Zauważ, że mając prawo SEND do portu zadania innego zadania, możliwe jest wykonywanie takich działań na innym zadaniu.
Ponadto, port_task jest również portem vm_map
, który pozwala na odczyt i manipulację pamięcią wewnątrz zadania za pomocą funkcji takich jak vm_read()
i vm_write()
. To zasadniczo oznacza, że zadanie z prawami SEND do portu zadania innego zadania będzie mogło wstrzyknąć kod do tego zadania.
Pamiętaj, że ponieważ jądro jest również zadaniem, jeśli ktoś zdoła uzyskać uprawnienia SEND do kernel_task
, będzie mógł sprawić, że jądro wykona cokolwiek (jailbreaki).
Wywołaj mach_task_self()
aby uzyskać nazwę dla tego portu dla zadania wywołującego. Ten port jest tylko dziedziczony przez exec()
; nowe zadanie utworzone za pomocą fork()
otrzymuje nowy port zadania (jako specjalny przypadek, zadanie również otrzymuje nowy port zadania po exec()
w binarnym pliku suid). Jedynym sposobem na uruchomienie zadania i uzyskanie jego portu jest wykonanie "port swap dance" podczas wykonywania fork()
.
Oto ograniczenia dostępu do portu (z macos_task_policy
z binarnego AppleMobileFileIntegrity
):
Jeśli aplikacja ma com.apple.security.get-task-allow
entitlement, procesy od tego samego użytkownika mogą uzyskać dostęp do portu zadania (zwykle dodawane przez Xcode do debugowania). Proces notaryzacji nie pozwoli na to w wersjach produkcyjnych.
Aplikacje z com.apple.system-task-ports
entitlement mogą uzyskać port zadania dla dowolnego procesu, z wyjątkiem jądra. W starszych wersjach nazywało się to task_for_pid-allow
. To jest przyznawane tylko aplikacjom Apple.
Root może uzyskać dostęp do portów zadań aplikacji nie skompilowanych z wzmocnionym czasem wykonywania (i nie od Apple).
Port nazwy zadania: Niewłaściwa wersja portu zadania. Odnosi się do zadania, ale nie pozwala na jego kontrolowanie. Jedyną rzeczą, która wydaje się być dostępna przez to, jest task_info()
.
Wątki również mają powiązane porty, które są widoczne z zadania wywołującego task_threads
oraz z procesora za pomocą processor_set_threads
. Prawo SEND do portu wątku pozwala na użycie funkcji z podsystemu thread_act
, takich jak:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
Każdy wątek może uzyskać ten port, wywołując mach_thread_sef
.
Możesz pobrać shellcode z:
Introduction to ARM64v8Skompiluj poprzedni program i dodaj uprawnienia, aby móc wstrzykiwać kod z tym samym użytkownikiem (w przeciwnym razie będziesz musiał użyć sudo).