Writeup: "Android UnCrackable L3" (mas.owasp.org)

  • Название: Android UnCrackable L3
  • Категория: reverse (android)
  • Платформа:
Иногда я буду ссылаться на предыдущие две статьи, посвящённые крякми L1 и L2, поэтому рекомендую к ознакомлению. А на очереди третьего уровня уровень сложности крякми от OWASP - уже довольно проблематичный экземпляр. Но обо всём по порядку!

При заходе в приложение нас встречает нелицеприятная, но вполне знакомая картина:

1723670290925.png


Что радует - из приложения нас моментально не выкидывает, как в L2. Можно было бы, как я и раньше делал, воспользоваться модулем DenyList и надеяться, что приложение не задетектит рут... Но сегодня мы пожалеем тех, у кого этого модуля нет, и поступим по другому: пы пропатчим само приложение!

Для начала откроем его в JADX-GUI (сразу MainActivity):

1723670415788.png


Сразу с самого начала бросается в глаза xorkey - это нам может пригодиться, запоминаем.

1723670524176.png


А здесь можно видеть один из методов защиты от патчеров - проверка библиотек и classes.dex на CRC-хэш. Запоминаем потенциальное место для патчинга и смотрим дальше.

1723670689921.png


Здесь идёт проверка на дебаггинг и наличие рута. Методы checkRoot1, 2, и 3 остались с прошлых крякми, там всё то же самое. Если посмотреть на этот же код в smali, можно увидеть следующее:

1723670960819.png


Каждая проверка if-nez ведёт на метку cond_0041, куда нам точно не нужно попасть. В таком случае, можно было бы просто удалить эти строчки, а последнюю - if-eqz v0, :cond_0046 - просто заменить на goto. А можно просто из метки cond_0041 сделать пустышку, удалив вызов метода showDialog(). Это куда проще - этим и воспользуемся. И заодно, раз уж мы тут, можно и предоставить себе возможность спокойно дебажить приложение! Для этого понадобится убрать ещё 2 строчки:

1723671753530.png


Но не забывайте - в таком случае в собранном из отредактированных smali-файлов classes.dex изменится CRC-хэш, а значит, нам нужно пропатчить ещё и тот кусок кода. Если хорошенько туда присмотреться, то можно понять, что самое опасное для нас - это последний System.exit():

1723671897265.png


Можно было бы удалить этот вызов - пускай программа высчитывает хэши и логирует об ошибках на здоровье. А можно просто добавить return-void сразу после объявления метода - и на выходе verifyLibs() окажется просто функцией, которая ничего не делает. Как вы захотите это обойти и захотите ли - дело ваше.

А нам для нашего грядущего патчинга JADX недостаточно (без плагинов). Для этого нужно самим распаковать приложение в smali через (я буду пользоваться удобной GUI-версией). Декомпилируем наш apk:

1723672312909.png


