Order Of Six Angles

Main Logo

A security researcher's blog about reverse-engineering, malware and malware analysis

Home | RU | Translations | Tools | Art | About

12 December 2019

tags: android - crackme

Решаем "OWASP UnCrackable App for Android Level 1" или учим smali

Я расскажу о своем решении заданий на реверс андроид приложений, (от OWASP). Эти задания даются в качестве демонстративных примеров в OWASP Mobile Security Testing Guide. Я не читал этот гайд и буду пытаться сделать задания, опираясь только на свои обрывки знаний об андроиде. Скорей всего есть более быстрые и короткие пути решения, эти решения просто являются моими.

UnCrackable App for Android Level 1

Первый уровень встречает нас таким описанием

This app holds a secret inside. Can you find it?
Objective: A secret string is hidden somewhere in this app. Find a way to extract it.

Скачиваем apk, устанавливаем на android эмулятор, запускаем. Видим такое окно

На фоне видем форму, в которую по всей видимости нам надо ввести secret. Но форма не доступна, приложение определило, что было запущено на эмуляторе (рутованый) и закрылось. Давайте для начала разберемся, откуда берется это окно и попробуем убрать его. Для этого нам нужно посмотреть исходный код apk и изменить его. Есть два способа получения исходного кода приложения - декомпилирование и декодирование в байткод. Декомпиляция даст нам java классы, которые удобнее читать. Но внести изменения в них и собрать заново в apk мы с большей вероятностью не сможем. Поэтому мы будем использовать утилиту apktool, чтобы получить исходный код в виде smali. Его сложнее читать, но при изменении приложение пересоберается безболезненно. Декодируем.

Теперь нам надо открыть исходник Activity, который открывается при старте. Для этого откроем файл AndroidManifest.xml в папке base.

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="owasp.mstg.uncrackable1">
    <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme">
        <activity android:label="@string/app_name" android:name="sg.vantagepoint.uncrackable1.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Находим имя класса sg.vantagepoint.uncrackable1.MainActivity. По этому же пути в папке base/smali лежит исходник, открываем его и смотрим метод OnCreate(), так как именно он запускается самым первым.

Немного о smali

Андроид приложения компилируются в байткод, который исполняется виртуальной машиной Dalvik. Сам байткод тяжело читаем, поэтому его удобочитаемая для человека форма, называется smali. smali - это аналог языка ассемблера, но для андроида. Он на самом деле достаточно просто. Чтобы дальнейший текст был более понятен, изучим основные команды (полная информация вы найдете в официальной документации:

const-string v0, “test” - кладем строку “test” в переменную v0 (правильней говорить “в регистр”, но опустим это)

invoke-direct - вызов нестатичного метода

invoke-static - вызов статичного метода

invoke-virtual - вызов метода, который не является private, static или final

if-nez - “if-not-equal-zero”, условие “если меньше или равно нулю” (аналогично применимо к if-eqz, if-gez, if-lez и т.д.)

move-result v0, move-result-object v0, … - поместить результат выполнение функции в переменную v0

Продолжим. В теле метода OnCreate() видим такой код

    :cond_0 //метка для перехода (как label в Си для goto)
    const-string v0, "Root detected!" // помещаем в переменную v0 строку
    
    // invoke-direct означаем вызов нестатиченого метода. v0 - это передаваемый параметр
    invoke-direct {p0, v0}, Lsg/vantagepoint/uncrackable1/MainActivity;->a(Ljava/lang/String;)V

Судя по тексту, метод a() выполняется, когда приложение обнаружило рут. Если мы посмотрим на него

.method private a(Ljava/lang/String;)V
    .locals 3

    new-instance v0, Landroid/app/AlertDialog$Builder;

    invoke-direct {v0, p0}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;)V

    invoke-virtual {v0}, Landroid/app/AlertDialog$Builder;->create()Landroid/app/AlertDialog;

    move-result-object v0

    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setTitle(Ljava/lang/CharSequence;)V

    const-string p1, "This is unacceptable. The app is now going to exit."

    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setMessage(Ljava/lang/CharSequence;)V

    const-string p1, "OK"

    new-instance v1, Lsg/vantagepoint/uncrackable1/MainActivity$1;

    invoke-direct {v1, p0}, Lsg/vantagepoint/uncrackable1/MainActivity$1;-><init>(Lsg/vantagepoint/uncrackable1/MainActivity;)V

    const/4 v2, -0x3

    invoke-virtual {v0, v2, p1, v1}, Landroid/app/AlertDialog;->setButton(ILjava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)V

    const/4 p1, 0x0

    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setCancelable(Z)V

    invoke-virtual {v0}, Landroid/app/AlertDialog;->show()V

    return-void
