Download_Link
Участник клуба
Когда вы думаете о защите приложения от обратной инженерии, в первую очередь приходят на ум такие слова, как обфускация и шифрование. Но это только часть решения проблемы. Вторая половина — это обнаружение и защита от самих обратных инструментов: отладчиков, эмуляторов, 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;
Либо использовать такую функцию, позаимствованную из приложения rootinspector:
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. Он как раз и отвечает за диалог предоставления прав:
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, имеет несколько файлов в файловой системе:
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, вам необходимо запустить службу в изолированном процессе и проверить список подключенных файловых систем.
Способ был описан в статье Detecting Magisk Hide, а исходный код proof of concept выложен на GitHub. Способ работает до сих пор на самой последней версии 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;
}
Это слегка модифицированная функция из репозитория anti-emulator. Ее аналог на языке С будет нетрудно найти на 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:
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.");
}
}
Выводы
Конечно, это далеко не все способы обратной инженерии и анализа поведения приложений. Есть много менее эффективных или узкоспециализированных методов. Поэтому ниже я приведу список литературы и проектов на GitHub, который вам обязательно стоит прочитать. Там вы найдете более подробное объяснение некоторых методов защиты, описанных в этой статье, а также многих других методов.
Не воспринимайте информацию, изложенную в статье, как рецепт абсолютной защиты. Такого рецепта нет. Мы просто даем себе передышку, замедляем исследования, но не делаем это невозможным. Все это бесконечная игра в кошки-мышки, в которой исследователь ломает очередную защиту, а разработчик придумывает ей более изощренную замену.
Важный момент: я дам вам несколько различных методов защиты, и у вас может возникнуть соблазн сгруппировать их в класс (или собственную библиотеку) и, что удобно для вас, запустить их один раз в начале приложения. Не стоит этого делать, механизмы защиты должны быть распределены по всему приложению и запускаться в разное время. Это значительно усложняет жизнь злоумышленнику, который может определить назначение класса / библиотеки и полностью заменить его большим контуром.
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;
Либо использовать такую функцию, позаимствованную из приложения rootinspector:
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
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, вам необходимо запустить службу в изолированном процессе и проверить список подключенных файловых систем.
Способ был описан в статье Detecting Magisk Hide, а исходный код proof of concept выложен на GitHub. Способ работает до сих пор на самой последней версии 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;
}
Это слегка модифицированная функция из репозитория anti-emulator. Ее аналог на языке С будет нетрудно найти на 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 среди загруженных в рантайм пакетов.
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.");
}
}
}
Конечно, это далеко не все способы обратной инженерии и анализа поведения приложений. Есть много менее эффективных или узкоспециализированных методов. Поэтому ниже я приведу список литературы и проектов на GitHub, который вам обязательно стоит прочитать. Там вы найдете более подробное объяснение некоторых методов защиты, описанных в этой статье, а также многих других методов.