Order Of Six Angles

Main Logo

A security researcher's blog about reverse-engineering, malware and malware analysis

Home | RU | Translations | Tools | Art | About

4 July 2020

tags: android - malware

Новый способ внедрения вредоносного кода в андроид приложения

Предисловие

Авторы идеи: Ербол & Thatskriptkid

Автор рисунка: @alphin.fault instagram

Автор статьи и proof-of-concept кода: Thatskriptkid

Proof-of-Concept

Целевая аудитория статьи - люди, которые имеют представление о текущем способе заражения андроид приложений через патчинг smali кода и хотят узнать о новом и более эффективном способе. Если вы не знакомы с текущей практикой заражения, то прочитайте мою статью - Воруем эцп, используя Man-In-The-Disk, глава - “Создаем payload”. Техника описанная здесь, полностью была придумана нами, в сети отсутствует какое-либо описание подобного способа.

Наш способ:

  1. Не использует баги или уязвимости андроида
  2. Не предназначен для крякинга приложений (удаление рекламы, лицензии и т.д.)
  3. Предназначен для добавления вредоносного кода, без какого-либо вмешательства в работу целевого приложения или его внешний вид.

Недостатки текущего подхода

Способ внедрения вредоносного кода, с помощью декодирования приложения до smali кода и его патчинг - является единственным и широко практикуемым на сегодняшний день. smali/backsmali - единственный инструмент, используемый для этого. На основе него строятся все известные инфекторы, например:

  1. backdoor-apk
  2. TheFatRat
  3. apkwash
  4. kwetza

Малварь также использует smali/backsmali и патчинг. Схема работы трояна Android.InfectionAds.1:

Декодирование и патчинг предполагают изменение оригинального classesN.dex файла. Это приводит к двум проблемам:

  1. Выход за пределы лимита в 65536 методов в одном DEX файле, если вредоносного кода слишком много
  2. Приложение может проверять целостность DEX файлов

Декодирование/дизассемблирование DEX - это сложный процесс, требующий постоянного обновления и сильно зависящий от версии андроида.

Практически все доступные инструменты для заражения/модификации написаны на Java и/или зависят от JVM - это сильно сужает область использования и делает невозможным запуск инфектора на роутерах, встроенных системах, системах без JVM и т.д.

Описание нового подхода

В андроиде существует несколько типов запуска приложений, один из них называется cold start. Cold start - запуск приложения впервые.

Выполнение приложения начинается с создания Application объекта. Большинство андроид приложений имеют свой Application класс, который должен наследоваться от основного класса android.app.Applciation. Пример класса:

package test.pkg;
import android.app.Application;
public class TestApp extends Application {

    public TestApp() {}

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

Класс test.pkg.TestApp должен быть прописан в AndroidManifest.xml. Пример манифеста:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="Test"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestApp">
    </application>
</manifest>

Процесс запуска такого приложения:

Были определены основные требования к нашей технике заражения:

  1. Выполнение вредоносного кода, при старте приложения
  2. Сохранение всех этапов процесса запуска оригинального приложения

Внедрение вредоносного кода происходило в стадии алгоритма cold start:Application Object creation->Application Object Constructor. Был создан вредоносный Application класс, внедрен в приложение и прописан в AndroidManifest.xml, вместо изначального. Чтобы сохранять прежнюю цепочку выполнения, он был наследован от test.pkg.TestApp.

Вредоносный Application класс:

package my.malicious;
import test.pkg;
public class InjectedApp extends TestApp {

    public InjectedApp() {
        super();
        executeMaliciousPayload();
    }
}

Модифицированный AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="Test"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="my.malicious.InjectedApp">
    </application>
</manifest>

Процесс запуска вредоносного кода внутри зараженного приложения (красным выделены модификации):

Примененные модификации:

  1. В приложение добавлен класс my.malicious.InjectedApp
  2. В AndroidManifest.xml заменена строка test.pkg.TestApp на my.malicious.InjectedApp

Преимущества нового подхода

Существует возможность применить необходимые модификации к APK:

