Защита приложений для android

battarismos

Активный участник
ДРУЗЬЯ ФОРУМА

battarismos

Активный участник
ДРУЗЬЯ ФОРУМА
Регистрация
23 Сен 2020
Сообщения
92
Реакции
5
Репутация
8
Когда вы думаете о защите приложения от обратной инженерии, в первую очередь приходят на ум такие слова, как обфускация и шифрование. Но это только часть решения проблемы. Вторая половина — это обнаружение и защита от самих обратных инструментов: отладчиков, эмуляторов, Frida и т. Д. В этой статье мы рассмотрим методы, используемые мобильным программным обеспечением и вредоносными программами, чтобы скрыться от этих инструментов.

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

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

Root
Права root — один из основных инструментов отмены. Root позволяет запускать Frida без исправления приложения, использовать модули Xposed для изменения поведения приложения и отслеживания приложений, а также изменять параметры системы низкого уровня. В общем, наличие root явно указывает на то, что среде выполнения нельзя доверять. Но как его найти?

Самый простой вариант — поискать исполняемый файл su в одном из системных каталогов:

  • /sbin/su
  • /system/bin/su
  • /system/bin/failsafe/su
  • /system/xbin/su
  • /system/sd/xbin/su
  • /data/local/su
  • /data/local/xbin/su
  • /data/local/bin/su
Бинарник su всегда присутствует на рутованном устройстве, ведь именно с его помощью приложения получают права root. Найти его можно с помощью примитивного кода на Java:

