Процесс скрытия root можно увидеть в логах Magisk

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

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

                                 WARNING

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

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

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

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

private boolean isTestKeyBuild() {

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

Magisk

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

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

Процесс скрытия root можно увидеть в логах Magisk

                    Про­цесс скры­тия 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 сре­ди заг­ружен­ных в ран­тайм пакетов.

В статье 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.");
}
}
}
 

Frida

 
Ее Величество Frida! Этот удивительный инструмент позволяет вам перехватить любой вызов функции в вашем тестовом приложении, прочитать все его аргументы и заменить тело вашей собственной реализацией JavaScript. Frida не только занимает почетное место в наборе инструментов любого инвертора, но также служит основой для многих других утилит более высокого уровня.
 
Об­наружить Frida мож­но мно­жес­твом раз­ных спо­собов. В статье The Jiu-Jitsu of Detecting 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 на устрой­стве).

В статье Detect Frida for Android автор при­водит еще три спо­соба:

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

При­меры исполь­зования пос­ледних трех тех­ник опуб­ликова­ны в ре­пози­тории на 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.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
}

Выводы

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

                                                   www

  • Android Anti-Reversing Defenses — гла­ва из сво­бод­ной кни­ги Mobile Security Testing Guide о защите от ревер­са;
  • Android Anti-Hooking Techniques in Java — статья о спо­собах обна­ружить Xposed и Cydia Substrate;
  • The Jiu-Jitsu of Detecting Frida — опи­сание спо­собов обна­ружить Frida;
  • Detect Frida for Android — еще нес­коль­ко спо­собов обна­ружить Frida;
  • SafetyNet: Google’s tamper detection for Android — статья о прин­ципе работы инс­тру­мен­та SafetyNet, исполь­зующе­го мно­гие из опи­сан­ных тех­ник опре­делить ком­про­мета­цию устрой­ства;
  • RootBeer — готовая биб­лиоте­ка, помога­ющая обна­ружить пра­ва root;
  • anti-emulator — репози­торий с нес­коль­кими тех­никами детек­та эму­лято­ра и дебаг­гера;
  • DetectMagiskHide — готовое при­ложе­ние для детек­та Magisk.
 
 

 

               

 

 

Click to rate this post!
[Total: 1 Average: 5]

Leave a reply:

Your email address will not be published.