Всем привет! Меня зовут Максим и я занимаюсь исследованиями источников данных. В своей работе периодически приходится сталкиваться с исследованием android-приложений. В этой статье я хочу показать базовые методы реверс-инжиниринга и исследования android-приложений. Сегодняшний подопытный - Crackme 1 из репозитория owasp-mastg. The Mobile Application Security Testing Guide (MASTG) - проект, который рассказывает о безопасности android-приложений и в качестве песочницы предлагает пройти 7 crackme, 5 из которых предназначены для android.

В рамках данной статьи я буду предполагать, что у вас есть минимальный опыт реверс-инжиниринга других приложений, исполняемых файлов или библиотек, а так же опыт работы с adb

Первичный осмотр приложения

После скачивания приложения у нас есть множество вариантов как его запустить, вплоть до физического устройства. Я привык работать с Android Virtual Device (AVD) так как он позволяет создавать эмуляторы с разными архитектурами и разными версиями системы. Прежде чем запускать приложение первым делом необходимо посмотреть, с чем нам предстоит работать. Для этого, и в дальнейшем я буду использовать jadx. Импортируем приложение в интерфейс и открываем AndroidManifest.xml. Данный файл описывает метаданные приложения, в больших приложениях он может быть до 2000 строк и более.

AndroidManifest нашего приложения
AndroidManifest нашего приложения

Эта строка говорит нам о том, что тут есть только 1 activity <activity android:label="@string/app_name" android:name="sg.vangatepoint.uncrackable1.MainActivity">. Далее нужно перейти по пути sg.vantagepoint.uncrackable1.MainActivity и посмотреть как оно выглядит. Перед этим стоит посмотреть, какие библиотеки и другие ресурсы использует приложение в файлах, которые тянет с собой приложение. В нашем случае никаких библиотек нет, что позволяет нам использовать любую архитектуру в AVD.

Картинка с MainActitivy

Как мы видим, jadx успешно смог декомпилировать главую activity и вся логика у нас легко читается. Судя по всему в методе onCreate происходит какая-то проверка на root и на факт присутствия дебаггера. Также есть метод verify который проверяет, удалось ли пройти crackme или нет.

На первый взгляд все понятно, попробуем запустить приложение.

Запуск на эмуляторе

Первым делом приложение нужно установить. Сделать это можно через adb-консоль или путем простого перетаскивания .apk-файла в интерфейс эмулятора. Первый вариант обычно нужен для более чтения лога в случае ошибки, однако сейчас нам это не пригодится.

Запущенное приложение

При запуске у нас появляется сообщение, которое мы видели выше в коде jadx. Судя по всему какая-то из проверок на root прошла, и больше мы ничего не можем сделать в приложении, кроме как закрыть его. Чаще всего я работаю на AVD с root-правами, так как работать с урезаной системой может быть проблематично, поэтому попробуем обойти проверки на root-права, которые делает приложение.

Двойным кликом по классу c переходим в определение класса и смотрим на код проверок

Проверки на root-права
Листинг проверок
Листинг проверок

Как видно на скриншоте здесь имеются 3 проверки, разберем каждую отдельно.

Проверка c.a проверяет, есть ли где-то в PATH-директориях бинарник su. Проверка c.b проверяет, если ли строка test-keys в каком-нибудь из тегов сборки. Проверка c.c проверяет, если ли на системе популярные приложения для эскалации прав в системе. Я уверен, что сработала как минимум проверка c.a так как у меня на системе точно есть su, а значит необходимо обойти эту проверку.

Обход проверки

Если бы это было crackme в виде исполняемого файла, например, .exe первым делом я бы попробовал поменять в коде проверок простые опкоды, которые позволяют просто их выключить, например для первой проверки вместо тела цикла вставить

mov eax, 0
ret

или поменять jne на je в цикле чтобы его пропустить, однако в случае с android-приложением так сделать не получится, потому что apk-файл представляет собой zip-архив, который в дальнейшем используется системой для запуска приложений. Более подробно можно прочитать, например, тут. То есть для патча проверок необходимо было бы декомпилировать приложение, внести изменения в smali-код, собрать приложение, подписать его, поставить на AVD и проверить результат. Для нашего случая это было бы долго, поэтому я хочу воспользоваться более простым и динамическим инструментом - frida. frida - инструмент для диначеского анализа, который позволяет работать с приложениями под разные ОС, в том числе и под android.

Для работы с frida я буду использовать frida-tools. Для того, чтобы работать с frida нам надо поставить на систему frida-server. Нужную версию вы сможете найти здесь. Через adb загружаем на систему сервер и запускаем его.

Далее для обхода проверок нам надо написать патч вышеописанных методов.

