Содержание статьи
Когда мы говорим о плагинах и модульных приложениях, то в первую очередь имеем в виду обычный пользовательский софт. Однако модульный дизайн может быть не менее полезен при разработке руткитов и бэкдоров. Обычными средствами обновлять такой софт слишком палевно, да и не всегда возможно, а вот незаметно подгрузить модуль с новой функциональностью по сети — это пожалуйста. А если вынести ключевую функциональность в модули и удалять их сразу после загрузки, то можно серьезно подпортить жизнь реверсеру.
Вместо введения
В классической 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 модулей с множеством методов, принимающих несколько параметров, нужно придумать что-то более удобное. Например, использовать заранее определенный интерфейс, который будет реализовывать каждый модуль.
Применив такой подход к приведенному выше примеру, мы получим следующие три файла:
- Файл ModuleInterface.java с описанием API:
package com.example.modules; public interface ModuleInterface { public void run(); }
- Файл 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!!!"); } }
- Новый загрузчик модуля (помести в свое приложение):
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
, из которого затем их можно будет подгрузить с помощью загрузчика.
Выводы
Создать приложение для Android, которое сможет обновлять само себя и расширять свою функциональность незаметно для пользователя, довольно легко, и автор этих строк неоднократно видел применение такого метода в реальных троянах. Для чего эту технику будешь использовать ты — решать только тебе, однако я хотел бы напомнить, что наш журнал не поощряет любые незаконные действия, а автор статьи будет совсем не рад узнать, что научил кого-то писать трояны, которых для Android и так уже слишком много.