android
February 8, 2023

Хакерим понемногу

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

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

C чего всё началось

Итак, была поставлена задача интеграции некоего сервиса (образно, верификации человека по улыбке) в наш продукт, а также написание тестового мобильного приложения, в котором можно было бы отладить всё взаимодействие и поставлять исходники заказчикам в качестве примера, чтобы им было проще реализовать интеграцию на своей стороне.

На сайте сервиса была довольно подробная документация и мобильный SDK для Android и iOS. Естественно, SDK поставляется без исходников в виде собранной библиотеки.

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

Был довольно быстро накидан каркас тестового приложения, подключен SDK по документации.

Проблема 1

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

Обидно, досадно, ну ладно: будем тестировать на реальном устройстве. Но обиду я затаил.

Проблема 2

Запускаю на реальном устройстве, вроде бы всё сделано по инструкции, но в итоге получаю ошибку в этом дополнительном приложении “Что-то пошло не так, попробуйте позже”. И, собственно, никаких логов нет.

В adb logcat тоже ничего вразумительного. Дальше были пляски с бубном, многократные прочтения документации, отчаяние, слёзы, письма в поддержку.

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

Как же понять что пошло не так?

Хорошо, что злые хакеры уже придумали MITM атаку для прослушивания трафика. Для тех, кто не знает, что это такое - вот определение из википедии:

Атака посредника, или атака «человек посередине» (англ. Man in the middle (MITM)) — вид атаки в криптографии и компьютерной безопасности, когда злоумышленник тайно ретранслирует и при необходимости изменяет связь между двумя сторонами, которые считают, что они непосредственно общаются друг с другом. Является методом компрометации канала связи, при котором взломщик, подключившись к каналу между контрагентами, осуществляет вмешательство в протокол передачи, удаляя или искажая информацию.

Итак, идея такая - делаем MITM атаку на телефон и слушаем весь его трафик. Что для этого можно использовать?

Атаку можно проводить на разных сетевых уровнях, например, на сетевом или на прикладном. Применение того или иного подхода зависит от ситуации и от способа передачи данных.

Можно попробовать перехватить весь трафик телефона, для этого читаем про ARP Spoofing, вооружаемся утилитами arpspoof и wireshark, настраиваем свой комп как маршрутизатор, для этого достаточно поднять точку доступа WIFI.

Не буду подробно описывать этот способ, потому что он мне не подошел, так как мобильное приложение сервиса общается по HTTPS со своим бэкендом. А как мы знаем, HTTPS нельзя прослушать. Таким образом этой атакой мы можем прослушивать только незашифрованный трафик. Из плюсов можно отметить, что мы видим все сетевые IP пакеты, в том числе UDP, RTSP и прочие.

Возможно у читателя возникнет вопрос, а почему же всё-таки нельзя прослушать HTTPS напрямую?

Безопасность HTTPS (TLS) держится на трёх столпах:

  1. сертификатах с открытым и закрытым ключом;
  2. протоколе Диффи-Хеллмана для безопасного получения ключа симметричного шифрования;
  3. одном из алгоритмов криптостойкого симметричного шифрования.

TLS сертификат позволяет нам доверять домену, с которым мы осуществляем соединение. Домен прописан в сертификате, сертификат подписан закрытым ключом, который есть только у центра сертификации. А по цепочке доверия проверяем, что корневой сертификат есть в списке доверенных корневых сертификатов на нашей машине.

Протокол Диффи-Хеллмана довольно хитроумен и позволяет двум и более сторонам получить общий секретный ключ, используя незащищенный от прослушивания канал связи. Именно этот протокол не дает возможности расшифровать поток данных, который передаётся по HTTPS, когда мы пытаемся прослушивать на сетевом уровне.

Далее в работу вступает один из алгоритмов криптостойкого симметричного шифрования, обычно это DES, AES, ГОСТ 34.12. Для шифрования данных используется секретный ключ, полученный при помощи протокола Диффи-Хеллмана.

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

