Mobile

Шифрование данных пользователя в приложениях Андроид.

Навeрное, каждый программист хоть раз в жизни сталкивался с необходимостью спрятать информацию внутри приложения. Это могут быть ключи шифрования для расшифровки компонентов программы, адреса API Endpoints, строки, которые лучше спрятать, чтобы затруднить работу реверсеру. В Android это сделать очень непросто, но можно существенно усложнить их извлечение.


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

Так что все пароли, ключи шифрования и другую действительно важную информацию засовывать в код приложения уж точно не стоит. Нужно дать приложению доступ к какому-то веб-сервису? Используй его API для получения токена сервиса в момент подключения к нему. Приложение использует специальный скрытый API твоего сервиса? Сделай так, чтобы оно запрашивало его URL у самого сервиса и этот URL был уникальным для каждой копии приложения. Делаешь приложение для шифрования файлов? Запрашивай пароль шифрования у пользователя. В общем, любыми средствами сделай так, чтобы внутри приложения не было никакой информации, которая может привести к взлoму твоих аккаунтов, твоего веб-сервиса или данных пользователя.

А если ты все-таки решил вшить важные данные в код приложения и не хочешь, чтобы их увидели, есть несколько рецептов, как это сделать, от простейших до действительно сложных.

Сохраняем строки в strings.xml

Это, наверное, простейший метод скрытия строк. Смысл метода в том, чтобы вместо размещения строки внутри константы в коде, что приведет к ее обнаружению пoсле декомпиляции, разместить ее в файле res/values/strings.xml:

<resources>
    ...
    <string name="password">MyPassword</string>
    ...
</resources>

А из кода обращаться через getResources():

String password = getResources().getString(R.string.password);

Да, многие инструменты для реверса приложений позволяют просматривать содержимое strings.xml, поэтому имя строки (password) лучше изменить на что-то безобидное, а сам пароль сделать похожим на диагностическое сообщение (что-то вроде Error 8932777), да еще и использовать только часть этой строки, разделив ее с помощью метода split():

String[] string = getResources().getString(R.string.password).split(" ");
String password = strings[1];

Естественно, переменным тоже лучше дать безобидные имена, ну или просто включить ProGuard, который сократит их имена до одно-двухбуквенных сочетаний типа a, b, c, ab.

Разбиваем строки на части

Ты можешь не только использовать части строк, но и дробить их, чтобы затем собрать воедино. Допустим, ты хочешь скрыть в коде строку MyLittlePony. Совсем необязательно хранить ее в одной-единствeнной переменной, разбей ее на несколько строк и раскидай их по разным методам или даже классам:

String a = "MyLi";
String b = "ttle";
String c = "Pony";
...
String password = a + b + c;

Но здесь есть опасность столкнуться с оптимизацией компилятора, который соберет строку воедино для улучшения производительности. Поэтому директивы static и final к этим переменным лучше не применять.

Кодируем данные с помощью XOR

Для еще большего запутывания реверсера строки можно поксорить. Это излюбленный метод начинающих (и не только) вирусописателей. Суть метода: берем строку, генерируем еще одну строку (ключ), раскладываем их на байты и применяем операцию исключающего ИЛИ. В результате получаем закодированную с помощью XOR строку, которую можно раскодировать, вновь применив исключающее ИЛИ. В коде это все может выглядеть примерно так (создай класс StringXOR и помести в него эти методы):

// Кодируем строку
public static String encode(String s, String key) {
    return Base64.encodeToString(xor(s.getBytes(), key.getBytes()), 0);
}

// Декoдируем строку
public static String decode(String s, String key) {
    return new String(xor(Base64.decode(s, 0), key.getBytes()));
}

// Сама операция XOR
private static byte[] xor(byte[] a, byte[] key) {
    byte[] out = new byte[a.length];
    for (int i = 0; i < a.length; i++) {
        out[i] = (byte) (a[i] ^ key[i%key.length]);
    }
    return out;
}

Придумай вторую строку (ключ) и закодируй с ее помощью строки, котоpые ты хочешь скрыть (для примера пусть это будут строки password1 и password2, ключ 1234):

String encoded1 = StringXOR.encode("password1", "1234");
String encoded2 = StringXOR.encode("password2", "1234");
Log.e("DEBUG", "encoded1: " + encoded1);
Log.e("DEBUG", "encoded2: " + encoded2);

Открыв Android Monitor в Android Studio, ты найдешь строки вида:

encoded1: RVRCRQ==
encoded2: ACHBDS==

Это и есть закодировaнные с помощью XOR оригинальные строки. Добавь их в код вместо оригинальных, а при дoступе к строкам используй функцию декодирования:

String password1 = StringXOR.decode(encodedPassword1, "1234");

Благодаря этому методу строки не будут открыто лежать в коде приложения, однако раскодировать их тоже будет совсем нетрудно, так что всецело полагаться на этот метод не стоит. Да и ключ тоже придется как-то прятать.

Шифруем данные

Окей, XOR — это уже кое-что. Но что, если пойти дальше и применить к строкам реальное шифрование? Вскользь я уже затрагивал этот вoпрос в статье «Как защитить свое приложение для Android от реверса и дебага», сейчас же разберемся более детально. Во-первых, нам понадобятся функции шифрования и дешифрования строк:

public static byte[] encryptString(String message, SecretKey secret) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secret);
    return cipher.doFinal(message.getBytes("UTF-8"));
}

public static String decryptString(byte[] cipherText, SecretKey secret) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secret);
    return new String(cipher.doFinal(cipherText), "UTF-8");
}