.end method

то увидим, что в нем создается AlertDialog с надписью “This is unacceptable. The app is now going to exit.” и кнопкой. Это совпадает, с тем что мы видели на старте. А это значит, что переход по метке :cond_0 выводит нам это окно. Давайте посмотрим, что нас приводит к нему, возвращаемся в метод OnCreate() и смотрим код, с нужным условием.

    //invoke-static вызов статичного метода
    invoke-static {}, Lsg/vantagepoint/a/c;->a()Z 

    move-result v0
    // if-nez - "if not equal zero" - если результат работы функции не равен 0, то мы кричим, что обнаружили рут
    // аналогично с функцией b() ниже
    if-nez v0, :cond_0

    invoke-static {}, Lsg/vantagepoint/a/c;->b()Z

    move-result v0

    if-nez v0, :cond_0

Очевидно, что функции sg/vantagepoint/a/c;->a(), sg/vantagepoint/a/c;->b() и если почитать остальной код метода - Lsg/vantagepoint/a/c;->c() - чекают рут. Они находятся в классе sg.vantagepoint.a.c, посмотрим на них

Код a()


    ...

    new-instance v5, Ljava/io/File;

    const-string v6, "su"

    invoke-direct {v5, v4, v6}, Ljava/io/File;-><init>(Ljava/lang/String;Ljava/lang/String;)V

    invoke-virtual {v5}, Ljava/io/File;->exists()Z
    ...

Здесь проверяется наличие файла “su”. Это пример кода проверки рута, на рутованом телефоне всегда будет находиться бинарник su. Поиск осуществляется в getenv(“PATH”)


    ...

    const-string v0, "PATH"

    invoke-static {v0}, Ljava/lang/System;->getenv(Ljava/lang/String;)Ljava/lang/String;

    ...

Посмотрим на второй метод sg/vantagepoint/a/c;->b()


    ...

    sget-object v0, Landroid/os/Build;->TAGS:Ljava/lang/String;

    if-eqz v0, :cond_0

    const-string v1, "test-keys"

    invoke-virtual {v0, v1}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z

    ...

Здесь используется другой способ обнаружения рута, а именно в переменной Build.TAGS (лежит в /system/build.prop, на исходники можно посмотреть тут) идет поиск строки test-keys. Дело в том, что когда ядро подписывается официальным ключом, то в Build.TAGS будет лежать строка “Release-Keys”. “test-key” означает, что ядро было подписано тестовым публичным ключом AOSP.

Остался последний метод c()

    invoke-static {}, Lsg/vantagepoint/a/c;->c()Z

    move-result v0

    if-eqz v0, :cond_1

Он вот такой

.method public static c()Z
    ...

    const-string v0, "/system/app/Superuser.apk"

    const-string v1, "/system/xbin/daemonsu"

    const-string v2, "/system/etc/init.d/99SuperSUDaemon"

    const-string v3, "/system/bin/.ext/.su"

    const-string v4, "/system/etc/.has_su_daemon"

    const-string v5, "/system/etc/.installed_su_daemon"

    const-string v6, "/dev/com.koushikdutta.superuser.daemon/"

    ...

    invoke-direct {v5, v4}, Ljava/io/File;-><init>(Ljava/lang/String;)V

    invoke-virtual {v5}, Ljava/io/File;->exists()Z

   ...
.end method

Здесь, мы по заранее захардкоженым пустям, ищем бинарники, которые используются эксплоитами, для рутования.

Все это мы проделали, чтобы увидеть техники рутования и немного изучить smali. Для решения задачи, так глубоко рабираться в этом, не так необходимо. Мы можем просто все проверки выпилить из кода. После выпиливания всех проверок на рут, наш метод OnCreate() выглядит так

# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 1

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    const/high16 p1, 0x7f030000

    invoke-virtual {p0, p1}, Lsg/vantagepoint/uncrackable1/MainActivity;->setContentView(I)V

    return-void
.end method

После изменения, нам надо заново собрать и подписать приложение. Собираем командой

apktool b base

