R0 CREW

Взлом Adnroid приложений с помощью Frida (Часть 2, CrackMe 1)

frida
android
crackme
reverse
ru
#1

Оригинал: codemetrix.net

Теперь, после знакомства с Frida в первой части, мы готовы к решению небольшого CrackMe используя Frida. После того что мы узнали о Frida это должно быть очень легко, ну теоретически. Если вы хотите повторять за мной, скачайте:

Кончено, я предполагаю, что вы уже установили Frida (версии 9.1.16 или позднее) на свой компьютере и запустили соответствующий frida-server на устройстве с правами супер-пользователя. Для этого руководства я буду использовать образ Android 7.1.1 ARM запущенный в эмуляторе.

Установите Uncrackable Crackme Level 1 на ваше устройство:

adb install sg.vantagepoint.uncrackable1.apk

Подождите пока он установится, затем запустите его (оранжевая иконка в правом нижнем углу):

После запуска приложение сообщит, что не оно не работает на устройстве с правами супер-пользователя:

Если вы нажмете “ОК” приложение сразу же закроется. Хм. Нехорошо. Похоже мы не сможем решить CrackMe таким способом. А разве мы ожидали другого? Давайте разберемся что происходит, разберем как работает приложение внутри.

Сконвертируем APK в jar c помощью dex2jar:

michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apk 

dex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar

Теперь загрузим файл а BytecodeViewer (или любой другой дизассемблер на ваш выбор, главное что бы он поддерживал Java). Вы также можете попробовать загрузить APK напрямую в BytecodeViewer или открыть его classes.dex, но у меня это не работало, поэтому я предварительно сконвертировал в jar с помощью dex2jar.

В BytecodeViewer выберете View->Panel 1->CFR->Java для использования CFR Decompiler. Вы можете включить отображение smali кода во втором окне (Panel 2) если вы хотите сравнивать вывод декомпилятора с дизассемблированным листингом (который зачастую точнее декомпилированного кода).

Вывод CFR Decompiler для MainActivity:

package sg.vantagepoint.uncrackable1;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.uncrackable1.a;
import sg.vantagepoint.uncrackable1.b;
import sg.vantagepoint.uncrackable1.c;

public class MainActivity
extends Activity {
    private void a(String string) {
        AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
        alertDialog.setTitle((CharSequence)string);
        alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
        alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
        alertDialog.show();
    }

    protected void onCreate(Bundle bundle) {
        if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) {
            this.a("Root detected!"); //This is the message we are looking for
        }
        if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) {
            this.a("App is debuggable!");
        }
        super.onCreate(bundle);
        this.setContentView(2130903040);
    }

    public void verify(View object) {
        object = ((EditText)this.findViewById(2131230720)).getText().toString();
        AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
        if (a.a((String)object)) {
            alertDialog.setTitle((CharSequence)"Success!");
            alertDialog.setMessage((CharSequence)"This is the correct secret.");
        } else {
            alertDialog.setTitle((CharSequence)"Nope...");
            alertDialog.setMessage((CharSequence)"That's not it. Try again.");
        }
        alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new c(this));
        alertDialog.show();
    }
}

Взглянув на остальные декомпилированные файлы, можно заметить, что приложение достаточно небольшое, и возможно, мы могли бы решить CrackMe просто разобрав алгоритм расшифровки и преобразования строк. Однако, после знакомства с Frida разве мы не можем сделать проще?

Сперва посмотрим как приложение проверяет наличие root. Прямо над сообщением “Root detected” можно увидеть код:

if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())

Если вы перейдете в класс sg.vantagepoint.a.c вы увидите различные проверки на root:

