Когда вы думаете о защите приложения от обратной инженерии, в первую очередь приходят на ум такие слова, как обфускация и шифрование. Но это только часть решения проблемы. Вторая половина — это обнаружение и защита от самих обратных инструментов: отладчиков, эмуляторов, Frida и т. Д. В этой статье мы рассмотрим методы, используемые мобильным программным обеспечением и вредоносными программами, чтобы скрыться от этих инструментов.
Не воспринимайте информацию, изложенную в статье, как рецепт абсолютной защиты. Такого рецепта нет. Мы просто даем себе передышку, замедляем исследования, но не делаем это невозможным. Все это бесконечная игра в кошки-мышки, в которой исследователь ломает очередную защиту, а разработчик придумывает ей более изощренную замену.
Важный момент: я дам вам несколько различных методов защиты, и у вас может возникнуть соблазн сгруппировать их в класс (или собственную библиотеку) и, что удобно для вас, запустить их один раз в начале приложения. Не стоит этого делать, механизмы защиты должны быть распределены по всему приложению и запускаться в разное время. Это значительно усложняет жизнь злоумышленнику, который может определить назначение класса / библиотеки и полностью заменить его большим контуром.
Права root — один из основных инструментов отмены. Root позволяет запускать Frida без исправления приложения, использовать модули Xposed для изменения поведения приложения и отслеживания приложений, а также изменять параметры системы низкого уровня. В общем, наличие root явно указывает на то, что среде выполнения нельзя доверять. Но как его найти?
Самый простой вариант — поискать исполняемый файл 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) {// Телефон не рутован}Еще один вариант — найти среди установленных на устройство приложений менеджер прав 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, имеет несколько файлов в файловой системе:
Еще один косвенный признак — прошивка, подписанная тестовыми ключами. Это не всегда подтверждает наличие root, но точно говорит о том, что на устройстве установлен кастом:
private boolean isTestKeyBuild() {
String buildTags = android.os.Build.TAGS;return buildTags != null && buildTags.contains("test-keys");}Все эти методы обнаружения корневого доступа работают нормально, пока вы не встретите устройство с 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=1ro.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, поэтому для обращения к нему мы используем рефлексию.Один из обратных методов — запустить приложение под управлением отладчика. Злоумышленник может декомпилировать ваше приложение, а затем создать проект с тем же именем в 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 в синтетическом файле / изменится с нуля на 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;}Xposed — это фреймворк для изменения времени работы приложений. И хотя он в основном используется для установки системных изменений и настроек приложений, существует множество модулей, которые вы можете использовать для отмены и взлома вашего приложения. Это различные модули для отключения закрепления SSL, трассировщики, такие как Inspection, и самописные модули, которые могут каким-либо образом изменять приложение.
Есть три действенных способа обнаружения Xposed:
libexposed_art.so и xposedbridge.jar в файле /proc/self/maps ;de.robv.android.xposed.XposedBridge среди загруженных в рантайм пакетов.В статье Android Anti-Hooking Techniques in Java приводится реализация третьего метода одновременно для поиска 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.");}}}1. Поиск библиотек frida-agent и frida-gadget в файле /:
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 найдена */}}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);}В статье Detect Frida for Android автор приводит еще три способа:
/proc/<pid>/fd .Примеры использования последних трех техник опубликованы в репозитории на GitHub.
Некоторые производители включают в свои прошивки функцию клонирования приложений (параллельные приложения на 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.pathval packageName = context.packageNameval pathDotCount = path.split(".").size-1val packageDotCount = packageName.split(".").size-1if (path.contains(DUAL_APP_ID_999) || pathDotCount > packageDotCount) {return false}return true}Конечно, это далеко не все способы обратной инженерии и анализа поведения приложений. Есть много менее эффективных или узкоспециализированных методов. Поэтому ниже я приведу список литературы и проектов на GitHub, который вам обязательно стоит прочитать. Там вы найдете более подробное объяснение некоторых методов защиты, описанных в этой статье, а также многих других методов.
Чтобы взломать сеть Wi-Fi с помощью Kali Linux, вам нужна беспроводная карта, поддерживающая режим мониторинга…
Работа с консолью считается более эффективной, чем работа с графическим интерфейсом по нескольким причинам.Во-первых, ввод…
Конечно, вы также можете приобрести подписку на соответствующую услугу, но наличие SSH-доступа к компьютеру с…
С тех пор как ChatGPT вышел на арену, возросла потребность в поддержке чата на базе…
Если вы когда-нибудь окажетесь в ситуации, когда вам нужно взглянуть на спектр беспроводной связи, будь…
Elastic Security стремится превзойти противников в инновациях и обеспечить защиту от новейших технологий злоумышленников. В…