An apparently benign app distribution scheme which has all it takes to turn (very) ugly

This articles discusses a recent Android sample from January 2021. It was first scanned on the 11th, but according to its certificate probably released on the 7th. Similar samples were seen in December 2020. sha256:f699f9e50e8401943321d757a9c1bab367473f102c0abfb57367e9252aae7fde


It is immediately clear the sample is packed. When the sample is launched, a class is called. This loads another DEX, which is decrypted from assets file qoh.

Loads a DEX, and instantiates an object of class “”, then invokes method “ywraafdeerq”. Strangely, I was unable to locate that method in the dynamic DEX.

The name of class StubApplication reminds us of Tencent, Baidu or Qihoo packers, but not quite, because, in that case, the asset decryption is performed by a native library named

Native decryption library. Method a() returns the path for the payload DEX. Method b() decrypts the asset “qoh” in a JAR file.
Disassembly of the native library — from Radare2. A function named “readAssert” (typo as in code) reads an asset file and outputs data to x.jar

Note you can get the unpacked JAR’s full path without pain with Dexcalibur or House. And then, do an adb pull to retrieve the file.

Dexcalibur hook logs show DexClassLoader is called on fullpathname/x.jar.
With House, if you enumerate class loaders (see Enumeration tab), you get the path of the unpacked asset

The sample has only been compiled for ARMEABI-v7a. Consequently, to run or hook the sample, you’ll need to get an Android emulator for ARM.

Downloading plugins

The unpacked JAR consists of a manifest and a classes.dex file. We find the main activity which was referenced in the packer.

The main activity instantiates an object of class “go”. This is where the rest happens.

The sample next queries a first URL to get the IP address for a “version” server. The sample posts to the version server details of the phone (IMSI, MAC address, IMEI, model…) and receives a customized list of apps to download. Finally, the sample downloads the apps and loads them silently.

Two remote servers are involved. The first one makes the IP address of the second configurable.

JSON answer from the version server. Slightly edited for readability.

An easy way to follow the queries is with a Frida hook on the URL class. I also hooked loadApk and getRealDex which are part of the dynamically loaded DEX from x.jar.

(frida-env) axelle@boostix $ frida -U -l ./dynhook.js -f com.lt7qmgb699f.mnf6viyhwlt
/ _ | Frida 14.2.8 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at
Spawning `com.lt7qmgb699f.mnf6viyhwlt`...
[*] Hooking dynamic class / method
Spawned `com.lt7qmgb699f.mnf6viyhwlt`. Use %resume to let the main thread start executing!
[Android Emulator 5554::com.lt7qmgb699f.mnf6viyhwlt]-> %resume
[Android Emulator 5554::com.lt7qmgb699f.mnf6viyhwlt]-> [+] DexClassLoader constructor hook: dexpath=/data/user/0/com.lt7qmgb699f.mnf6viyhwlt/app_c/x/x.jar
[*] hooking go...
[*] hooking loadApk...
[*] hooking getRealDex...
URL: hXXp://
URL: hXXp://
URL: hXXp://
URL: hXXp://
[+] downloadFile: url=hXXp:// dir=/storage/emulated/0/Android/data/com.lt7qmgb699f.mnf6viyhwlt/files/nwplugin/ localname=
URL: hXXp://
URL: hXXp://
[+] getRealDex: srcdex=/storage/emulated/0/Android/data/com.lt7qmgb699f.mnf6viyhwlt/files/nwplugin/plugin_wxgjzq3050_1102a
[+] getRealDex returns dex=/data/user/0/com.lt7qmgb699f.mnf6viyhwlt/app_sex/win.apk
[+] loadApk: dex=/storage/emulated/0/Android/data/com.lt7qmgb699f.mnf6viyhwlt/files/nwplugin/plugin_wxgjzq3050_1102a
[+] getRealDex: srcdex=/storage/emulated/0/Android/data/com.lt7qmgb699f.mnf6viyhwlt/files/nwplugin/plugin_wxgjzq3050_1102a
[+] getRealDex returns dex=/data/user/0/com.lt7qmgb699f.mnf6viyhwlt/app_sex/win.apk
[+] DexClassLoader constructor hook: dexpath=/data/user/0/com.lt7qmgb699f.mnf6viyhwlt/app_sex/win.apk

This mechanism is particularly dangerous because, even if at a given moment the downloaded apps are not malicious, there is no way to ascertain the same channel won’t be used later to push malicious ones. Or the first server could be hacked or hijacked to return the IP address of a malicious version server. Additionally, the plugin we downloaded (plugin_wxgjzq3050_1102a) is encrypted, and decrypted by getRealDex(). We have all the components for a stealthy install of malware.

This is some very lame obfuscation found in the downloaded plugin. A bit strange to use something so simple, when the rest of the distribution of the package is complex…

Malicious or not?

Despite a very dangerous download & install mechanism, I am not entirely convinced this sample is really malicious. This is because, so far, in the end, the distributed apps seemed at most scam or adware. Some AV detect the sample as a “Penguin” trojan dropper. It does indeed drop an APK. There is only little information what the Penguin family is, but it seems we should find code that puts a window on top of all others. I wasn’t able to locate such code in this sample, so probably bad naming. So, I named it Riskware/Tenpack!Android. Until it grows up bad…

To summarize, plain malicious or not, Penguin or not, we need to keep an eye on such samples, because they can turn very bad one day. This is a sample you certainly don’t want on your smartphone 😏

— Cryptax

PS. Have you seen this packer? Or any other comment, please feel free to reply.

Mobile and IoT malware researcher. The postings on this account are solely my own opinion and do not represent my employer.