Skip to content

From Encrypted Blob to Trojan: Decrypting the mParivahan Dropper's Payload (Part 2)

In Part 1 we covered the parivahansewaa.apk dropper's phishing vector, anti-analysis tricks, and its VPN-sinkhole trick for blinding Google Play Protect. This part covers how its encrypted second-stage payload is decrypted, and what the resulting trojan — spoofing the package name com.transport.gov.in — actually does once it's running.

Payload decryption pipeline

The primary payload sits inside assets/wpjfts (4,637,501 bytes), decrypted by a three-tier pipeline implemented in com.tmobile.android.provider.xhvc9n.o:

Encrypted asset: wpjfts
[0..15]: XOR key | [16..31]: RC4 key | [32..end]: ciphertext

1. XOR with the first 16 bytes
2. Modified RC4 (4 KSA rounds)
3. Stream slice: [0..11] IV (12 bytes), [12..end] AES-GCM ciphertext + tag
4. AES-256-GCM decrypt (key = XOR of two hardcoded 32-byte arrays)
5. Zlib decompression (magic: MSZ1)
6. Split-container unpack (magic: MSP1) -> base.apk + split_config.dex.apk

Reproducing the decryption

We reproduced the routine in Python to independently verify the extracted artifacts and their hashes:

import zipfile, zlib, struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

with zipfile.ZipFile("parivahansewaa.apk") as z:
    raw = z.read("assets/wpjfts")

# Hardcoded key arrays recovered from o.java
b1 = [124, 57, 44, 42, -25, -119, -28, 19, 127, 125, -95, 41, -87, 116, -117, -83,
      -126, -44, 120, 21, 88, -128, 31, 88, 106, -73, -70, -103, 42, -106, -78, 124]
b2 = [33, -81, -33, 39, 112, -32, 41, 34, 29, 66, -53, -122, -6, 26, 1, 90,
      -68, -11, 59, 53, -118, -117, -28, -103, -71, 53, -31, 60, -29, 21, -57, -98]
key = bytes([(x & 0xFF) ^ (y & 0xFF) for x, y in zip(b1, b2)])

# Layer 1: XOR
bArr2, bArr3 = raw[:16], raw[16:32]
bArr4 = bytearray(raw[32:])
for i in range(len(bArr4)):
    bArr4[i] ^= bArr2[i % 16]

# Layer 2: 4-round RC4 KSA
s = list(range(256))
for _ in range(4):
    j = 0
    for i in range(256):
        j = (j + s[i] + (bArr3[i % 16] & 0xFF)) & 0xFF
        s[i], s[j] = s[j], s[i]

bArr5 = bytearray(len(bArr4))
i = j = 0
for idx in range(len(bArr4)):
    i = (i + 1) & 0xFF
    j = (j + s[i]) & 0xFF
    s[i], s[j] = s[j], s[i]
    bArr5[idx] = (s[(s[i] + s[j]) & 0xFF] & 0xFF) ^ bArr4[idx]

# Layer 3: AES-GCM, then zlib
iv, ct = bytes(bArr5[:12]), bytes(bArr5[12:])
pt = AESGCM(key).decrypt(iv, ct, None)
decomp = zlib.decompress(pt[4:])

# Unpack the MSP1 split-APK container
magic, count = struct.unpack_from("<II", decomp, 0)
offset = 8
for _ in range(count):
    n_len = struct.unpack_from("<H", decomp, offset)[0]; offset += 2
    name = decomp[offset:offset + n_len].decode("utf-8"); offset += n_len
    sz = struct.unpack_from("<I", decomp, offset)[0]; offset += 4
    with open(name, "wb") as f:
        f.write(decomp[offset:offset + sz])
    offset += sz

Extracted artifacts

Artifact Size SHA-256
base.apk 1,305,613 bytes 5cbce92c326d64438ac22a61210a7bad40fe72ec25a96d52fd7005879684b451
split_config.dex.apk 10,744,151 bytes 0cb02ec8cb3c30e735083b739c6499a116e497b28ad23f236d6c5b9bd0ea4a49

Both are signed under alias DROPPERK, issued to CN=NxtGen mParivahan Updates (SHA-256: 25:F5:B4:03:A7:6E:D9:A5:97:B9:4E:F9:50:7B:9D:EC:FB:CF:4E:10:AF:E9:A1:E0:FC:37:77:16:85:33:B2:AD).

Stage 2: Trojan Core Analysis (com.transport.gov.in)

The dropped package spoofs com.transport.gov.in — identical to the real government transport package naming scheme — with the core trojan logic under com.app.messenger.

Lure config (assets/config.json): app name "NxtGen mParivahan", admin_token/user_id v_GpgVz5NrflZHqM8GBeBjWGIhp3GGsNhF0_duyACUI, app type mparivahanv3. The lure prompts victims for a ₹1 "Verification Fee" to harvest card, net-banking, or UPI credentials.

Command & control

Endpoint Purpose
https://api.cloudsettle.org Primary C2 backend
wss://api.cloudsettle.org/ws/tunnel/device-b Reverse interactive command tunnel
POST /sms/batch Bulk SMS/OTP exfiltration
POST /call-logs/batch Call log upload
POST /pin-data Gzip-compressed stolen UPI/banking PINs (X-Device-ID)
GET /api/staged/fetch/ Dynamic secondary module delivery

Firebase (used as a dynamic C2 failover — see Part 3): project andromeda-d389d, app ID 1:651390976306:android:53c99731aa157ea66f972b, sender ID 651390976306.

SMS & OTP theft engine

SmsReceiver (priority 999) and InboxReceiver (priority 998) register for SMS_RECEIVED/SMS_DELIVER at the highest broadcast priority available, intercepting bank 2FA OTPs before the user's default messaging app even sees them. SmsPollingWorker/SmsUploadWorker periodically harvest the full SMS store and batch-upload it to /sms/batch.

Accessibility Service abuse (SystemAccessibilityService)

Binding to BIND_ACCESSIBILITY_SERVICE gives the trojan continuous surveillance and automation over the UI:

  • Automated permission granting — watches for and taps "Allow" in both English (allow, allow all the time, while using the app, grant, permit, accept, turn on, enable) and Hindi (अनुमति दें, हमेशा अनुमति दें, ऐप का उपयोग करते समय, अनुमति, हां, ठीक है) prompts.
  • OEM battery/autostart bypass — targets MIUI, ColorOS, FuntouchOS, and EMUI security apps (com.miui.securitycenter, com.miui.powerkeeper, com.coloros.safecenter) to self-enable background execution.
  • Anti-uninstall shielding — watches the foreground package; if the victim opens com.android.settings, com.android.packageinstaller, or a device security manager, it fires performGlobalAction(GLOBAL_ACTION_HOME) to bounce them back to the home screen before they can remove it.
  • UPI PIN theft — an invisible touch overlay ("ghost overlay") records keystroke coordinates to extract 4–6 digit PINs when the foreground app is one of 60+ targeted payment apps, including PhonePe, Google Pay, Paytm, BHIM, CRED, SBI UPI, HDFC PayZapp, Axis Pay, MobiKwik, BharatPe, Groww, Jupiter Money, and OneCard.

Part 3 zooms out to the wider campaign — 24 typosquatted repositories sharing this infrastructure, the Firebase-based dynamic C2 failover, a full IOC list, and takedown report templates for CERT-In, GitHub, and Google.