Unicorn Engine. Анализируем нативные библиотеки в приложениях для Android

Лучшая практика защиты мобильных приложений — это перенос кода, который отвечает за криптографию, хранение ключей для API и проверку целостности системы, в нативные библиотеки. Это существенно увеличивает сложность анализа защищенности приложения, тем самым выдвигая все больше требований к исследователю. Но так ли это на самом деле?

Для начала нужно разобраться, что собой представляет Unicorn Engine. Это эмулятор процессора, он поддерживает множество архитектур и сам является мультиплатформенным. У Unicorn Engine в принципе нет сложных подсистем. Ты сам занимаешься разметкой памяти и загрузкой данных, эмулятор не понимает команды из std, поэтому их необходимо реализовывать самостоятельно или вообще пропускать.

Существует множество решений, которые способны трассировать команды на хостовую систему, загружать исполняемые файлы в память и многое другое, так зачем тогда использовать Unicorn Engine?

При исследовании нативных библиотек часто не нужно эмулировать работу всего процесса. Нам достаточно смоделировать работу какой-то конкретной функции, не используя AVD или полноценные эмуляторы Android/iOS, чтобы получить результат отдельно от основного процесса или устройства.

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

Восстанавливать весь алгоритм достаточно трудоемко и требует большого количества знаний — как в криптографии, так и в Reverse Engineering. Здесь нам может помочь Unicorn Engine: определив, как передаются входящие параметры, мы можем проэмулировать работу искомой функции без понимания алгоритма ее работы.

В этой статье мы исследуем упрощенный вариант подписи данных.

 

Тестовый стенд

Для демонстрации атаки мы будем использовать самописное приложение для Android, которое считывает данные из полей ввода в JSON и генерирует подпись, используя некий алгоритм в нативной библиотеке. Наша цель — получить такую же подпись и научиться генерировать валидную подпись для любых данных. В реальной жизни этот JSON с подписью отправлялся бы на сервер, но здесь этот момент опускается.

В нативном приложении реализован некий кастомный алгоритм подписи. Его сложно назвать криптостойким, но для демонстрации он идеален: не очень объемный, но не слишком простой, как обычный XOR. Все необходимые исходники ты можешь найти на моем GitHub.

Также нам понадобится Android Studio и Android SDK с NDK, установленный Unicorn Engine и устройство или эмулятор для запуска. В этой статье я буду использовать AVD x86.

На устройстве (эмуляторе) должен находиться gdbserver, который можно найти по такому адресу:

<android-sdk>/ndk-bundle/prebuilt/<device-system>/gdbserver

Я обычно перемещаю его в /data/local/gdbserver на устройстве.

 

Собираем информацию

Начнем анализ с того, что загрузим наше приложение в Android Studio: File → Profile or Debug APK. Когда проект загрузится, нам нужно исправить Run/Debug Configurations: во вкладке Debugger переключить Debug type в Java. Если этого не сделать, то к приложению будет подключен отладчик из Android Studio и подключить свой мы уже не сможем.

Прежде чем начать, давай попробуем запустить приложение и ввести тестовые данные test/pass. Запишем полученную подпись, так как она нам еще пригодится.

77 21 4f 57 4c 64 00 2e 39 01 4c 4e 7e 00 2e 2f 01 48 4a 7e 00 7b 6c 51 5c 09 37 00 7c 62 50 4b 09 70

 

Реверс APK

Для начала найдем основной класс. Он находится в loony/com/nativeexample/MainActivity.

.field getSign:Landroid/widget/Button;
.field loginField:Landroid/widget/EditText;
.field passwordField:Landroid/widget/EditText;
.field sign:Landroid/widget/TextView;

Видим в начале объявление кнопки и двух полей для заполнения.

.line 28
const-string v0, «native-lib»
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

Ниже происходит загрузка библиотеки и объявлен нативный метод .method public native magic(Ljava/lang/String;)[I, который принимает строку, а на выходе возвращает массив чисел. В том же классе есть функция .method private getHexString(I)Ljava/lang/String;, которая принимает массив чисел и возвращает hex-строку.

Посмотрим, что происходит при создании класса, и перейдем в onCreate.

new-instance v1, Lloony/com/nativeexample/MainActivity$1;
invoke-direct {v1, p0}, Lloony/com/nativeexample/MainActivity$1;->(Lloony/com/nativeexample/MainActivity;)V
invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V

Создается новый обработчик нажатий, в него передается экземпляр loony/com/nativeexample/MainActivity$1, перейдем туда. Нас интересуют функции, которые отвечают за действия, в нашем случае это только onClick.

В коде видно, что создается org/json/JSONObject;, считываются данные из loginField,passwordField и помещаются в JSONObject с ключами login и password соответственно.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *