Davis Cat

Diving into a non documented WhatsApp anti-censorship feature

I discovered a non documented anti-censorship feature in WhatsApp that allows users to bypass censorship in certain countries.

WhatsApp Anti-Censorship Networking psiphon

I discovered a non documented anticensorship feature in WhatsApp

When I started analyzing the app my initial goal wasn’t to find anything like this, but I stumbled across the package ca.psiphon in the decompiled code. I was confused at first, thinking my sample apk was infected with malware or part of a p2p network, but after meticulously analyzing the code it turned out to be much more interesting than that: it was a full Psiphon tunnel implementation, which is a well known anticensorship tool.

It’s interesting that apparently this has never been documented or talked about online, as I couldn’t find any articles, tweets, reddit posts, nothing. Not even writeups with the endpoints.

The config

I immediately opened its package and inside its content found a reference to /assets/proxyservice_config.json which is a configuration file for the Psiphon tunnel. I was surprised to find that there was no information about this feature online, no articles, tweets, reddit posts, nothing. The only thing I could find were some github repos with decompiled code.

proxyservice_config.json

The configuration file is a JSON file that contains various parameters for the Psiphon tunnel, like: SponsorId, PropagationChannelId, ClientPlatform, ClientVersion, EmitClientAddress, EmitDiagnosticNotices, EmitDiagnosticNetworkParameters, ServerEntrySignaturePublicKey, PushPayloadSignaturePublicKey, RemoteServerListSignaturePublicKey, AdditionalParameters, DisableServerEntriesReporter, ConnectionWorkerPoolSize, TransformHostNameProbability, TLSFragmentClientHelloProbability and EstablishTunnelTimeoutSeconds.

I have truncated some of it’s values for convenience:

{
    "SponsorId" : "80...",
    "PropagationChannelId" : "89...",
    "ClientPlatform" : "meta",
    "ClientVersion" : "%d",
    "EmitClientAddress": true,
    "EmitDiagnosticNotices": true,
    "EmitDiagnosticNetworkParameters": true,
    "ServerEntrySignaturePublicKey": "sHu...",
    "PushPayloadSignaturePublicKey": "9/X8...",
    "RemoteServerListSignaturePublicKey": "MII...",
    "AdditionalParameters": "ZFZE7E...",
    "DisableServerEntriesReporter": true,
    "ConnectionWorkerPoolSize": 30,
    "TransformHostNameProbability": 0.9,
    "TLSFragmentClientHelloProbability": 0.5,
    "EstablishTunnelTimeoutSeconds": 0
}

Regional allowlist

The regional allowlist is a feature that allows WhatsApp to enable the Psiphon tunnel only for certain countries. This is done using two feature flags: 18724 and 18725. The values of these flags are CSV strings that contain the ISO country codes of the allowed countries.

Said countries are just an example, as of now we can’t disclose said values.

However, funnily enough there’s one that comes hardcoded in the decompiled code, which is Russia (RU):

builder4.put(18725, "RU");
builder4.put(18724, "RU");

here we also noticed this other url which i’ve never seen before https://mmg.whatsapp.net/proxygen/health which replies with PROXY-IS-ALIVE, this is likely a ping endpoint for the Psiphon tunnel, to check if it’s alive and working.

package p000X;

import android.app.Application;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/* JADX INFO: renamed from: X.0gv, reason: invalid class name and case insensitive filesystem */
/* JADX INFO: loaded from: classes.dex */
public final class C15130gv {
    public final Application A00 = C00I.A00();
    public final C05Z A01 = C05V.A00(59);
    public final C05Z A02 = C05V.A00(53);
    public final C05Z A04 = C05V.A00(191);
    public final C05Z A03 = C05V.A00(58);

    // Pick config
    public static final boolean A00(C15130gv c15130gv) {
        return !((C08T) c15130gv.A04.A00.get()).A0O() || ((C0CA) c15130gv.A03.A00.get()).A01() == 0;
    }

    // See if a VPN is in use, if so, disable the Psiphon tunnel to avoid conflicts
    public final C047507k A01() {
        C00D c00d;
        int i;
        String str;
        Application application = this.A00;
        if (A00(this)) {
            c00d = (C00D) this.A01.A00.get();
            i = 24068;
        } else {
            c00d = (C00D) this.A02.A00.get();
            i = 24067;
        }
        boolean z = false;
        if ((!c00d.A0r(i)) && AbstractC37631fy.A00(application)) {
            str = "vpn_in_use";
        } else {
            z = true;
            str = "";
        }
        return new C047507k(z, str);
    }

    // Alternative feature flags
    public final boolean A02() {
        C00D c00d;
        int i;
        if (A00(this)) {
            c00d = (C00D) this.A01.A00.get();
            i = 28186;
        } else {
            c00d = (C00D) this.A02.A00.get();
            i = 28189;
        }
        return c00d.A0r(i);
    }

    // Check if the user is in an allowed country, if so, enable the Psiphon tunnel
    public final boolean A03(String str) {
        C00D c00d;
        int i;
        if (A00(this)) {
            c00d = (C00D) this.A01.A00.get();
            i = 18724;
        } else {
            c00d = (C00D) this.A02.A00.get();
            i = 18725;
        }
        String strA0Y = c00d.A0Y(i);
        C00m.A0A(strA0Y, 0);
        C06N.A0B(str, strA0Y, "proxy_service", "User Region / Allowed Region: %s / %s");
        Locale locale = Locale.ROOT;
        C00m.A07(locale);
        String lowerCase = strA0Y.toLowerCase(locale);
        C00m.A06(lowerCase);
        List listA0m = AbstractC05720Bs.A0m(lowerCase, new String[]{","}, 0);
        ArrayList arrayList = new ArrayList();
        for (Object obj : listA0m) {
            if (!((String) obj).equals("")) {
                arrayList.add(obj);
            }
        }
        String lowerCase2 = str.toLowerCase(locale);
        C00m.A06(lowerCase2);
        return arrayList.contains(lowerCase2);
    }
}

There’s this other method that checks if the user has a current VPN connection, and if so, it disables the Psiphon tunnel. This is likely to avoid conflicts between the two VPNs.

libproxyservice.so

First of all, this binary is not part of the APK, it is downloaded dynamically using Google Play’s. Here’s a small TLDR of what we know so far:

  1. reginal allowlist is checked using feature flags 18724/18725 (code above)
  2. if the user is in an allowed country, the app triggers Google Play Dynamic Module Delivery to download the proxyservice module
  3. the module contains the libproxyservice.so and proxy_service embedded server entries
  4. the binary is responsible for establishing the Psiphon tunnel and connecting to the embedded servers
{
  "assets/proxyservice/libs.txt": "37c3eb9fe437d9392cc9b174da3876bb1b2c76e51b13b12bfe5c2f5458ac0afa",
  "assets/proxyservice/voltron_meta_embedded_server_entries": "0dd547935296c17697270c4cd4a56d648bab1a5b31c75e1589f2de7214523762"
}```

```json
{
  "lib/arm64-v8a/libgojni.so": "d6e86fcdf8a899fcde997c45ad89992ba0dc141d8afdc9d994c50fb100a81717"
}

lib proxy is downloaded with Voltron, which i will later explain, and contains the Psiphon tunnel implementation, which is responsible for establishing the connection to the embedded servers.

In the base APK the packages are not included but we can still find references to it.

Voltron

Voltron is Meta’s internal codename for their dynamic module delivery system, built on top of Google Play’s App Bundle / Dynamic Feature Modules. It’s not exclusive to proxyservice, other modules like i18n use the same infrastructure to ship code and assets post installation instead of bundling everything into the base APK.

This appears to be non documented, as I couldn’t find any information about it online. The only thing I could find were some github repos with decompiled code.

Not every module is downloaded immediately. OxygenScheduledInstallerJobService (an Android JobService) handles deferred/retry installs for modules that failed to download or weren’t needed right away:

public boolean onStartJob(JobParameters jobParameters) {
    // Runs on a background job, checks for missing modules, retries install
}

public final List A00(C12350bO c12350bO) {
    // Reads a map of "AppModules::ScheduledInstallRequestTimestamp"
    // Cross-checks which modules are still missing via ModuleApkUtil$ModuleResolver
    // Returns list of modules that still need installing
}

public boolean onStopJob(JobParameters jobParameters) {
    // If there are still missing modules, request retry (return true)
    // Otherwise, don't reschedule (return false)
}

conclusion

Using static analysis I discovered an undocumented WhatsApp anticensorship feature that can route traffic through a Psiphon tunnel for users in certain regions. It’s gated by regional allowlists and feature flags, and implemented inside a dynamically downloaded module—requiring dynamic testing to fully evaluate its behavior and limits.