Первая часть статьи

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

 

Вместо введения

В классической Java есть класс под названием java.lang.ClassLoader. Его задача — загружать байт-код указанного класса (файл с расширением .class) в виртуальную машину во время исполнения приложения. Затем можно создать объект этого класса и вызывать его методы с помощью рефлексии. Такой вот способ динамической загрузки кода, который можно использовать для написания приложений с расширяемой функциональностью, или, попросту говоря, поддержкой плагинов.

В Android нет виртуальной машины Java и нет класса ClassLoader, но есть его аналог DexClassLoader, выполняющий ровно ту же функцию, но в отношении байт-кода Dalvik (и файлов .dex вместо .class соответственно). И, в отличие от настольной Java, где проще положить нужный jar-файл в CLASSPATH и не возиться с динамической загрузкой, в Android такой подход дает действительно много преимуществ, главное из которых в том, что функциональность приложения можно расширять и обновлять незаметно для пользователя и ни о чем его не спрашивая. В любой момент твое приложение может скачать файл с классом с сервера, загрузить, а затем удалить файл.

Кроме этого, классы можно хранить прямо в пакете APK и загружать во время старта приложения. Профит здесь в том, что код загружаемых классов будет отделен от кода самого приложения и находиться в APK «не по адресу»; инструменты типа apktool, которыми так любят пользоваться реверсеры, их просто не увидят. С другой стороны, это скорее защита от дурака, так как нормальный реверсер быстро смекнет, что к чему.

Как бы там ни было, динамическая загрузка классов — очень полезная штука при написании не совсем «белых» приложений, поэтому любой security-специалист должен знать, как этот механизм работает и как он используется в троянах.

 

Простейший пример

Чтобы все написанное далее было проще понять, сразу приведу пример рабочего загрузчика классов:

// Путь до jar-архива с нашим классом
String modFile = "/sdcard/myapp/module.jar";
// Путь до приватного каталога приложения
String appDir = getApplicationInfo().dataDir;

// Подгружаем файл с диска
DexClassLoader classLoader = new DexClassLoader(modFile, appDir, null, getClass().getClassLoader());

// Загружаем класс, создаем объект и пробуем вызвать метод run() с помощью рефлексии
try {
  Class c = classLoader.loadClass("com.example.modules.simple.Module");
  Method m = c.getMethod("run", null);
  m.invoke(c.newInstance(), null);
} catch (Exception e) {
  e.printStackTrace();
}

В целом здесь все просто: код загружает jar-архив /sdcard/myapp/module.jar с нашим классом, загружает из него класс com.example.modules.simple.Module, создает объект и вызывает метод run(). Обрати внимание на три момента:

  • DexClassLoader умеет загружать как «просто» файлы .dex, так и jar-архивы, последние предпочтительнее из-за сжатия и возможности использовать цифровую подпись;
  • второй аргумент конструктора DexClassLoader — это каталог, который он использует для сохранения оптимизированного байт-кода (odex), для простоты мы указываем приватный каталог самого приложения;
  • в качестве аргумента метода loadClass всегда необходимо указывать адрес класса вместе с именем пакета.

Чтобы проверить данный код на работоспособность, создадим простейший модуль:

package com.example.modules.simple.Module;

import android.util.Log;

public class Module {
  public void run() {
    Log.d("Module", "I am alive!!!");
  }
}

Не торопись создавать новый проект в Android Studio, можешь накидать этот код в блокноте и собрать его в jar-архив прямо из командной строки:

javac -classpath /путь/до/SDK/platforms/android-23/android.jar Module.java
/путь/до/SDK/build-tools/23.0.3/dx --dex --output=module.jar Module.class

Удостоверься, что каталоги platforms/android-23 и build-tools/23.0.3 существуют, в твоем случае их имена могут отличаться.

Если все пройдет гладко, на выходе ты получишь файл module.jar. Останется только добавить код загрузчика в приложение, положить module.jar на карту памяти, собрать и запустить приложение.

 

Долой рефлексию

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

Применив такой подход к приведенному выше примеру, мы получим следующие три файла:

  1. Файл ModuleInterface.java с описанием API:
     package com.example.modules;
     public interface ModuleInterface {
       public void run();
     }
    
  2. Файл Module.java с реализацией нашего модуля:
     package com.example.modules.simple.Module;
     import android.util.Log;
     public class Module implements ModuleInterface {
       public void run() {
         Log.d("Module", "I am alive!!!");
       }
     }
    
  3. Новый загрузчик модуля (помести в свое приложение):
     String modFile = "/sdcard/myapp/module.jar";
     String appDir = getApplicationInfo().dataDir;
     DexClassLoader classLoader = new DexClassLoader(modFile, appDir, null, getClass().getClassLoader());
     // Загружаем класс и создаем объект с интерфейсом ModuleInterface
     ModuleInterface module;
     try {
       Class<?> class = classLoader.loadClass("com.example.modules.simple.Module");
       module = (ModuleInterface) class.newInstance();
     } catch (Exception e) {
       e.printStackTrace();
     }
     module.run()
    

