Захист iOS-додатків: найкращі практики, інструменти та методи
Всім привіт! Мене звати Сергій. Я працюю iOS Tech Lead у Futurra Group, де відповідаю за розробку мобільних рішень і драйвлю технічну досконалість всередині команди.
У світі, де мобільні застосунки визначають майже кожен аспект нашого повсякденного життя — від банкінгу до спілкування — захист iOS-додатків уже не опція, а необхідність.
Уявімо ситуацію: ваш застосунок набирає обертів, тисячі нових користувачів реєструються щодня, фідбек переважно позитивний, з’являється інтерес з боку інвесторів. І раптом — катастрофа: витік даних. Саме це сталося з Timehop у 2018 році, коли через погано захищений API зловмисники отримали доступ до персональних даних понад 21 мільйона користувачів, включно з іменами, email-адресами, номерами телефонів і соціальними токенами. Одне слабке місце в безпеці — і довіра зруйнована.
Це не поодинокий виняток, а попередження. Навіть в умовах суворо контрольованої екосистеми Apple ваш iOS-застосунок за замовчуванням не є безпечним. У цій статті ми розберемо перевірені практики безпеки, зручні для розробників інструменти та battle-tested техніки, які допоможуть створити застосунок, що буде не просто працездатним, а по-справжньому захищеним. Я також покажу, як убезпечнити застосунок відповідно до стандарту OWASP MASVS (Mobile Application Security Verification Standard).
OWASP MASVS визначає вичерпний набір вимог до безпеки, які застосовні до мобільних застосунків як на iOS, так і на Android. Ці вимоги організовані у два рівні: L1 та L2.
L1 описує базові засоби захисту, яким має відповідати будь-який мобільний застосунок незалежно від його призначення. Натомість L2 призначений для застосунків, що працюють із високочутливими даними — такими, як наш.
Окрім основних вимог, стандарт доповнений детальним чеклістом на основі Mobile Security Testing Guide (MSTG), який можуть використовувати команди тестування для валідації реалізації та перевірки повної відповідності вимогам.
Ключові практики безпеки для кожного iOS-застосунку
У цьому розділі розглянемо базові заходи безпеки, які мають бути реалізовані в будь-якому iOS-додатку — незалежно від його розміру, призначення чи аудиторії. Від безпечного зберігання даних та шифрованої комунікації до коректної обробки аутентифікації — ці підходи формують фундамент для створення надійних і безпечних застосунків.
SSL pinning
За замовчуванням, коли встановлюється HTTPS-з’єднання, клієнт перевіряє SSL-сертифікат сервера. Однак ця перевірка лише гарантує, що сертифікат підписаний довіреним центром сертифікації (Certificate Authority, CA), але не те, що він належить саме вашому серверу. Така залежність від системного сховища довірених сертифікатів створює потенційну вразливість: зловмисник може скористатися цією моделлю довіри, встановивши на пристрій шкідливий сертифікат і таким чином реалізувавши атаку man-in-the-middle (MitM).
Більшість iOS-застосунків використовують TLS (Transport Layer Security) для захищеної комунікації. Проте якщо застосунок явно не визначає, яким сертифікатам він довіряє, він автоматично успадковує довіру від системного сховища сертифікатів iOS, яке може бути скомпрометоване. Якщо користувач встановить шкідливий кореневий сертифікат, зловмисник зможе перехоплювати та розшифровувати трафік, потенційно отримуючи доступ до чутливих даних — таких як облікові дані або персональна інформація.
Ще гірше, якщо трафік передається у відкритому вигляді через HTTP: у такому разі будь-хто, хто перебуває в тій самій мережі, може його прочитати. Щоб перевірити, чи дозволяє ваш застосунок такий трафік, перегляньте файл Info.plist у секції налаштувань NSAppTransportSecurity.
![]()
Щоб захиститися від цього, можна використати SSL pinning — техніку, за якої застосунок налаштовується так, щоб довіряти лише конкретним сертифікатам або публічним ключам. Під час встановлення з’єднання (handshake) застосунок порівнює сертифікат сервера з «прикріпленим» (pinned) сертифікатом або ключем. Якщо вони збігаються — з’єднання продовжується, якщо ні — воно переривається.
Існує два основні підходи до pinning:
- Certificate pinning — зберігання безпосередньо серверного сертифіката всередині застосунку.
- Public key pinning — зберігання хеша публічного ключа сертифіката.
Apple рекомендує використовувати хеші публічних ключів центру сертифікації (CA), оскільки це забезпечує гнучкість: коли сертифікат оновлюється, немає потреби випускати нову версію застосунку, якщо використовується той самий CA.

