// 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