Чтобы подписать, генерируем keystore

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

И собственно подписываем

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore my_application.apk alias_name

Запускаем, и теперь нас больше не беспокоит никакое окно!

Дальше нас ждет input, вводим что-нибудь и видим еще одно окно

Чтобы найти код, отвечающий за проверку ввода, делаем поиск по исходникам и находим функцию, которая отвечает за вывод этого окна - .method public verify(Landroid/view/View;)V, открываем и видим

    invoke-static {p1}, Lsg/vantagepoint/uncrackable1/a;->a(Ljava/lang/String;)Z

    move-result p1

    if-eqz p1, :cond_0

    const-string p1, "Success!"

    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setTitle(Ljava/lang/CharSequence;)V

    const-string p1, "This is the correct secret."

    :goto_0
    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setMessage(Ljava/lang/CharSequence;)V

    goto :goto_1

    :cond_0
    const-string p1, "Nope..."

    invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setTitle(Ljava/lang/CharSequence;)V

    const-string p1, "That\'s not it. Try again."

Здесь, в зависимости от результат выполнения функции Lsg/vantagepoint/uncrackable1/a;->a(Ljava/lang/String;) Выводится Success либо Try again. Откроем эту функцию. В ней прменяется base64 к некой строки и далее она сравнивается.

.method public static a(Ljava/lang/String;)Z
    .locals 5

    const-string v0, "8d127684cbc37c17616d806cf50473cc"

    const-string v1, "5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc="

    const/4 v2, 0x0

    invoke-static {v1, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B

    move-result-object v1

    new-array v2, v2, [B

    :try_start_0
    invoke-static {v0}, Lsg/vantagepoint/uncrackable1/a;->b(Ljava/lang/String;)[B

    move-result-object v0

    invoke-static {v0, v1}, Lsg/vantagepoint/a/a;->a([B[B)[B

    move-result-object v0
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    goto :goto_0

    :catch_0
    move-exception v0

    const-string v1, "CodeCheck"

    new-instance v3, Ljava/lang/StringBuilder;

    invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V

    const-string v4, "AES error:"

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v0}, Ljava/lang/Exception;->getMessage()Ljava/lang/String;

    move-result-object v0

    invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    move-object v0, v2

    :goto_0
    new-instance v1, Ljava/lang/String;

    invoke-direct {v1, v0}, Ljava/lang/String;-><init>([B)V

    invoke-virtual {p0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result p0

    return p0
.end method

Давайте напишем патч, чтобы посмотреть, с какой строкой сравнивается наш ввод. Для этого создадим просто андроид приложение, с таким кодом, оно просто выводит окно, текстом

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Toast.makeText(getApplicationContext(),"Hello Javatpoint",Toast.LENGTH_SHORT).show();
    }
}

Переведем это в smali код

const-string v0, "Hello Javatpoint"

    const/4 v1, 0x0

    invoke-static {p1, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p1

    invoke-virtual {p1}, Landroid/widget/Toast;->show()V

Берем этот код, и вставляем в метод, где сравнивается секретная строка, с нашим вводом


# direct methods
.method public static a(Ljava/lang/String;)Z
    .locals 5

    const-string v0, "8d127684cbc37c17616d806cf50473cc"

    const-string v1, "5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc="

    const/4 v2, 0x0

    invoke-static {v1, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B

    move-result-object v1

    new-array v2, v2, [B

    :try_start_0
    invoke-static {v0}, Lsg/vantagepoint/uncrackable1/a;->b(Ljava/lang/String;)[B

    move-result-object v0

    invoke-static {v0, v1}, Lsg/vantagepoint/a/a;->a([B[B)[B

    move-result-object v0
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    goto :goto_0

    :catch_0
    move-exception v0

    const-string v1, "CodeCheck"

    new-instance v3, Ljava/lang/StringBuilder;

    invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V

    const-string v4, "AES error:"

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v0}, Ljava/lang/Exception;->getMessage()Ljava/lang/String;

    move-result-object v0

    invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0
   
    const/4 v1, 0x0

    invoke-static {p1, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p1

    invoke-virtual {p1}, Landroid/widget/Toast;->show()V

    invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    move-object v0, v2

    return p0
.end method

Мы пропатчили программу кодом, который выводит строку, которая является флагом к решению. Собераем наше приложение и запустим.

Вот и флаг! На этом расход

Вверх