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 rawbyte[]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]
- The app enumerates visible networks with
ConnectivityManager.getAllNetworks()andgetLinkProperties(). - Instead of
Network.bindSocket()(which enforces VPN lockdown), it createsnew DatagramSocket(new InetSocketAddress(<physical-ip>, 0)). That path reaches the kernelbind()syscall, which only checks whether the local IP exists on an interface. - A later UDP
connect()does not transmit traffic but helps associate the socket with the physical networknetId/SO_MARK. - The app passes the connected socket plus attacker-controlled bytes to a Binder method.
- 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 mapStub.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 freshDatagramSocket/Socketusing attacker-influenced metadata. - Compare kernel-local operations (
bind(), UDPconnect()) 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.