Для деталей реалізації Apple надає офіційні рекомендації.
Запобігання витоку даних
Немає сумнівів, що зі зростанням кількості мобільних застосунків зловмисники стають дедалі більш витонченими в тому, як вони експлуатують вразливості для отримання доступу до конфіденційних даних користувачів. Одна з поширених, але часто недооцінених загроз — це витік даних, який може відбуватися ненавмисно. Наприклад, коли застосунок переходить у фоновий режим, система робить скриншот для прев’ю в меню багатозадачності, потенційно «засвічуючи» чутливу інформацію.
Щоб зменшити цей ризик, розробники можуть застосувати кілька ефективних підходів:
- Додавати повноекранний оверлей, наприклад розмитий blur-екран, коли застосунок переходить у фон.
- Очищувати поля введення з чутливими даними, особливо ті, де містяться паролі або персональна інформація, — це підвищує рівень приватності, не погіршуючи досвід користувача.
- Автоматично перенаправляти користувача на безпечний екран, наприклад екран входу або домашній екран, коли застосунок знову стає активним.
var blurView: UIVisualEffectView?
func sceneWillResignActive(_ scene: UIScene) {
let blurEffect = UIBlurEffect(style: .light) // .dark, .extraLight
blurView = UIVisualEffectView(effect: blurEffect)
blurView?.frame = UIScreen.main.bounds
blurView?.isUserInteractionEnabled = false
if let window = UIApplication.shared.windows.first, let blurView = blurView {
window.addSubview(blurView)
}
}
func sceneDidBecomeActive(_ scene: UIScene) {
blurView?.removeFromSuperview()
blurView = nil
}
Такі запобіжні заходи допомагають гарантувати, що конфіденційні дані не будуть розкриті через системні знімки екрана або ненавмисну видимість у фоновому режимі.
Захист даних
Фізична крадіжка смартфона може бути не менш небезпечною, ніж цифрові атаки. Для зловмисника доступ до персональних даних жертви часто цінніший, ніж перепродаж самого пристрою. Саме тому надійний захист чутливої інформації локально — особливо у випадках втрати чи крадіжки телефону — критично важливий.
Щоб зменшити цей ризик, ми реалізували біометричну аутентифікацію в поєднанні з PIN-кодом, який запитується під час кожного запуску застосунку. Водночас важливо памʼятати, що некоректне використання біометричних API може, навпаки, створити нові вразливості.
Огляд варіантів зберігання даних в iOS
iOS надає кілька механізмів для зберігання даних — кожен із власним призначенням, рівнем безпеки та поведінкою щодо збереження. Правильний вибір варіанту є критично важливим і для продуктивності, і для безпеки.
1. UserDefaults
- Призначення: Зберігання невеликих обсягів даних, таких як налаштування.
- Безпека: ❌ Не шифрується. Дані доступні навіть тоді, коли пристрій заблокований.
- Використовувати для: Лише неконфіденційних даних.
- Не зберігати: Паролі, токени, PII (персонально ідентифіковану інформацію).
2. Keychain
- Призначення: Безпечне зберігання чутливих облікових даних, токенів або паролів.
- Безпека: ✅ Дані шифруються з використанням апаратних засобів безпеки пристрою. Можуть бути привʼязані до біометрії, коду-пароля або стану розблокування через SecAccessControl.
- Використовувати для: Паролів, access-токенів, приватних ключів.
- Підтримує: Привʼязку до біометрії, iCloud Keychain (якщо не використовується рівень доступу ThisDeviceOnly).
- Призначення: Апаратний копроцесор для керування ключами та біометричними даними.
- Безпека: ✅✅ Максимальний рівень. Ізольований від основної ОС та CPU.
- Використовувати для: Приватних ключів, даних Face ID/Touch ID, високовартісних цифрових активів.
- Доступ: Через Keychain API з використанням kSecAttrTokenIDSecureEnclave.
4. Файлова система (Documents, Library, Caches)
- Призначення: Зберігання контенту, створеного користувачем, медіафайлів, офлайн-кешу, завантажених файлів.
- Безпека: ⚠️ За замовчуванням не шифрується, якщо не ввімкнено захист через NSFileProtection.
- Використовувати для: Документів користувача, файлів, які потрібні між запусками застосунку.
- Рекомендації: Для чутливих файлів використовуйте атрибути NSFileProtectionComplete або сильніші.
- Призначення: Локальне зберігання структурованих даних (наприклад, офлайн-база даних).
- Безпека: ⚠️ Не шифрується за замовчуванням. За потреби шифрування потрібно реалізовувати окремо.
- Використовувати для: Складних моделей даних, реляційних структур.
- Рекомендація: Поєднувати з файловим захистом або шифрувати окремі поля вручну.
6. iCloud Storage (iCloud Drive / iCloud Key-Value Store / CloudKit)
- Призначення: Синхронізація даних між пристроями через хмарну інфраструктуру Apple.
- Безпека: ✅ Дані шифруються під час передачі та у стані спокою. CloudKit підтримує приватні й публічні бази даних.
- Використовувати для: Крос-девайсної синхронізації неконфіденційних або явним чином схвалених користувачем даних.
- Зауваження: Не використовуйте для паролів, якщо вони не захищені й не погоджені користувачем.
- Призначення: Легка key-value синхронізація між пристроями через iCloud.
- Безпека: Дані шифруються через iCloud, але механізм не призначений для зберігання високочутливих секретів.
- Використовувати для: Невеликих налаштувань користувача, що синхронізуються між пристроями.
- Не підходить для: Токенів, облікових даних або персональних даних.
8. Тимчасове сховище (tmp Directory)
- Призначення: Зберігання даних, які не повинні зберігатися між запусками застосунку.
- Безпека: ❌ Дані не шифруються, і система може видалити їх у будь-який момент.
- Використовувати для: Тимчасових даних, наприклад проміжної обробки зображень, кешу.
- Не використовувати для: Будь-чого чутливого або важливого.
- Призначення: Спільне використання даних між застосунками одного розробника або між основним застосунком та його розширеннями.
- Безпека: Залежить від того, які саме API для зберігання використовуються (Keychain, FileManager тощо).
- Використовувати для: Обміну даними між застосунком і розширеннями (наприклад, віджети, Share Extensions).
- Рекомендація: Застосовувати ті самі рівні захисту (наприклад, файловий захист, Keychain), що й у основному застосунку.

