Продолжаем цикл статей про аудит iOS-приложений. В этой статье расскажем непосредственно о самом анализе и как пользоваться инструментами из прошлой статьи. Мы хотим показать начинающим исследователям безопасности мобильных приложений, где искать баги, как они могут выглядеть и от чего можно отталкиваться при самостоятельном и более углубленном исследовании мобильных приложений. Для демонстрации мы будем использовать специальные мобильные приложения (SecureStorev1 и SecureStorev2), в которых содержатся некоторые базовые уязвимости.

Приложения были взяты из курса Hacking and Pentesting iOS Applications, который можно назвать свежим (последнее обновление от 07.2021) относительно альтернативных вариантов и содержащим большое количество примеров уязвимостей.

Мы обсудим:

  1. Как подготовить Attack Surface исследуемого приложения.

  2. Какие уязвимости можно найти при анализе.

Подготовка Attack Surface

Под "Attack Surface" понимается область исследования приложения.

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

Все установленные приложения попадают в папку /var/mobile/Containers/Data/Application/ внутри файловой системы. Соответственно, полный путь до данных, с которыми работает приложение внутри песочницы, выглядит следующим образом:

/var/mobile/Containers/Data/Application/<Application ID>

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

/private/var/containers/Bundle/Application/<Bundle ID>

Узнать Application ID, Bundle ID и полные пути до контейнеров можно с помощью Objection (console way) или Grapefruit (GUI way), которые помогают подготовить Attack Surface и в дальнейшем исследовать приложение более комфортным образом.

Приступаем к исследованию и подключаем наше iOS-устройство по USB.

Objection

Сначала необходимо узнать Identifier исследуемого приложения (SecureStorev1). Сделать это можно с помощью команды frida-ps:

$ frida-ps -Uia

В выводе видим, что Indentifier - cst.securestorev1

Identifier исследуемого приложения
Identifier исследуемого приложения

Подключаемся через Objection:

$ objection -g 'cst.securestorev1' explore

И смотрим контейнеры приложения:

cst.securestorev1 on (iPhone: 14.7.1) [usb] # env

Objection. Контейнеры приложения
Objection. Контейнеры приложения

Grapefruit