private static boolean findSu() {
String[] paths = { "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su" };
for (String path : paths) {
if (new File(path).exists()) return true;

Либо использовать такую функцию, позаимствованную из приложения

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

:

jboolean Java_com_example_statfile(JNIEnv * env, jobject this, jstring filepath) {
jboolean fileExists = 0;
jboolean isCopy;
const char * path = (*env)->GetStringUTFChars(env, filepath, &isCopy);
struct stat fileattrib;

if (stat(path, &fileattrib) < 0) {
__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat error: [%s]", strerror(errno));
} else
{
__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat success, access perms: [%d]", fileattrib.st_mode);
return 1;
}

return 0;
}

Еще один вариант — попробовать не просто найти, а запустить бинарник su:

try {
Runtime.getRuntime().exec("su");
} catch (IOException e) {
// Телефон не рутован
}

Если его нет, система выдаст IOException. Но здесь нужно быть осторожным: если устройство все‑таки имеет права root, пользователь увидит на экране запрос этих самых прав.

Еще один вариант — найти среди установленных на устройство приложений менеджер прав root. Он как раз и отвечает за диалог предоставления прав:

  • com.thirdparty.superuser
  • eu.chainfire.supersu
  • com.noshufou.android.su
  • com.koushikdutta.superuser
  • com.zachspong.temprootremovejb
  • com.ramdroid.appquarantine
  • com.topjohnwu.magisk
Для поиска можно использовать такой метод:

private static boolean isPackageInstalled(String packagename, Context context) {
PackageManager pm = context.getPackageManager();
try {
pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}

Искать можно и по косвенным признакам. Например, SuperSU, некогда популярное решение для получения прав root, имеет несколько файлов в файловой системе:

  • /system/etc/init.d/99SuperSUDaemon
  • /system/xbin/daemonsu — SuperSU
Еще один косвенный признак — прошивка, подписанная тестовыми ключами. Это не всегда подтверждает наличие root, но точно говорит о том, что на устройстве установлен кастом:

private boolean isTestKeyBuild() {
String buildTags = android.os.Build.TAGS;
return buildTags != null && buildTags.contains("test-keys");
}

Magisk
Все эти методы обнаружения корневого доступа работают нормально, пока вы не встретите устройство с root-правами на Magisk. Это так называемый бессистемный метод рутирования, когда вместо размещения компонентов для корневого доступа в файловой системе поверх нее монтируется другая файловая система (оверлей), содержащая эти компоненты.

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


Процесс скрытия root можно увидеть в логах Magisk
Но в MagiskHide есть недостаток. Дело в том, что если приложение, которое находится в списке для скрытия корневого каталога, запускает службу в изолированном процессе, Magisk также отключит для него оверлей, но этот оверлей останется в списке подключенных файловых систем (/ proc / self / mounts). Следовательно, чтобы обнаружить Magisk, вам необходимо запустить службу в изолированном процессе и проверить список подключенных файловых систем.

Способ был описан в статье

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, а исходный код proof of concept выложен на

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. Способ работает до сих пор на самой последней версии Magisk — 20.4

Эмулятор
Реверсеры часто используют эмулятор для запуска подопытного приложения. Поэтому нелишним будет внести в приложение код, проверяющий, не запущено ли оно в виртуальной среде. Сделать это можно, прочитав значение некоторых системных переменных. Например, стандартный эмулятор Android Studio устанавливает такие переменные и их значения:

ro.hardware=goldfish
ro.kernel.qemu=1
ro.product.model=sdk

Прочитав их значения, можно предположить, что код исполняется в эмуляторе:

public static boolean checkEmulator() {
try {
boolean goldfish = getSystemProperty("ro.hardware").contains("goldfish");
boolean emu = getSystemProperty("ro.kernel.qemu").length() > 0;
boolean sdk = getSystemProperty("ro.product.model").contains("sdk");

if (emu || goldfish || sdk) {
return true;
}
} catch (Exception e) {}

return false;
}

private static String getSystemProperty(String name) throws Exception {
Class sysProp = Class.forName("android.os.SystemProperties");
return (String) sysProp.getMethod("get", new Class[]{String.class}).invoke(sysProp, new Object[]{name});
}

Обрати внимание, что класс android.os.SystemProperties скрытый и недоступен в SDK, поэтому для обращения к нему мы используем рефлексию.

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

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

можно найти множество стандартных значений, а также готовые функции для обнаружения эмулятора.

Отладчик
Один из обратных методов — запустить приложение под управлением отладчика. Злоумышленник может декомпилировать ваше приложение, а затем создать проект с тем же именем в Android Studio, вставить полученные из него источники и начать отладку без компиляции проекта. В этом случае приложение само покажет вам свою логику работы.

Чтобы провернуть такой финт, взломщику придется пересобрать приложение с включенным флагом отладки (android:debuggable=»true»). Поэтому наивный способ защиты состоит в простой проверке этого флага:

public static boolean checkDebuggable(Context context){
return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;

}

Чуть более надежный способ — напрямую спросить систему, подключен ли отладчик:

public static boolean detectDebugger() {
return Debug.isDebuggerConnected();
}

То же самое в нативном коде:

JNIEXPORT jboolean JNICALL Java_com_test_debugging_DebuggerConnectedJNI(JNIenv * env, jobject obj) {
if (gDvm.debuggerConnected || gDvm.debuggerActive) {
return JNI_TRUE;
}
return JNI_FALSE;
}

Приведенные методы помогут обнаружить отладчик на базе протокола JDWP (как раз тот, что встроен в Android Studio). Но другие отладчики работают по‑другому, и методы борьбы с ними будут иными. Отладчик GDB, например, получает контроль над процессом с помощью системного вызова ptrace(). А после использования ptrace флаг TracerPid в синтетическом файле /proc/self/status изменится с нуля на PID отладчика. Прочитав значение флага, мы узнаем, подключен ли к приложению отладчик GDB:

public static boolean hasTracerPid() throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/status")), 1000);
String line;

while ((line = reader.readLine()) != null) {
if (line.length() > tracerpid.length()) {
if (line.substring(0, tracerpid.length()).equalsIgnoreCase(tracerpid)) {
if (Integer.decode(line.substring(tracerpid.length() + 1).trim()) > 0) {
return true;
}
break;
}
}
}

} catch (Exception exception) {
e.printStackTrace()
} finally {
reader.close();
}

return false;
}

Это слегка модифицированная функция из репозитория

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. Ее аналог на языке С будет нетрудно найти на Stack Overflow.

Другой метод работы с отладчиками на основе ptrace — попытаться подключиться к себе (процессу приложения) в качестве отладчика. Для этого вам нужно выполнить форк (из нативного кода), а затем попытаться вызвать системный вызов ptrace:


Есть и другие способы найти встроенный отладчик IDA Pro: найдите в файле / proc / net / tcp строку 00000000: 23946 (это порт отладчика по умолчанию). К сожалению, этот метод больше не работает с Android 9.

В более старых версиях Android также можно было напрямую искать процесс отладчика в системе, где приложение просто просматривает дерево процессов в файловой системе / proc и ищет такие строки, как gdb и gdbserver, в файлах / proc / PID / cmdline. Начиная с Android 7, доступ к файловой системе / proc запрещен (кроме информации о текущем процессе).

Xposed
Xposed — это фреймворк для изменения времени работы приложений. И хотя он в основном используется для установки системных изменений и настроек приложений, существует множество модулей, которые вы можете использовать для отмены и взлома вашего приложения. Это различные модули для отключения закрепления SSL, трассировщики, такие как Inspection, и самописные модули, которые могут каким-либо образом изменять приложение.

Есть три действенных способа обнаружения Xposed:

  • поиск пакета de.robv.android.xposed.installer среди установленных на устройство;
  • поиск libexposed_art.so и xposedbridge.jar в файле /proc/self/maps;
  • поиск класса de.robv.android.xposed.XposedBridge среди загруженных в рантайм пакетов.
В статье

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

приводится реализация третьего метода одновременно для поиска Xposed и Cydia Substrate. Подход интересен тем, что мы не ищем напрямую классы в рантайме, а просто вызываем исключение времени исполнения и затем ищем нужные классы и методы в стектрейсе:

try {
throw new Exception("blah");
}
catch(Exception e) {
int zygoteInitCallCount = 0;

for(StackTraceElement stackTraceElement : e.getStackTrace()) {
if(stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) {
zygoteInitCallCount++;
if(zygoteInitCallCount == 2) {
Log.wtf("HookDetection", "Substrate is active on the device.");
}
}

if (stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") && stackTraceElement.getMethodName().equals("invoked")) {
Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate.");
}

if (stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && stackTraceElement.getMethodName().equals("main")) {
Log.wtf("HookDetection", "Xposed is active on the device.");
}

if (stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && stackTraceElement.getMethodName().equals("handleHookedMethod")) {
Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed.");
}
}
}

Frida
Ее Величество Frida! Этот удивительный инструмент позволяет вам перехватить любой вызов функции в вашем тестовом приложении, прочитать все его аргументы и заменить тело вашей собственной реализацией JavaScript. Frida не только занимает почетное место в наборе инструментов любого инвертора, но также служит основой для многих других утилит более высокого уровня.

Обнаружить Frida можно множеством разных способов. В статье

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

приводится три (на самом деле четыре, но первый уже неактуален) способа это сделать.

  1. Поиск библиотек frida-agent и frida-gadget в файле /proc/self/maps:
char line[512];
FILE* fp;

fp = fopen("/proc/self/maps", "r");

if (fp) {
while (fgets(line, 512, fp)) {
if (strstr(line, "frida")) {
/* Frida найдена */
}
}

fclose(fp);
}

Может закончиться неудачей, если взломщик изменит имена библиотек.

2. Поиск в памяти нативных библиотек строки «LIBFRIDA»:

static char keyword[] = "LIBFRIDA";
num_found = 0;

int scan_executable_segments(char * map) {
char buf[512];
unsigned long start, end;

sscanf(map, "%lx-%lx %s", &start, &end, buf);

if (buf[2] == 'x') {
return (find_mem_string(start, end, (char*)keyword, 8) == 1);
} else {
return 0;
}
}

void scan() {
if ((fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY, 0)) >= 0) {

while ((read_one_line(fd, map, MAX_LINE)) > 0) {
if (scan_executable_segments(map) == 1) {
num_found++;
}
}

if (num_found > 1) {
/* Frida найдена */
}
}

Взломщик может перекомпилировать Frida с измененными строками.

3. Проход по всем открытым TCP-портам, отправка в них dbus-сообщения AUTH и ожидание ответа Frida:

for(i = 0 ; i <= 65535 ; i++) {
sock = socket(AF_INET , SOCK_STREAM , 0);
sa.sin_port = htons(i);

if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "FRIDA DETECTION [1]: Open Port: %d", i);

memset(res, 0 , 7);

send(sock, "\x00", 1, NULL);
send(sock, "AUTH\r\n", 6, NULL);

usleep(100);

if (ret = recv(sock, res, 6, MSG_DONTWAIT) != -1) {
if (strcmp(res, "REJECT") == 0) {
/* Frida найдена */
}
}
}
close(sock);
}

