Международная конференция разработчиков
и пользователей свободного программного обеспечения

Продвинутая обфускация констант в Android-приложениях

Алексей Хлебников, Oslo, Norway

LVEE 2018

Android is very popular mobile platform and amount of apps, that work with sensitive information, for example banking apps, grows every day. Thus anti-reverse-engineering technologies are now increasingy more important. In this presentation I will tell how to obfuscate sensitive data in the Android byte code by moving it to encrypted files that will be decrypted by native code, and how to automate this process. I will use strings as examples of data, but the described technique can be applied to other types of data as well.

Что такое обфускация и зачем она нужна

Обфускация – преобразование информации, например кода или данных, таким образом, чтобы эту информацию было трудно понять и ей воспользоваться. По сути, это “прятание информации на виду”.

Открытый код и отрытые данные программ – это обычно хорошо. Но не всегда. У компьютерных преступников есть такая забава: взять какую-нибудь важную программу, например банк-клиент, изменить её немного, чтоб она слала пароли на сервер злоумышленника, или сразу деньги на его счёт, и обманом заставить технически неграмотных пользователей эту программу установить. Или с помощью трояна пропатчить установленную программу прямо на компьютере пользователя.

Для того, чтобы провернуть такое дело, злоумышленнику надо понять, как атакуемая программа работает, то есть провести реверс-инжиниринг. И вот тут ему очень помогут и открытый код, и открытые данные. Если они есть. Значит надо код и данные от злоумышленника скрыть, а если нельзя скрыть – то обфусцировать. К сожалению, обфускация не может полностью блокировать реверс-инжиниринг. Но она может этот процесс более сложным, долгим и дорогим. В результате, выгода злоумышленника в случае успешной атаки снижается или вообще сходит на нет. Чем сложнее – тем меньше людей вообще смогут провести атаку. И конечно, многие киберпреступники просто переключатся на другую цель, полегче.

Обфусцировать в программе можно разное. Эта статья упоминает об обфускации кода программы и особенное внимание уделяет обфускации строк. Почему это важно – будет написано ниже.

Реверс-инжиниринг и Android

В последнее время большую популярность приобрела платформа Android. Для Android написано множество мобильных банк-клиентов. И все их пользователи нуждаются в защите от киберпреступников. Основным языком программирования под Android является Java. Это компилируемый язык, но для него существуют и декомпиляторы. Радует, что большинство из них поставляется под открытыми лицензиями:

  • CFRMIT
  • Fernflower – Apache 2.0
  • JADX – Apache 2.0
  • JEB – Proprietary
  • Krakatau – GPL 3.0
  • Procyon – Apache 2.0
  • Устаревшие: Candle (Apache 2.0), JAD (Proprietary), JD (GPL 3.0)

Как мы видим, FLOSS-сообщество проделало большую работу по разработке инструментов реверс-инжиниринга для Java. Некоторые декомпиляторы довольно качественные и позволяют восстановить Java-код, близкий к оригиналу, если не используется обфускация. Некоторые декомпиляторы имеют GUI, для других есть сторонние GUI. Существуют даже “агрегатор декомпиляторов” Konloch Bytecode Viewer – GUI-программа под лицензией GPL 3.0, позволяющая декомпилировать пятью декомпиляторами сразу и сравнивать результат. Таким образом, противодействие реверс-инжинирингу Android-приложений – не такая уж простая задача.

Также хочется отметить следующие инструменты реверс-инжиниринга:

  • Apktool – Apache 2.0
  • dex2jar – Apache 2.0
  • smali/baksmali – BSD-like

Про Apktool и Smali мы сейчас поговорим подробней.

Обфускация строк в Android на Java

Одна из вещей, которые представляют интерес для взломщиков, особенно на стадии статического анализа – это строки в исходном коде. Они могут содержать много интересного, например:

  • Секретные токены, ключи и даже пароли
  • Сертификаты
  • Хэши для проверки безопасности
  • Ключевые слова протокола для общения с сервером
  • Имена старых менее безопасных протоколов и методов, всё ещё поддерживаемых для совместимости
  • Имена алгоритмов, например методов шифрования
  • “Секретные” URL
  • Номера банковских счетов

Существуют обфускаторы строк, которые заменяют строки в программе на вызовы обфусцированных Java-функций, которые эти строки возвращают. Например, такая функциональность есть в DexGuard. Пример работы DexGuard:


public class Class1
{
    private static final byte[] string_data = new byte[] {
        110, -49, 71, -112, 33, -6, -12, 12, -25, -8, -33,
        47, 17, -4, -82, 82, 4, -74, 33, -35, 18, 7, -25, 31
    };

    private static String string_function(int var0, int var1_1, int var2_2) {
        var2_2 = var2_2 * 4 + 4;
        int var7_3 = var0 * 3 + 83;
        int var6_4 = -1;
        byte[] var3_5 = string_data;
        ...
        do {
            ...
            var4_7[++var1_1] = (byte)var5_8;
            if (var1_1 == var8_6 - 1) {
                return new String(var4_7, 0);
            }
            var2_2 = var5_8;
            var5_8 = var3_5[var0];
            var7_3 = var0;
        } while (true);
    }
}

Но Java есть Java, и такая обфускация легко ломается. Примеры описаний взломов DexGuard:

  • https://www.pnfsoftware.com/blog/a-look-inside-dexguard/
  • https://ajinabraham.com/blog/reversing-dexguard-string-encryption

Обфускация строк: не обязательно Java

