Золотая лихорадка, связанная с искусственным интеллектом, в самом разгаре.

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

Этому способствует одна убийственная и повсеместно распространенная ошибка: привязка API-ключей ChatGPT непосредственно к приложению ИИ. Она упрощает кражу ваших ключей, растрату ваших кредитов и накопление неоплаченных долгов на ваших счетах. 

Вы должны усвоить один из основных законов безопасности:

Не храните API-ключи на клиенте. Никогда.

Я продемонстрирую вам два метода, которые позволяют злоумышленникам получить доступ к вашим API-ключам, и расскажу, как можно избежать подобного.

Декомпиляция приложения

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

Неформатированные строки в коде

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

func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = "12345678-90ab-cdef-1234-567890abcdef"
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}

Этот код компилируется и предоставляется пользователям в виде бандла .app, содержащего весь исполняемый код.

Получить доступ к .ipa-файлу любого приложения в App Store, который содержит бандл .app, довольно просто (ссылка). И это первое, что попытается сделать злоумышленник, желающий украсть ваши ключи.

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

Самое ценное находится в Bev — UNIX-исполняемом файле моего стороннего проекта. Он содержит весь скомпилированный и связанный код приложения.

С помощью встроенной UNIX-команды strings можно инспектировать каждую неформатированную строку, которая находится внутри этого скомпилированного исполняемого файла.

strings Bev

Естественно, вывод будет довольно многословным:

Помимо неформатированных строк, вы увидите множество имен символов для скомпилированных функций.

Но в самом низу этого списка будет приз:

12345678-90ab-cdef-1234-567890abcdef

Неформатированная строка для API-ключа, доступная всем — ее легко найдет любой человек, умеющий обращаться с регулярными выражениями (regex).

Info.plist

Возможно, вы не собираетесь помещать неформатированные строки в свое приложение, где они могут быть легко декомпилированы. 

Вы помещаете их в Info.plist, где им и следует находиться.

Точка API-вызова становится сложнее, и теперь получить это значение не так-то легко.

func callChatGPTAPI(prompt: String) async throws -> Data {
    let url = URL(string: "https://api.chatgpt.com")!
    var request = URLRequest(url: url)
    let apiKey = getAPIKeyPlist()
    request.addValue(apiKey, forHTTPHeaderField: "Authorization")
    return try await URLSession.shared.data(for: request).0
}

func getAPIKeyPlist() -> String? {
    Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String
}

Здесь несколько подуровней сложности. Вместо непосредственного жесткого написания кода ключей, вы можете использовать несколько наборов ключей для разработки и производства. Можно создавать файлы .xcconfig для каждой среды, которые разрешаются в значения Info.plist во время сборки.

Если вы фанатичный приверженец защиты информации, то знаете, что никогда не следует коммитить секреты в систему управления версиями исходного кода. Возможно, вы еще более продвинуты в вопросах безопасности и надежно храните API-ключи в своем CI/CD-конвейере, внедряя их в .xcconfig или Info.plist как часть сценариев сборки.

Но все это напрасно.

Взгляните еще раз на это декомпилированное приложение, доступное любому, у кого есть MacBook, iPhone и достаточно свободного времени:

Значения, помещенные в Info.plist, являются общедоступными независимо от того, как они туда попали.

Обфусцированные строки

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

В действительности, что бы вы ни фантазировали на счет своего алгоритма обфускации и используемой соли, стоит поручить ChatGPT написать пару функций для обфускации и декодирования API-ключей.

private func obfuscate(key: String) -> String? {
    let salt = "00000000-0000-0000-0000-000000000000"
    guard let keyData = key.data(using: .utf8),
          let saltData = salt.data(using: .utf8) else {
        return nil
    }
    let obfuscatedData = zip(keyData, saltData).map { $0 ^ $1 }
    let obfuscatedBase64 = Data(obfuscatedData).base64EncodedString()
    return obfuscatedBase64
}

private func decode(obfuscated: String) -> String? {
    let salt = "00000000-0000-0000-0000-000000000000"
    guard let obfuscatedData = Data(base64Encoded: obfuscated),
          let saltData = salt.data(using: .utf8) else {
        return nil
    }
    let apiKeyData = zip(obfuscatedData, saltData).map { $0 ^ $1 }
    return String(bytes: apiKeyData, encoding: .utf8)
}

Получение ключа во время выполнения программы сводится к декодированию неформатированной обфусцированной строки.

func callChatGPTAPI(prompt: String) async throws -> Data {    
    let url = URL(string: "https://api.chatgpt.com")!
    var request = URLRequest(url: url)
    let apiKey = getAPIKeyObfuscated()
    request.addValue(apiKey, forHTTPHeaderField: "Authorization")
    return try await URLSession.shared.data(for: request).0
}
    
