Полезные советы от Joy Dev: как правильно зарелизиться в сторе
С приходом санкций релиз в сторах превратился в творческую работу. Теперь это не просто загрузка кода, а сложный квест, требующий изобретательности, терпения и времени.
В этой статье мы расскажем, какие шаги помогают обойти ограничения и успешно разместить приложение в Play Market, App Store и других площадках. Готовы узнать секреты? Тогда читайте нашу статью!
А сфокусируемся мы на том, как обфускация затрудняет анализ кода во время ревью приложения при его релизе в стор.
Обфуска́ция(запутывание) кода — преобразование исходного или исполняемого кода программы в форму, которая сохраняет её функциональность, но делает значительно сложнее анализ, понимание алгоритмов работы и изменение кода при декомпиляции.
Она применятся для усложнения реверс инжиниринга, чтобы выявить уязвимости в коде и не допустить утечки данных.
Расскажем о комплексе мероприятий, таких как:
Удаление любых упоминаний бренда или имени проекта в коде: названий методов, переменных, ресурсов, методов API и т.п.
Обфускация и шифрование строковых констант и ресурсов
Затруднение отладки кода и пр.
Но прежде всего стоит поговорить о том, как вообще приложение может попасть в стор после блокировки — о камуфляжных приложениях.
Выпуск камуфляжных приложений
Это основной способ обойти блокировку крупных сторов, когда выпуск оригинального приложения уже невозможен. Он сложный, требующий вложения значительных ресурсов и не гарантирующий успех. Для его реализации понадобится применить весь комплекс мер, описанных в этой статье.
Давайте разберёмся, что такое камуфляжные приложения и для чего они нужны.
Камуфляжное приложение — «троянский конь», с помощью которого оригинальное приложение попадает в стор.
Изначально выпускается небольшое приложение, не связанное по функционалу с оригинальным, которое позже заменяется на оригинальное. Процесс разбит на несколько этапов.
Этап 1: Загружаем первую версию в стор
Приложение не должно быть слишком простым, чтобы финальный размер не вызывал подозрений. При этом не должно быть и слишком сложным, чтобы создание нового не требовало больших затрат на разработку. Ведь таких приложений потребуется несколько: некоторые забанят на ранних этапах, а другие понадобятся для выпуска последующих обновлений. Это может быть фитнес дневник или гид по заведениям — главное, чтобы постепенное увеличение размера приложения выглядело правдоподобно.
Этап 2: Выпускаем обновления
После выпуска первой версии, с определённым интервалом выпускаются обновления, которые кроме видимых обновлений функционала камуфляжа содержат в себе скрытые модули оригинального приложения.
Так, камуфляжное приложение постепенно разрастается, чтобы исполнить своё предназначение на финальном этапе. Это время используется, чтобы усыпить бдительность ревьюверов. Обычно данный этап занимает несколько месяцев и не менее десятка версий. При этом ревьюеру доступен только функционал камуфляжного приложения, внедрённые модули оригинального приложения скрыты.
Этап 3: Сбрасываем маски
В определённый момент, когда оригинальное приложение целиком выпущено в стор, камуфляж сбрасывается. Это реализуется через управление с бэкенда: фича тоггл, удалённый конфиг и пр.
Одновременно с этим по всем информационным каналам запускается маркетинговая кампания, призывающая пользователей скачивать новую версию приложения. Да, приложение будет заблокировано в течение нескольких часов, но этого достаточно, чтобы обновление получила значительная часть пользователей.
Выпуск камуфляжного приложения — творческий процесс, требующий соблюдения баланса между экономией вложенных ресурсов и правдоподобностью камуфляжа.
Скремблирование
Это изменение графа вызовов функций внутри программы и бинарного кода самих функций при полном сохранении работоспособности приложения. Данная техника предназначена для обхода алгоритмов попарного сравнения бинарных файлов, а также алгоритмов вычисления «цифрового следа», основанных на последовательности вызовов функций программы с использованием динамического анализа. Скремблирование — обратимый процесс: исходное сообщение можно восстановить, применив обратный алгоритм.
Примеры возможных алгоритмов скремблирования:
Добавление случайных полей в структуры и классы. Далее по коду они нигде не будут использоваться. Чтобы такие поля не мешали исполнению программы, они должны быть опциональными и иметь дефолтные значения.
struct MyModel {
var property1: String
var property2: Int
let fakeProperty: String? = ""
}
По аналогии с предыдущим способом, можно добавлять случайные методы. У них может быть случайный набор параметров и возвращаемое значение. Внутри метод будет просто возвращать случайное значение.
class MyClass {
let property1: String
let property2: Int
init(property1: String, property2: Int) {
self.property1 = property1
self.property2 = property2
}
func fakeFunction(arg1: String, arg2: Int) -> Int {
0
}
}
Также можно случайным образом создавать протоколы. В них можно добавлять случайные опциональные переменные и методы, реализация которых будет в расширениях к этим протоколам. Возвращать они будут, соответственно, случайные значения.
protocol FakeProtocol {
var fakeProperty: String { get }
func fakeFunction(arg: Int) -> Bool
}
extension FakeProtocol {
var fakeProperty: String {
return ""
}
func fakeFunction(arg: Int) -> Bool {
return false
}
}
class MyClass: FakeProtocol {
let property1: String
let property2: Int
init(property1: String, property2: Int) {
self.property1 = property1
self.property2 = property2
}
}
Можно менять способ вызова методов. Например, оборачивать вызовы в кложуры или другие случайные функции. Или создавать целые массивы фейковых функций и вызывать их в случайном порядке.
public extension Array {
@_optimize(none)
var value: Element {
self[0]
}
}
class MyClass {
func myFunction() {
let fakeClosure = {
// real code
}
let fakeArray = [fakeClosure]
return fakeArray.value()
}
Можно менять код самих функций, добавляя туда проверки переменных, которые всегда будут true. Это не изменит логику, при этом уменьшит схожесть кода при проверках бинарников.
public enum FakeWorker {
static var shouldExecute = defaultValue()
private static func defaultValue() -> Bool {
true
}
}
public func myFunction() {
// ...
if FakeWorker.shouldExecute {
// real code
}
//...
}
По аналогии с пунктом выше, можно добавлять куски кода, оборачивая их в проверки, которые никогда не пройдут. Это могут быть простые принты или случайно сгенерированный код.
public enum FakeLogger {
static var needLog = defaultValue()
@_optimize(none)
private static func defaultValue() -> Bool {
false
}
@inline(never)
static func log(_ screenName: String = "") {
print(screenName)
}
}
public func myFunction() {
// ...
if FakeLogger.needLog {
FakeLogger.log()
}
//...
}
Таким образом, скремблирование достигается генерацией случайных вызовов функций и изменением кода самих функций или структур данных. Эти методы могут помочь защитить код и сделать его менее предсказуемым для анализа. Однако важно помнить, что чрезмерное усложнение кода может затруднить его поддержку и отладку, поэтому алгоритмы скремблирования лучше добавлять на стадии CI.
Для лучшего результата стоит пользоваться сразу несколькими алгоритмами. Чтобы это было удобно сделать, рекомендуем сгенерировать набор шаблонов для каждого алгоритма, содержащий правила его применения. На основе этих шаблонов скрипт может строить план скремблирования и генерировать конкретные значения для переменных. Финальный изменённый код может быть получен на основе этих значений, следуя инструкциям из плана, сгенерированного ранее.
Шифрование строк
Нужно для того, чтобы они не выдали приложение при анализе, так как извлечь их из сборки довольно просто, и могут возникнуть подозрения из-за значительного количества фраз на русском языке, упоминаний тематики или названия приложения. Кроме того, все повторяющиеся строки бизнес-логики в разных безбрендовых версиях могут помочь установить связь между ними.
По умолчанию шифруются все локализованные строки. Можно быть уверенным, что если строка локализована, то она зашифрована. Но можно добавить и шифрование не локализованных строк. Это различные строковые константы, встречающиеся в приложении.
Одним из способов шифрования может быть скрипт Fastlane, в котором будет реализован ваш алгоритм. Однако возможны исключения, обусловленные сложностью замены кода. Это надо учитывать при написании кода в приложении.
Шифрование будет производиться скриптами обезличивания на этапе сборки проекта. И если вы учтёте в коде приложения все нюансы замены строк для вашего алгоритма, то ничего дополнительно для шифрования делать не понадобится. Однако, несмотря на уже реализованное шифрование, не рекомендуется оставлять в коде упоминания проекта или терминов, указывающих на их связь с реальной областью применения.
Также для удобства разработки можно написать скрипт, который в реальном времени зашифрует текущий код. Скрипт лучше запускать руками из консоли, чтобы убедиться в корректности шифровки, не дожидаясь сборки всего проекта на CI.
Сборка приложения
Перед сборкой проекта необходимо убедиться в удалении всех отладочных символов и информации, которая может помочь в анализе кода. Код, нежелательный в релизной сборке, может быть помечен как if DEBUG.
При сборке приложения рекомендуется проставить динамическую линковку сторонних зависимостей: когда они загружаются динамически, тогда код не включается непосредственно в исполняемый файл приложения. Это затрудняет анализ и реверс инжиниринг. Динамическая линковка позволяет разделить логику приложения на модули. Это значит, что критически важные части кода могут быть вынесены в отдельные библиотеки, усложняя их обнаружение и анализ. Также можно использовать обфусцированные версии сторонних библиотек.
Ещё до установки подов сторонние фреймворки можно переименовать. Сгенерируйте таблицу таких переименований, чтобы позже их раскодировать.
Далее замените бандл приложения, чтобы не было возможности связать его с другими продуктами компании (или другими версиями безбренда). Это надо сделать во всех plist«ах или файлах, где он указывается.
После этого можно запускать все скрипты скремблирования и обфускации, написанные ранее. Сюда же входят и предварительные шаги вроде генерации таблиц обфускации и создания случайных значений для некоторых алгоритмов.
Если у вас в проекте имеются таргеты, которые собираются только в дебаг версии, на данном этапе их можно удалить из проекта, так как при сборке с релизной схемой они будут выдавать ошибки.
Изменение структуры проекта
К этому пункту лучше подготовиться, так как манипулировать мы будем объёмными логическими единицами, механизм взаимодействия которых нужно не нарушить в процессе. Хорошо если проект разрабатывался в многомодульной архитектуре изначально. Это поможет проработать стратегию для управления его модулями и применять на них различные методы случайным образом, создавая большую хаотичность.
Первым делом избавляемся от всего в основном модуле. Оставляем только точку входа: в этом случае он будет состоять в большей степени из описательных и конфигурационных файлов, которые далее мы и будем изменять.
Выносим содержимое в отдельные модули. С точки зрения разработки это могут быть разбитые иерархически слои проекта или большая степень раздробленности с выносом в отдельный модуль каждой отдельной фичи (работа объёмная, но преимуществ намного больше, чем дополнительные возможности в целях этой статьи)
Так как мы собираемся динамически перестраивать структуру проекта при сборке, важно отказаться от стандартного способа описания проджект файла. На замену приходит возможность генерировать файл проекта инструментами XcodeGen, Tuist или им подобных. Это развяжет нам руки в дальнейших манипуляциях.
Как только мы настроили генерацию проектов, подключаем фантазию и рандомизируем всё, что можем:
1. Генерируем случайные названия для наших модулей, бандлов и скриптами модифицируем их в конфигурационных файлах.
2. Мы больше не привязаны в структуре проекта к названиям файлов и папок, соответственно переименовываем всё содержимое на случайные строки. Особенно файлы, так как после компиляции их названия останутся в dSYM архиве.
3. Если очень сильно разбили проект на модули, с помощью скрипта случайным образом сливаем по 2 случайных модуля в один и получаем на выходе скомпилированные модули с динамичным содержимым в целом.
Желательно иметь некий вайтлист модулей, для которых это делать не опасно, например, исключив модули с ассетами, чтобы не решать этот вопрос дополнительно. Важно не забыть в этой части пройтись по импортам, не только по конфигурации модулей.
4. Проделав все манипуляции, собираем проджект файл проекта. Работать в нём не получится, т.к. мы всё перемешали, но нам и не нужно, используем эти скрипты только при сборке релизного билда.
Сокрытие ресурсов
Ресурсы могут занимать приличную часть проекта. При этом их достаточно сложно модифицировать, не повредив. Например, тяжело внести изменения в растровое изображение, не меняя цвет или размер пикселей. Такие файлы легко обнаружить при сравнении архива побайтово или через хеши этих файлов. Также важно, что в конечном архиве они остаются в чистом виде: они не компилируются в машинный код и на них не распространяется даже дефолтная обфускация.
Но это же и поможет нам в дальнейшей работе с ними.
Так как ресурсы не являются частью скомпилированного приложения, нам необходимо выполнить 2 этапа:
обеспечить попадание ресурсов в архив с приложением
обеспечить приложению возможность восстановить доступ к этим ресурсам на запуске
Для этого будем использовать архивирование. Сначала компилируем исходный код с ресурсами, чтобы избежать ошибок компиляции. Затем на стадии копирования ресурсов убираем их из бандла модуля/приложения. Вместо них собираем ресурсы в один zip архив и закрываем его паролем. Архив кладём как единственный ресурс в нашем приложении. Посмотреть содержимое такого архива без пароля не получится никаким анализатором. В качестве дополнительного шага храним хеши предыдущих архивов, чтобы отсечь вероятность повторения, если между релизами не было изменений. Так мы отсекаем возможность провести сравнительный анализ наших ресурсов, не модифицируя их.
Вторым шагом на запуске приложения делаем обратную процедуру. Находим в каждом бандле архив и разархивируем содержимое, используя известный нам пароль. Важно сделать это до первого обращения к ресурсу, иначе наше приложение упадёт. Это всё можно сделать из кода приложения. Также важно проработать версионность и проверки для оптимизации этого процесса, чтобы каждый запуск приложения не был долгим.
Инъекция ненужного кода
Достаточно спорный механизм по нескольким причинам.
Во-первых, анализатор свифта неплохо умеет находить ненужный код. Если мы собираем релизную сборку, то мы вряд ли будем готовы отказаться от флагов оптимизации релизных сборок. Если мы добавим элементарный код, который ничего не делает, например лишние переменные или неиспользуемые файлы, компилятор обнаружит и вырежет его как ненужный. Такие действия будут бесполезны.
Во-вторых, если мы берём в расчёт, что это должен быть код, работу которого сложно проверить в рамках компиляции (например, случайные записи в отдельный файл UserDefaults), то такие частые манипуляции могут помешать скорости работы приложения.
Одна из практик, которую мы рассматривали — расстановка дополнительных вызовов в асинхронном коде. Такой код уже сам по себе предполагает не моментальное выполнение инструкций, а создание для пользователя видимого эффекта в скорости работы приложения.
Сама возможность разбавить наш код дополнительными вызовами позволит сделать его запутанным и неоднообразным, что будет мешать анализаторам.
Вот мы и поделились с вами секретами успешного релиза в сторах.
Надеемся, что наша статья поможет вам. Будем ждать ваши комментарии и благодарности.