Хакер - За семью замками. Защищаем приложение для Android от отладчиков, эмуляторов и Frida

Хакер - За семью замками. Защищаем приложение для Android от отладчиков, эмуляторов и Frida

hacker_frei

https://t.me/hacker_frei

Евгений Зобнин 

Содержание статьи

  • Root
  • Magisk
  • Эмулятор
  • Отладчик
  • Xposed
  • Frida
  • Клонирование
  • Выводы

Ког­да задумы­ваешь­ся о защите при­ложе­ния от ревер­са, в пер­вую оче­редь на ум при­ходят такие сло­ва, как обфуска­ция и шиф­рование. Но это толь­ко часть решения проб­лемы. Вто­рая полови­на — это детект и защита от самих инс­тру­мен­тов ревер­са: отладчи­ков, эму­лято­ров, 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;

}

return false;

}

Ли­бо исполь­зовать такую фун­кцию, поза­имс­тво­ван­ную из при­ложе­ния 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. Это так называ­емый systemless-метод рутин­га, ког­да вмес­то раз­мещения ком­понен­тов для root-дос­тупа в фай­ловой сис­теме поверх нее под­клю­чают дру­гую фай­ловую сис­тему (овер­лей), содер­жащую эти ком­понен­ты.

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

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

Но есть в MagiskHide один изъ­ян. Дело в том, что, если при­ложе­ние, которое находит­ся в спис­ке для скры­тия root, запус­тит сер­вис в изо­лиро­ван­ном про­цес­се, 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, поэто­му для обра­щения к нему мы исполь­зуем реф­лексию.

В дру­гих эму­лято­рах зна­чения сис­темных перемен­ных могут быть дру­гими. На этой стра­нице есть таб­лица со зна­чени­ями сис­темных перемен­ных, которые могут пря­мо или кос­венно ука­зывать на эму­лятор. Там же при­веде­на таб­лица зна­чений сте­ка телефо­нии. Нап­ример, серий­ный номер SIM кар­ты 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:

void fork_and_attach()

{

int pid = fork();

if (pid == 0)

{

int ppid = getppid();

if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)

{

waitpid(ppid, NULL, 0);

ptrace(PTRACE_CONT, NULL, NULL);

}

}

}

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

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

XPOSED

Xposed — фрей­мворк для ран­тайм‑модифи­кации при­ложе­ний. И хотя в основном с его помощью уста­нав­лива­ют сис­темные модифи­кации и тви­ки при­ложе­ний, сущес­тву­ет мас­са модулей, которые могут быть исполь­зованы для ревер­са и взло­ма тво­его при­ложе­ния. Это и раз­личные модули для отклю­чения SSL Pinning, и трас­сиров­щики вро­де inspeckage, и самопис­ные модули, которые могут как угод­но изме­нять при­ложе­ние.

Есть три дей­ствен­ных спо­соба обна­руже­ния 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.

КЛОНИРОВАНИЕ

Не­кото­рые про­изво­дите­ли встра­ивают в свои про­шив­ки фун­кцию кло­ниро­вания при­ложе­ния (Parallel Apps в OnePlus, Dual Apps в Xiaomi и так далее), которая поз­воля­ет уста­новить на смар­тфон копию выб­ранно­го при­ложе­ния. Про­шив­ка соз­дает допол­нитель­ного Android-поль­зовате­ля с иден­тифика­тором 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

}

Ме­тод осно­ван на спо­собе, при­веден­ном в статье Preventing Android App Cloning.

ВЫВОДЫ

Ко­неч­но же, это далеко не все спо­собы борь­бы с ревер­сом и ана­лизом поведе­ния при­ложе­ний. Сущес­тву­ет мно­жес­тво менее эффектив­ных или слиш­ком узкоспе­циали­зиро­ван­ных спо­собов. Поэто­му ниже я при­веду спи­сок литера­туры и про­ектов на 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.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page