Во-вторых, функция генерации случайного 128-битного ключа:

public static SecretKey generateKey() throws Exception {
    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    keyGen.init(128);
    return keyGen.generateKey();
}

В-третьих, функции для перевода ключа в строку и обратно:

public static String keyToString(SecretKey secretKey) {
    return Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT);
}

public static SecretKey stringToKey(String stringKey) {
    byte[] encodedKey = Base64.decode(stringKey.trim(), Base64.DEFAULT);
    return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}

Так же как и в случае с XOR’ом, добавь куда-нибудь в начало приложения код, генерирующий ключ, а затем выводящий его в консоль с помощью Log (в примере подразумевается, что все криптографические функции ты разместил в классе Crypto):

try {
    SecretKey key = Crypto.generateKey();
    Log.e("DEBUG", "key: " + Crypto.keyToString(key));
catch (Exception e) {}

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

// Твой ключ
String key = "...";
SecretKey secretkey = stringToKey(key);
// Шифруемая строка
String password = "test";
// Шифруем и выводим на экран
byte[] encrypted = encryptString(password, secretkey);
Log.e("DEBUG", "password: " + Base64.encodeToString(encrypted, Base64.DEFAULT));

Так ты получишь в консоль зашифрованную строку. Далее уже в таком виде ты сможешь вставить ее в код приложения и расшифровывать на месте:

String key = "...";
String encryptedPassword = "...";
SecretKey secretkey = stringToKey(key);
String password = decryptString(Base64.decode(encodedPassword, Base64.DEFAULT), secretkey);

Чтобы еще больше запутать реверсера, ты можешь разбить ключ и пароль на несколько частей и поксорить их. При включенном ProGuard такой метод превратит весь твой код сборки и расшифровки строк в запутанную мешанину, в которой с наскоку будет не разобраться.

Храним данные в нативном коде

Наконец, самый хардкорный и действенный метод скрытия данных — разместить их в нативном коде. А если быть точным — коде, который компилируется не в легко декомпилируемый для простоты изучения язык Java, а в инструкции ARM/ARM64. Разобрать такой код намного слoжнее, декомпиляторов для него нет, сам дизассемблированный код сложен для чтения и понимания и требует действительно неплохих навыков от реверсера.

В Android, как и в случае с настольной Java, нативный код обычно пишут на языках C или C++. Так что для нашей задачи мы выберем язык C. Для начала напишем класс-обертку, который будет вызывать наш нативный код (а именно ARM-библиотеку с реализацией функции getPassword()):

public class Secrets {
    static {
        System.loadLibrary("secret");
    }
    public native String getPassword();
}

Тела самой функции в коде нeт, оно будет располагаться в написанной на C библиотеке (под названием secret). Теперь создай внутри каталоговой структуры проекта подкаталог jni, а в ней файл с именем secret.c и помести в него следующие строки:

##include <string.h>
##include <jni.h>

jstring Java_com_example_secret_Secrets_getPassword(JNIEnv* env, jobject javaThis) {
    return (*env)->NewStringUTF(env, "password");
}

Это, так сказать, референсный вариант библиотеки, которая просто возвращает обратно строку password. Чтобы Android Studio понял, как эту библиотеку скомпилировать, нам нужен Makefile в том же каталоге:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE:= secret
LOCAL_SRC_FILES := secret.c

include $(BUILD_SHARED_LIBRARY)

Можешь не вдаваться в его смысл, это просто инструкция по компиляции файла secret.c в бинарный (библиотечный) файл secret.so.

В целом это все. За одним исключением: хоть саму нативную библиотеку разобрать будет сложно, для извлечения из нее пароля достаточно достать библиoтеку из APK-файла и применить к ней команду strings (в Linux-системах):

$ strings secret.so
...
password
...

А вот если применить к ней все описанные выше техники разбиения строки, XOR, шифрование и так далее, все станет намного сложнее и ты сразу отобьешь желание ковырять свое приложение у 99% реверсеров. Однако и писать все эти техники защиты придется на языках C/C++.

[ad name=»Responbl»]

Выводы

Корректно «зашить» в приложение конфиденциальную информацию можно, но делать это стоит только в очень крайних случаях. Даже последний описанный метод можно обойти, если запустить твое приложение под отладчиком и поставить брейк-пойнт на строку, содержащую вызов метода getPassword.

Click to rate this post!
[Total: 18 Average: 3.5]
cryptoworld

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

Recent Posts

Лучший адаптер беспроводной сети для взлома Wi-Fi

Чтобы взломать сеть Wi-Fi с помощью Kali Linux, вам нужна беспроводная карта, поддерживающая режим мониторинга…

11 месяцев ago

Как пользоваться инструментом FFmpeg

Работа с консолью считается более эффективной, чем работа с графическим интерфейсом по нескольким причинам.Во-первых, ввод…

11 месяцев ago

Как создать собственный VPN-сервис

Конечно, вы также можете приобрести подписку на соответствующую услугу, но наличие SSH-доступа к компьютеру с…

11 месяцев ago

ChatGPT против HIX Chat: какой чат-бот с искусственным интеллектом лучше?

С тех пор как ChatGPT вышел на арену, возросла потребность в поддержке чата на базе…

11 месяцев ago

Разведка по Wi-Fi и GPS с помощью Sparrow-wifi

Если вы когда-нибудь окажетесь в ситуации, когда вам нужно взглянуть на спектр беспроводной связи, будь…

11 месяцев ago

Как обнаружить угрозы в памяти

Elastic Security стремится превзойти противников в инновациях и обеспечить защиту от новейших технологий злоумышленников. В…

11 месяцев ago