Можно воспользоваться механизмом HTTP-прокси. Как мы знаем из его определения, это промежуточный транзитный веб-сервер, который используется как посредник между пользователем и конечным сервером. Как раз через него могут проходить все HTTP/HTTPS запросы и ответы. И во всех операционках имеется встроенная поддержка HTTP прокси, в том числе и на Android.

Есть замечательная утилита для прослушки HTTP/HTTPS трафика, которая выступает как HTTP прокси - mitmproxy (https://mitmproxy.org). У неё есть консольный и веб интерфейс.

При запуске утилита выдает два адреса, на одном запущен HTTP прокси, на втором веб-интерфейс (в случае, если была запущена версия с веб-интерфейсом). Далее в настройках сети телефона (Android) указываю прокси:

После этого у нас пропадает на телефоне интернет 😅 Почему? Надо добавить сертификат прокси сервера в доверенные для того, чтобы работали HTTPS соединения.

Для этого в браузере на телефоне переходим на страницу http://mitm.it , и если правильно указан наш прокси, то должны увидеть его страницу с сертификатами под разные платформы. Скачиваем сертификат под Android и добавляем его в доверенные.

После этого мы должны увидеть весь трафик с устройства в веб-интерфейсе.

Но тут нас поджидает ещё одна засада. Начиная с Android 7 в манифесте приложения должно быть явно разрешено использование установленных пользовательских сертификатов. Если этого разрешения нет, то добавленный сертификат попросту игнорируется, и соединение с HTTP прокси выдает ошибку. В Google Chrome, Youtube и некоторых других стандартных приложениях пользовательские сертификаты разрешены,  мы видим их трафик. Но в приложении сервиса, конечно же, такого разрешения не было.

Хорошо если есть под рукой телефон с Android ниже 7 версии, и приложение поддерживает запуск на этой версии. В моем случае в приложении указана минимальная версия Android 8. Приехали?

Нет. Мы не сдаемся! Всем хорошо известно, что APK (формат пакетов приложений в Android) это просто ZIP архив, его можно переименовать в .zip и распаковать как обычно.

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

Не отчаиваемся и нагугливаем утилиту apktool (https://ibotpeaches.github.io/Apktool/). Эта утилита позволяет декомпилировать APK и собрать потом его обратно.

Декомпилируем:

apktool decode app-cool-smile-verification-release_1.2.3.apk

Заходим в папку app-cool-smile-verification-release_1.2.3 и видим наш AndroidManifest.xml в первозданном виде.

Добавляем по инструкции параметр networkSecurityConfig в секцию application манифеста (https://developer.android.com/training/articles/security-config):

<application
    android:networkSecurityConfig="@xml/network_security_config">

И файл res/xml/network_security_config.xml со следующим содержанием:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
 <base-config>
   <trust-anchors>
     <certificates src="system" />
     <certificates src="user" />
   </trust-anchors>
 </base-config>
</network-security-config>

Собираем назад наше приложение и получаем модифицированный apk:

apktool build app-cool-smile-verification-release_1.2.3

Заливаем на телефон и радуемся жизни! Ведь теперь мы видим весь трафик этого приложения в mitmproxy. И сразу становится понятно, почему что-то пошло не так и кто виноват.

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

Будем лечить от проверки на рута!

Как оказалось, это не так уж и сложно. Apktool декомпилирует все классы приложения, которые находятся в dex файлах в виде байткода для виртуальной машины Android в файлы *.smali. SMALI - это образно ассемблер для виртуальной машины Android - язык, описывающий байткод.

Вот пример класса KotlinNullPointerException:

.class public Lkotlin/KotlinNullPointerException;
.super Ljava/lang/NullPointerException;
.source "KotlinNullPointerException.kt"
# direct methods
.method public constructor <init>()V
   .locals 0
   .line 1
   invoke-direct {p0}, Ljava/lang/NullPointerException;-><init>()V
   return-void
.end method

.method public constructor <init>(Ljava/lang/String;)V
   .locals 0
   .line 2
   invoke-direct {p0, p1}, Ljava/lang/NullPointerException;-><init>(Ljava/lang/String;)V
   return-void
.end method

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

> grep -R "На устройстве обнаружен root-доступ"

res/values/strings.xml: <string name="msg_err_app_sec_7">
На устройстве обнаружен root-доступ или установлена модифицированная 
прошивка. Это может нарушать безопасность работы приложения. 
Пожалуйста, напишите нашей техподдержке</string>

Ок, нашли в ресурсах, логично. Теперь ищем идентификатор msg_err_app_sec_7:

> grep -R "msg_err_app_sec_7"

res/values/public.xml:    <public type="string" name="msg_err_app_sec_7" 
id="0x7f120196" />

Находим ещё один идентификатор - 0x7f120196

> grep -R 0x7f120196

smali/ru/coolsmile/presentation/error/a.smali:    const v3, 0x7f120196

Вот он наш файл. Открываем этот файл и находим там упоминание RootAvailableAppException:

.line 2
   :cond_0
   instance-of v0, v6, Lru/rtlabs/ebs/core/api/app/exception/RootAvailableAppException;
    
   if-eqz v0, :cond_1
    
   const v2, 0x7f120126
    
   const v3, 0x7f120196 # это наш код, который мы искали

Судя по окружающему коду это секция catch, в которой ловятся все исключения, и отображается текст ошибки. Значит надо искать, где кидается это исключение.

> grep -R RootAvailableAppException

smali/n/a/a/b/c/b/a/a$a.smali:    new-instance v0, Lru/rtlabs/ebs/core/api/app/exception/RootAvailableAppException;
smali/n/a/a/b/c/b/a/a$a.smali:    invoke-direct {v0, p1, v1}, Lru/rtlabs/ebs/core/api/app/exception/RootAvailableAppException;-><init>(ILjava/lang/String;)V

Открываем файл и находим примерно такой код:

move-result v0

    if-nez v0, :cond_0 # здесь условный оператор, который кидает на метку cond_0

    .line 2
    iget-object p1, p0, Ln/a/a/b/c/b/a/a$b;->e:Ljava/lang/Object;

    return-object p1 # а тут похоже всё хорошо

    .line 3
    :cond_0 # с этого места начинается код кидания исключения
    iget-object v0, p0, Ln/a/a/b/c/b/a/a$b;->c:Lkotlin/u/b/a;

    invoke-interface {v0}, Lkotlin/u/b/a;->a()Ljava/lang/Object;

    .line 4
    new-instance v0, Lru/rtlabs/ebs/core/api/app/exception/RootAvailableAppException;

    invoke-virtual {p1}, Ljava/lang/Integer;->intValue()I

    move-result p1

    iget-object v1, p0, Ln/a/a/b/c/b/a/a$b;->d:Ljava/lang/String;

    invoke-direct {v0, p1, v1}, Lru/rtlabs/ebs/core/api/app/exception/RootAvailableAppException;-><init>(ILjava/lang/String;)V

    throw v0 # вот тут кидается наше исключение
.end method

Судя по всему, если мы удалим условный оператор, то код пойдет по успешной ветке.

Удаляем, собираем APK, заливаем на эмулятор… Барабанная дробь.

Да! Теперь наше многострадальное приложение начинает спокойно работать в эмуляторе!

В заключение

Для тех, кто смог дочитать эту статью до конца, хочу лишь сказать, что взлом это не только плохо, но и хорошо )  И иногда очень помогает в повседневной работе, чтобы разобраться с чем-нибудь, на что утеряны исходники или их никогда и не было. Часто есть желание посмотреть сетевой обмен даже своего мобильного или серверного приложения с внешним миром, именно реальный, а не тот, что мы видим в логах.

А разработчикам я бы дал несколько советов:

  • основное - не хранить в коде приложения любую критичную информацию, такую как пароли, ключи шифрования;
  • также можно усложнить жизнь взломщику, добавив проверку на сертификат разработчика, которым подписывается приложение;
  • сделать несколько уровней проверок, например, первая видимая - для пользователя, вторая - отложенная, будет завершать приложение с какой-нибудь неочевидной ошибкой при нарушении условия.

Для меня это было интересное приключение. Искренне надеюсь, что мой опыт кому-нибудь пригодится.

Спасибо за внимание. Следите за нашими новыми статьями.