Продвинутая обфускация констант в Android-приложениях
LVEE 2018
Что такое обфускация и зачем она нужна
Обфускация – преобразование информации, например кода или данных, таким образом, чтобы эту информацию было трудно понять и ей воспользоваться. По сути, это “прятание информации на виду”.
Открытый код и отрытые данные программ – это обычно хорошо. Но не всегда. У компьютерных преступников есть такая забава: взять какую-нибудь важную программу, например банк-клиент, изменить её немного, чтоб она слала пароли на сервер злоумышленника, или сразу деньги на его счёт, и обманом заставить технически неграмотных пользователей эту программу установить. Или с помощью трояна пропатчить установленную программу прямо на компьютере пользователя.
Для того, чтобы провернуть такое дело, злоумышленнику надо понять, как атакуемая программа работает, то есть провести реверс-инжиниринг. И вот тут ему очень помогут и открытый код, и открытые данные. Если они есть. Значит надо код и данные от злоумышленника скрыть, а если нельзя скрыть – то обфусцировать. К сожалению, обфускация не может полностью блокировать реверс-инжиниринг. Но она может этот процесс более сложным, долгим и дорогим. В результате, выгода злоумышленника в случае успешной атаки снижается или вообще сходит на нет. Чем сложнее – тем меньше людей вообще смогут провести атаку. И конечно, многие киберпреступники просто переключатся на другую цель, полегче.
Обфусцировать в программе можно разное. Эта статья упоминает об обфускации кода программы и особенное внимание уделяет обфускации строк. Почему это важно – будет написано ниже.
Реверс-инжиниринг и Android
В последнее время большую популярность приобрела платформа Android. Для Android написано множество мобильных банк-клиентов. И все их пользователи нуждаются в защите от киберпреступников. Основным языком программирования под Android является Java. Это компилируемый язык, но для него существуют и декомпиляторы. Радует, что большинство из них поставляется под открытыми лицензиями:
- CFR – MIT
- 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
Автоматизация процесса
Итак, процесс автоматизации нашей продвинутой обфускации будет выглядеть так:
- Декодируем APK, получаем код в Smali
- Убираем все строки из Smali-кода в шифрованный файл
- Вместо убранных строк вызовы в native .so-библиотеку
- Собираем APK с изменённым Smali-кодом, шифрованным файлом и native .so-библиотекой для расшифровки
- ???
- 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
Назад