После этого рядом с ним у вас должна появиться папка со smali-кодом, ресурсами приложения и прочим. Так как JADX под капотом использует apktool, то имена классов и методов, что там, что там, совпадают. Поэтому нам нужен тот самый MainActivity по пути smali/sg/vantagepoint/uncrackable3/, который мы смотрели раньше. В нашем случае он разделился на несколько файлов MainActivity$n. Открываем их в любом текстовом редакторе, ищем непонравившиеся нам строчки (они не в одном файле), и смело их удаляем или комментируем (# в начале).

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

1723673478693.png


Переместите папку в другое место и соберите всё, как полагается:

1723673647837.png


Установим приложение на устройство и попробуем зайти в него:

1723673755028.png


Получилось! Мы успешно пропатчили приложение, обойдя его защиту. Чем не повод для гордости?

Но не стоит забывать о главной цели - нам нужно достать подходящую под формочку строку. В этот раз разнообразим нашу методологию и попробуем frida. Не буду вещать сейчас, как она устанавливается, скажу лишь, что мне помог этот . Подключаем отладку через adb, запускаем на смартфоне frida-server и проверяем его работоспособность с помощью frida-ps -Ua. Если всё прошло хорошо, то на выходе вы получите список пакетов с их названиями (все лишние я замазал):

1723674395646.png


Если же нет - гугл в помощь, товарищи. Из наблюдаемых нами ивентов, которые могут помочь нам с решением, присутствует только кнопка Verify, с соответствующим метод onClick. Попробуем с помощью frida-trace просмотреть, какой класс отвечает за эту кнопку. Конечная команда будет выглядеть так: frida-trace -U -F -i "*!onClick" (U - usb-устройство, F - текущее открытое приложение, j - паттерн вида class!method). Пробуем:

1723680807160.png


И получаем такую хренотень. Гугл на такую ошибку ничего дельного не выдаёт, что же делать? После получаса активного поиска причины этой ошибки и листинга логов (logcat | grep -i "uncrackable") я нашёл её:

1723675992528.png


Всё-таки фриду приложение видит и защищается от неё. И если в самом приложении мы уже все виды защиты нейтрализовали, остаются только подключаемые библиотеки - не зря же их на CRC проверяли?

Сначала глянем libfoo.so (x86_64) в IDA, для начала откроем строки:

1723676446162.png


А вот и то, что мы видели в логах! Посмотрим, где эта строка используется:

1723676504659.png


А вот и ещё одна мешающая нам защита - уже от frida и xposed. Её логика проста: она постоянно читает данные из /proc/self/maps, а когда ей это не удаётся - ругается. Можно пропатчить её так: сразу после начала, до каких-либо пушей (пока сверху в стеке у нас лежит адрес возврата) прописать ret, чтобы функция сразу же умирала. Для этого вам очень поможет этот - он может переводить байткод в ассемблерные инструкции и наоборот. Первой инструкцией в функции идёт push rbp (0x55) - её можно заменить на ret (0xC3). Обратите внимание - когда вы патчите программу, вам необходимо, чтобы общее количество байт было одинаково. Соответственно, если патчить без жутких костылей, то количество байт в заменяющей инструкции должно не превосходить количество байт в заменяемой. Если они равны - прекрасно! Если в заменяющей меньшее количество байт, то добавьте, сколько нужно до равного числа, nop'ов (0x90). В нашем случае обе инструкции занимают 1 байт, соответственно, нам лишь нужно переписать 55 на C3:

1723677259261.png

1723677278778.png


Если вы всё сделали правильно, то у вас должен смениться код:

1723677324059.png


После этого не забудьте применить ваш патч к исходному файлу:

1723677477667.png

.
Ну и, конечно, с помощью apktool соберите заново приложение с новой библиотекой - думаю, с этим у вас проблем не возникнет, только закройде IDA и удалите все остаточные файлы её базы данных, а то будет ошибка при сборке. И, наконец, попробуем с пропатченным приложением и его пропатченной библиотекой запустить всё ту же frida-trace:

1723680944196.png


Опять то же самое... Чекаем логи:

1723677949237.png


А помните, что у нас не одна библиотека, а целых 4? Кажется, нам придётся патчить именно ту библиотеку, архитектура которой совпадает с архитектурой вашего устройства, об этом я сразу как-то не подумал... Чтобы узнать, какая именно архитектура вам нужна, пропишите следующее:

Bash:
adb shell getprop ro.product.cpu.abi

Я на всякий случай распишу все возможные варианты: для патча Arm-библиотек я пользовался сайтом, он мне очень помог. Для библиотеки arm64-v8a достаточно заменить первые 4 байта на C0 03 5F D6 (RET). А вот при патчинге библиотеки armeabi-v7a не забывайте немножко теории: там инструкции могут распознаваться в двух режимах: ARM и Thumb, каждый со своими опкодами. IDA поддерживает оба и позволяет переключаться между ними через виртуальный регистр, о чём она вас при открытии библиотеки предупредит (переключение на Alt+G). В режиме Thumb нужно заменить первые 2 байта на 70 47, чтобы у вас получилась инструкция BX LR (аналог RET).

И теперь, пропатчив всё что только движется, снова собираем этот треклятый apk, устанавливаем, запускаем - и:

1723681103741.png


И наши старания оправдались!!! Отозвались 40 функций - фрида работает! Давайте ещё раз глянем, что нам нужно сделать:

1723682388666.png


1723682402229.png


Судя по всему, check_code() загружается как нативная функция из библиотеки libfoo.so. Узреем её логику работы:

1723682577440.png


В 26-й строчке наглядно видно, что байты одного массива соответствующе ксорятся с другим. Напоминает xorkey из начала? Причём v4 - судя по всему наш xorkey - мы получаем в 22 строчке, а вот второй массив (v9) мелькает только в функции sub_FA0, куда передаётся его адрес. Можно предположить, что там - то, что там будет то, что при XOR с xorkey даст правильный ответ. И всё было бы хорошо - можно было бы изучить sub_FA0 и узнать, как геренируется этот секрет, но ревёрсить эту функцию... Ну...

1723683437210.png


...не самая лучшая идея. Мы настроили нашу программу под свободный дебаггинг/тамперинг, так что у нас есть 2 пути: либо можно решить через frida (находим базовый адрес libfoo.so, добавляем оффсет функции, и через функции onEnter/onLeave вытаскиваем готовый массив), либо можно отладить это дело через gdb. Признаюсь - этот райтап я писал долгое время, в процессе решения. Изначально я планировал решить крякми через Frida, но почему-то она отчаянно не хотела находить нужную функцию (мистика =] ), поэтому я представлю своё решение через отладку... Прямо в IDA!

