// HackTricks · Mobile

Android VPN Bypass

Android VPN Bypass

Hidden Binder APIs, Confused Deputies and VPN Lockdown Bypasses

A hidden Java API can remain reachable when its underlying Binder transaction is exposed to apps and the service omits a caller-identity or permission check. A useful triage pattern is: untrusted app → Binder registration of attacker-controlled state → privileged system component later replays that state.[1][3]

Typical red flags:

  • A Binder method accepts a ParcelFileDescriptor, socket-like object, or raw byte[] payload from an app.
  • The AIDL/service path has no @EnforcePermission, enforceCallingOrSelfPermission(), checkCallingPermission(), UID allowlist, or SELinux restriction.
  • The privileged side later recreates the socket or packet as system_server/system UID and sends it on a network chosen from saved metadata.
  • The payload is treated as protocol-specific data but the implementation never validates that it really matches the expected frame type.

The following pattern was reported in the Android 16 QUIC graceful-close implementation. Treat transaction numbers and method layouts as build-specific; confirm them against the target framework rather than copying a constant from another release.[1]

  1. The app enumerates visible networks with ConnectivityManager.getAllNetworks() and getLinkProperties().
  2. Instead of Network.bindSocket() (which enforces VPN lockdown), it creates new DatagramSocket(new InetSocketAddress(<physical-ip>, 0)). That path reaches the kernel bind() syscall, which only checks whether the local IP exists on an interface.
  3. A later UDP connect() does not transmit traffic but helps associate the socket with the physical network netId/SO_MARK.
  4. The app passes the connected socket plus attacker-controlled bytes to a Binder method.
  5. When the socket dies, a privileged component recreates the flow and sends the bytes as UID 1000, bypassing per-app VPN policy because the policy is enforced against the original app UID, not the privileged deputy.

Minimal direct-transaction pattern from an app when the SDK method is hidden but the Binder entry point is reachable:[1][2]

IBinder svc = (IBinder) Class.forName("android.os.ServiceManager")
    .getMethod("getService", String.class).invoke(null, "connectivity");
Parcel data = Parcel.obtain();
try {
    data.writeInterfaceToken("android.net.IConnectivityManager");
    data.writeTypedObject(pfd, 0);
    data.writeByteArray(payload);
    svc.transact(TXN_REGISTER, data, null, IBinder.FLAG_ONEWAY);
} finally {
    data.recycle();
}

Practical review workflow:

  • Decompile framework*.jar, APEX jars and vendor service jars, then map Stub.onTransact() cases to method names and parameter types.
  • Prioritise transactions that combine network objects + raw bytes + delayed execution.
  • Check whether the privileged path calls helpers such as Network.bindSocket(), Os.write(), or creates a fresh DatagramSocket/Socket using attacker-influenced metadata.
  • Compare kernel-local operations (bind(), UDP connect()) with Android policy-aware wrappers (Network.bindSocket()) because the former may shape metadata without triggering the higher-level policy gate.
  • For confirmation, register the state, kill the app/process, and watch for a later packet from the device’s real interface or another action performed by the privileged service.

Defensive clues:

  • Binder methods that should be protocol-specific must validate the payload format instead of accepting arbitrary bytes.
  • Privileged services should re-check the original caller UID/network policy before replaying traffic or reconstructing sockets on behalf of apps.
  • Platform updates are the primary remediation for a system-service flaw; application-layer VPNs cannot reliably patch a confused deputy in system_server.

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.