Одним із ключових аспектів використання Keychain є визначення політик контролю доступу, які задають умови, за яких збережені дані можуть бути розшифровані та прочитані.

Apple надає приклад коду для реалізації біометричного входу, але деякі з цих прикладів є небезпечними — вони часто покладаються на простий булевий прапорець для визначення успішної аутентифікації. Зловмисники можуть легко його підмінити.
Приклад реалізації біометричного логіну з документації Apple:
Task {
do {
try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Log in to your account")
state = .loggedin
} catch let error {
print(error.localizedDescription)
// Fall back to a asking for username and password.
// ...
}
}
✅ Безпечніший підхід: використання прапорів контролю доступу
Набагато надійніша реалізація передбачає створення елементів Keychain із чітко визначеним контролем доступу, привʼязаних до біометричної аутентифікації та конкретних станів розблокування пристрою.
Потрібно визначити два ключові елементи:
- Рівень доступу (Access level) — визначає, коли саме дані можуть бути доступні (наприклад, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly гарантує доступ лише тоді, коли пристрій розблоковано і на ньому встановлено код-пароль. Суфікс ThisDeviceOnly не дозволяє цим даним синхронізуватися через iCloud або потрапляти до резервних копій).

2. Рівень аутентифікації (Authentication level) — визначає, чи потрібна біометрична аутентифікація перед доступом до даних.
Використання SecAccessControlCreateFlags.userPresence додає вимогу біометричної перевірки та автоматично запускає резервний сценарій (наприклад, введення коду-пароля), якщо біометрична валідація не пройшла.
Приклад: Створення та доступ до захищеного елемента Keychain
Створення Access Control:
let access = SecAccessControlCreateWithFlags(nil, // Use the default allocator. kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, nil) // Ignore any error.
Додавання даних до Keychain:
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrAccount as String: account, kSecAttrServer as String: server, kSecAttrAccessControl as String: access as Any, kSecUseAuthenticationContext as String: context, kSecValueData as String: password]
Читання даних з Keychain:
let context = LAContext() context.localizedReason = "Access your password on the keychain" let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecUseAuthenticationContext as String: context, kSecReturnData as String: true]
Шифрування персональних даних
Ручне керування процесами шифрування та розшифрування в iOS-застосунках є схильним до помилок і може призвести до критичних вразливостей. Без надійного криптографічного підґрунтя чутливі дані користувачів стають легкою ціллю для зловмисників.
Щоб спростити й одночасно посилити процес шифрування, ми обрали фреймворк Apple CryptoKit — нативний Swift API, який надає безпечні та ефективні криптографічні операції. На відміну від сторонніх рішень, CryptoKit краще інтегрується з системою, забезпечує вищу продуктивність і активно підтримується Apple з урахуванням оптимізацій та захистів на рівні ОС.
Розглянемо практичний приклад безпечного зберігання та відновлення облікових даних користувача з використанням CryptoKit у поєднанні з Keychain.
Приклад на Swift: хешування, деривація ключа за допомогою HKDF та зберігання в Keychain (CryptoKit)
import Foundation
import CryptoKit
import Security
func storeCredentials(username: String, password: String) -> Bool {
// 1. Generate random salt
let salt = UUID().uuidString
let saltData = Data(salt.utf8)
// 2. Hash username with salt
let usernameData = Data(username.utf8) + saltData
let usernameHash = SHA256.hash(data: usernameData)
// 3. Hash password with salt
let passwordData = Data(password.utf8) + saltData
let passwordHash = SHA256.hash(data: passwordData)
// 4. Derive symmetric key using HKDF
let hkdfKey = HKDF<SHA256>.deriveKey(
inputKeyMaterial: SymmetricKey(data: passwordData),
salt: saltData,
info: Data("auth-key".utf8),
outputByteCount: 32
)
// 5. Convert everything to storable format
let encodedUsername = Data(usernameHash).base64EncodedString()
let encodedPassword = Data(passwordHash).base64EncodedString()
let keyMaterial = hkdfKey.withUnsafeBytes { Data($0).base64EncodedString() }
// 6. Combine all parts
let record = "user:\(encodedUsername);pass:\(encodedPassword);key:\(keyMaterial);salt:\(salt)"
guard let recordData = record.data(using: .utf8) else { return false }
// 7. Prepare and store in Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "com.example.credentials",
kSecValueData as String: recordData,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary) // Remove old entry if exists
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
Пояснення: End-to-End захист облікових даних
Ця функція демонструє повний пайплайн безпечної обробки та зберігання облікових даних користувача в iOS-застосунку з використанням нативних криптографічних інструментів Apple.
Що саме відбувається:
- Salt Generation:
Створюється випадкова сіль, яка гарантує унікальність хешів навіть тоді, коли різні користувачі мають однаковий пароль. Використання солі також захищає від атак із використанням rainbow-таблиць. - SHA256 Hashing:
Імʼя користувача та пароль хешуються алгоритмом SHA256 разом із сіллю, утворюючи незворотні дайджести. - HKDF Key Derivation:
За допомогою реалізації HKDF у CryptoKit ми отримуємо надійний симетричний ключ із пароля та солі. Цей ключ надалі можна використовувати для шифрування, HMAC-перевірок або безпечного зберігання сесій. - Secure Storage in Keychain:
Усі оброблені значення (хеш логіна, хеш пароля, ключ і сіль) серіалізуються та зберігаються в Keychain, що гарантує доступ до даних лише тоді, коли пристрій розблокований, і виключно на поточному пристрої (kSecAttrAccessibleWhenUnlockedThisDeviceOnly).
Такий підхід забезпечує надійний захист від офлайн-атак, а за коректної реалізації на стороні сервера дає змогу побудувати високобезпечний механізм аутентифікації.
Узгодження ключів (Key Agreement)
У безпечній iOS-розробці гарантування приватної та захищеної від підміни комунікації між двома сторонами є однією з базових вимог — але й серйозним викликом. Чутливі дані, що передаються незахищеними або некоректно реалізованими каналами, ризикують бути перехопленими, зміненими чи використаними для імперсонації, що ставить під загрозу і довіру користувачів, і цілісність застосунку.
Щоб знизити ці ризики, ми інтегруємо у модель безпеки криптографічні протоколи узгодження ключів — зокрема, перевірений часом обмін ключами Diffie—Hellman, реалізований через нативний фреймворк Apple CryptoKit.
Як це працює на практиці
У нашій реалізації обидві сторони — наприклад, клієнт і сервер — незалежно генерують власні пари ключів із використанням еліптичної криптографії Curve25519 через CryptoKit.
Покроково це виглядає так:
- Key Generation:
Кожна сторона генерує приватний ключ (Curve25519.KeyAgreement.PrivateKey). На його основі обчислюється публічний ключ, який потім передається іншій стороні навіть через незахищений канал. - Key Agreement:
Після отримання публічного ключа іншої сторони кожен учасник використовує власний приватний ключ, щоб обчислити спільний секрет за допомогою методу sharedSecretFromKeyAgreement. - Key Derivation:
Спільний секрет пропускається через HKDF (HMAC-based Key Derivation Function) разом із сіллю, у результаті чого формується симетричний ключ потрібної довжини (наприклад, 32 байти). - Key Match:
Оскільки обидві сторони використовували відповідні пари приватного й публічного ключів та однакову сіль, вони отримують ідентичні симетричні ключі — при цьому сам секрет ніколи не передається по мережі.
Цей процес гарантує, що навіть якщо зловмисник перехопить публічні ключі під час передачі, без доступу до приватних ключів він не зможе обчислити спільний секрет. Отриманий симетричний ключ надалі може використовуватися для шифрування трафіку за допомогою AES-GCM або інших методів аутентифікованого шифрування.
Такий підхід забезпечує сильну конфіденційність, пряму (forward) секретність та усуває ризики, пов’язані з витоком статичних ключів або хардкодингом секретів у застосунку.
import Foundation
import CryptoKit
// MARK: - Simulating Two Parties: Alice and Bob
// Alice generates her private key
let alicePrivateKey = Curve25519.KeyAgreement.PrivateKey()
// Alice shares this public key with Bob
let alicePublicKey = alicePrivateKey.publicKey
// Bob generates his private key
let bobPrivateKey = Curve25519.KeyAgreement.PrivateKey()
// Bob shares this public key with Alice
let bobPublicKey = bobPrivateKey.publicKey
// MARK: - Each side computes the shared secret
// Alice receives Bob's public key and computes shared secret
let aliceSharedSecret = try alicePrivateKey.sharedSecretFromKeyAgreement(with: bobPublicKey)
// Bob receives Alice's public key and computes shared secret
let bobSharedSecret = try bobPrivateKey.sharedSecretFromKeyAgreement(with: alicePublicKey)
// Both sides derive a symmetric key from the shared secret
let aliceSymmetricKey = aliceSharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: "someSaltValue".data(using: .utf8)!,
sharedInfo: Data(),
outputByteCount: 32
)
let bobSymmetricKey = bobSharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: "someSaltValue".data(using: .utf8)!,
sharedInfo: Data(),
outputByteCount: 32
)
// MARK: - Verifying both sides produced the same key
if aliceSymmetricKey == bobSymmetricKey {
print("✅ Shared symmetric key successfully derived and matched!")
} else {
print("❌ Keys do not match -- something went wrong.")
}
Узгоджене генерування ключів гарантує, що жоден реальний секрет не передається мережею. Навіть якщо зловмисник перехопить обмін публічними ключами, він не зможе обчислити фінальний спільний ключ без доступу до приватного ключа.
Інтегруючи протокол узгодження ключів Diffie—Hellman у наш захищений канал комунікації, ми позбуваємось необхідності хардкодити або передавати статичні ключі, посилюючи криптографічний фундамент застосунку. Такий підхід забезпечує конфіденційний, forward-secure обмін даними, що особливо цінно для peer-to-peer-комунікацій, захищеного обміну повідомленнями та шифрованих API-запитів.
Двофакторна автентифікація (2FA)
Класичної парольної автентифікації вже недостатньо, щоб надійно захищати чутливі облікові записи користувачів — особливо з огляду на сучасні вектори атак, такі як credential stuffing та фішинг.
Надійні механізми автентифікації — це камінь безпечної розробки iOS-застосунків. Один із найефективніших і найпоширеніших підходів — двофакторна автентифікація (2FA), яка додає ще один рівень захисту поверх пароля користувача.
Вимагаючи два різні фактори підтвердження особи — зазвичай щось, що користувач знає (пароль), і щось, чим він володіє (одноразовий пароль з обмеженим часом дії, TOTP) — 2FA суттєво знижує ймовірність несанкціонованого доступу, навіть якщо пароль став відомий третім особам або використовується повторно.
Ми інтегрували 2FA в наш застосунок за допомогою перевірених інструментів, таких як Google Authenticator та Authy, які забезпечують безпечну генерацію одноразових токенів на основі часу. Ці бібліотеки пропонують розробникам зручний інтерфейс і надійну криптографічну основу.
Щоб залишатися стійкими до нових загроз, важливо періодично переглядати реалізацію 2FA, узгоджувати її з актуальними стандартами безпеки (наприклад, TOTP/HOTP згідно з RFC 6238/4226) і відстежувати користувацьку активність на предмет можливих зловживань.
Результатом стає більш захищений і надійний досвід користування застосунком, який утримує баланс між безпекою та зручністю — критична вимога в сучасному середовищі кіберзагроз.
Ось приклад того, як реалізувати перевірку TOTP (Time-based One-Time Password) в iOS за допомогою Swift. Ви можете використати бібліотеку OneTimePassword — open-source-рішення, сумісне з Google Authenticator, Authy та іншими клієнтами.
Приклад на Swift: валідація TOTP за допомогою OneTimePassword
Спочатку додайте бібліотеку через Swift Package Manager:
https://github.com/mattrubin/OneTimePassword.git
Генерація секрету та перевірка TOTP-коду:
import OneTimePassword
func generateAndVerifyCode(secretString: String, userInputCode: String) -> Bool {
guard let secretData = base32DecodeToData(secretString) else {
print("Invalid secret")
return false
}
// 1. Create a generator using the shared secret
var generator = Generator(
factor: .timer(period: 30), // standard 30-second interval
secret: secretData,
algorithm: .sha1,
digits: 6
)
// 2. Generate the current valid code
let currentPassword = try? generator?.password(at: Date())
// 3. Compare with the code entered by the user
return currentPassword == userInputCode
}
Пояснення:
- secretString — це спільний секрет, який зберігається в застосунку-аутентифікаторі користувача (наприклад, Authy або Google Authenticator), а також на вашому бекенді.
- Користувач вводить
6-значний код, згенерований у своєму застосунку. - Ваш застосунок обчислює очікуваний код на основі поточного часу за алгоритмом TOTP і порівнює його з введеним значенням.
Додаткові рекомендації:
- Зберігайте секрети у форматі Base32.
- Під час налаштування 2FA генеруйте QR-код із URI формату otpauth://, щоб спростити додавання облікового запису в Google Authenticator або подібні застосунки.
- Захищайте збережені секрети за допомогою Keychain + Secure Enclave.
- Під час валідації на сервері дозвольте допуск у ±1 крок часу (time-step), щоб компенсувати можливий дрейф годинника.
Signature Verification
Без надійної перевірки цілісності коду iOS-застосунки залишаються вразливими до серйозних загроз безпеці — зокрема встановлення несанкціонованого, зміненого або шкідливого коду. Подібні компрометації можуть поставити під загрозу приватність користувачів, конфіденційність даних і навіть стабільність самого пристрою.
Apple протидіє цій загрозі за допомогою жорсткої системи підпису коду, яка є каменем безпечної екосистеми застосунків. Кожен iOS-додаток має бути підписаний дійсним сертифікатом, виданим Apple. Це гарантує автентичність, походження та цілісність виконуваного коду до того, як він буде запущений на пристрої.