Метод хорошо работает при использовании frida-server (на рутованном устройстве), но бесполезен, если приложение было перепаковано с включением в него frida-gadget (этот способ обычно применяют, когда невозможно получить root на устройстве).

В статье

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

автор приводит еще три способа:

  1. Поиск потоков frida-server и frida-gadget, которые Frida запускает в рамках процесса подопытного приложения.
  2. Поиск специфичных для Frida именованных пайпов в каталоге /proc/<pid>/fd.
  3. Сравнение кода нативных библиотек на диске и в памяти. При внедрении Frida изменяет секцию text нативных библиотек.
Примеры использования последних трех техник опубликованы в

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.

Клонирование
Некоторые производители включают в свои прошивки функцию клонирования приложений (параллельные приложения на OnePlus, двойные приложения на Xiaomi и т. Д.), Которая позволяет вам установить копию выбранного приложения на ваш смартфон. Прошивка создает дополнительного пользователя Android с ID 999 и устанавливает копию приложений от вашего имени.

Некоторые приложения на рынке предлагают такую же функциональность (Dual Space, Clone App, Multi Parallel). Они работают по-другому: создают изолированную среду для приложения и устанавливают его в свой личный каталог.

С помощью второго метода твое приложение могут запустить в изолированной среде для изучения. Чтобы воспрепятствовать этому, достаточно проанализировать путь к приватному каталогу приложения. К примеру, приложение с именем пакета com.example.app при нормальной установке будет иметь приватный каталог по следующему пути:

/data/user/0/com.example.app/files

При создании клона с помощью одного из приложений из маркета путь будет уже таким:

/data/data/com.ludashi.dualspace/virtual/data/user/0/com.example.app/files

А при создании клона с помощью встроенных в прошивку инструментов — таким:

/data/user/999/com.example.app/files

Соберем все вместе и получим такой метод для детекта изолированной среды:

private const val DUAL_APP_ID_999 = "999"

fun checkAppCloning(context: Context): Boolean {
val path: String = context.filesDir.path
val packageName = context.packageName

val pathDotCount = path.split(".").size-1
val packageDotCount = packageName.split(".").size-1

if (path.contains(DUAL_APP_ID_999) || pathDotCount > packageDotCount) {
return false
}

return true
}

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