❯ file d5b948179c8cc33e4cbeebe0369c7dca452d968646a588d730341c3879d64880.apk d5b948179c8cc33e4cbeebe0369c7dca452d968646a588d730341c3879d64880.apk: Zip archive data, at least v2.0 to extract, compression method=deflate ❯ apktool d d5b948179c8cc33e4cbeebe0369c7dca452d968646a588d730341c3879d64880.apk Exception in thread "main" com.android.tools.smali.dexlib2.dexbacked.ZipDexContainer$NotAZipFileException at com.android.tools.smali.dexlib2.dexbacked.ZipDexContainer.getZipFile(ZipDexContainer.java:178) at com.android.tools.smali.dexlib2.dexbacked.ZipDexContainer.getEntries(ZipDexContainer.java:90) at com.android.tools.smali.dexlib2.dexbacked.ZipDexContainer.getEntry(ZipDexContainer.java:125) at brut.androlib.smali.SmaliDecoder.<init>(SmaliDecoder.java:46) at brut.androlib.ApkDecoder.decode(ApkDecoder.java:74) at brut.apktool.Main.cmdDecode(Main.java:523) at brut.apktool.Main.main(Main.java:320)
First red flag: apktool crashes immediately with a NotAZipFileException, no legitimate app has any reason to break a standard ZIP parser this hard.
Same behavior using jadx-gui…
Investigating the ZIP file reveals that bit 0 of the general purpose flag — the “encrypted” flag — is set on the file entries:
1 2 3 4 5 6
>>> import zipfile >>> z = zipfile.ZipFile("sample.apk") >>> info = z.getinfo("AndroidManifest.xml") >>> hex(info.flag_bits) '0x801' >>>
According to the ZIP specification, this flag indicates an “encrypted file”. However, this is not an encrypted ZIP file — the archive was intentionally modified to trick tools such as apktool.
Another weird thing: a lot of “fake files” are present, which sounds like BadPack obfuscation (see Unit42’s BadPack writeup).
The AndroidManifest.xml is not fully well-formed, likely due to manual tampering with the AXML structure, same story, another tool-breaking trick, but its content is fully recoverable.
With the manifest and permissions in hand, the next step is looking at what the decompiled Java code actually does with them. Two classes stand out immediately: KtBs9YhCfDjN (the fake install screen) and GrPu2MwZeAkX (the VPN service).
publicclassGrPu2MwZeAkXextendsVpnService { publicstaticvolatile GrPu2MwZeAkX instance; public ParcelFileDescriptor d;
publicstaticnativevoidstopVpn(); publicnativevoidonCreate(); publicnativevoidonDestroy(); publicnativeintonStartCommand(Intent intent, int i, int i2); }
This seems to mirror the behavior described in this detailed CYFIRMA research on KYCShadow, though the disguise itself differs: KYCShadow poses as a KYC identity-verification flow, while this sample poses as a VPN app.
Footprint of the native library
Digging into the decompiled Java code shows the use of a JNI library (keyword native):
1 2 3
android/analysis/sample-fixed ❯ file lib/armeabi-v7a/libhhcbcu.so lib/armeabi-v7a/libhhcbcu.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, for Android 21, built by NDK r27 (12077973), BuildID[sha1]=ca8cdcae7249e876a5a2b33efaae0ce672389263, stripped
This kind of targeting — messaging apps rather than banking apps — recalls Sturnus, a recently documented Android banking trojan that specifically targets WhatsApp, Telegram, and Signal users.
Deep dive into the native library
With the Java code obfuscated and sabotaged, an intuitive reversing game begins.
An eye-catching symbol quickly appears: Java_com_qmvxhkjp_rnbtzwlc_Sec_nativeGetStr. After some renaming and structure creation, the function looks like this:
It performs a direct lookup into a 75-entry table of (pointer, length) pairs.
Tracing cross-references to the AES S-box leads to the Sec_nativeGetStr function. The conclusion: this function decrypts strings using AES-CTR, with the id embedded as part of the counter block. The key is also easily identifiable:
1 2
00024f30 data_24f30: 00024f30 a2 e8 56 bf 495252 a8 354b1e ea 07 c0 c5 ef ..V.IRR.5K......
Running the snippet against the table yields the full set of decrypted strings:
This blob was extracted from the ZIP repaired by Malfixer:
1 2 3
android/analysis/sample-fixed ❯ file assets/nvcgehin assets/nvcgehin: data
Unsurprisingly, this blob is also encrypted:
Entropy sits close to 8 bits/byte across the entire file, the signature of properly encrypted (or at least well-compressed) data, with no recognizable header or magic bytes anywhere.
Now, the idea is to check whether any code in the .so handles assets directly. This search does not show any relevant clues in the compiled library.
Given the blob’s high entropy, and of course knowing AES is already used elsewhere in this library, it’s worth checking whether the same routines are involved here too. Tracing cross-references to the AES S-box a second time reveals another function:
The nonce and the key can be spotted:
1 2 3 4 5 6
00025140 payload_key: 00025140 f5 090d 29 cc 4f11 df 7b ca 1e 813d 68600f ...).O..{...=h`.
The domain and filename impersonate Claro, a major Latin American telecom carrier, the naming pattern strongly suggests a fake “5G network update” lure, matching the fake “Update Available” Play Store screen we found decrypted inside the .so library earlier. This seems to point to a campaign targeting Spanish-speaking users in Latin America.
What looked like a simple fake VPN app turned out to be a fully layered dropper: ZIP-level anti-analysis, a sabotaged manifest, a sabotaged DEX, and a native library hiding two separate AES keys, all retrieved without ever running a single line of the malware. The second stage is another APK \o/ Its analysis will be covered in a follow-up article :)