public static boolean a()
    {
        String[] a = System.getenv("PATH").split(":");
        int i = a.length;
        int i0 = 0;
        while(true)
        {
            boolean b = false;
            if (i0 >= i)
            {
                b = false;
            }
            else
            {
                if (!new java.io.File(a[i0], "su").exists())
                {
                    i0 = i0 + 1;
                    continue;
                }
                b = true;
            }
            return b;
        }
    }
    
    public static boolean b()
    {
        String s = android.os.Build.TAGS;
        if (s != null && s.contains((CharSequence)(Object)"test-keys"))
        {
            return true;
        }
        return false;
    }
    
    public static boolean c()
    {
        String[] a = new String[7];
        a[0] = "/system/app/Superuser.apk";
        a[1] = "/system/xbin/daemonsu";
        a[2] = "/system/etc/init.d/99SuperSUDaemon";
        a[3] = "/system/bin/.ext/.su";
        a[4] = "/system/etc/.has_su_daemon";
        a[5] = "/system/etc/.installed_su_daemon";
        a[6] = "/dev/com.koushikdutta.superuser.daemon/";
        int i = a.length;
        int i0 = 0;
        while(i0 < i)
        {
            if (new java.io.File(a[i0]).exists())
            {
                return true;
            }
            i0 = i0 + 1;
        }
        return false;
    }

Используя Frida мы можем сделать так, что бы все эти методы возвращали false, перезаписав их как мы делали еще в первой части руководства. Но что происходит, когда функция возвращает true, потому что обнаруживает root. Как мы видели в функции a, находящейся в MainActivityпроисходит открытие диалога. Так же устанавливается обработчик onClickListener на нажатие кнопки “OK”.

alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));

Реализация onClickListenerдовольно небольшая:

package sg.vantagepoint.uncrackable1;

class b implements android.content.DialogInterface$OnClickListener {
    final sg.vantagepoint.uncrackable1.MainActivity a;
    
    b(sg.vantagepoint.uncrackable1.MainActivity a0)
    {
        this.a = a0;
        super();
    }
    
    public void onClick(android.content.DialogInterface a0, int i)
    {
        System.exit(0);
    }
}

После события нажатия приложение закрывается, вызывая System.exit(0). Таким образов все, что нам нужно сделать это избежать закрытия приложения, переписав метод onClickв Frida. Создайте файл uncrackable1.js и разместите там следующий код:

setImmediate(function() { //prevent timeout
    console.log("[*] Starting script");

    Java.perform(function() {

      bClass = Java.use("sg.vantagepoint.uncrackable1.b");
      bClass.onClick.implementation = function(v) {
         console.log("[*] onClick called");
      }
      console.log("[*] onClick handler modified")

    })
})

Если вы прочитали первый пост из этой серии статей, скрипт должен быть вам понятен: мы оборачиваем наш код функцией setImmediateдля предотвращения тайм-аутов (возможно вам это и не понадобится), а затем вызываем, Java.perform чтобы использовать функции Frida для работы с Java. Затем начинается настоящая магия: мы получаем класс который реализует интерфейс OnClickListenerи перезаписываем его метод onClick. В нашей версии эта функция просто пишет какой-то консольный вывод. В отличие от оригинала, выход из приложения не выполняется. Поскольку оригинал функции onClickHandlerподменяется на нашу внедренную функцию с помощью Frida, она никогда не выполнится и приложение больше не закроется, когда мы нажмем кнопку “OK” в диалоге. Давайте проверим это.
Откройте приложение (пусть оно снова отобразит диалоговое окно «Root detected»)

и внедрите скрипт:

frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1

Дайте Frida несколько секунд для применения изменений кода, пока вы не увидите сообщение “onClick handler modified” (возможно вы получите shell раньше, из-за оборачивания в setImmediate функция выполняется в фоне):

Затем нажмите кнопку “ОК” в приложении. Если все прошло хорошо приложение не закроется.

Отлично, диалог исчезает, и мы можем ввести пароль. Давайте введем что-нибудь и посмотрим, что происходит:

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

Снова смотрим в MainActivityи видим функцию:

public void verify(View object) {

Она вызывает метод a из класса sg.vantagepoint.uncrackable1.a

if (a.a((String)object)) {

Это декомпилированный код класса sg.vantagepoint.uncrackable1.a:

package sg.vantagepoint.uncrackable1;

import android.util.Base64;
import android.util.Log;

/*
 * Exception performing whole class analysis ignored.
 */
public class a {
    public static boolean a(String string) {
        byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
        byte[] arrby2 = new byte[]{};
        try {
            arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby);
        }
        catch (Exception var2_2) {
            Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage()));
        }
        if (!string.equals(new String(arrby2))) return false;
        return true;
    }

    public static byte[] b(String string) {
        int n = string.length();
        byte[] arrby = new byte[n / 2];
        int n2 = 0;
        while (n2 < n) {
            arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16));
            n2 += 2;
        }
        return arrby;
    }
}

Обратите внимание на вызов string.equals для сравнения строк в конце метода a и создание строки arrby2 в блоке try выше. arrby2 это значение возвращаемое из функции sg.vantagepoint.a.a.a. string.equals сравнивает наш ввод с arrby2. Итак, нам нужно получить значение возвращаемое из sg.vantagepoint.a.a.a.

Мы можем начать реверс-инжинерить преобразование строк и функций расшифровки и работать с оригинальным зашифрованными строками, которые содержаться в вышеприведенном коде. Или мы можем оставить все эти действия приложению, не заботясь о них и просто перехватить возвращаемое значение функции sg.vantagepoint.a.a.a. Возвращаемое значение является уже расшифрованной строкой (в виде массива байтов) с которой сравнивается наш ввод. Следующий скрипт нам в этом поможет:

        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
            retval = this.a(arg1, arg2);
            password = ''
            for(i = 0; i < retval.length; i++) {
               password += String.fromCharCode(retval[i]);
            }

            console.log("[*] Decrypted: " + password);
            return retval;
        }
        console.log("[*] sg.vantagepoint.a.a.a modified");

Мы перезаписываем функцию sg.vantagepoint.a.a.a, перехватываем ее возвращаемое значение, а после преобразовываем в читаемую строку. Это расшифрованная строка, которую мы ищем, поэтому вы выводим ее в консоли и надеемся, что получили решение.

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

setImmediate(function() {
    console.log("[*] Starting script");

    Java.perform(function() {
        
        bClass = Java.use("sg.vantagepoint.uncrackable1.b");
        bClass.onClick.implementation = function(v) {
         console.log("[*] onClick called.");
        }
        console.log("[*] onClick handler modified")


        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
            retval = this.a(arg1, arg2);
            password = ''
            for(i = 0; i < retval.length; i++) {
               password += String.fromCharCode(retval[i]);
            }

            console.log("[*] Decrypted: " + password);
            return retval;
        }
        console.log("[*] sg.vantagepoint.a.a.a modified");


    });

});

Давайте запустим этот скрипт. Как и прежде сохраните его как uncrackable1.js и выполните (если Frida не сделает это автоматически):

frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1

Дождитесь сообщения о модификации sg.vantagepoint.a.a.a, после чего нажмите “ОК” в диалоге Root detected введите что-нибудь в поле секретного кода и нажмите проверить. Проверку мы не прошли, но посмотрим на вывод во Frida:

michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
     ____
    / _  |   Frida 9.1.16 - A world-class dynamic instrumentation framework
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
                                                                                
[*] Starting script

[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] onClick handler modified

[*] sg.vantagepoint.a.a.a modified

[*] onClick called.

[*] Decrypted: I want to believe

Хорошо. Теперь мы имеем расшифрованную строку I want to believe. Вот и все, давайте проверим, что она действительно подходит:

Я надеюсь, что к этому моменту вы, по крайне мере, немного впечатлены тем, что можете делать с Frida и какие возможности открывает динамическая бинарная иструментация.

© Translated by norver special for r0 Crew
Уроки взлома Adnroid приложений с помощью Frida