Это все. Теперь мы можем работать с модулем, как с обычным объектом. Более того, система сама отбракует модули (классы), несовместимые с интерфейсом, еще на этапе загрузки, поэтому нам не придется задаваться вопросами, а есть ли в модуле нужный нам метод.

 

Когда модулей много

С одним модулем разобрались, но что, если их будет много? Как вести учет этих модулей и не потеряться среди них? На самом деле все просто — для этого можно использовать hashmap. Еще раз изменим загрузчик:

String modDir = "/sdcard/myapp/";
String appDir = getApplicationInfo().dataDir;

File[] files = new File(modDir).listFiles();

Map<String, ModuleInterface> modules = new HashMap<>();

// Загружаем все jar-файлы из указанного каталога, создаем для каждого из них объект и помещаем в хешмап modules
for (File file : files) {
  DexClassLoader classLoader = new DexClassLoader(file, appDir, null, getClass().getClassLoader());
  try {
    Class<?> class = classLoader.loadClass("com.example.modules." + file.getName().replace(".jar", "") + ".Module");
    module = (ModuleInterface) class.newInstance();
    ModuleInterface obj = (ModuleInterface) loadedClass.newInstance();
    modules.put(file.getName().replace(".jar", ""), obj);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

Данный код загружает все jar-файлы из указанного каталога, загружает их классы Module, создает на их основе объекты и помещает их в хешмап modules. Обрати внимание на трюк, использованный при загрузке класса и размещении объекта в хешмапе. Он нужен для простоты: вместо того чтобы выяснять принадлежность каждого модуля/класса к пакету, мы просто условились, что имя jar-файла модуля будет соотноситься с именем пакета по схеме com.example.modules.ИМЯ_JAR_ФАЙЛА, так что мы сразу знаем полный адрес класса каждого модуля.

Например, приведенный ранее модуль принадлежит пакету com.example.modules.simple (см. директиву package), поэтому его необходимо включить в jar-архив simple.jar (меняем --output=module.jar на --output=simple.jar в команде сборки). Когда придет время создать новый модуль (к примеру, remote_shell), первой строчкой в его исходниках ты укажешь package com.example.modules.remote_shell.Module; и запакуешь скомпилированный байт-код в jar-архив remote_shell.jar.

Имя jar-файла (без расширения) используется также в качестве ключа в хешмапе, поэтому, зная имя модуля, всегда можно запустить его методы:

ModuleInterface module = modules.get("имя_модуля");
module.run();
Загрузчик модулей в реальном приложении
Загрузчик модулей в реальном приложении
 

Берем модули с собой

На данном этапе у нас уже есть приложение, способное загружать неограниченное количество модулей из указанного каталога и с удобством работать с ними. Осталось разобраться с тем, как распространять эти модули. Самый очевидный вариант — загружать их с сервера, пусть наш «троян» делает это раз в день, а после скачивания модулей запускает загрузчик модулей, чтобы подгрузить их в приложение. Рассказывать, как сделать это, я не буду, здесь все элементарно, и решение этой задачи ты найдешь в любой вводной статье или книге про разработку для Android.

Еще один вариант — включить модули в пакет с приложением. В этом случае наш троян будет иметь доступ к необходимым модулям сразу после первого запуска, что защитит от проблем с выходом в сеть. Когда же сеть появится, он сможет догружать модули с сервера, если это необходимо.

Чтобы включить модули в APK, их необходимо поместить в каталог assets внутри проекта (в нашем случае в assets/modules), а затем реализовать распаковщик модулей в нужный нам каталог. В коде это будет выглядеть примерно так:

String[] moduleList = context.getAssets().list("modules");
String unpackPath = "/sdcard/myapp/";

File unpackDir = new File(unpackPath);
unpackDir.mkdirs()

try {
  BufferedInputStream bis;
  OutputStream dexWriter;
  final int BUF_SIZE = 8 * 1024;

  for (String moduleFile : moduleList) {
    try {
      bis = new BufferedInputStream(context.getAssets().open("modules/" + moduleFile));
      dexWriter = new BufferedOutputStream(new FileOutputStream(unpackPath + "/" + moduleFile));
      byte[] buf = new byte[BUF_SIZE];
      int len;
      while((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
        dexWriter.write(buf, 0, len);
      }
      dexWriter.close();
      bis.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
} catch (IOException e) {
  e.printStackTrace();
}

Все очень просто. Код находит модули внутри пакета и поочередно копирует их в каталог /sdcard/myapp, из которого затем их можно будет подгрузить с помощью загрузчика.

Включаем модули в APK-пакет
Включаем модули в APK-пакет
 

Выводы

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

Оставить мнение