Java.perform(function () {
    var it = setInterval(function () {
        try {
            console.log('hooking...')
            const clazz = Java.use('sg.vantagepoint.a.c')

            clazz.a.implementation = function () {
                return false;
            }
            clazz.b.implementation = function () {
                return false;
            }
            clazz.c.implementation = function () {
                return false;
            }
            clearInterval(it)
            console.log('...success')
        } catch (e) {
            console.log("failed!");
        }
    }, 10);
});

Общая идея обхода в том, что мы берем каждый метод нашего класса, и заменяем их имплементацию на return false . Более подробно о возможностях, которые предоставляет данный инструмент можно почитать в документации.

Запустить приложение можно через

frida -D emulator-5554 -l .\1\root_bypass.js -f owasp.mstg.uncrackable1

-D emulator-5554 - параметр, который указывает, как можно соединиться с эмулятором. Информация о настройке и запуске AVD с ADB не входит в scope данной статьи, однако находится, например, тут

Ииии... после запуска приложения таким образом у нас больше не появляется сообщение о руте.

Приложение без root-детектора
Обошли проверку на root
Обошли проверку на root

При попытке ввести любой текст у нас появляется ошибка, листинг кода которой мы видели в jadx выше. Посмотрим, что за метод a.a(), который определяет успешность прохождения данного crackme. Хочу пояснить, что с самого начала мы могли бы таким же образом пропатчить a.a также, как пропатчить проверку на root-права, однако, как мне кажется, суть данного crackme не в этом.

Листинг класса sg.vantagepoint.uncrackable1.a

Как мы видим, данный метод использует какое-то шифрование, судя по всему AES. Копнем чуть глужбе и посмотрим метод sg.vantagepoint.a.a.a

Листинг sg.vantagepoint.a.a.a

Как мы видим, здесь используется AES. Начинаем вспоминать, что AES - симметричный алгоритм шифрования. Также стоит посмотреть на sg.vantagepoint.uncrackable1.b, где происходит какая-то непонятная человеку магия. Очевидно, разбирать логику этой магии не хочется, и при решении этого crackme я просто хотел вставить всю логику в пустое android-приложение и выполнить его чтобы увидеть результат, однако, еще раз внимательно посмотрев на метод sg.vantagepoint.uncrackable1.a.a я понял, что как-либо взаимодействовать с этой магической логикой нам вообще не нужно. Попробуйте подумать сами, прежде чем читать дальше.

Как мы видим, наш метод a.a принимает строку, которую в дальнейшем сравнит с результатом шифромагии и здесь мы опять-таки могли просто вернуть true, и пойти дальше заниматься своими делами, однако мы можем совпользоваться готовой логикой написанных методов и таким образом получить текст, который уже непосредственно можно ввести в окошко. Для этого снова воспользуемся frida. Дополним наш патч, который будет использовать готовые объекты кода.

Готовое решение
Java.perform(function () {
    var it = setInterval(function () {
        try {
            console.log('hooking...')
            const clazz = Java.use('sg.vantagepoint.a.c')

            clazz.a.implementation = function () {
                return false;
            }
            clazz.b.implementation = function () {
                return false;
            }
            clazz.c.implementation = function () {
                return false;
            }
            clearInterval(it)
            console.log('...success')
        } catch (e) {
            console.log("failed!");
        }
    }, 10);
    decode()
});

const decode = () => {
    const aes_encoder_clazz = Java.use('sg.vantagepoint.a.a') // method - a
    const local_encode_clazz = Java.use('sg.vantagepoint.uncrackable1.a') // method b - encode,
    const base64_clazz = Java.use('android.util.Base64') // method decode

    const local_encode_result = local_encode_clazz.b("8d127684cbc37c17616d806cf50473cc");
    const b64_decode_result = base64_clazz.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0);
    const aes_result = aes_encoder_clazz.a(local_encode_result, b64_decode_result)
    console.log("result: ")
    console.log(aes_result)
}

Я всегда стараюсь прописывать в Java.use именно классы по 2 причинам:

  1. так как часто используются много методов 1 класса

  2. иногда именно use метода кидает ошибку о том, что метод не найдет, однако работает вышепреведенный вариант, обращение к методу через точку работает. Если кто знает в чем здесь магия - поделитесь в комментариях

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

for ch in chars:
    print(chr(ch), end='')
Здесь находится ответ на crackme

73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101

При вводе ответа в поле для ввода получаем заветное сообщение

Решенный crackme

В итоге с помощью небольших инструментов, не вдаваясь в подробности smali-кода, без пересборки приложения и других вещей у нас получилось пройти crackme 1 OWASP MASTG.

Буду рад прочитать ваши пожелания, замечания и прочее, ведь это моя первая статья. Чукча - не писатель, чукча - всё-подряд-ломатель.

Комментарии (0)