Root Detection — mechanizm zabezpieczający aplikacje Android przed uruchomieniem na urządzeniach z uprawnieniami superużytkownika. Aplikacje bankowe, płatnicze i korporacyjne blokują lub ograniczają funkcjonalność na zrootowanych urządzeniach, ponieważ dostęp root usuwa ograniczenia piaskownicy Android i umożliwia przechwytywanie ruchu, odczytywanie pamięci procesów oraz podmianę danych. Według OWASP Mobile Top 10 (2024), brak Root Detection należy do kategorii M8 (Security Decisions via Untrusted Inputs). Root Detection opiera się na kombinacji statycznych sprawdzeń systemu plików i dynamicznej analizy zachowania w runtime.
Najważniejsze
Root Detection — programowy mechanizm określający obecność dostępu root na urządzeniu Android. Dostęp root zapewnia pełną kontrolę nad systemem operacyjnym, umożliwiając aplikacjom i skryptom wykonywanie poleceń z UID 0. Na zrootowanym urządzeniu tracona jest izolacja aplikacji (Android Sandbox), co umożliwia przechwytywanie wprowadzania z klawiatury, odczytywanie baz danych SQLite innych aplikacji, wstrzykiwanie kodu do procesów oraz podmianę certyfikatów SSL w zaufanym magazynie.
Dla aplikacji finansowych i korporacyjnych praca na zrootowanym urządzeniu stanowi niedopuszczalne ryzyko: włamywacz uzyskuje dostęp do tokenów, kluczy sesyjnych i danych osobowych. Organy regulacyjne, w tym PCI Security Standards Council, wymagają od aplikacji płatniczych wykrywania i reagowania na dostęp root. W odpowiedzi na to programiści Android wbudowują Root Detection jako część strategii proaktywnej ochrony.
Istnieją dwa podejścia do wykrywania: statyczne, analizujące system plików i zainstalowane pakiety, oraz dynamiczne, wykonujące sprawdzenia w runtime. Podejście kombinowane uważane jest za najbardziej niezawodne, ponieważ pokrywa różne wektory obejścia. Według badań NowSecure (2025), 76% aplikacji bankowych w top 100 Google Play zawiera jakąś formę Root Detection.
Metody statyczne wykonywane są przy starcie aplikacji i sprawdzają oznaki dostępu root pozostawiane przez narzędzia rootujące w systemie plików. Metody te nie wymagają wykonywania uprzywilejowanych poleceń i działają w kontekście zwykłej aplikacji.
Główna oznaka dostępu root — obecność pliku wykonywalnego su w standardowych ścieżkach: /system/bin/su, /system/xbin/su, /sbin/su, /su/bin/su. Aplikacja sprawdza istnienie pliku przez File.exists() lub natywną implementację access() z libc. Dodatkowo można spróbować wykonać su --version lub su -c id i sprawdzić kod wyjścia.
Typowe aplikacje do zarządzania dostępem root: Superuser, SuperSU, Magisk Manager, KingRoot. Ich obecność sprawdzana jest przez PackageManager.getPackageInfo() lub odczyt katalogu /data/app/. Pakiety do sprawdzenia: com.topjohnwu.magisk, eu.chainfire.supersu, com.noshufou.android.su, com.thirdparty.superuser, com.koushikdutta.superuser, com.zacharee1.systemuituner.
Android przechowuje informacje o stanie systemu we właściwościach systemowych dostępnych przez System.getProperty i Build.TAGS. Jeśli wartość Build.TAGS zawiera test-keys, a nie release-keys — wskazuje to na niestandardowe oprogramowanie układowe, często z dostępem root. Dodatkowo sprawdzane są ro.build.tags, ro.debuggable i ro.secure przez odczyt /system/build.prop.
public class RootDetectionChecker {
private static final String[] SU_PATHS = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/su/bin/su",
"/system/sd/xbin/su"
};
public boolean checkRootByFiles() {
for (String path : SU_PATHS) {
if (new File(path).exists()) {
return true;
}
}
return false;
}
public boolean checkRootByPackages(Context ctx) {
String[] packages = {
"com.topjohnwu.magisk",
"eu.chainfire.supersu",
"com.noshufou.android.su",
"com.koushikdutta.superuser"
};
for (String pkg : packages) {
try {
ctx.getPackageManager().getPackageInfo(pkg, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
// nie znaleziono pakietu
}
}
return false;
}
}
Metody dynamiczne wykonywane są podczas działania aplikacji i analizują środowisko wykonawcze. W przeciwieństwie do statycznych, mogą wykryć rootowanie ukryte przez Magisk Hide lub Zygisk, ponieważ sprawdzają zachowanie systemu, a nie tylko strukturę plików.
Przy dostępie root niektóre partycje systemowe są montowane z flagą rw (read-write) zamiast ro (read-only). Aplikacja czyści /proc/mounts i sprawdza, czy /system jest zamontowany jako ro. Jeśli /system jest zamontowany jako rw — to oznaka zmodyfikowanego systemu. Dodatkowo sprawdzana jest obecność montowania /su przez Magisk.
Android Safe Mode wyłącza aplikacje innych firm, w tym menedżery root. Poprawna implementacja Root Detection może sprawdzić, czy urządzenie działa w trybie bezpiecznym. Jeśli aplikacja wykrywa, że menedżery root nie są widoczne, ale binarny plik su istnieje — to oznaka Magisk Hide.
Próba wykonania su -c id przez ProcessBuilder lub Runtime.exec — bezpośredni test dostępu root. Jednak Magisk może przechwycić to wywołanie. Bardziej niezawodną opcją jest sprawdzenie przez kod natywny: otwarcie /proc/1/limits lub /proc/self/maps i analiza UID uruchomionych procesów. Jeśli aplikacja może uzyskać UID 0 lub odczytać pliki dostępne tylko dla root — urządzenie jest skompromitowane.
public boolean checkRootDynamically() {
// Sprawdzanie flag kompilacji
String buildTags = Build.TAGS;
if (buildTags != null && buildTags.contains("test-keys")) {
return true;
}
// Sprawdzanie montowania /system
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("/proc/mounts"))
);
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("/system")
&& line.contains("rw")) {
reader.close();
return true;
}
}
reader.close();
} catch (IOException e) {
// błąd odczytu montowania
}
return false;
}
Root Detection zaimplementowany w Java jest łatwo omijany przez moduły Xposed lub Frida, które przechwytują metody Java i podmieniają zwracane wartości. Natywna implementacja w C++ przez JNI jest znacznie bardziej odporna: narzędzia dynamicznej analizy działające na poziomie Java nie widzą natywnych wywołań libc, takich jak stat, access, popen i dlopen.
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <vector>
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_checker_RootCheck_nativeCheck(
JNIEnv* env, jobject instance) {
std::vector<const char*> paths = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/data/local/su"
};
struct stat st;
for (const char* path : paths) {
if (stat(path, &st) == 0) {
return JNI_TRUE;
}
}
return JNI_FALSE;
}
Natywne sprawdzenie nie używa Java API, co czyni je niewidocznym dla narzędzi obejścia działających na poziomie Dalvik/ART. Dla dodatkowej ochrony zaleca się przechowywanie stałych (lista ścieżek) nie w sekcji read-only, ale obliczanie ich przez proste funkcje odwracalne. Wywołanie stat z libc bezpośrednio odwołuje się do jądra Linux, omijając opakowania Java, i nie można go przechwycić przez Xposed.
Twórcom zabezpieczeń konieczne jest zrozumienie istniejących metod obejścia, aby zbudować odporny system wykrywania. Każda metoda obejścia wymaga środka zaradczego na odpowiednim poziomie.
Magisk — najpopularniejsze narzędzie rootowania na Android 9–14. Magisk Hide ukrywa obecność su z /proc i podmienia wyniki sprawdzania ścieżek. Magisk działa na poziomie jądra i przechwytuje stat() i access() zanim zobaczy je aplikacja. Środek zaradczy: sprawdzenie obecności samego Magisk przez obecność /sbin/.magisk lub sprawdzenie przez odczyt własnych map — Magisk wbudowuje swoją bibliotekę w każdy proces.
Frida — narzędzie dynamicznej instrumentacji, które może przechwytywać funkcje natywne przez Ptrace lub Dobby. Frida zastępuje wartość zwracaną każdego sprawdzenia, podmieniając wynik stat na ENOENT. Środek zaradczy: sprawdzenie integralności funkcji natywnych przez obliczanie sumy kontrolnej instrukcji w pamięci i wykrywanie Frida przez analizę /proc/self/maps na obecność frida-agent.so lub frida-helper.
Root Detection zaimplementowany w Java jest usuwany w 2–3 minuty: APK jest rozpakowywane przez apktool, w kodzie smali poprawiana jest wartość zwracana metody na false, APK jest ponownie składane i podpisywane. Środek zaradczy: przeniesienie krytycznej logiki do kodu natywnego i sprawdzenie podpisu cyfrowego aplikacji w runtime przez Signature API lub porównanie hasha APK z wzorcem na serwerze.
Efektywny Root Detection opiera się na wielowarstwowej architekturze. Żadna metoda osobno nie zapewnia wystarczającego poziomu ochrony. Kombinacja sprawdzeń statycznych i dynamicznych, kodu natywnego i weryfikacji serwerowej daje maksymalną odporność.
Nie polegaj wyłącznie na sprawdzeniu po stronie klienta. Wysyłaj na serwer wyniki Root Detection wraz z jednorazowym tokenem sesji. Serwer podejmuje decyzję o blokadzie lub ograniczeniu funkcjonalności. Zapobiega to atakom na poziomie API, gdzie aplikacja kliencka może być zmodyfikowana, a serwer pozostaje zaufaną stroną.
Kod Root Detection powinien być zaciemniony. Jeśli atakujący widzi w jadx wyraźną sekwencję sprawdzeń ścieżek su — obejście zajmie minuty. Użyj ProGuard lub DexGuard do zaciemnienia przepływu sterowania i szyfrowania ciągów znaków. Obfuskacja zwiększa czas analizy kodu zabezpieczeń z kilku minut do kilku godzin.
Lista sprawdzanych ścieżek, pakietów i wskaźników powinna być aktualizowana z każdą wersją aplikacji. Nowe narzędzia rootowania i obejścia pojawiają się co miesiąc. Statyczna lista, niezmieniana od roku, nie wykrywa nowoczesnych metod. Zaleca się pobieranie aktualnych sygnatur z serwera przy starcie aplikacji przed wykonaniem sprawdzeń.
Często zadawane pytania
Root Detection chroni przed uruchomieniem aplikacji na urządzeniu, gdzie piaskownica Android jest wyłączona. Na zrootowanym urządzeniu każda aplikacja może czytać dane innych aplikacji. Aplikacje bankowe i płatnicze są obowiązane blokować działanie na zrootowanych urządzeniach zgodnie z wymaganiami PCI DSS i zaleceniami OWASP Mobile Security.
Magisk Hide wykorzystuje mechanizm montowania przestrzeni nazw (mount namespace). Dla każdego procesu wskazanego na liście wyjątków Magisk tworzy izolowaną przestrzeń nazw, gdzie binarny plik su jest niewidoczny. Wywołania systemowe stat, access i open w tej przestrzeni nazw nie widzą plików Magisk. Wykryć Magisk można przez sprawdzenie obecności /proc/self/maps i wyszukanie zrzutu magisk.
Tak, jeśli aplikacja nie sprawdza integralności swojego kodu. Przez Frida można przechwycić metodę Java sprawdzenia i wymusić zwrócenie false. Środek zaradczy — natywna implementacja krytycznej logiki w C++ i sprawdzenie integralności przez hash pliku DEX. Bez obfuskacji każdy Root Detection w Java jest omijany w 5–10 minut.
SafetyNet (przestarzałe) i Play Integrity API — to serwerowe sprawdzenia od Google potwierdzające integralność urządzenia. Obejmują one sprawdzenie bootloadera, podpisu systemowego i statusu root. Play Integrity API — zalecany zamiennik SafetyNet, dający trzy poziomy: BASIC, DEVICE i STRONG. Root Detection po stronie klienta uzupełnia atestację serwerową.
Zainstaluj aplikację na rzeczywistym zrootowanym urządzeniu (np. Pixel z Magisk). Sprawdź, czy blokada zadziałała. Następnie spróbuj ukryć root przez Magisk Hide dla swojej aplikacji i uruchom ponownie test. Do dogłębnego sprawdzenia użyj Frida do przechwycenia docelowych metod i upewnij się, że natywna ochrona nie daje się obejść.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również