func getAPIKeyObfuscated() -> String? {
    decode(obfuscated: "AQIDBAUGBwgACQBRUgBTVFVWAAECAwQABQYHCAkAUVJTVFVW")
}

Хотя читать это немного сложнее, чем неформатированные строки, достаточно мотивированный хакер все равно сможет легко украсть ваши ключи.

Если вы используете такой небезопасный подход, как кодирование base-64 или XORing, учтите: его можно легко перепрограммировать. Даже с приличным алгоритмом и солью злоумышленники могут воспользоваться такими инструментами, как Frida, чтобы извлечь ваши API-ключи из памяти устройства во время выполнения.

Обфускация обеспечивает безопасность через неясность (security by obscurity), но это неэффективный подход. Какой бы надежной ни была обфускация, в определенный момент API-ключ должен быть декодирован и отправлен в API.

Это приводит нас к последнему и, возможно, самому простому способу кражи API-ключей.

Мониторинг сетевого трафика

Это одна из самых мощных техник в вашем распоряжении.

Бесплатные инструменты вроде Proxyman (или Charles Proxy для опытных пользователей) позволят вам просмотреть весь трафик, входящий и исходящий из приложения или сайта.

Через несколько секунд после загрузки программы сразу же обнаружите в сетевом запросе auth-заголовок, содержащий API-ключ.

12345678-90ab-cdef-1234-567890abcdef

Я привык делать еще кое-что, собираясь приступить к новой работе: проверяю сетевой трафик, чтобы выяснить, работает ли он через REST-дом или GraphQL-магазин.

Защита от проксирования

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

Он проверяет TLS-сертификаты, представленные внутренними серверами, на соответствие сертификатам, хранящимся на устройстве. Это предотвращает атаки типа «незаконный посредник» и блокирует корневой сертификат прокси-сервера сетевых прокси-инструментов.

Чтобы реализовать SSL-пиннинг на iOS, можно создать декоратор сетевых запросов с помощью URLSessionDelegate, реализующего urlSession(didReceive: URLAuthenticationChallenge).

Но, как вы уже догадались, SSL-пиннинг — еще не все, что вам требуется. Специалисты реверс-инжиниринга (обратной разработки) могут использовать инструменты для обхода пиннинга, в том числе и на устройствах с джейлбрейком.

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

Когда я был относительно молодым инженером без достаточного опыта, по наивности внедрил SSL-пиннинг с помощью корневого EC2-сертификата, принадлежащего AWS. Естественно, на той же неделе со стороны AWS была проведена ротация сертификата. В общем, центры тестов на COVID-19 в Великобритании не работали в течение самых долгих 59 минут в моей жизни.

Что же делать?

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

Теперь, когда вы знаете, чего не надо делать, поговорим о том, что можно сделать.

Хотя клиентская сторона по своей природе подвержена опасностям, гораздо проще защитить бэкенд от злоумышленников. «Хрестоматийный» подход к защите сторонних API-ключей заключается в размещении их в менеджере секретов (периодически ротируемом) и проксировании сетевых запросов через ваш бэкенд.

При таком подходе вы можете аутентифицировать сетевые запросы с помощью собственных систем и минимизировать потенциальный вектор атаки.

Если вы еще не развернули собственную инфраструктуру, Firebase Cloud Functions — простой способ реализовать этот промежуточный слой.

В то же время можете добавить еще несколько уровней защиты своего устройства с помощью таких инструментов, как AppAttest на iOS, Play Integrity на Android, а также путем внедрения библиотек RASP (runtime application self-protection — защита безопасности приложения во время выполнения). Они не позволят вашему приложению работать на устройствах с джейлбрейком и защитят от инструментов реверс-инжиниринга.

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

Заключение

Не храните ключи API на клиенте. Никогда.

Любой, кто скажет вам, что ваши API-ключи в безопасности на клиенте, либо глупец, либо злоумышленник.

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

Сетевое проксирование — мощный инструмент, используемый как во благо, так и во зло. Такие механизмы защиты, как SSL-пиннинг, могут принести больше проблем, чем пользы, если вы не знаете, что делаете. Механизмы защиты на стороне клиента, такие как проверка целостности приложения или библиотеки для обнаружения реверс-инжиниринга, помогают повысить уровень защиты приложения или сайта.

Если хотите, чтобы ваши API-ключи (в разумной степени) были в безопасности, храните все на бэкенде или используйте такие продукты, как Firebase Cloud Functions. Можете спать спокойно, зная, что обеспечили безопасность своим API-ключам. Насколько это возможно.

Блог Jacob’s Tech Tavern не несет никакой ответственности, если, последовав рекомендациям Jacob Bartlett (автора блога и этой статьи), вы выставите счет на $5 000 ChatGPT или Firebase.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jacob Bartlett: How I Stole Your ChatGPT API Keys

Предыдущая статьяC++: полное руководство по std::stoi