Запускаем Grapefruit и переходим в веб-интерфейс (по умолчанию поднимается на http://127.0.0.1:31337):

$ igf

В веб-интерфейсе выбираем наш девайс и исследуемое приложение:

Grapefruit. Панель
Grapefruit. Панель

Попадаем на страницу с данными о приложении:

Grapefruit. Securestorev1 Basic
Grapefruit. Securestorev1 Basic

Из увиденного делаем вывод, что поиск интересующих нас данных будет производиться в следующих директориях:

  • /private/var/containers/Bundle/Application/F4C38F12-B7C2-401B-A483-FBF7451A8FD3

  • /var/mobile/Containers/Data/Application/03BB8F81-5684-438C-8021-CD40A5720F19

При дальнейшем исследовании можно использовать как Objection, так и Grapefruit или просто командную строку на iOS-устройстве после подключения по ssh. Так как Grapefruit и так имеет интуитивно понятный интерфейс, то в статье будем пользоваться Objection для лучшего понимания его возможностей и командой строкой iOS-устройства.

Ищем уязвимости

Небезопасное хранение данных

Как и упоминалось выше, "Небезопасное хранение данных" – одна из самых распространенных уязвимостей. Заключается она в том, что разработчики хранят критичные пользовательские данные в контейнерах приложений в открытом виде.

Для продуктивного поиска в Data Storage сначала можно сделать дамп директорий
из Attack Surface сразу же после установки приложения. Потом, пользуясь
функционалом приложения, сравниваем, что изменилось в файлах Data Storage либо в лайв-режиме, либо снова через дамп

Найти эти данные обычно можно в следующих местах:

.plist-файлы

Файлы plist – файлы с XML-структурой, содержащие пары ключ-значение. Это
способ хранения постоянных данных, но иногда разработчики кладут туда
конфиденциальную информацию, чего делать нельзя.

Можно найти в директориях Attack Surface с помощью find:

iPhone $ find . -name '*.plist'

Просматривать .plist-файлы можно либо через plutil в командной строке, либо через Objection:

cst.securestorev1 on (iPhone: 14.7.1) [usb] # ios plist cat <filename>

Анализируем файлы нашего приложения и находим auth_token пользователя в открытом виде внутри userdetails.plist:

auth_token в userdetails.plist
auth_token в userdetails.plist

Файлы локальной БД

Локальная база данных – еще одно место, где часто встречаются пользовательские данные в открытом виде. Самые популярные варианты реализации локальной БД – это SQLite или Realm, файлы для которых можно найти в директориях Attack Surface:

$ find . \( -name "*.db" -o -name "*.realm" \)

В директории из примера выше можно увидеть файл bankdetails.db. Objection позволяет удобно просматривать файлы БД с помощью команды sqlite connect:

cst.securestorev1 on (iPhone: 14.7.1) [usb] # sqlite connect bankdetails.db

После подключения к файлу БД можно вызвать справку через ? для ознакомления с возможностями исследования файла:

Делаем листинг доступных таблиц и забираем данные пользователя в открытом виде:

SQLite @ bankdetails.db > .tables

SQLite @ bankdetails.db > select * from bankdetails

Кэш

Отдельно стоит упомянуть про кэширование сетевых запросов в iOS-приложениях, обычно такая информация находится по пути:

/var/mobile/Containers/Data/Application/<Application ID>/Library/Caches/<Application Identifier>

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

Так это выглядит в нашем приложении:

Коннектимся к файлу Caches.db, который находится в директории /var/mobile/Containers/Data/Application/03BB8F81-5684-438C-8021-CD40A5720F19/Library/Caches/cst.securestorev1

cst.securestorev1 on (iPhone: 14.7.1) [usb] # sqlite connect Cache.db

Делаем листинг таблиц и забираем данные:

SQLite @ bankdetails.db > .tables

SQLite @ bankdetails.db > select * from cfurl_cache_receiver_data

SQLite @ bankdetails.db > select * from cfurl_cache_response

NSUserDefaults и Keychain

И, конечно же, не забываем про проверку NSUserDefaults и Keychain.

Очень частой ошибкой при разработке мобильных приложений является хранение критичных пользовательских данных в NSUserDefaults. Проверить это через Objection можно с помощью следующей команды:

# ios nsuserdefaults get

Если в выводе видны, например, аутентификационные токены или другая важная пользовательская информация, то это считается уязвимостью. Все подобные данные должны храниться в Keychain, но стоит отметить, что не в открытом виде, а в зашифрованном. Проверить содержимое Keychain через Objection можно так:

# ios keychain dump --json <output_filename>

Локальная аутентификация

Если процесс аутентификации в приложении завязан на внутренних проверках – это плохое решение, т.к. обмануть такие проверки не составляет особого труда.

Вспомним про файл userdetails.plist, auth_token в нем – это строка в MD5. Предположим, что там зашит просто логин пользователя (securestore) и проверим это:

$ md5 -s securestore

MD5 ("securestore") = 212174768840da1c6a1604c8b485a0ee

Предположение подтвердилось, а значит, мы можем попробовать изменить этот файл и положить туда логин другого зарегистрированного пользователя (attacker).

Objection позволяет скачивать файлы с iOS-устройства:

cst.securestorev1 on (iPhone: 14.7.1) [usb] # file download userdetails.plist

Получаем значение attacker в MD5 и помещаем его в наш файл:

$ md5 -s attacker

MD5 ("attacker") = 3f858cf8cfd59f25010e71b6b5671428

Через Objection загружаем файл обратно на iOS-устройство:

cst.securestorev1 on (iPhone: 14.7.1) [usb] # file upload userdetails.plist

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

Server-Side проверки

Это один из самых больших блоков проверок, который по сути является тестированием серверного API. Для мобильной версии могут быть отдельные "ручки", в которых либо
могут найтись баги, которых нет в веб-версии, либо может быть необычная
реализация логики, которой можно воспользоваться для чейна какой-нибудь
цепочки.

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

SQL Injection

Снова вернемся к auth_token'у. Он используется при запросе от клиента к серверу для получения данных о счете:

Эксплуатируем SQL Injection и получаем данные пользователя attacker:

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

SQL Injection via End-to-end encryption

Теперь при получении данных о счете в том же запросе token передается в зашифрованном виде:

С помощью frida-trace находим все методы всех классов, которые связаны с шифрованием внутри приложения. Для этого используем выражение с вайлдкардами:

$ frida-trace -U -m "*[* *crypt*]" -n SecureStorev2

Нашлось 388 методов, также frida-trace создала скрипты под каждый из методов в папке handlers:

После обновления деталей счета пользователя securestore видим в выводе активного окна frida-trace, какие методы использовались:

Разберемся, что происходит внутри метода AES256EncryptedDataUsingKey класса NSData с помощью скрипта для frida и сдампим ключ шифрования.

Скрипт для frida будет выглядеть следующим образом:

if (ObjC.available) {
 
try {
 
var className = "NSData";
var funcName = "- AES256EncryptedDataUsingKey:error:";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
 
Interceptor.attach(hook.implementation, {
 
    onEnter: function(args)
    {
    console.log("\n[+] Class Name: " + className);
    console.log("[+] Method Name: " + funcName);
    var enc_key = new ObjC.Object(args[2]); // <-- переменные метода в Frida начинаются со второго индекса
    var buf = enc_key.bytes().readByteArray(enc_key.length());
    console.log("[+] Encryption Key: " + enc_key.toString());
    console.log("[+] Encryption Key dump: " + hexdump(buf, { ansi: true }));
    },
 
    onLeave: function(returnvalue)
    {
    console.log('Return Value: ');
    }
 
    });
 
}
catch(error)
{
 console.log("[!] Exception: " + error.message);
}
}
 
else {
 
console.log("Objective-C Runtime is not available!");
 
}

Заинжектимся с написанным скриптом в наше приложение:

$ frida -U SecureStorev2 -l AES256EncryptedDataUsingKey.js

И после обновления данных о банковском счете увидим ключ шифрования:

В первой версии приложения в качестве токена передавалась MD5-строка с логином пользователя. Проверим, правильно ли происходит ее шифрование со сдампленным ключом с помощью скрипта на python:

import Crypto
import Crypto.Random
from Crypto.Cipher import AES
import base64
import sys
 
 
def pad_data(data):
    if len(data) % 16 == 0:
        return data
    databytes = bytearray(data)
    padding_required = 15 - (len(databytes) % 16)
    databytes.extend(b'\x80')
    databytes.extend(b'\x00' * padding_required)
    return bytes(databytes)
 
def unpad_data(data):
    if not data:
        return data
 
    data = data.rstrip(b'\x00')
    if data[-1] == 128:
        return data[:-1]
    else:
        return data
 
def generate_aes_key():
    rnd = '35e685a1411a605852ec1045ef7f16f10e68dc18bdc1f13fa240a6996edb306c'
    rnd = rnd.decode('hex')
    return rnd
 
def encrypt(key, iv, data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = pad_data(data)
    return aes.encrypt(data)
 
def decrypt(key, iv, data):
    data = data.decode('base64')
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = aes.decrypt(data)
    return unpad_data(data)
 
def test_crypto (input):
    key = generate_aes_key()
    iv = '00000000000000000000000000000000'
    iv = iv.decode('hex')
    encoded = encrypt(key, iv, input)
    return encoded
 
if __name__ == '__main__':
    encrypted = test_crypto(sys.argv[1])
    encrypted = encrypted.encode('base64')
    print encrypted
$ python aesencrypt.py 212174768840da1c6a1604c8b485a0ee
wk3eQ9tu9LM+eq3I0pzst1I191hWOa9f+dpmRBQ6kCU=

Полученное значение совпадает со значением токена в запросе. Значит, теперь мы можем зашифровать наш SQL Injection payload и подставить его в запрос:

$ python aesencrypt.py "securestore' OR '1'='1"
R4Dc8atsDx7T5c488eIedqXpw76hcUtc/d4Euvrre/Y=

В этой статье мы показали, как подготовить Attack Surface исследуемого приложения, познакомились с базовыми уязвимостями, которые могут встречаться в iOS-приложениях, и рассказали, как пользоваться различными инструментами во время анализа. Надеемся, что данный материал будет полезен тем, кто начинает свой путь в исследовании мобильных приложений и станет хорошей отправной точкой для дальнейшего прокачивания скиллов в этой области :)

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


  1. YuriyPashkov
    29.07.2022 18:32
    -2

    Спасибо за статью, очень интересный материал.

    Как лучше всего шифровать SQLite-базу в приложении? Беглое гугление говорит об инструменте под названием SQLCipher. В реальной работе, насколько падает производительность при работе с зашифрованной БД? Разработчики SQLCipher заявляют о 5-15% примерно, но хотелось бы услышать отзывы от тех, кто использовал сей инструмент в продакшене.


    1. storoj
      29.07.2022 19:59
      +3

      Зачем вообще что-то шифровать? Почитать базу из Documents можно наверное только с jailbreak, а если он у юзера есть, то зачем пытаться сопротивляться?

      Плюс sqlite в iOS должен вроде как поддерживать Encryption Extensions: https://www.sqlite.org/see/doc/release/www/index.wiki