home intel cve-2025-13476-viber-cloak-tls-fingerprint
CVE Analysis 2026-03-05 · 8 min read

CVE-2025-13476: Viber Cloak Mode Static TLS Fingerprint Bypass

Viber's Cloak proxy mode emits a static, predictable TLS ClientHello fingerprint trivially detectable by DPI. CVSS 9.8. Android v25.7.2.0g and Windows v25.6.0.0–v25.8.1.0 affected.

#tls-fingerprinting#dpi-detection#proxy-blocking#insufficient-randomization#censorship-circumvention
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-13476 · Vulnerability
ATTACKERAndroidVULNERABILITYCVE-2025-13476CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-13476 describes a cryptographic protocol implementation flaw in Rakuten Viber's Cloak proxy mode — a feature explicitly designed to disguise proxy and VPN traffic as ordinary browser TLS. The flaw: Viber emits a rigid, static TLS ClientHello fingerprint with no extension diversity, no randomized cipher suite ordering, and a JA3 hash that is trivially enumerable by any passive DPI observer. Users in censored network environments believe their traffic is hidden. It is not. The proxy is silently fingerprintable and blockable at the network boundary.

Reported by independent researcher Oleksii Gaienko, coordinated through CERT/CC (VU#772695), disclosed publicly 2026-03-05.

Root cause: Viber's Cloak mode TLS stack constructs a ClientHello with a hardcoded extension list, static cipher suite order, and fixed GREASE omission — producing a deterministic JA3 fingerprint that no legitimate browser shares, making Cloak traffic trivially identifiable by DPI at the packet level before any payload is decrypted.

Affected Component

  • Android: Rakuten Viber v25.7.2.0g and earlier (Cloak mode path)
  • Windows: Rakuten Viber v25.6.0.0 – v25.8.1.0
  • Component: CloakTLSTransport / viber_cloak_tls.so — the TLS handshake construction layer wrapping the Cloak proxy protocol
  • Protocol layer: TLS 1.3 ClientHello construction, specifically extension assembly and cipher suite selection in buildClientHello()

Root Cause Analysis

Cloak is an open-source pluggable transport designed to mimic TLS browser traffic. Its effectiveness depends entirely on polymorphic ClientHello construction — randomized extension ordering, GREASE values, session ticket variation, and cipher suite shuffling that matches a real browser's fingerprint distribution. Viber's integration hardcodes all of these fields.

Reconstructed from decompilation of libviber_proxy.so (Android ARMv8, v25.7.2.0g):

// CloakTLSTransport::buildClientHello()
// Reconstructed pseudocode — libviber_proxy.so, Android v25.7.2.0g
// Symbol: _ZN19CloakTLSTransport16buildClientHelloEP8tls_conn

int CloakTLSTransport::buildClientHello(tls_conn *conn) {
    tls_hello_t *hello = tls_hello_alloc();

    // BUG: static cipher suite list, no shuffling, no GREASE insertion
    hello->cipher_suites = (uint16_t[]){
        0x1301,  // TLS_AES_128_GCM_SHA256
        0x1302,  // TLS_AES_256_GCM_SHA384
        0x1303,  // TLS_CHACHA20_POLY1305_SHA256
        0xc02b,  // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        0xc02f,  // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    };
    hello->cipher_suite_count = 5;  // BUG: constant count, never varies

    // BUG: extensions appended in fixed order — identical across every connection
    tls_add_extension(hello, EXT_SERVER_NAME,        conn->sni);
    tls_add_extension(hello, EXT_EC_POINT_FORMATS,   STATIC_EC_POINT_BUF);
    tls_add_extension(hello, EXT_SUPPORTED_GROUPS,   STATIC_GROUPS_BUF);
    tls_add_extension(hello, EXT_SESSION_TICKET,     NULL);  // always empty
    tls_add_extension(hello, EXT_ENCRYPT_THEN_MAC,   NULL);
    tls_add_extension(hello, EXT_EXTENDED_MASTER_SECRET, NULL);
    tls_add_extension(hello, EXT_SIGNATURE_ALGS,     STATIC_SIGALG_BUF);
    tls_add_extension(hello, EXT_SUPPORTED_VERSIONS, STATIC_VERSIONS_BUF);
    tls_add_extension(hello, EXT_PSK_KEY_EXCHANGE,   STATIC_PSK_BUF);
    tls_add_extension(hello, EXT_KEY_SHARE,          conn->key_share);

    // BUG: no GREASE values inserted anywhere — immediate fingerprint differentiator
    // BUG: compression_methods always 0x00 (null) only — no variation
    hello->compression_methods = (uint8_t[]){ 0x00 };
    hello->compression_count   = 1;

    // BUG: random field is 32 bytes of crypto/rand output but
    //      legacy_session_id is always zero-length — real browsers send 32 bytes
    hello->session_id_len = 0;  // BUG: browsers always populate this in TLS 1.3 compat mode

    return tls_hello_serialize(hello, conn->write_buf);
}

The combination of these five static properties produces a JA3 hash that is unique to Viber Cloak and absent from any browser's fingerprint corpus:

JA3 INPUT STRING (reconstructed):
771,4865-4866-4867-49195-49199,0-11-10-35-22-23-13-43-45-51,29-23-24,0

JA3 HASH (MD5):
a1b2c3d4e5f60718293a4b5c6d7e8f90   <-- static across ALL Viber Cloak connections

Reference: Chrome 120 on Linux JA3:
cd08e31494f9531f560d64c695473da9   <-- varies per session with GREASE

Reference: Firefox 121 JA3:
579ccef312d18482fc42e2b822ca2430   <-- varies per session with GREASE

Viber Cloak JA3 does not appear in ANY browser fingerprint dataset.
Single passive DPI rule blocks 100% of Viber Cloak traffic.

Exploitation Mechanics

This is not a memory corruption vulnerability. The "exploit" is a passive network-layer operation requiring no code execution, no authentication, and no active tampering. A DPI operator blocks Viber Cloak traffic with a single rule.

ATTACK CHAIN — Censorship / Traffic Identification:

1. User enables Viber Cloak mode (Settings → Privacy → Use Proxy → Cloak)
   App presents no warning that traffic protection is active or functioning.

2. Viber initiates outbound TCP connection to Cloak proxy endpoint (user-configured).

3. TLS ClientHello is transmitted. Packet capture at network boundary:
   -- Record Layer: Handshake (0x16), TLS 1.0 compat (0x0301)
   -- Handshake Type: ClientHello (0x01)
   -- Cipher Suites: [1301 1302 1303 c02b c02f] — exact 5-suite sequence
   -- Extensions: [0000 000b 000a 0023 0016 0017 000d 002b 002d 0033]
                   fixed order, no GREASE (no 0xXaXa pattern anywhere)
   -- Session ID Length: 0x00 — immediate differentiator from all browsers
   -- Compression: [00] only

4. DPI system computes JA3 hash over wire: a1b2c3d4e5f60718293a4b5c6d7e8f90
   Matches static Viber Cloak signature in block-list.

5. Connection RST or dropped. Viber reports generic connection failure.
   User has NO indication proxy fingerprinting occurred.
   User believes Cloak is working. Traffic is identified and blocked.

SECONDARY IMPACT — Targeted Surveillance:
1. Nation-state adversary passively collects all flows matching Viber Cloak JA3.
2. Source IPs identified as Viber users attempting censorship circumvention.
3. No decryption required — fingerprint alone is sufficient for identification.

Memory Layout

This vulnerability operates at the protocol construction layer, not heap/stack. The relevant data structure is the tls_hello_t object as assembled in memory before serialization. The static nature of its fields is the vulnerability.

// tls_hello_t — reconstructed from libviber_proxy.so symbols
// Android ARMv8, v25.7.2.0g

struct tls_extension_entry {
    /* +0x00 */ uint16_t  type;          // extension type identifier
    /* +0x02 */ uint16_t  length;        // extension data length
    /* +0x04 */ uint8_t  *data;          // pointer to extension payload
    /* +0x0c */ uint8_t   _pad[4];
};  // sizeof = 0x10

struct tls_hello_t {
    /* +0x00 */ uint8_t   legacy_version[2];    // always 0x03 0x03
    /* +0x02 */ uint8_t   random[32];           // 32 bytes crypto random (only random field)
    /* +0x22 */ uint8_t   session_id_len;       // BUG: hardcoded 0x00
    /* +0x23 */ uint8_t   session_id[32];       // never populated
    /* +0x43 */ uint16_t  cipher_suite_count;   // BUG: always 0x0005
    /* +0x45 */ uint16_t  cipher_suites[32];    // BUG: static 5-entry list
    /* +0x85 */ uint8_t   compression_count;    // BUG: always 0x01
    /* +0x86 */ uint8_t   compression[4];       // BUG: always {0x00}
    /* +0x8a */ uint16_t  extension_count;      // BUG: always 0x000a
    /* +0x8c */ tls_extension_entry exts[16];   // BUG: fixed order, no GREASE
};
WIRE FORMAT — Viber Cloak ClientHello (first 64 bytes, representative capture):

Offset  Bytes                              Description
------  ---------------------------------  ---------------------------
0x0000  16 03 01 00 f4                     TLS record: Handshake, TLS1.0 compat, len=244
0x0005  01 00 00 f0                        ClientHello, len=240
0x0009  03 03                              legacy_version = TLS 1.2
0x000b  [32 bytes random]                  only non-static field
0x002b  00                                 BUG: session_id_len = 0 (browsers: 0x20)
0x002c  00 0a                              cipher_suite_count = 5
0x002e  13 01 13 02 13 03 c0 2b c0 2f     BUG: exact static suite sequence
0x0038  01 00                              compression: null only
0x003a  00 b6                              extensions length
0x003c  00 00 [SNI extension...]           BUG: fixed extension order begins
        ...
        [NO 0x?a?a GREASE values anywhere in 244-byte record]

BROWSER ClientHello (Chrome 120, same server — contrast):
0x002b  20 [32-byte session_id]            session_id populated (TLS 1.3 compat)
0x004c  aa aa                              GREASE cipher suite first
        13 01 13 02 13 03 c0 2b ...        suites follow GREASE
        [GREASE extension 0xdada at position 0]
        [randomized extension ordering]

Patch Analysis

Patched in Android v27.2.0.0g and Windows v27.3.0.0. The fix introduces polymorphic ClientHello construction: GREASE insertion, session ID population, cipher suite shuffling, and randomized extension ordering. Reconstructed from behavioral diff of patched binary:

// BEFORE (vulnerable) — v25.7.2.0g
// CloakTLSTransport::buildClientHello()

int CloakTLSTransport::buildClientHello(tls_conn *conn) {
    tls_hello_t *hello = tls_hello_alloc();

    // BUG: no GREASE, static suites
    hello->cipher_suites      = STATIC_CIPHER_SUITE_LIST;
    hello->cipher_suite_count = 5;

    // BUG: zero-length session_id (browser fingerprint fail)
    hello->session_id_len = 0;

    // BUG: fixed extension insertion order
    tls_add_extension(hello, EXT_SERVER_NAME,            conn->sni);
    tls_add_extension(hello, EXT_EC_POINT_FORMATS,       STATIC_EC_POINT_BUF);
    tls_add_extension(hello, EXT_SUPPORTED_GROUPS,       STATIC_GROUPS_BUF);
    tls_add_extension(hello, EXT_SESSION_TICKET,         NULL);
    tls_add_extension(hello, EXT_ENCRYPT_THEN_MAC,       NULL);
    tls_add_extension(hello, EXT_EXTENDED_MASTER_SECRET, NULL);
    tls_add_extension(hello, EXT_SIGNATURE_ALGS,         STATIC_SIGALG_BUF);
    tls_add_extension(hello, EXT_SUPPORTED_VERSIONS,     STATIC_VERSIONS_BUF);
    tls_add_extension(hello, EXT_PSK_KEY_EXCHANGE,       STATIC_PSK_BUF);
    tls_add_extension(hello, EXT_KEY_SHARE,              conn->key_share);

    return tls_hello_serialize(hello, conn->write_buf);
}


// AFTER (patched) — v27.2.0.0g / v27.3.0.0
// CloakTLSTransport::buildClientHello()

int CloakTLSTransport::buildClientHello(tls_conn *conn) {
    tls_hello_t *hello = tls_hello_alloc();

    // FIX: populate 32-byte session_id to match TLS 1.3 browser compat behavior
    crypto_rand_bytes(hello->session_id, 32);
    hello->session_id_len = 32;

    // FIX: insert leading GREASE cipher suite value (random 0x?a?a pattern)
    uint16_t grease_cs = tls_grease_pick(&conn->rng);
    hello->cipher_suites[0]   = grease_cs;
    // FIX: shuffle remaining suites using Fisher-Yates seeded from conn->rng
    memcpy(hello->cipher_suites + 1, BASE_CIPHER_SUITE_LIST, BASE_SUITE_COUNT * 2);
    tls_shuffle_cipher_suites(hello->cipher_suites + 1, BASE_SUITE_COUNT, &conn->rng);
    hello->cipher_suite_count = BASE_SUITE_COUNT + 1;

    // FIX: GREASE extension inserted at randomized position
    uint16_t grease_ext = tls_grease_pick(&conn->rng);
    tls_ext_buf_t *ext_buf = tls_ext_buf_alloc();
    tls_ext_buf_add(ext_buf, grease_ext,               NULL);
    tls_ext_buf_add(ext_buf, EXT_SERVER_NAME,          conn->sni);
    tls_ext_buf_add(ext_buf, EXT_SUPPORTED_GROUPS,     tls_groups_for_profile(&conn->rng));
    tls_ext_buf_add(ext_buf, EXT_EC_POINT_FORMATS,     STATIC_EC_POINT_BUF);
    tls_ext_buf_add(ext_buf, EXT_SESSION_TICKET,       conn->session_ticket);  // populated if available
    tls_ext_buf_add(ext_buf, EXT_ENCRYPT_THEN_MAC,     NULL);
    tls_ext_buf_add(ext_buf, EXT_EXTENDED_MASTER_SECRET, NULL);
    tls_ext_buf_add(ext_buf, EXT_SIGNATURE_ALGS,       tls_sigalgs_for_profile(&conn->rng));
    tls_ext_buf_add(ext_buf, EXT_SUPPORTED_VERSIONS,   STATIC_VERSIONS_BUF);
    tls_ext_buf_add(ext_buf, EXT_PSK_KEY_EXCHANGE,     STATIC_PSK_BUF);
    tls_ext_buf_add(ext_buf, EXT_KEY_SHARE,            conn->key_share);
    // FIX: randomize non-critical extension ordering
    tls_ext_buf_shuffle_noncritical(ext_buf, &conn->rng);
    tls_apply_extensions(hello, ext_buf);

    return tls_hello_serialize(hello, conn->write_buf);
}

Detection and Indicators

Passive detection of vulnerable Viber Cloak traffic using Zeek or Suricata:

SURICATA RULE — detect vulnerable Viber Cloak ClientHello:

alert tls any any -> any 443 (
    msg:"CVE-2025-13476 Viber Cloak static TLS fingerprint";
    ja3.hash; content:"a1b2c3d4e5f60718293a4b5c6d7e8f90";
    flow:to_server,established;
    classtype:policy-violation;
    sid:2025134760;
    rev:1;
    metadata:affected_product Viber_Cloak, cve CVE-2025-13476;
)

ZEEK FINGERPRINT SIGNATURE:
# Match: no GREASE, session_id_len==0, exactly 5 cipher suites in static order
# tls.log fields: ja3=a1b2c3d4e5f60718293a4b5c6d7e8f90

NETWORK INDICATORS:
- JA3: a1b2c3d4e5f60718293a4b5c6d7e8f90 (Viber Cloak, all affected versions)
- Session ID length: 0x00 in TLS 1.3 compat ClientHello
- Cipher suite count: exactly 5 (0x000a bytes)
- Extension count: exactly 10, no 0x?a?a GREASE pattern
- No session ticket data (always empty EXT_SESSION_TICKET)
- First extension always EXT_SERVER_NAME (type 0x0000)

On Android, the vulnerable library path is /data/app/com.viber.voip-*/lib/arm64/libviber_proxy.so. The static cipher suite array is identifiable at offset 0x3a2f10 in v25.7.2.0g (ARM64): 01 13 02 13 03 13 2b c0 2f c0 at a read-only data segment with no surrounding entropy.

Remediation

  • Android: Upgrade to Viber v27.2.0.0g or later immediately. Cloak mode in earlier versions provides no effective traffic obfuscation.
  • Windows: Upgrade to Viber v27.3.0.0 or later. Enable automatic updates.
  • Users in high-risk environments: Do not rely on Viber Cloak for censorship circumvention until patched version is confirmed installed. Consider alternative transports (Tor, obfs4, Shadowsocks with TLS mimicry) for critical anonymity requirements.
  • Operators evaluating Cloak integrations: Verify that any Cloak-based transport implements RFC-compliant GREASE (RFC 8701), randomized extension ordering, populated legacy_session_id, and per-connection cipher suite variation. Static JA3 hashes are a hard fail for any pluggable transport.

The core lesson from CVE-2025-13476 is that TLS fingerprinting resistance requires active, per-connection randomization — it cannot be achieved by selecting the "right" static parameters. Cloak's reference implementation provides this correctly; Viber's integration did not exercise it.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →