macOS IPC - Inter Process Communication
Last updated
Last updated
Dowiedz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE) Dowiedz 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ą Komunikacji Międzyprocesowej Mach (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 IPC Mach. Może być używany do wysyłania i odbierania wiadomości.
Każdy proces ma tabelę IPC, w której można znaleźć porty mach procesu. Nazwa portu mach to właściwie liczba (wskaźnik do obiektu jądra).
Proces może również wysłać nazwę portu z pewnymi uprawnieniami do innego zadania, a jądro spowoduje, że ta pozycja pojawi się w tabeli IPC innego zadania.
Prawa portu, które określają, jakie operacje może wykonać zadanie, są kluczowe dla tej komunikacji. Możliwe prawa portu to (definicje stąd):
Prawo odbierania, które pozwala na odbieranie wiadomości wysłanych do portu. Porty Mach są kolejkami MPSC (wielu producentów, jeden konsument), co oznacza, że może istnieć tylko jedno prawo odbierania dla każdego portu w całym systemie (w przeciwieństwie do potoków, gdzie wiele procesów może trzymać deskryptory plików do końca odczytu jednego potoku).
Zadanie z prawem odbierania może odbierać wiadomości i tworzyć prawa wysyłania, pozwalając na wysyłanie wiadomości. Początkowo tylko własne zadanie ma prawo odbierania nad swoim portem.
Jeśli właściciel prawa odbierania 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żna klonować, więc zadanie posiadające prawo wysyłania może sklonować prawo i przekazać je trzeciemu zadaniu.
Zauważ, że prawa portu mogą również być przekazywane za pomocą wiadomości Mac.
Prawo wysłania raz, które pozwala na wysłanie jednej wiadomości do portu, a następnie zniknie.
To prawo nie może być sklonowane, ale można je przenieść.
Prawo zestawu portów, które oznacza zestaw portów zamiast pojedynczego portu. Usuwanie wiadomości z zestawu portów usuwa wiadomość z jednego z zawartych portów. Zestawy portów mogą być używane do nasłuchiwania na kilku portach jednocześnie, podobnie jak select
/poll
/epoll
/kqueue
w Unix.
Martwa nazwa, która nie jest faktycznym prawem portu, ale jedynie miejscem. Gdy port jest niszczony, 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ą również być klonowane, więc zadanie może zduplikować i przekazać prawo trzeciemu zadaniu. To, w połączeniu z pośrednim procesem znanym jako serwer startowy, umożliwia efektywną komunikację między zadaniami.
Porty plików pozwalają na zamknięcie deskryptorów plików w portach Mac (za pomocą praw portów Mach). Możliwe jest utworzenie fileport
z danego FD za pomocą fileport_makeport
i utworzenie FD z fileport 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 posiadania już prawa do wysłania wiadomości Mach. Jak więc ustanowić pierwszą komunikację?
W tym celu zaangażowany jest serwer startowy (launchd w systemie Mac), ponieważ każdy może uzyskać prawo WYSYŁANIA do serwera startowego, możliwe jest poproszenie go o prawo do wysłania wiadomości do innego procesu:
Zadanie A tworzy nowy port, uzyskując prawo ODBIERANIA nad nim.
Zadanie A, będąc posiadaczem prawa ODBIERANIA, generuje prawo WYSYŁANIA dla portu.
Zadanie A nawiązuje połączenie z serwerem startowym i wysyła mu prawo WYSYŁANIA dla portu, które wygenerowało na początku.
Pamiętaj, że każdy może uzyskać prawo WYSYŁANIA do serwera startowego.
Zadanie A wysyła wiadomość bootstrap_register
do serwera startowego, aby powiązać dany port z nazwą jak com.apple.taska
Zadanie B współdziała z serwerem startowym, aby wykonać wyszukiwanie serwera startowego dla nazwy usługi (bootstrap_lookup
). Aby serwer startowy mógł odpowiedzieć, zadanie B wyśle mu prawo WYSYŁANIA do portu, które wcześniej utworzyło wewnątrz wiadomości wyszukiwania. Jeśli wyszukiwanie jest udane, serwer duplikuje prawo WYSYŁANIA otrzymane od zadania A i przekazuje je zadaniu B.
Pamiętaj, że każdy może uzyskać prawo WYSYŁANIA do serwera startowego.
Dzięki temu prawu WYSYŁANIA, Zadanie B jest zdolne do wysłania wiadomości do Zadania A.
Dla komunikacji dwukierunkowej zazwyczaj zadanie B generuje nowy port z prawem ODBIERANIA i prawem WYSYŁANIA, a przekazuje prawo WYSYŁANIA do Zadania A, aby mogło wysyłać wiadomości do ZADANIA B (komunikacja dwukierunkowa).
Serwer startowy nie może uwierzytelnić nazwy usługi twierdzonej przez zadanie. Oznacza to, że zadanie potencjalnie mogłoby podawać się za dowolne zadanie systemowe, na przykład fałszywie twierdząc nazwę usługi autoryzacji, a następnie zatwierdzając każde żądanie.
Następnie Apple przechowuje nazwy usług dostarczanych przez system w bezpiecznych plikach konfiguracyjnych, znajdujących się w chronionych katalogach SIP: /System/Library/LaunchDaemons
i /System/Library/LaunchAgents
. Obok każdej nazwy usługi przechowywany jest również powiązany plik binarny. Serwer startowy utworzy i będzie trzymał prawo ODBIERANIA dla każdej z tych nazw usług.
Dla tych predefiniowanych usług, proces wyszukiwania różni się nieco. Gdy nazwa usługi jest wyszukiwana, launchd uruchamia usługę dynamicznie. Nowy schemat postępowania wygląda następująco:
Zadanie B inicjuje wyszukiwanie serwera startowego dla nazwy usługi.
launchd sprawdza, czy zadanie jest uruchomione, i jeśli nie, uruchamia je.
Zadanie A (usługa) wykonuje rejestrację serwera startowego (bootstrap_check_in()
). Tutaj serwer startowy tworzy prawo WYSYŁANIA, zatrzymuje je i przekazuje prawo ODBIERANIA do Zadania A.
launchd duplikuje prawo WYSYŁANIA i wysyła je do Zadania B.
Zadanie B generuje nowy port z prawem ODBIERANIA i prawem WYSYŁANIA, a przekazuje prawo WYSYŁANIA do Zadania A (usługi), aby mogło wysyłać wiadomości do ZADANIA B (komunikacja dwukierunkowa).
Jednak ten proces dotyczy tylko predefiniowanych zadań systemowych. Zadania spoza systemu nadal działają zgodnie z opisem pierwotnym, co potencjalnie mogłoby pozwolić na podawanie się za inne zadania.
Dlatego serwer startowy nigdy nie powinien ulec awarii, w przeciwnym razie cały system ulegnie awarii.
Znajdź więcej informacji tutaj
Funkcja mach_msg
, będąca w zasadzie wywołaniem systemowym, jest wykorzystywana do wysyłania i odbierania komunikatów Mach. Funkcja wymaga, aby komunikat został przesłany jako argument początkowy. Ten komunikat musi rozpoczynać się od struktury mach_msg_header_t
, po której następuje właściwa zawartość komunikatu. Struktura jest zdefiniowana następująco:
Procesy posiadające prawo odbierania mogą odbierać wiadomości na porcie Mach. Z kolei nadawcy otrzymują prawo wysyłania lub prawo wysłania raz. Prawo wysłania raz służy wyłącznie do wysłania pojedynczej wiadomości, po czym staje się nieważne.
Początkowe pole msgh_bits
to mapa bitowa:
Pierwszy bit (najbardziej znaczący) służy do wskazania, czy wiadomość jest złożona (więcej informacji poniżej)
i 4. bit są używane przez jądro
5 najmniej znaczących bitów 2. bajtu mogą być używane do voucher: innego rodzaju portu do wysyłania kombinacji klucz/wartość.
5 najmniej znaczących bitów 3. bajtu mogą być używane do lokalnego portu
5 najmniej znaczących bitów 4. bajtu mogą być używane do zdalnego portu
Typy, które można określić w voucherze, lokalnych i zdalnych portach to (z mach/message.h):
Na przykład MACH_MSG_TYPE_MAKE_SEND_ONCE
można użyć do wskazania, że prawo do jednorazowego wysłania powinno być wygenerowane i przesłane dla tego portu. Można także określić MACH_PORT_NULL
, aby uniemożliwić odbiorcy odpowiedź.
Aby osiągnąć łatwą komunikację dwukierunkową, proces może określić port mach w nagłówku mach o nazwie port odpowiedzi (msgh_local_port
), gdzie odbiorca wiadomości może wysłać odpowiedź na tę wiadomość.
Zauważ, że tego rodzaju komunikacja dwukierunkowa jest używana 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 wyjaśniono wcześniej, aby utworzyć komunikację dwukierunkową.
Pozostałe pola nagłówka wiadomości to:
msgh_size
: rozmiar całego pakietu.
msgh_remote_port
: port, na który wysłana jest ta wiadomość.
msgh_voucher_port
: vouchery mach.
msgh_id
: ID tej wiadomości, który jest interpretowany przez odbiorcę.
Zauważ, że wiadomości mach są wysyłane przez port mach
, który jest kanałem komunikacji jednego odbiorcy i wielu nadawców wbudowanym w jądro mach. Wiele procesów może wysyłać wiadomości do portu mach, ale w dowolnym momencie tylko jeden proces może je czytać.
Wiadomości są następnie tworzone przez nagłówek mach_msg_header_t
, a następnie przez ciało i stopkę (jeśli istnieje), która może udzielić zgody na odpowiedź. W tych przypadkach jądro musi tylko przekazać wiadomość z jednego zadania do drugiego.
Stopka to informacja dodana do wiadomości przez jądro (nie może być ustawiona przez użytkownika), którą można zażądać podczas odbierania wiadomości za pomocą flag MACH_RCV_TRAILER_<trailer_opt>
(istnieje różna informacja, którą można zażądać).
Jednak istnieją inne bardziej skomplikowane wiadomości, takie jak te przekazujące dodatkowe prawa portów lub udostępniające pamięć, gdzie jądro musi również przesł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-bitowej architekturze wszystkie deskryptory mają 12 bajtów, a typ deskryptora znajduje się w jedenastym. W 64-bitowej architekturze rozmiary są zróżnicowane.
Jądro skopiuje deskryptory z jednego zadania do drugiego, ale najpierw tworzy kopię w pamięci jądra. Ta technika, znana jako "Feng Shui", została wykorzystana w kilku exploitach do zmuszenia jądra do kopiowania danych w swojej pamięci, umożliwiając procesowi wysłanie deskryptorów do samego siebie. Następnie proces może odbierać wiadomości (jądro je zwolni).
Możliwe jest również przesłanie praw portu do podatnego procesu, a prawa portu pojawią się w procesie (nawet jeśli nie są obsługiwane).
Zauważ, że porty są powiązane z przestrzenią nazw zadania, więc aby utworzyć lub wyszukać port, przestrzeń nazw zadania jest również przeszukiwana (więcej w mach/mach_port.h
):
mach_port_allocate
| mach_port_construct
: Tworzy port.
mach_port_allocate
może również tworzyć zestaw portów: prawo odbioru w grupie portów. Za każdym razem, gdy otrzymywana jest wiadomość, wskazuje się port, z którego pochodzi.
mach_port_allocate_name
: Zmienia nazwę portu (domyślnie 32-bitowa liczba całkowita).
mach_port_names
: Pobiera nazwy portów z docelowego miejsca.
mach_port_type
: Pobiera prawa zadania do nazwy.
mach_port_rename
: Zmienia nazwę portu (podobnie jak dup2 dla deskryptorów plików).
mach_port_allocate
: Przydziela nowy ODBIÓR, ZESTAW_PORTÓW lub DEAD_NAME.
mach_port_insert_right
: Tworzy nowe prawo w porcie, w którym masz ODBIÓR.
mach_port_...
mach_msg
| mach_msg_overwrite
: Funkcje używane do wysyłania i odbierania wiadomości mach. Wersja nadpisania pozwala określić inny bufor do odbioru wiadomości (w przeciwnym razie zostanie on po prostu ponownie użyty).
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 zbadanie wysłanych i otrzymanych wiadomości.
Na przykład rozpocznij debugowanie dowolnej aplikacji, którą można debugować, ponieważ załaduje `libSystem.B, która będzie używać 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 rodzaj mach_msg_bits_t
jest bardzo powszechny, aby umożliwić odpowiedź.
Nazwa to domyślna nazwa nadana portowi (sprawdź, jak zwiększa się w pierwszych 3 bajtach). ipc-object
to zasłonięty 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żna również użyć procesxp, aby zobaczyć 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 przydziela port, tworzy prawo wysyłania dla nazwy org.darlinghq.example
i wysyła je do serwera rozruchowego, podczas gdy nadawca poprosił o prawo wysyłania tej nazwy i użył go do wysłania wiadomości.
Istnieją pewne specjalne porty, które pozwalają wykonywać określone wrażliwe czynności lub uzyskiwać dostęp do określonych wrażliwych danych w przypadku, gdy zadania mają uprawnienia SEND nad nimi. Sprawia to, że te porty są bardzo interesujące z perspektywy atakującego nie tylko ze względu na możliwości, ale także dlatego, że jest możliwe udostępnianie uprawnień 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. Ponadto w przeszłości root mógł wywołać host_set_special_port
i przejąć dowolny port, co pozwalało na przykład na obejście sygnatur kodu poprzez przejęcie HOST_KEXTD_PORT
(obecnie SIP zapobiega temu).
Porty te są podzielone na 2 grupy: pierwsze 7 portów należą do jądra, gdzie 1 to HOST_PORT
, 2 to HOST_PRIV_PORT
, 3 to HOST_IO_MASTER_PORT
, a 7 to HOST_MAX_SPECIAL_KERNEL_PORT
.
Te zaczynające się od numeru 8 należą do 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
: Pobierz informacje o procesorze
host_info
: Pobierz informacje o hoście
host_virtual_physical_table_info
: Tabela stron wirtualnych/fizycznych (wymaga MACH_VMDEBUG)
host_statistics
: Pobierz statystyki hosta
mach_memory_info
: Pobierz układ pamięci jądra
Port hosta Priv: Proces z uprawnieniem SEND do tego portu może wykonywać przywilejowane czynności, takie jak wyświetlanie danych rozruchowych lub próba załadowania rozszerzenia jądra. Proces musi być rootem, aby uzyskać to uprawnienie.
Ponadto, aby wywołać interfejs API kext_request
, konieczne jest posiadanie innych uprawnień com.apple.private.kext*
, które są udzielane tylko binariom Apple.
Inne rutyny, które można wywołać, to:
host_get_boot_info
: Pobierz machine_boot_info()
host_priv_statistics
: Pobierz przywilejowane statystyki
vm_allocate_cpm
: Przydziel ciągłą pamięć fizyczną
host_processors
: Wyślij prawo do procesorów hosta
mach_vm_wire
: Spraw, aby pamięć była rezydentna
Ponieważ root ma dostęp do tego uprawnienia, mógłby wywołać host_set_[special/exception]_port[s]
, aby przejąć specjalne porty hosta lub wyjątki.
Możliwe jest zobaczenie wszystkich specjalnych portów hosta, uruchamiając:
Są to porty zarezerwowane dla dobrze znanych usług. Można je uzyskać/ustawić, wywołując task_[get/set]_special_port
. Można je znaleźć w pliku task_special_ports.h
:
Z tutaj:
TASK_KERNEL_PORT[wysyłka prawej strony task-self]: Port używany do kontrolowania tego zadania. Służy do wysyłania wiadomości, które wpływają na zadanie. Jest to port zwracany przez mach_task_self (patrz poniżej Porty Zadań).
TASK_BOOTSTRAP_PORT[wysyłka prawej strony bootstrap]: Port rozruchowy zadania. Służy do wysyłania wiadomości żądających zwrotu innych portów usług systemowych.
TASK_HOST_NAME_PORT[wysyłka prawej strony host-self]: Port używany do żądania informacji o zawierającym hoście. Jest to port zwracany przez mach_host_self.
TASK_WIRED_LEDGER_PORT[wysyłka prawej strony ledger]: Port określający źródło, z którego to zadanie pobiera swoją przewodzoną pamięć jądra.
TASK_PAGED_LEDGER_PORT[wysyłka prawej strony ledger]: Port określający źródło, z którego to zadanie pobiera swoją domyślną pamięć zarządzaną pamięć.
Początkowo Mach nie miał "procesów", miał "zadania", które uważano za bardziej jak kontener wątków. Kiedy Mach został połączony z BSD, każde zadanie zostało skorelowane z procesem BSD. Dlatego każdy proces BSD ma szczegóły potrzebne do bycia 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)
: Pobierz prawo WYSYŁKI dla portu zadania związane z określonym przez 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 WYSYŁKI do innego zadania).
pid_for_task(task, &pid)
: Mając prawo WYSYŁKI do zadania, znajdź, do którego PID jest to zadanie powiązane.
Aby wykonać działania wewnątrz zadania, zadanie potrzebowało prawa WYSYŁKI do siebie, wywołując mach_task_self()
(które używa task_self_trap
(28)). Dzięki temu uprawnieniu zadanie może wykonać kilka działań, takich jak:
task_threads
: Pobierz prawo WYSYŁKI do wszystkich portów zadań wątków zadania
task_info
: Pobierz informacje o zadaniu
task_suspend/resume
: Wstrzymaj lub wznow 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 WYSYŁKI do portu zadania z innego zadania, możliwe jest wykonanie takich działań na innym zadaniu.
Ponadto, port zadania 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()
. Oznacza to w zasadzie, że zadanie mające prawa WYSYŁKI do portu zadania innego zadania będzie w stanie wstrzyknąć kod do tego zadania.
Pamiętaj, że ponieważ jądro jest również zadaniem, jeśli ktoś uzyska uprawnienia WYSYŁKI do kernel_task
, będzie w stanie sprawić, że jądro wykona cokolwiek (jailbreak).
Wywołaj mach_task_self()
aby uzyskać nazwę tego portu dla zadania wywołującego. Ten port jest dziedziczony tylko podczas exec()
; nowe zadanie utworzone za pomocą fork()
otrzymuje nowy port zadania (jako szczególny przypadek, zadanie również otrzymuje nowy port zadania po exec()
w binarnym suid). Jedynym sposobem na uruchomienie zadania i uzyskanie jego portu jest wykonanie "port swap dance" podczas fork()
.
Oto ograniczenia dostępu do portu (z macos_task_policy
z binarnego AppleMobileFileIntegrity
):
Jeśli aplikacja ma uprawnienie com.apple.security.get-task-allow
, procesy od tego samego użytkownika mogą uzyskać dostęp do portu zadania (zazwyczaj dodawane przez Xcode do debugowania). Proces notaryzacji nie zezwoli na to w wersjach produkcyjnych.
Aplikacje z uprawnieniem com.apple.system-task-ports
mogą uzyskać port zadania dla dowolnego procesu, z wyjątkiem jądra. W starszych wersjach nazywano to task_for_pid-allow
. Jest to przyznawane tylko aplikacjom Apple.
Root może uzyskać dostęp do portów zadań aplikacji nie skompilowanych z utwardzonym środowiskiem wykonawczym (i nie od Apple).
Port nazwy zadania: Nieuprzywilejowana wersja portu zadania. Odwołuje się do zadania, ale nie pozwala na jego kontrolę. Jedyną dostępną przez niego rzeczą wydaje się być task_info()
.
Wątki mają również powiązane porty, które są widoczne dla zadania wywołującego task_threads
i dla procesora z processor_set_threads
. Prawo WYSYŁKI do portu wątku pozwala na korzystanie z 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ć kod shell z:
Introduction to ARM64v8Skompiluj poprzedni program i dodaj uprawnienia umożliwiające wstrzykiwanie kodu z tym samym użytkownikiem (jeśli nie, będziesz musiał użyć sudo).