macOS IPC - Inter Process Communication
Komunikacja Mach za pomocą portów
Podstawowe informacje
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
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
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
.
Ustanowienie komunikacji
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ą jakcom.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.
Komunikat Mach
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ć).
Skomplikowane Wiadomości
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).
Interfejsy API portów Mac
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).
Debugowanie mach_msg
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ź.
Wylicz porty
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
Przykład kodu
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.
Przywilejowane porty
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.
Specjalne porty hosta
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 procesorzehost_info
: Pobierz informacje o hościehost_virtual_physical_table_info
: Tabela stron wirtualnych/fizycznych (wymaga MACH_VMDEBUG)host_statistics
: Pobierz statystyki hostamach_memory_info
: Pobierz układ pamięci jądraPort 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
: Pobierzmachine_boot_info()
host_priv_statistics
: Pobierz przywilejowane statystykivm_allocate_cpm
: Przydziel ciągłą pamięć fizycznąhost_processors
: Wyślij prawo do procesorów hostamach_vm_wire
: Spraw, aby pamięć była rezydentnaPonieważ 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:
Zadanie Specjalne Porty
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ęć.
Porty Zadań
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 przezpid
i przekaż je do wskazanegotarget_task_port
(który zazwyczaj jest zadaniem wywołującym, które użyłomach_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 zadaniatask_info
: Pobierz informacje o zadaniutask_suspend/resume
: Wstrzymaj lub wznow zadanietask_[get/set]_special_port
thread_create
: Utwórz wątektask_[get/set]_state
: Kontroluj stan zadaniai 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 podczasexec()
; nowe zadanie utworzone za pomocąfork()
otrzymuje nowy port zadania (jako szczególny przypadek, zadanie również otrzymuje nowy port zadania poexec()
w binarnym suid). Jedynym sposobem na uruchomienie zadania i uzyskanie jego portu jest wykonanie "port swap dance" podczasfork()
.Oto ograniczenia dostępu do portu (z
macos_task_policy
z binarnegoAppleMobileFileIntegrity
):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 totask_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()
.
Porty Wątków
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
.
Wstrzykiwanie kodu Shell w wątek za pomocą portu zadania
Możesz pobrać kod shell z:
Skompiluj poprzedni program i dodaj uprawnienia umożliwiające wstrzykiwanie kodu z tym samym użytkownikiem (jeśli nie, będziesz musiał użyć sudo).
Last updated