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

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
 

Обфускаторы

На самом деле ты уже должен быть знаком как минимум с одним методом обфускации (запутывания) кода. В прошлой статье мы внедряли зловредную функциональность в WhatsApp, и если ты внимательно читал статью и сам пробовал декомпилировать WhatsApp, то наверняка заметил, что большинство классов приложения, почти все его методы и переменные имеют странные имена: aa, ab либо что-то вроде 2F323988C, если смотреть код с помощью декомпилятора jadx.

Это и есть обфускация, и я могу с полной уверенностью утверждать, что проделана она с помощью инструмента ProGuard из комплекта Android Studio. Именно он выдает на выходе такие странные имена классов, методов и переменных, а кроме того, удаляет неиспользуемый код и оптимизирует некоторые участки приложения с помощью инлайнинга методов.

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

public int a(int a, int b) {
    return a + b;
}

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

if (this.f6259xa29f3207 != null) {
    this.f6259xa29f3207.m9502x15f0e18c(false);
    this.f6259xa29f3207 = null;
}
if (this.f6269x98c88933 != null) {
    this.f6269x98c88933.m9388x97b0a138();
    this.f6269x98c88933 = null;
}

И так на тысячи строк вперед, а дальше твой декомпилятор может поперхнуться кодом и выдать вместо Java нечто вроде этого:

/* JADX WARNING: inconsistent code. */
/* Code decompiled incorrectly, please refer to instructions dump. */
private synchronized void m8713x6e6c3e67() {
    /*
    r16 = this;
    r14 = 0;
    r7 = 2;
    r9 = 0;
    r12 = 500; // 0x1f4 float:7.0E-43 double:2.47E-321;
    r6 = 1;
    ...

Недурно, не правда ли? А теперь представь, что эти строки состоят не из обычных символов алфавита, а из символов Unicode (так делает DexGuard, коммерческая версия ProGuard) или наборов вроде l1ll1, повторяющихся раз этак пятьдесят. Разработчик вполне может применить и более мощные средства обфускации, нашпиговав приложение бессмысленным кодом. Такой код не будет выполнять никаких полезных функций, но направит тебя совершенно не в ту сторону, что грозит как минимум потерей времени.

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

private static final byte[] zb = 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};

Это зашифрованная строка, которая расшифровывается во время исполнения приложения. Такую защиту предлагают DexGuard, Allatory и многие другие обфускаторы. Она действительно способна остановить очень многих, но соль в том, что если есть зашифрованный текст, значит, в коде должен быть и дешифратор. Его очень легко найти с помощью поиска по имени переменной (в данном случае zb). При каждом ее использовании всегда будет вызываться метод, дешифрующий строку. Выглядеть это может примерно так:

a.a(zb)

Здесь метод a() класса a и есть дешифратор. Поэтому, чтобы узнать, что внутри зашифрованной строки, нужно просто добавить в дизассемблированный код приложения вызов функции Log.d("DEBUG", a.a(zb)) и собрать его обратно (как это сделать, описано в первой статье цикла). После запуска приложение само выдаст в лог дешифрованную строку. Лог можно просмотреть либо подключив смартфон к компу и вызвав команду adb logcat, либо с помощью приложения CatLog для Android (требует root).

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

DexGuard имеет функцию скрытия классов, которая работает следующим образом. Байт-код скрываемого класса извлекается из приложения, сжимается с помощью алгоритма Gzip и записывается обратно в приложение в форме массива байтов (byte[]). Далее в приложение внедряется загрузчик, который извлекает код класса из массива и с помощью рефлексии создает на его основе объект, а затем вызывает нужные методы. И конечно же, DexGuard использует этот трюк для скрытия дешифратора, а также кода других классов по желанию разработчика. Более того, скрытые в массивах классы могут быть зашифрованы с помощью скрытого в другом массиве дешифратора!

Так что, если ты имеешь дело с приложением, имена классов в котором написаны на китайском или языке смайликов, а по коду разбросаны странные массивы длиной от нескольких сот элементов до десятков тысяч, знай — здесь поработал DexGuard.

С рефлексией вместо прямого вызова методов объекта ты можешь столкнуться и в других обстоятельствах, не связанных со скрытием классов. Рефлексия может быть использована просто для обфускации (как в случае с обфускатором Allatory). Тогда вместо такого кода:

StatusBarManager a = (StatusBarManager) context.getSystemService("statusbar");
statusBarManager.expandNotificationsPanel();

ты увидишь нечто вроде этого:

Object a  = context.getSystemService("statusbar");
Class.forName("android.app.StatusBarManager");
Method c = b.getMethod("expandNotificationsPanel");
c.invoke(a);

А если используется шифрование — это:

Object a  = context.getSystemService(z.z("c3RhdHVzYmFyCg=="));
Class<?> b = Class.forName(z.z("YW5kcm9pZC5hcHAuU3RhdHVzQmFyTWFuYWdlcgo="));
Method c = b.getMethod(z.z(ZXhwYW5kTm90aWZpY2F0aW9uc1BhbmVsCg==));
c.invoke(a);

В данном случае я закодировал строки в Base64, поэтому их легко «раскодировать» с помощью команды

$ echo строка | base64 -d

Но в реальном приложении тебе, скорее всего, придется расшифровать все эти строки с помощью описанного выше способа просто для того, чтобы понять, какие объекты и методы вызывает приложение в своей работе.

 

Упаковщики

А еще есть упаковщики. Это другой вид защиты, основанный не на запутывании кода, а на его полном скрытии от глаз реверсера. Работает он так. Оригинальный файл classes.dex (содержащий код приложения) переименовывается, шифруется и перемещается в другой каталог внутри пакета APK (это может быть каталог assets, res или любой другой). Место оригинального classes.dex занимает распаковщик, задача которого — загрузить в память оригинальный classes.dex, расшифровать его и передать ему управление. Для усложнения жизни реверсера основная логика распаковщика реализуется на языке си, который компилируется в нативный код ARM с применением средств обфускации и защиты от отладки (gdb, ptrace).

Хороший упаковщик создает очень большие проблемы для анализа кода приложения. В ряде случаев единственный действенный вариант борьбы с ними — это снятие дампа памяти процесса и извлечение из него уже расшифрованного кода classes.dex. Но есть и хорошие новости: упаковщик накладывает серьезные ограничения на функциональность приложения, приводит к несовместимостям и увеличенному расходу памяти. Так что разработчики обычных приложений используют упаковщики редко, зато их очень любят создатели разного рода троянов и вирусов.

Вычислить наличие упаковщика в APK совсем нетрудно. Для этого достаточно взглянуть на содержимое каталога lib/armeabi. Если ты найдешь в нем файл libapkprotect2.so, значит, применен упаковщик ApkProtect, файл libsecexe.so — Bangle, libexecmain.so — ljiami.

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для статей, опубликованных более двух месяцев назад.


12 комментария

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Кеш-атаки по сторонним каналам. Что произошло в области утечек на аппаратном уровне за последние два года

Несмотря на то что до 2016 года существовало лишь несколько публикаций о кеш-атаках на сма…