Содержание статьи
- Зачем это вообще нужно?
- Что такое реверс-инжиниринг и как он работает
- Базовые принципы защиты
- Обфускация кода
- Инструменты обфускации для Swift/Objective-C
- Symbol Stripping
- Ручная обфускация критичных участков
- Обфускация строк
- Антиотладка (anti-debugging)
- Проверка через sysctl
- Защита через ptrace
- Проверка через sysctl с усложнением
- Проверка целостности времени выполнения
- Детект джейлбрейка
- Проверка файлов Cydia
- Проверка возможности записи в системные директории
- Проверка через fork()
- Проверка через вызов system()
- Комбинированный подход
- Код-подпись и runtime integrity checks
- Проверка code signature изнутри
- Проверка хеша бинарника
- Проверка целостности ресурсов
- Защита от Frida и runtime-хуков
- Детект Frida по открытым портам
- Детект по загруженным библиотекам
- Защита критичных функций через inline assembly
- Затруднение хуков через непрямые вызовы
- Шифрование данных и ключей
- Keychain для секретов
- Шифрование локальных файлов
- Обфускация ключей шифрования
- SSL pinning
- Certificate pinning
- Public key pinning
- Использование TrustKit
- Server-side-валидация
- Проверка подписи запросов
- Attestation
- Rate limiting и аномальная активность
- Мониторинг и реакция на инциденты
- Аналитика безопасности
- Remote kill switch
- Crash reporting
- Обфускация логики через нативный код
- Создание статической библиотеки на C
- Коммерческие решения
- Что не работает
- Безопасность через неясность (security through obscurity)
- Переменные с флагом «это взломанная версия»
- Чрезмерное усложнение
- Тестирование защиты
- Самостоятельное тестирование
- Привлечение специалистов
- Bug bounty
- Юридические аспекты
- Несколько советов из личного опыта
- Заключение
Мы разберем, как работают атаки на мобильные приложения, какие инструменты используют взломщики и, главное, как противостоять этим угрозам на практике. Мы пройдем путь от базовых методов обфускации кода до продвинутых техник runtime-защиты, антиотладки и детекта джейлбрейка.
Я поделюсь примерами кода, которые можно сразу внедрить в проект, и расскажу о подводных камнях, с которыми сталкивался лично. Эти знания пригодятся при разработке финтех‑приложений, игр с внутриигровыми покупками, корпоративных решений — словом, везде, где важна безопасность и целостность кода.
Зачем это вообще нужно?
Когда я только начинал работать с iOS, мне казалось, что платформа Apple достаточно закрыта сама по себе. App Store Review, код‑подпись, песочница — разве этого недостаточно? Практика показала обратное.
Приложения взламывают по разным причинам. Кто‑то хочет обойти платные функции или внутриигровые покупки. Кто‑то ищет API-ключи и токены, чтобы злоупотреблять ими позже. Кто‑то просто изучает твое приложение, чтобы создать конкурентный продукт. А иногда злоумышленники модифицируют легитимное приложение, добавляя в него вредоносный код, и распространяют через сторонние магазины.
Джейлбрейк значительно упрощает взлом. На устройствах с джейлом можно запускать произвольный код, перехватывать системные вызовы, инжектить библиотеки в процессы. Но даже без джейлбрейка возможностей достаточно — дампинг приложения из памяти, статический анализ бинарника, манипуляции с файловой системой через резервную копию.
Что такое реверс-инжиниринг и как он работает
Реверс‑инжиниринг iOS-приложения — это процесс извлечения информации о его внутреннем устройстве без доступа к исходному коду. Цель может быть разной: понять алгоритмы работы, найти уязвимости, извлечь секретные данные или модифицировать поведение.
Типичный порядок действий атакующего выглядит так:
- Получение IPA-файла. Легитимный способ — скачать свое купленное приложение с устройства через iTunes или через iMazing. Нелегитимный — использовать специализированные сервисы, которые предоставляют декриптованные IPA.
- Декриптование бинарника. Приложения из App Store зашифрованы FairPlay DRM. На джейлбрейк‑устройстве инструменты вроде Clutch или frida-ios-dump позволяют дампить приложение из памяти в декриптованном виде.
- Статический анализ. С помощью Hopper, IDA Pro или Ghidra бинарник дизассемблируется. Код на Swift частично деобфусцируется благодаря метаинформации, которую компилятор оставляет в бинарнике. Objective-C вообще читается почти как исходники — все имена методов, классов и протоколов сохраняются.
- Динамический анализ. Запуск приложения под отладчиком (LLDB), использование Frida для runtime-хуков, перехват сетевых запросов через Charles или Burp Suite.
- Модификация. Патчинг бинарника (изменение инструкций процессора), инжект собственных библиотек, подмена ресурсов, пересборка IPA и установка через сайдлоадинг.
Я как‑то проводил эксперимент с собственным приложением. Установил Hopper, загрузил туда свой бинарник — и был крайне удивлен. Все названия методов, структуры классов, даже строковые константы с API-эндпоинтами были видны как на ладони. Вот тогда и пришло понимание, что защита нужна.
Базовые принципы защиты
Прежде чем перейти к конкретным техникам, важно понять философию. Абсолютной защиты не существует. Любой механизм можно обойти, если у атакующего достаточно времени, знаний и мотивации. Наша задача — максимально усложнить процесс, сделать его экономически невыгодным.
Я придерживаюсь принципа эшелонированной обороны (defense in depth). Одна техника легко обходится. Десять техник, примененных одновременно, заставят злоумышленника потратить дни или недели работы. Часто этого достаточно, чтобы он переключился на более легкие цели.
Второй важный момент — баланс безопасности и производительности. Некоторые техники защиты могут замедлить приложение или усложнить разработку. Нужно оценивать риски конкретно для твоего продукта. Игра с покупками за реальные деньги требует серьезной защиты. Если у тебя простая утилита для заметок, то, возможно, не стоит париться.
Обфускация кода
Обфускация — это преобразование кода таким образом, чтобы он оставался функционально идентичным, но стал менее понятным при анализе. В мире iOS это сложнее, чем в Android, потому что в Apple не предоставили готовых инструментов для обфускации.
Инструменты обфускации для Swift/Objective-C
Для Swift есть несколько инструментов:
- SwiftShield — опенсорсное решение, которое переименовывает классы, методы, свойства перед компиляцией;
- Obfuscator-LLVM — форк компилятора LLVM со встроенной обфускацией на уровне IR.
Честно говоря, с этими инструментами я работал мало. SwiftShield требует аккуратной настройки и может ломать проекты с большим количеством зависимостей. Obfuscator-LLVM — мощная штука, но требует пересборки всего проекта custom-компилятором, что усложняет CI/CD.
Для Objective-C можно использовать скриптовую обфускацию — автоматическое переименование классов и методов перед компиляцией с помощью регулярных выражений. Но это хрупкое решение.
Symbol Stripping
Первое и самое простое — это strip symbols. Xcode по умолчанию включает эту опцию для Release-конфигурации. В Build Settings выставляешь
Strip Debug Symbols During Copy: YES
Strip Linked Product: YES
Symbols Hidden by Default: YES
Это удаляет отладочную информацию из бинарника. Имена функций, не входящие в публичный API, будут заменены адресами памяти. Селекторы Objective-C все равно останутся видимыми (они нужны для работы runtime), но функции на C и часть кода на Swift станут менее читаемыми.
Ручная обфускация критичных участков
Иногда проще не автоматизировать обфускацию всего проекта, а вручную защитить самые важные участки. Например, алгоритм проверки лицензии или шифрования данных.
Простейший прием — инлайнинг и разбиение логики. Вместо одной понятной функции делаешь цепочку вызовов с неочевидными именами:
func validateLicense(_ key: String) -> Bool { return checkFormat(key) && verifyChecksum(key) && pingServer(key)}Обфусцируем:
func a(_ s: String) -> Bool { return b(s) && c(s) && d(s)}private func b(_ s: String) -> Bool { // Проверка формата}private func c(_ s: String) -> Bool { // Проверка контрольной суммы}private func d(_ s: String) -> Bool { // Пинг сервера}Да, это выглядит страшно и нечитаемо даже для тебя. Но в этом и смысл. Только критичные участки, не весь проект.
Обфускация строк
Строковые константы в бинарнике — это подарок для атакующего. API-ключи, URL эндпоинтов, сообщения об ошибках — все лежит в plaintext в секции __cstring.
Простейшее решение — шифрование строк. Создаешь функцию, которая расшифровывает строку в runtime:
func decrypt(_ encrypted: [UInt8], key: UInt8) -> String { let decrypted = encrypted.map { $0 ^ key } return String(bytes: decrypted, encoding: .utf8) ?? ""}let apiKey = decrypt([0x48, 0x65, 0x6c, 0x6c, 0x6f], key: 0x20)Ключ шифрования можно хранить в коде (тоже обфусцированно) или генерировать динамически. Да, это XOR-шифрование, которое ломается за секунды, но лучше, чем ничего. Для более серьезной защиты можешь использовать AES с ключом, собираемым из разных частей кода.
Я пробовал автоматизировать это через скрипт Build Phase. Скрипт находит все строковые литералы в проекте, шифрует их, заменяет вызовами функции расшифровки. Работает, но требует осторожности — легко сломать интерфейсы и локализацию.
Антиотладка (anti-debugging)
Отладчик — главный инструмент при динамическом анализе. Атакующий запускает приложение под LLDB, ставит брейк‑пойнты, изучает состояние памяти, пошагово выполняет код. Наша задача — детектировать отладчик и либо завершить приложение, либо изменить его поведение.
Проверка через sysctl
Системный вызов sysctl позволяет запросить информацию о процессе. Флаг P_TRACED указывает, что процесс отслеживается отладчиком:
import Foundationfunc isDebuggerAttached() -> Bool { var info = kinfo_proc() var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] var size = MemoryLayout<kinfo_proc>.stride let result = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0) if result != 0 { return false } return (info.kp_proc.p_flag & P_TRACED) != 0}if isDebuggerAttached() { fatalError("Debugger detected")}Эту проверку желательно делать не один раз при старте, а периодически в рантайме. Атакующий может присоединиться к процессу уже после запуска.
Защита через ptrace
Системный вызов ptrace позволяет процессу запретить присоединение отладчика. Вызываешь ptrace(, и всё — LLDB больше не сможет присоединиться к процессу.
#import <sys/types.h>#import <sys/sysctl.h>#import <dlfcn.h>typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);void disablePtrace() { void *handle = dlopen(NULL, RTLD_GLOBAL | RTLD_NOW); ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(handle, "ptrace"); ptrace_ptr(31, 0, 0, 0); // PT_DENY_ATTACH = 31 dlclose(handle);}
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