Коли застосунок встановлюється або запускається, операційна система виконує сувору перевірку його підпису коду. Це забезпечує ланцюжок довіри — від Apple до розробника і далі до самого застосунку — що допомагає запобігти виконанню зміненого або неперевіреного коду.
Розробники, які входять до Apple Developer Program, мають підписувати свої застосунки сертифікатами, виданими Apple. Це додає ще один рівень контролю й підвищує загальну безпечність екосистеми App Store. Користувачі виграють від цієї системи, оскільки можуть бути впевненими, що застосунки на їхніх пристроях надходять із перевірених та надійних джерел.
Більше того, Apple застосовує перевірку підпису коду не тільки до основного бінарного файла застосунку, але й до динамічно підключених фреймворків та бібліотек, гарантуючи, що жодна частина оточення runtime не може бути тихо змінена або перехоплена.
Цей end-to-end механізм перевірки цілісності демонструє прагнення Apple підтримувати безпечну мобільну екосистему, захищаючи як користувачів, так і розробників від ризиків, пов’язаних з ненадійним програмним забезпеченням.
Розширені техніки безпеки з OWASP MASVS
У цьому розділі ми глибше занурюємося в просунуті стратегії безпеки, засновані на стандарті OWASP Mobile Application Security Verification Standard (MASVS). Розділ орієнтований на розробників, які працюють над застосунками, що оперують чутливими або високоризиковими даними. Тут розглядаються захисти рівня L2, моделювання загроз, протидія реверс-інжинірингу, безпечна реалізація криптографії та практичне використання чекліста тестування MSTG.
Виявлення jailbreak
Пристрої з jailbreak становлять суттєву загрозу для безпеки мобільних застосунків. На зламаному iPhone критичні системні обмеження обходяться, що дозволяє зловмисникам маніпулювати поведінкою застосунків, обходити перевірки безпеки, витягувати конфіденційні дані та впроваджувати шкідливий код. Саме тому визначення факту jailbreak у runtime є важливим компонентом будь-якого захищеного iOS-застосунку.
Щоб спростити й зробити ефективнішим виявлення jailbreak, ми об’єднали найкращі рішення в одну бібліотеку — SecurityKit, легкий та динамічний open-source-фреймворк, спеціально створений для задач безпеки в iOS, включно з детекцією jailbreak.
Налаштування Info.plist
Щоб забезпечити коректне виявлення jailbreak, необхідно оновити файл Info.plist вашого застосунку, дозволивши перевірку на наявність поширених URL-схем, пов’язаних з jailbreak-застосунками:
<key>LSApplicationQueriesSchemes</key> <array> <string>cydia</string> <string>undecimus</string> <string>sileo</string> <string>zbra</string> <string>filza</string> </array>
Базова перевірка на наявність Jailbreak
Щоб виконати просту перевірку true/false на jailbreak за допомогою SecurityKit:
if SecurityKit.isJailBroken() {
print("This device is jailbroken")
} else {
print("This device is not jailbroken")
}
Перевірка на Jailbreak із детальним повідомленням
Якщо вам потрібен додатковий контекст щодо того, який саме індикатор спрацював під час виявлення jailbreak, використовуйте наступний метод:
let jailbreakStatus = SecurityKit.isJailBrokenWithErrorMessage()
if jailbreakStatus.jailbroken {
print("This device is jailbroken")
print("Because: \(jailbreakStatus.errorMessage)")
} else {
print("This device is not jailbroken")
}
Перевірка на Jailbreak зі списком невдалих перевірок
Для глибшої діагностики ви можете отримати список усіх перевірок, які не пройдено:
let jailbreakStatus = SecurityKit.isJailBrokenWithErrorDetects()
if jailbreakStatus.jailbroken {
print("This device is jailbroken")
print("The following checks failed: \(jailbreakStatus.errorDetects)")
}
Інтегруючи SecurityKit, розробники отримують потужний, гнучко налаштовуваний шар для виявлення jailbreak без необхідності власноруч будувати складні перевірки. Це допомагає гарантувати, що ваш застосунок поводиться безпечно та передбачувано — навіть у високоризикових середовищах.
Виявлення симулятора
Запуск iOS-застосунку в симуляторі корисний для розробки й тестування, але водночас може відкрити можливості для зловживань, якщо певні дії не обмежені або не відстежуються. На відміну від реальних пристроїв, симулятори не забезпечують апаратні межі безпеки та можуть дозволяти обхід важливих перевірок, зокрема детекції jailbreak, біометричної автентифікації та захищеного зберігання даних.
Щоб запобігти некоректному використанню та захистити чутливі операції, часто потрібно визначати, чи запущено застосунок у симуляторі, і відповідно реагувати — наприклад, відключати критичний функціонал, логувати діагностичні події або показувати попередження в продакшн-збірках.
У нашій бібліотеці SecurityKit реалізовано простий і надійний механізм виявлення симулятора. Скористайтеся таким методом із SecurityKit, щоб визначити, чи працює застосунок наразі в симуляторі:
if SecurityKit.isSimulator() {
print("app is running on the simulator")
} else {
print("app is not running on the simulator")
}
Ця перевірка дає змогу умовно вмикати або вимикати функціональність, яка не повинна працювати в середовищі симулятора — наприклад, взаємодію з продакшн-API, виконання чутливих криптографічних операцій або доступ до обмежених ресурсів.
Виявлення реверс-інжинірингу
Реверс-інжиніринг є однією з найсерйозніших загроз для iOS-застосунків. Зловмисники можуть намагатися досліджувати, змінювати або інʼєктувати власну логіку в роботу вашого застосунку, щоб обійти перевірки безпеки, викрасти дані, вимкнути оплату або експлуатувати бізнес-логіку. Поширені інструменти на кшталт Frida, Needle чи cycript часто використовуються для маніпуляції внутрішніми механізмами застосунку в рантаймі.
Щоб протидіяти таким загрозам, важливо проактивно виявляти ознаки запуску інструментів реверс-інжинірингу на пристрої. З цією метою ми зібрали найефективніші підходи до детекції та додали їх до нашої бібліотеки SecurityKit, яка забезпечує виявлення підозрілої поведінки, інструментів та змін у системі в рантаймі.
Скористайтеся таким методом, щоб визначити, чи демонструє пристрій будь-які ознаки активності реверс-інжинірингу:
if SecurityKit.isReverseEngineered() {
print("This device has reverse engineering tools")
} else {
print("This device does not have reverse engineering tools")
}
Цей метод повертає true, якщо виявлено будь-які відомі індикатори — наприклад, підозрілі файли, бібліотеки, порти або системні прапорці.
Цей метод використовується для визначення стану реверс-інжинірингу зі списком невдалих перевірок (failed checks):
let reStatus = SecurityKit.isReverseEngineeredWithErrorDetect()
if reStatus.reverseEngineered {
print("SecurityKit: This device has evidence of reverse engineering")
print("SecurityKit: The following detects failed: \(reStatus.errorDetect)")
}
Це дає змогу логувати або цілеспрямовано реагувати на конкретні спроби реверс-інжинірингу, зокрема:
- завантажені бібліотеки на кшталт FridaGadget або libcycript
- наявність файлів, наприклад /usr/sbin/frida-server
- відкриті порти, які часто використовуються інструментами (наприклад, 27042 для Frida, 4444 для Needle)
- kernel-прапорці, що вказують на моніторинг процесу (наприклад, P_SELECT)
SecurityKit обгортає всі ці перевірки в єдину надійну утиліту, даючи змогу відстежувати одразу кілька векторів реверс-інжинірингу з мінімальними накладними витратами. Ви можете використовувати цю логіку детекції, щоб:
- запускати тихі сповіщення або логування
- блокувати виконання чутливого функціоналу
- адаптувати поведінку застосунку в «ворожому» середовищі
Виявлення дебагера
Виявлення та обробка факту підключення дебагера є критично важливим заходом безпеки в iOS-розробці — особливо для застосунків, що працюють із чутливими даними, преміальним функціоналом або пропрієтарною логікою. Коли зловмисник під’єднує дебагер (наприклад, LLDB) до вашого застосунку, він може досліджувати пам’ять, обходити рантайм логіку, ставити breakpoints або втручатися в процес білда.
З цією метою ми зібрали найкращі підходи в нашій бібліотеці, яка надає кілька рівнів виявлення та протидії дебагеру в runtime. Вона допомагає ідентифікувати підключення дебагера, виявляти інʼєктовані breakpoints або watchpoints і, за потреби, проактивно забороняти дебагінг.
Виявлення факту дебагінгу застосунку
Цей метод використовується для визначення того, чи перебуває застосунок у стані дебагінгу:
let isDebugged: Bool = SecurityKit.isDebugged()
Використовуйте це, щоб запускати логування, блокувати чутливий функціонал або примусово завершувати роботу застосунку за підозрілих умов.
Цей метод використовується для блокування дебагера та підвищення стійкості застосунку:
SecurityKit.denyDebugger()
Це проактивно блокує доступ дебагера та ускладнює реверс-інженерам підключення інструментів інструментування на кшталт LLDB або Frida.
Цей метод використовується для виявлення того, чи був застосунок запущений кимось/чимось іншим, а не через LaunchD (тобто застосунок було запущено дебагером)
let isNotLaunchD: Bool = SecurityKit.isParentPidUnexpected()
Цей метод використовується для виявлення того, чи встановлено будь-які breakpoints у функції:
func denyDebugger() {
// Add a breakpoint here to test detection
}
typealias FunctionType = @convention(thin) () -> ()
let func_denyDebugger: FunctionType = denyDebugger // Required type annotation
let func_addr = unsafeBitCast(func_denyDebugger, to: UnsafeMutableRawPointer.self)
let hasBreakpoint: Bool = SecurityKit.hasBreakpointAt(func_addr, functionSize: nil)
Цей метод використовується для виявлення того, чи використовується watchpoint. Watchpoint — це різновид breakpoint, яка «стежить» за ділянкою пам’яті, пов’язаною з певним елементом даних.
func testWatchpoint() -> Bool {
var ptr = malloc(9)
var count = 3
return SecurityKit.hasWatchpoint()
}
Розмістіть watchpoint-и LLDB на цих змінних, щоб протестувати механізм виявлення (наприклад, watchpoint set expression ptr тощо).
Integrity Detection
Один із найчастіше недооцінених аспектів безпеки мобільних застосунків — це гарантія цілісності бінарника, тобто впевненість у тому, що застосунок не було змінено після підпису й публікації. Зловмисники можуть намагатися перепакувати застосунок, модифікувати код, інʼєктувати шкідливу логіку або встановити на пристрій підроблену, змінену версію.
Щоб захиститися від цього, застосунок має виконувати runtime-перевірки власної цілісності. Ми використовуємо наш SecurityKit, який надає інструменти для валідації цілісності підпису, перевірки неочікувано завантажених бібліотек і контролю того, що ключові ідентифікатори (такі як bundle ID або provisioning profile) відповідають очікуваним значенням.
Цей метод використовується для виявлення того, чи було застосунок модифіковано:
if SecurityKit.isTampered(
[.bundleID("com.app.bundle"),
.mobileProvision("your-mobile-provision-sha256-value")]
).result {
print("SecurityKit: I have been Tampered.")
} else {
print("SecurityKit: I have not been Tampered.")
}
Це гарантує, що запущений застосунок відповідає очікуваним ідентифікаційним атрибутам. Якщо хтось повторно підпише або перепакує ваш застосунок з іншим bundle ID чи зміненим файлом .mobileprovision, ця перевірка не пройде.
Цей метод використовується для отримання значення SHA256-хешу виконуваного файла у вказаному образі:
// Manually verify SHA256 hash value of a loaded dylib
if let hashValue = SecurityKit.getMachOFileHashValue(.custom("SecurityKit")),
hashValue == "6d8d460b9a4ee6c0f378e30f137cebaf2ce12bf31a2eef3729c36889158aa7fc" {
print("SecurityKit: I have not been Tampered.")
} else {
print("SecurityKit: I have been Tampered.")
}
Це дає змогу порівняти поточний бінарник під час виконання з очікуваним хешем. Це потужний спосіб переконатися, що в ядро виконуваного коду не було внесено жодних змін.
Цей метод використовується для пошуку всіх завантажених динамічних бібліотек (dylibs) у вказаному образі:
if let loadedDylib = SecurityKit.findLoadedDylibs() {
print("SecurityKit: Loaded dylibs: \(loadedDylib)")
}
Моніторинг рантайм середовища на наявність підозрілих або неочікуваних бібліотек додає ще один шар захисту від модифікацій, реверс-інжинірингу та атак із динамічним інструментуванням.
Вбудовуючи такі runtime-перевірки, ви створюєте «самоперевіряючий» застосунок, який активно захищається від змін, перепакування та спроб інʼєкції коду.
Виявлення hook-інʼєкцій (Hook Detection)
Hooking — це техніка, яку широко використовують у реверс-інжинірингу для перехоплення та зміни поведінки функцій під час виконання. Зловмисники можуть застосовувати hooking, щоб обходити перевірки безпеки, змінювати бізнес-логіку або непомітно виводити чутливі дані, не змінюючи при цьому оригінальний бінарник. Популярні фреймворки для hook-інʼєкцій — MSHookFunction із Substrate, FishHook від Facebook, а також runtime-swizzling методів Objective-C (часто використовується інструментами на кшталт FLEX).
Виявлення та нейтралізація спроб hooking є критично важливими для збереження цілісності та очікуваної поведінки iOS-застосунку. Ми використовуємо нашу бібліотеку для детекції різних технік hook-інʼєкцій і, в окремих випадках, для відновлення початкової реалізації функцій.
Виявлення MSHookFunction
MSHookFunction дає змогу замінювати реалізацію нативних функцій кодом, який контролює зловмисник. Щоб визначити, чи була функція перехоплена (hooked) за допомогою MSHook:
func denyDebugger() { ... }
typealias FunctionType = @convention(thin) ()->()
let func_denyDebugger: FunctionType = denyDebugger // `: FunctionType` is must
let func_addr = unsafeBitCast(func_denyDebugger, to: UnsafeMutableRawPointer.self)
let isMSHooked: Bool = SecurityKit.isMSHooked(func_addr)
Щоб відновити та викликати оригінальну функцію, яку було перехоплено:
func denyDebugger(value: Int) { ... }
typealias FunctionType = @convention(thin) (Int)->()
let funcDenyDebugger: FunctionType = denyDebugger
let funcAddr = unsafeBitCast(funcDenyDebugger, to: UnsafeMutableRawPointer.self)
if let originalDenyDebugger = SecurityKit.denyMSHook(funcAddr) {
// Call orignal function with 1337 as Int argument
unsafeBitCast(originalDenyDebugger, to: FunctionType.self)(1337)
} else {
denyDebugger()
}
Виявлення FishHook
FishHook — це популярна бібліотека для повторного привʼязування (rebinding) символів у динамічних бібліотеках під час виконання. Її можна використовувати для перехоплення викликів системних функцій (наприклад, NSLog, abort) або ваших власних символів. SecurityKit дає змогу перепривʼязати ці символи назад до їхніх оригінальних реалізацій.
Глобальне перепривʼязування перехопленого символу:
SecurityKit.denySymbolHook("$s10Foundation5NSLogyySS_s7CVarArg_pdtF") // Foudation's NSlog of Swift
NSLog("Hello Symbol Hook")
SecurityKit.denySymbolHook("abort")
abort()
Перепривʼязування перехопленого символу для конкретного завантаженого образу:
for i in 0..<_dyld_image_count() {
if let imageName = _dyld_get_image_name(i) {
let name = String(cString: imageName)
if name.contains("SecurityKit"), let image = _dyld_get_image_header(i) {
SecurityKit.denySymbolHook("dlsym", at: image, imageSlide: _dyld_get_image_vmaddr_slide(i))
break
}
}
}
Виявлення RuntimeHook
Hooking на рівні Objective-C runtime (method swizzling) дозволяє зловмисникам динамічно перехоплювати та замінювати реалізації методів. Ця техніка часто використовується інструментами на кшталт FLEX.
Щоб визначити, чи був метод Objective-C перехоплений під час виконання:
class SomeClass {
@objc dynamic func someFunction() { ... }
}
let dylds = ["SecurityKit", ...]
let isRuntimeHook: Bool = SecurityKit.isRuntimeHook(
dyldAllowList: dylds,
detectionClass: SomeClass.self,
selector: #selector(SomeClass.someFunction),
isClassMethod: false
)
Підсумок
У сучасному ландшафті мобільної безпеки захист iOS-застосунків вимагає багаторівневої стратегії, яка поєднує захищену комунікацію, надійну автентифікацію, потужний захист даних і проактивні механізми безпеки під час виконання.
Поєднуючи ці підходи — багато з яких реалізовані в нашій open-source бібліотеці SecurityKit — ви можете створювати iOS-застосунки, що значно краще протистоять модифікаціям, несанкціонованому доступу та компрометації даних, забезпечуючи користувачам безпечний і надійний досвід.
Також запрошую до мого Telegram-каналу про iOS-розробку, де вже понад рік ділюся цікавими матеріалами та практичними інсайтами: t.me/iOSDevsUA
Зв’язатися зі мною: LinkedIn.
3 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за змістовну статтю. Не варто забувати і потрібно бути обережним з бібліотеками, які можуть бути скомпроментованими.
Так, ти правий. Якщо ти маєш на увазі нашу бібліотеку SecurityKit, то вона лежить на GitHub з відкритим кодом
Насправді дуже корисна стаття але практика показує, що ключі в Keychain теж потрібно шифрувати