  1. Без дизассемблирования/сборки DEX
  2. Без декодирования/кодирования манифеста
  3. Без внесения изменений в оригинальные DEX файлы

Данные факты позволяют заражать практически любое существующее приложение, без ограничений. Добавление своего класса и изменение манифеста работает намного быстрее, чем декодирование. Внедренный нашей техникой вредоносный код стартует сразу, а не по определенному событию, так как мы внедряемся в самое начало процесса запуска приложения. Описанная техника заражения не зависит от архитектуры и версии андроида (за небольшим исключением).

PoC для демонстрации был написан на Go и готов к расширению до полноценного инструмента. PoC компилируется в один целостный бинарный файл и не использует никаких зависимостей в рантайме. Использование Go позволяет, с помощью кросс-компиляции, собрать инфектор для практически любой архитектуры и ОС.

Тестирование приложений, зараженных PoC проводилось на:

NOX player 6.6.0.8006-7.1.2700200616, Android 7.1.2 (API 25), ARMv7-32

NOX player 6.6.0.8006-7.1.2700200616, Android 5.1.1 (API 22), ARMv7-32

Android Studio Emulator, Android 5.0 (API 21), x86

Android Studio Emulator, Android 7.0 (API 24), x86

Android Studio Emulator, Android 9.0 (API 28), x86_64

Android Studio Emulator, Android 10.0 (API 29), x86

Android Studio Emulator, Android 10.0 (API 29), x86_64

Android Studio Emulator, Android API 30, x86

Xiaomi Mi A1

Удалось удачно заразить огромное количество приложений (по понятным причинам имена скрыты). Удалось заразить приложения, которые не поддаются декодированию, с помощью smali/backsmali, а значит и любого существующего инструмента.

Выявление необходимых модификаций в AndroidManifest.xml и патчинг

Одной из модификаций, необходимой для заражения, является замена строки в AndroidManifest.xml. Существует возможность пропатчить строку, без декодирования/кодирования манифеста.

APK содержат манифест в бинарном, закодированном виде. Структура бинарного манифеста не документирована и представляет собой кастомный алгоритм кодирования XML от Google. Для удобства было создано описание на языке Kaitai Struct, которое может быть использовано, а качестве документации.

Структура AndroidManifest.xml (в скобках - размер в байтах):

Для определения изменений в манифесте, после патчинга оригинального имени Application класса на вредоносное, были разработаны два приложения, с разными имена классов. Приложения были собраны в APK и распакованы, для получения бинарных манифестов.

Пример оригинального манифеста, с именем Application - test.pkg.TestApp:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qoogle.service.outbound.thread.safe.eng.packages.packas.pack.level.random">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="MinDEX"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestApp">
    </application>

</manifest>

Пример пропатченного манифеста, с именем Application - test.pkg.TestAppAAAAAAAAA:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qoogle.service.outbound.thread.safe.eng.packages.packas.pack.level.random">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="MinDEX"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestAppAAAAAAAAA">
    </application>

</manifest>

Длина полного имени класса увеличилась на 9 символов. Оба файла были открыты в HexCmp, для получения диффа.

Изменения, которым подвергся манифест и объяснение причин:

field offset description diff_count explanation
header.file_len 0x4 Длина всего файла 0x10 В оригинальном манифесте было 0х2 байта выравнивания, в измененном они не требуются.
Строки в бинарном манифесте хранятся в формате UTF-16, то есть один символ занимает 0x2 байта.
Итого, мы увеличили строку на 9 символов (0x12 байт) минус 0x2 байта выравнивания, получаем разницу 0x10 байт
header.string_table_len 0xC Длина массива строк 0x10 Строка находится в общем массиве строк. Объяснение разницы в 0x10 байт такая же как у header.file_len
string_offset_table.offset 0x7C Оффсет до строки, следующей после измененной 0x12 В string_offset_table хранятся оффсеты до строк в массиве строк манифеста. Так как длина строки увеличилась,
следующая за ней строка сдвинулась дальше на 0x12 байт. Выравниваниеи здесь не учитывается, так как оффсеты
расположены до массива строк.

field offset description diff_count explanation
strings.len 0x2EA Длина строки 0x9 Количество символов, на которое увеличилась строка

В структуре манифеста, приведенной в начале, после strings следует padding, для выравнивания resource_header. В оригинальном манифесте последняя строка uses-sdk заканчивается по оффсету 0x322 (оранжевым), а значит были добавлены два байта выравнивания (зеленым) для resource_header

В модифицированном варианте, string_table заканчивается на оффсете 0x334 (оранжевым) и далее сразу следует resource_header (красным), который не требует выравнивания.

Структура AndroidManifest.xml, с указанием полей, которые необходимо пропатчить, для замены имени оригинального Applciation класса на вредоносный (выделены красным):

Proof-of-Concept код, разработанный для статьи, реализовывает эти модификации в методе manifest.Patch().

Создание файлов, для внедрения в целевое приложение

Второй модификацией, необходимой для заражения, является внедрение класса, с вредоносным кодом. Для сохранения оригинальной цепочки запуска приложения, в него должен быть внедрен Application класс, родительским классом которого должен является оригинальный Applciation класс. На этапе подготовке внедряемых файлов, оно неизвестно. Поэтому, при создании класса, необходимо было использовать имя-заглушку z.z.z.

Изначальное состояние приложения и внедряемого DEX:

После получения оригинального имени Application класса из манифеста, заглушка была пропатчена:

Процедура заражения завершается добавлением вредоносного DEX в целевое приложение:

Так как классы с вредоносным кодом могут иметь разный код, они были вынесены в отдельный DEX. Это также было сделано для упрощения процедуры патчинга заглушки.

Имена классов в DEX располагаются в алфавитном порядке. Имя Application класса целевого приложения может начинаться с любой буквы. Для предсказуемости порядка строк, после патчинга, имя заглушки было выбрано равным z.z.z.

Для подготовки внедряемых файлов, был создан проект в Android Studio, с тремя классами.

Класс InjectedApp. Его полное имя:

aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa.InjectedApp

Это имя должно удовлетворять двум правилам:

  1. Оно должно быть длиннее любого имени Application класса любого целевого приложения

  2. Оно должно быть выше в алфавитном порядке любого имени Application класса любого приложения

Класс InjectedApp, который будет выполняться вместо Application class целевого приложения:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends z {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

Задача класса начать выполнение вредоносного кода, который находится в другом DEX:

        payload p = new payload();
        p.executePayload();

Класс payload содержит вредоносный код:

package aaaaaaaaaaaa;

import android.util.Log;

public class payload {

    public void executePayload() {
            Log.i("HELL", "Hello, I'm a malicious payload");
    }
}

Полное имя класса должно удовлетворять следующему правилу:

  1. Оно должно быть выше по алфавиту любого имени класса Application любого приложения

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

  1. Содержать класс с именем:

aaaaaaaaaaaa.payload

  1. Класс должен содержать метод

public void executePayload()

Класс-заглушка z.z.z, полное имя которого будет пропатчено на полное имя Applciation класса целевого приложения.

package z.z;

import android.app.Application;

public class z extends Application {
}

Класс должен соблюдать условие:

  1. Полное имя класса должно быть ниже по алфавиту полных имен классов InjectedApp и payload.
  2. Полное имя класса должно быть короче любых полных имен Application классов любых приложений.

В соответствии с разработанной схемой внедрения, классы InjectedApp и payload были скомпилированы в отдельные DEX. Для этого в Android Studio была выполнена сборка APK командой Android Studio->Generate Signed Bundle/APK->release. Скомпилированные .class файлы создались в папке app\build\intermediates\javac\release\classes.

Компилирование .class файлов в DEX, с помощью d8:

d8 --release --min-api 16 --no-desugaring InjectedApp.class --output .

d8 --release --min-api 16 --no-desugaring payload.class --output .

Получившиеся DEX должны быть добавлены в целевое приложение.

Выявление необходимых модификаций в DEX и патчинг

После патчинга заглушки z.z.z на полное имя Application класса целевого приложения, структура DEX изменится. Для выявления модификаций, в Android Studio было создано два приложения с именами классов разной длины.

Класс InjectedApp, наследуемый от z.z.z, в первом приложении:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends z {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

Класс InjectedApp, наследуемый от z.z.zzzzzzzzzzzzzzzz во втором приложении:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends zzzzzzzzzzzzzzzz {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

Длина имени класса увеличилась на 15 символов. Компилируем оба класса отдельно в DEX:

d8 --release --min-api 16 --no-desugaring InjectedApp.class --output .

Откроем получившиеся DEX в программе HexCMP:

Официальная документация по структуре DEX

field offset description diff_count explanation
header_item.checksum 0x8 Контрольная сумма full При любом изменении DEX контрольная сумма пересчитывается
header_item.signature 0xC Хэш full При любом изменении DEX хэш пересчитывается
header_item.file_size 0x20 Размер файла 0x10 Размер строки увеличился на 0xF, плюс 0x1 байт выравнивания
header_item.map_off 0x34 оффсет до map 0x10 map находится после массива строк, поэтому оффсет был увеличен, с учетом выравнивания
header_item.data_size 0x68 размер data секции 0x10 Data секция находится после массива строк, поэтому оффсет был увеличен, с учетом выравнивания
map.class_def_item.class_data_off 0xE8 оффсет до данных класса 0xF Данная структура не требует выравнивания, поэтому значение увеличилось на количество добавленных символов
map_list.debug_info_item 0x114 debug информация Не важно Поле хранит данные, необходимые для корректного вывода, при краше. Поле можно игнорировать

field offset description diff_count explanation
string_data_item.utf16_size 0x1B3 размер строки 0xF Строки в DEX хранятся в формате MUTF-8, где один символ занимает 1 байт

Изменения в конце файла:

field offset description diff_count explanation
map.class_data_item.offset 0x29C оффсет до class_data_item 0xF Структура class_data_item следует сразу за массивом строк и не требует выравнивания
map.annotation_set_item.entries.annotation_off_item 0x2A8 оффсет до аннотаций 0x10 Выравнивание учитывается
map.map_list.offset 0x2B4 оффсет до map_list 0x10 Выравнивание учитывается

Proof-of-Concept код, разработанный для статьи, реализовывает эти модификации в методе mydex.Patch().

Результаты

Для применения необходимых модификаций, был разработан PoC, который работает по алгоритму:

  1. Распаковывание файлов APK
  2. Парсинг AndroidManifest.xml
  3. Нахождение имени Application класса
  4. Патчинг оригинального имени Application на aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa.InjectedApp в AndroidManifest.xml
  5. Патчинг заглушки z.z.z на оригинальное имя Application класса
  6. Добавление в APK двух DEX (один с InjectedApp Application классом, второй с вредоносными классами)
  7. Запаковывание всех файлов в новый APK

Ограничения нового подхода

Данная техника не будет работать с приложениями, удовлетворяющие всем условиям одновременно:

  1. minSdkVersion <= 20
  2. Не используют в зависимостях библиотеку androidx.multidex:multidex или com.android.support:multidex
  3. Запускаются на андроиде версии меньше, чем Android 5.0 (API level 21)

Тем самым предполагается, что приложение имеет один DEX файл. Ограничение применимо из-за того, что версии андроида до Android 5.0 (API level 21) используют виртуальную машину Dalvik, для запуска кода. По умолчанию, Dalvik воспринимает только один DEX файл в APK. Чтобы обойти это ограничение, необходимо использовать вышеприведенные библиотеки. Версии андроида после Android 5.0 (API level 21), вместо Dalvik, используют систему ART, которая нативно поддерживает несколько DEX файлов в приложении, так как при установке приложения она прекомпилирует все DEX в один .oat файл. Подробности написаны в официальной документации.

Дальнейшие улучшения PoC

  1. Если у приложения нет своего Application класс, то необходимо добавлять InjectedApp в AndroidManifest.xml
  2. Добавление в AndroidManifest.xml своих тегов
  3. Подписание APK
  4. Избавление от декодирования AndroidManifest.xml

FAQ

Q: Почему бы не использовать в полном имени InjectedApp символы подчеркивания, тем самым оно практически гарантировано будет по алфавиту выше любого имени Application класса целевого приложения?

A: Технически это возможно, но возникнут проблемы в Android 5 и будет следующая ошибка:

D/AndroidRuntime( 3891): Calling main entry com.android.commands.pm.Pm
D/DefContainer( 3414): Copying /mnt/shared/App/20200629234847850.apk to base.apk
W/PackageManager( 1802): Failed parse during installPackageLI
W/PackageManager( 1802): android.content.pm.PackageParser$PackageParserException: /data/app/vmdl1642407162.tmp/base.apk (at Binary XML file line #48): Bad class name ________.__________._0000000000000000000000000000000000000000000000000000000000000000.InjectedApp in package XXXXXXXXXXXXXXXXXXXXXX
W/PackageManager( 1802):        at android.content.pm.PackageParser.parseBaseApk(PackageParser.java:885)
W/PackageManager( 1802):        at android.content.pm.PackageParser.parseClusterPackage(PackageParser.java:790)
W/PackageManager( 1802):        at android.content.pm.PackageParser.parsePackage(PackageParser.java:754)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService.installPackageLI(PackageManagerService.java:10816)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService.access$2300(PackageManagerService.java:236)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService$6.run(PackageManagerService.java:8888)
W/PackageManager( 1802):        at android.os.Handler.handleCallback(Handler.java:739)
W/PackageManager( 1802):        at android.os.Handler.dispatchMessage(Handler.java:95)
W/PackageManager( 1802):        at android.os.Looper.loop(Looper.java:135)
W/PackageManager( 1802):        at android.os.HandlerThread.run(HandlerThread.java:61)
W/PackageManager( 1802):        at com.android.server.ServiceThread.run(ServiceThread.java:46)

Q: Почему бы вместо внедрения своего Application класса, не внедрять свой Activity и прописывать его в манифесте, вместо основного, ведь оно также стартует самым первым? Да, при таком способе payload выполнится чуть позже, но это не критично.

A: В этом подходе есть две проблемы. Первая - существуют приложения, которые используют в манифесте очень много тегов activity-alias, которые ссылаются на имя основного активити. В этом случае нам придется патчить не одну строку в манифесте, а несколько. Также это затрудняет парсинг и нахождение имени нужного Activity. Вторая - основной Activity запускается в главном UI потоке, что накладывает некоторые ограничения на вредоносный код.

Q: Но ведь в Application классе нельзя использовать сервисы. Какой может быть вредоносный код без сервисов?

A: Во-первых, это ограничение введено в версии андроида, начиная с API 25. Во-вторых, это ограничение касается андроид приложений в целом, а не конкретно Application класса. В третьих, сервисы использовать можно, но не обычные, а foreground.

Q: Ваш PoC не работает

A: В этом случае удостоверьтесь, что:

  1. Оригинальное приложение работает
  2. Все пути к файлам в PoC корректны
  3. В apkinfector.log нету ничего необычного
  4. Имя оригинального Application класса в пропатченном InjectedApp.dex действительно находится на своем месте
  5. Целевое приложение использует свой Application класс. Иначе, неработоспособность PoC - предсказуема

Если ничего не помогло, то попробуйте поиграть с параметром --min-api, когда компилируете классы. Если ничего не помогло, то создайте issue на github.

Q: Почему для заражения был выбран конструктор Application, а не метод OnCreate()?

A: Дело в том, что существуют приложения, у которых Application класс содержит метод OnCreate() с модификатором final. Если вы подложите свой Application с OnCreate(), то андроид выдаст ошибку:

06-28 07:27:59.770  2153  4539 I ActivityManager: Start proc 6787:xxxxxxxxx/u0a46 for activity xxxxxxxxx/.Main
06-28 07:27:59.813  6787  6787 I art     : Rejecting re-init on previously-failed class java.lang.Class<InjectedApp>:
 java.lang.LinkageError: Method void InjectedApp.onCreate() overrides final method in class LX/001; 
(declaration of 'InjectedApp' appears in /data/app/xxxxxxxxx-1/base.apk:classes2.dex)

Причины ошибки тут

if (super_method->IsFinal()) {
          ThrowLinkageError(klass.Get(), "Method %s overrides final method in class %s",
                            virtual_method->PrettyMethod().c_str(),
                            super_method->GetDeclaringClassDescriptor());
          return false;
        }

Андроид увидит, что super method - final и выдаст ошибку.

В Java, если вы не создали никакого конструктора, то компилятор создаст его за вас (без параметров). Если же вы создали конструктор с параметрами, то конструктор без параметров автоматически не создается. Так как мы вызываем конструктор без параметров, то вы можете подумать, что возникнет проблема, если app class целевого приложения содержит констурктор с параметрами. Но нет, именно для Application классов, андроид требует чтобы был дефолтный констурктор. Иначе вы получаете такую ошибку

06-28 08:51:54.647  8343  8343 D AndroidRuntime: Shutting down VM
06-28 08:51:54.647  8343  8343 E AndroidRuntime: FATAL EXCEPTION: main
06-28 08:51:54.647  8343  8343 E AndroidRuntime: Process: xxxxxxxxx, PID: 8343
06-28 08:51:54.647  8343  8343 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate application xxxxxxxxx.AppShell: java.lang.InstantiationException: java.lang.Class<xxxxxxxxx.AppShell> has no zero argument constructor
06-28 08:51:54.647  8343  8343 E AndroidRuntime:        at android.app.LoadedApk.makeApplication(LoadedApk.java:802)
Вверх