Чтобы вы не искали по всему интернету gdbserver под arm64, в IDA PRO (🏴‍☠️) в папке dbgsrv лежит любезно предоставленный HexRays android_server64. Его достаточно будет запустить из-под рута, через adb настроить форвард порта - и можно спокойно дебажить библиотеку android-приложения!

Bash:
adb push android_server64 /data/local/tmp
adb shell

А затем в самом шелле:

Bash:
su
cd /data/local/tmp
chmod +x android_server64
./android_server64

1723888441975.png


Затем настройте форвардинг используемого порта:

Bash:
adb forward tcp:23946 tcp:23946

Надеюсь, вы ещё не удалили папку с патченной библиотекой? Если нет, то круто - напомню, что нас интересует функция, которая вызывается с адресом массива, с которым будет идти проверка - на ней и ставьте бряк:

1723889433839.png


В качестве дебаггера выберите "Remote ARM Linux/Android debugger":

1723889538101.png


И дальше в Debugger -> Debugger Options в качестве адреса ставьте 127.0.0.1, а в качестве порта - тот, который вы форвардили

1723889640626.png


Теперь запускайте приложение на смартфоне, и в Debugger -> Attach to process выбирайте owasp.mstg.uncrackable3:

1723889808570.png


После этого Ида начнёт судорожно качать библиотеки и скажет: "Я нашла точно такую же библиотеку, как у вас открыта, но она в папке с самим приложением. Хотите её отлаживать?"

1723889922785.png


Мы, конечно же, хотим, поэтому нажимаем "Same". После этого вы остановитесь в непонятном месте - это лишь инициализация библиотеки, так что смело жмём "Continue Process" (F9). А уже после этого можно ввести что-нибудь в формочку и нажать verify - и вы остановитесь на нужной вам функции:

1723890415000.png


Перешагните через эту функцию, не заходя внутрь (F8) и посмотрите, что в переменной v8:

1723890533549.png


Эти 24 байта - и есть наш секретный массив, теперь мы его можем спокойно расшифровать, используя ранее упомянутый xorkey:

Python:
xor_key = "pizzapizzapizzapizzapizz"

memory = """[stack]:0000007FFA7C0CF8 DCB 0x1D
[stack]:0000007FFA7C0CF9 DCB    8
[stack]:0000007FFA7C0CFA DCB 0x11
[stack]:0000007FFA7C0CFB DCB 0x13
[stack]:0000007FFA7C0CFC DCB  0xF
[stack]:0000007FFA7C0CFD DCB 0x17
[stack]:0000007FFA7C0CFE DCB 0x49 ; I
[stack]:0000007FFA7C0CFF DCB 0x15
[stack]:0000007FFA7C0D00 DCB  0xD
[stack]:0000007FFA7C0D01 DCB    0
[stack]:0000007FFA7C0D02 DCB    3
[stack]:0000007FFA7C0D03 DCB 0x19
[stack]:0000007FFA7C0D04 DCB 0x5A ; Z
[stack]:0000007FFA7C0D05 DCB 0x1D
[stack]:0000007FFA7C0D06 DCB 0x13
[stack]:0000007FFA7C0D07 DCB 0x15
[stack]:0000007FFA7C0D08 DCB    8
[stack]:0000007FFA7C0D09 DCB  0xE
[stack]:0000007FFA7C0D0A DCB 0x5A ; Z
[stack]:0000007FFA7C0D0B DCB    0
[stack]:0000007FFA7C0D0C DCB 0x17
[stack]:0000007FFA7C0D0D DCB    8
[stack]:0000007FFA7C0D0E DCB 0x13
[stack]:0000007FFA7C0D0F DCB 0x14""".split()

secret = []
for i in range(len(memory)):
        if memory[i - 1] == "DCB":
                base = 10
                if "x" in memory[i]:
                        base = 16
                secret.append(int(memory[i], base))
for i in range(len(secret)):
        print(chr(ord(xor_key[i]) ^ secret[i]), end="")
print()

Запускаем:

1723890677083.png


Пробуем нашу строку:

1723890827909.png


Получилось! Поздравляю, мы решили уже довольно сложный крякми, в отличие от первых двух. И декомпилили приложение, и патчили smali, и даже обходили антидебаггинг в библиотеках под разные архитектуры, и дебажили одну из них... Но наши старания окупились!

Статья вышла немаленькой - я писал её три дня, в процессе решения самого крякми. Конечно, если бы получилось решить через Фриду, было бы чуточку полегче, но что есть, то есть - может, кто-то захочет решить по-своему =]

Надеюсь, этот немаленький райтап помог вам в нашей нелёгкой стезе.

Удачного ревёрса!

made 4 @rev_with_da_boys
 

Вложения

  • 1723681312178.png
    1723681312178.png
    7,9 КБ · Просмотры: 28
Последнее редактирование:
  • Нравится
Реакции: leo27, nsk и Edmon Dantes
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!