А что, если не ограничиваться Java, а вынести строки в native .so-библиотеку, то есть в машинный код? А ведь это возможно, с помощью JNI и Android NDK! Ведь для машинного кода существуют гораздо болеее продвинутые средства обфускации и шифрования. Из инструментов с открытым кодом на эту тему хочется отметить Obfuscator-LLVM. Этот проект основан на LLVM и Clang. Сейчас появилась коммерческая версия этого обфускатора, но OpenSource-версия также доступна, под BSD-like лицензией. Использование обфускаторов усложняет реверс-инжиниринг, но и сам машинный код гораздо труднее поддаётся реверс-инжинирингу. Декомпиляторы из машинного кода в С/C++ существуют, например Snowman (GPL 3.0), Avast RetDec (MIT), IDA Pro (Proprietary), но выдают гораздо менее качественный результат, чем Java-декомпиляторы.

Пример простого С++ кода, который возвращает строку в Java-код:


JNIEXPORT jstring JNICALL
Java_Class1_getSecretString(JNIEnv* env, jobject jthis)
{
    return env->NewStringUTF("supersecret");
}

Но строки можно не только возвращать по запросу, но и “заталкивать” в строковые поля Java-классов!

Пример:


void set_string(JNIEnv* env)
{
    auto clazz = env->FindClass("com/package/name/Class1");
    auto field = env->GetStaticFieldID(clazz, "secret", "[Ljava/lang/String;");
    auto java_string = env->NewStringUTF("supersecret");

    env->SetStaticObjectField(clazz, field, java_string);
}

Получается, можно совсем убрать строки из Java-кода и перенести их в С++ код. А ещё лучше – в шифрованный файл, из которого их будет доставать обфусцированный машинный код.

Но это довольно муторно, писать такие связки Java и C++ вручную. Можно ли это автоматизировать? К счастью, можно.

Smali

У Android есть свой ассемблер, называемый Smali. С помощью Apktool можно дизассемблировать Android byte-code в Smali и транслировать Smali в байт-код.

Пример дизассемблированного класса на Smali:


.class Lcalculator/Grapher;
.super Ljava/lang/Object;
.source "Grapher.java"

# static fields
.field public static final SCREENSHOT_DIR:Ljava/lang/String; = "/screenshots"

# virtual methods
.method public newEditable(Ljava/lang/CharSequence;)Landroid/text/Editable;
    .locals 1
    .param p1, "source"    # Ljava/lang/CharSequence;

    .prologue
    .line 11
    new-instance v0, Lcalculator/CalculatorEditable;

    invoke-direct {v0, p1}, Lcalculator/CalculatorEditable;-><init>(Ljava/lang/CharSequence;)V

    return-object v0
.end method

Автоматизация процесса

Итак, процесс автоматизации нашей продвинутой обфускации будет выглядеть так:

  1. Декодируем APK, получаем код в Smali
  2. Убираем все строки из Smali-кода в шифрованный файл
  3. Вместо убранных строк вызовы в native .so-библиотеку
  4. Собираем APK с изменённым Smali-кодом, шифрованным файлом и native .so-библиотекой для расшифровки
  5. ???
  6. PROFIT!

Как редактировать Smali-код

Возможно, автоматическое редактирование Smali-кода звучит как что-то очень сложное, но на практике это вполне выполнимая вещь.

Легче всего, если для строки уже существует поле в Java-классе:


public class Class1
{
    private final static String secret = "supersecret";
}

Тогда нужно будет сделать всего лишь такое изменение:


--- Class1.smali
+++ Class1.smali.obfu
@@ -5,5 +5,5 @@
 
 # static fields
-.field private static final secret:Ljava/lang/String; = "supersecret"
+.field private static final secret:Ljava/lang/String; = null

Естественно, С++-код, который будет это поле инициализировать, должен быть выполнен до использования этого Java-класса.

А что, если поля нет, например код выглядит вот так?


public class Class2
{
    void method1()
    {
        String secret = "supersecret";
    }
}

Если поля нет – его можно создать!

Diff будет таким:


--- Class2.smali
+++ Class2.smali.obfu
@@ -1,11 +1,15 @@
+# static fields
+.field private static final obfuString:Ljava/lang/String; = null
+
+
 # virtual methods
 .method method1()V
     .locals 1
 
     .prologue
     .line 5
-    const-string v0, "supersecret"
+    sget-object v0, LClass2;->obfuString:Ljava/lang/String;
 
     .line 6
     return-void
 .end method

А если не хочется создавать поле – можно создать native-функцию, реализация которой в native .so-библиотеке вернёт
нужную строку:


--- Class3.smali
+++ Class3.smali.obfu
@@ -1,11 +1,17 @@
+# direct methods
+.method private static native getObfuString()Ljava/lang/String;
+.end method
+
+
 # virtual methods
 .method method1()V
     .locals 1
 
     .prologue
     .line 5
-    const-string v0, "supersecret"
+    invoke-static {}, LClass3;->getObfuString()Ljava/lang/String;
+    move-result-object v0
 
     .line 6
     return-void
 .end method

Противодействие извлечению данных

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

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

  • Проверять неизменноcть программы (и .so-библиотеки, и APK)
  • Детектировать и/или блокировать дебаггер
  • Проверять неизменность кода программы в памяти
  • С++-часть может проверять Java-часть по некоему “парольному объекту”, или “парольной последовательности вызовов”
  • Генерировать ключ расшифровки данных из данных “правильного” APK
  • Не экпортировать функции выдачи данных в таблице символов .so, а регистрировать их с помощью RegisterNatives()
  • Если обнаружена подозрительная активность (дебаггер, изменённый APK) – отказываться загружаться, аварийно завершать
    процесс, направлять атакующего в honey pot, или возвращать фальшивые данные
  • Сообщать о попытке взлома на свой сервер

Abstract licensed under Creative Commons Attribution-ShareAlike 3.0 license

Назад