Всем привет! Меня зовут Дмитрий Фисько, я разрабатываю Yandex Mobile Ads SDK. Наша библиотека предназначена для монетизации мобильных приложений на платформе Android и iOS. Сегодня я хочу рассказать вам о том, как мы упростили разбор сложных ошибок интеграции SDK в Android-приложения. Возможно, наш опыт пригодится и вам.
Наши пользователи — разработчики мобильных приложений — не всегда читают документацию, так что иногда придумывают заковыристые способы использования SDK. Неправильная интеграция может снизить эффективность размещения рекламных объявлений, а значит, уменьшить доход разработчика. Чтобы помочь разработчикам лучше монетизировать приложения, мы создали превентивную систему мониторингов, которая анализирует показатели мобильного приложения. Если по мониторингам мы узнаём о проблеме, то связываемся с разработчиками и помогаем им найти источник проблемы и решить её.
К сожалению, не все ошибки интеграции SDK можно определить по мониторингам. Если такая ситуация возникает, мы обращаемся к партнёру, чтобы уточнить детали интеграции. Затем мы стараемся определить причину проблем и помочь их решить. Если даже этой информации недостаточно, чтобы определить причину ошибок, мы запрашиваем у партнёра разрешение на реверс-инжиниринг приложения. После разрешения мы начинаем смотреть на работу рекламного SDK в приложении как на чёрный ящик. Просматриваем сетевую активность через proxy, проверяем отображение рекламных view через Layout Inspector и др.
Просматривать сетевую активность приложения начиная с Android 7.0 проблематично, так как система по умолчанию не доверяет сертификатам, установленным пользователем. Установка сертификата необходима для просмотра SSL-трафика приложения через proxy. Решит проблему либо запуск приложения на версии Android младше 7.0, либо добавление network_security_config в приложение, например через Apktool. Просмотреть отображение рекламных view можно через утилиту Layout Inspector, запустив приложение на эмуляторе или на устройстве. При этом необходимо модифицировать файл AndroidManifest.xml, добавив через Apktool атрибут debuggable=true.
Если метода чёрного ящика оказалось недостаточно и воспроизвести проблему не удалось, можно посмотреть на логику работы приложения. Для этого можно воспользоваться утилитами для декомпиляции APK, такими как JADX, Bytecode Viewer. Но зачастую такой подход требует слишком много времени и не всегда приводит к результату. Поэтому, чтобы быстрее понимать, как приложение использует SDK изнутри, мы сделали скрипт для подстановки новой реализации SDK в уже собранное приложение.
Подмена кода SDK позволяет через классы изменённой версии SDK внедрить в приложение произвольный код и, например, включить режим дополнительного логирования. Алгоритм работы следующий. Скрипт:
Нужно декомпозировать приложение, разделить его на более мелкие единицы, чтобы появилась возможность изменять код классов SDK, не меняя при этом код приложения. Как подступиться к собранному приложению? Дизассемблируем DEX в smali-файлы.
В Android приложение хранит код в DEX-файлах. Извлечь их из приложения можно через утилиту unzip, так как APK — обычный архив с структурированным содержимым. У DEX-файлов бинарный формат для более плотной по сравнению с JAR упаковки кода. Из-за бинарности DEX нечеловекочитаем, поэтому изменять сам DEX нерационально. Первое, что приходит на ум, — декомпилировать DEX в Java. Такое преобразование возможно, но оно нетривиально и происходит с потерями работоспособности кода. Поэтому воспользуемся трансляцией в smali-код. Преобразование в smali позволяет точно передать инструкции из DEX в человекочитаемом виде с возможностью последующей конвертации в работоспособный код.
Вызов утилиты smali преобразует DEX в набор классов в smali-коде. При этом исходное расположении классов по подпакетам сохраняется.
Подготовим версию SDK на замену. Чтобы гарантировать воспроизводимость проблемы, версию SDK с включённым логированием создадим на основе той же версии, которая уже интегрирована в приложение. Один из простых способов узнать подключённую версию Yandex Mobile Ads SDK в приложении — просмотреть содержимое метода в классе MobileAds.getLibraryVersion() через Apk Analizer в Android Studio. Узнав используемую версию рекламного SDK, мы переключаемся на ветку данного релиза и собираем версию библиотеки с дополнительным логированием. В результате получаем AAR-файл. Он содержит ресурсы и код библиотеки. Нас в AAR-файле интересует лишь код в JAR-файле, так как в нашем SDK нет внешних ресурсов: все ресурсы заинлайнены непосредственно в код или приходят с бэкенда. Отсутствие файлов с ресурсами упрощает интеграцию SDK в IDE без поддержки современных систем сборки.
Чтобы сменить версию SDK в приложении на новую, приведём AAR к тому же состоянию, что и дизассемблированное приложение, т. е. из AAR получим набор smali-файлов. Преобразование происходит по цепочке: AAR → JAR → DEX → SMALI:
Получив smali-файлы приложения и SDK с логами, заменяем реализацию SDK новой. После чего пересобираем приложение. При трансляции в smali получаемые классы сохраняют расположение по подпакетам. Поэтому, если известен пакет, где лежат классы SDK, легко отличить класс библиотеки от классов приложения. Классы SDK могут быть распределены по нескольким DEX. Так что алгоритм, по которому подменяется реализация SDK, различается для приложения с одним DEX-файлом или несколькими.
В приложении с одним DEX мы просто скопируем новые smali-классы SDK поверх всех классов приложения и сгенерируем модифицированный DEX. Сгенерировать DEX-файл можно с помощью утилиты baksmali. На вход утилите подаётся директория с разбитыми по пакетам файлами классов в smali-коде. Пропустив через baksmali объединённые smali-файлы приложения и нового SDK, получаем модифицированный DEX-файл с изменённой логикой SDK.
Для приложения с MultiDex добавим отдельным DEX новый код SDK и удалим предыдущую версию SDK из остальных DEX-файлов приложения. Добавление новой версии SDK отдельным DEX позволит обойти ограничение количества методов в формате DEX. MultiDex автоматически догрузит добавленный DEX-файл с кодом SDK при его корректном наименовании. MultiDex ищет DEX-файлы по очереди, используя индекс в конце файла: сначала dex1, затем dex2 — и т. д. Если назвать файл c инкрементированным индексом, то MultiDex автоматически догрузит его в виртуальную машину. Таким образом через baksmali cгенерируем DEX-файлы на основе ранее полученных smali-файлов приложения, но с удалёнными классами старой версии SDK. А также соберём дополнительный DEX-файл с изменённой версией SDK, инкрементируя индекс в названии DEX-файла.
Мы получили DEX-файлы приложения с изменённой версией SDK. Дело за малым: заменим DEX-файлы в первоначально распакованном APK-файле приложения модифицированными DEX-файлами. И вызовом команды zip получим финальную версию APK, которую осталось лишь подписать. Подпишем отладочным ключом через apksigner, чтобы приложение можно было установить на устройство. Приложение с изменённой логикой SDK готово.
Алгоритм работает для большинства случаев, но иногда подменить реализацию SDK в приложении не получится. Причины этого:
Шаги, описанные в статье, мы автоматизировали простым bash-скриптом. У скрипта есть недостатки, однако он сильно ускоряет разбор сложных проблем при интеграции SDK в приложения партнёров. Хотя, мы редко применяем этот подход, так как зачастую находим решение на более ранних стадиях.
Из преобразования в smali можно извлечь дополнительную пользу: smali-файлы позволяют отлаживать приложение без исходников. Для запуска отладки нужно на основе smali-файлов приложения сгенерировать проект в Android Studio и приаттачить дебаггер к интересующему процессу. Подробнее написано в этой статье.
Наши пользователи — разработчики мобильных приложений — не всегда читают документацию, так что иногда придумывают заковыристые способы использования SDK. Неправильная интеграция может снизить эффективность размещения рекламных объявлений, а значит, уменьшить доход разработчика. Чтобы помочь разработчикам лучше монетизировать приложения, мы создали превентивную систему мониторингов, которая анализирует показатели мобильного приложения. Если по мониторингам мы узнаём о проблеме, то связываемся с разработчиками и помогаем им найти источник проблемы и решить её.
К сожалению, не все ошибки интеграции SDK можно определить по мониторингам. Если такая ситуация возникает, мы обращаемся к партнёру, чтобы уточнить детали интеграции. Затем мы стараемся определить причину проблем и помочь их решить. Если даже этой информации недостаточно, чтобы определить причину ошибок, мы запрашиваем у партнёра разрешение на реверс-инжиниринг приложения. После разрешения мы начинаем смотреть на работу рекламного SDK в приложении как на чёрный ящик. Просматриваем сетевую активность через proxy, проверяем отображение рекламных view через Layout Inspector и др.
Просматривать сетевую активность приложения начиная с Android 7.0 проблематично, так как система по умолчанию не доверяет сертификатам, установленным пользователем. Установка сертификата необходима для просмотра SSL-трафика приложения через proxy. Решит проблему либо запуск приложения на версии Android младше 7.0, либо добавление network_security_config в приложение, например через Apktool. Просмотреть отображение рекламных view можно через утилиту Layout Inspector, запустив приложение на эмуляторе или на устройстве. При этом необходимо модифицировать файл AndroidManifest.xml, добавив через Apktool атрибут debuggable=true.
Если метода чёрного ящика оказалось недостаточно и воспроизвести проблему не удалось, можно посмотреть на логику работы приложения. Для этого можно воспользоваться утилитами для декомпиляции APK, такими как JADX, Bytecode Viewer. Но зачастую такой подход требует слишком много времени и не всегда приводит к результату. Поэтому, чтобы быстрее понимать, как приложение использует SDK изнутри, мы сделали скрипт для подстановки новой реализации SDK в уже собранное приложение.
Подстановка новой реализации SDK в приложение
Подмена кода SDK позволяет через классы изменённой версии SDK внедрить в приложение произвольный код и, например, включить режим дополнительного логирования. Алгоритм работы следующий. Скрипт:
- дизассемблирует DEX-файлы приложения в smali;
- преобразует JAR-файл новой версии SDK в smali;
- подменяет реализацию smali-файлов SDK в smali-файлах приложения;
- пересобирает приложение c новой версией SDK.
Дизассемблирование DEX-файлов приложения в smali
Нужно декомпозировать приложение, разделить его на более мелкие единицы, чтобы появилась возможность изменять код классов SDK, не меняя при этом код приложения. Как подступиться к собранному приложению? Дизассемблируем DEX в smali-файлы.
В Android приложение хранит код в DEX-файлах. Извлечь их из приложения можно через утилиту unzip, так как APK — обычный архив с структурированным содержимым. У DEX-файлов бинарный формат для более плотной по сравнению с JAR упаковки кода. Из-за бинарности DEX нечеловекочитаем, поэтому изменять сам DEX нерационально. Первое, что приходит на ум, — декомпилировать DEX в Java. Такое преобразование возможно, но оно нетривиально и происходит с потерями работоспособности кода. Поэтому воспользуемся трансляцией в smali-код. Преобразование в smali позволяет точно передать инструкции из DEX в человекочитаемом виде с возможностью последующей конвертации в работоспособный код.
Вызов утилиты smali преобразует DEX в набор классов в smali-коде. При этом исходное расположении классов по подпакетам сохраняется.
Преобразование новой версии SDK в smali
Подготовим версию SDK на замену. Чтобы гарантировать воспроизводимость проблемы, версию SDK с включённым логированием создадим на основе той же версии, которая уже интегрирована в приложение. Один из простых способов узнать подключённую версию Yandex Mobile Ads SDK в приложении — просмотреть содержимое метода в классе MobileAds.getLibraryVersion() через Apk Analizer в Android Studio. Узнав используемую версию рекламного SDK, мы переключаемся на ветку данного релиза и собираем версию библиотеки с дополнительным логированием. В результате получаем AAR-файл. Он содержит ресурсы и код библиотеки. Нас в AAR-файле интересует лишь код в JAR-файле, так как в нашем SDK нет внешних ресурсов: все ресурсы заинлайнены непосредственно в код или приходят с бэкенда. Отсутствие файлов с ресурсами упрощает интеграцию SDK в IDE без поддержки современных систем сборки.
Чтобы сменить версию SDK в приложении на новую, приведём AAR к тому же состоянию, что и дизассемблированное приложение, т. е. из AAR получим набор smali-файлов. Преобразование происходит по цепочке: AAR → JAR → DEX → SMALI:
- из AAR с помощью утилиты unzip извлекаем JAR с кодом;
- JAR преобразуем в DEX через утилиту dxdump из Android SDK Tools;
- файлы в smali-коде получаем с помощью утилиты smali с DEX-файлом в качестве параметра.
Подмена реализации SDK
Получив smali-файлы приложения и SDK с логами, заменяем реализацию SDK новой. После чего пересобираем приложение. При трансляции в smali получаемые классы сохраняют расположение по подпакетам. Поэтому, если известен пакет, где лежат классы SDK, легко отличить класс библиотеки от классов приложения. Классы SDK могут быть распределены по нескольким DEX. Так что алгоритм, по которому подменяется реализация SDK, различается для приложения с одним DEX-файлом или несколькими.
Алгоритм подмены реализации SDK в приложении с одним DEX
В приложении с одним DEX мы просто скопируем новые smali-классы SDK поверх всех классов приложения и сгенерируем модифицированный DEX. Сгенерировать DEX-файл можно с помощью утилиты baksmali. На вход утилите подаётся директория с разбитыми по пакетам файлами классов в smali-коде. Пропустив через baksmali объединённые smali-файлы приложения и нового SDK, получаем модифицированный DEX-файл с изменённой логикой SDK.
Алгоритм подмены реализации SDK в приложении с MultiDex
Для приложения с MultiDex добавим отдельным DEX новый код SDK и удалим предыдущую версию SDK из остальных DEX-файлов приложения. Добавление новой версии SDK отдельным DEX позволит обойти ограничение количества методов в формате DEX. MultiDex автоматически догрузит добавленный DEX-файл с кодом SDK при его корректном наименовании. MultiDex ищет DEX-файлы по очереди, используя индекс в конце файла: сначала dex1, затем dex2 — и т. д. Если назвать файл c инкрементированным индексом, то MultiDex автоматически догрузит его в виртуальную машину. Таким образом через baksmali cгенерируем DEX-файлы на основе ранее полученных smali-файлов приложения, но с удалёнными классами старой версии SDK. А также соберём дополнительный DEX-файл с изменённой версией SDK, инкрементируя индекс в названии DEX-файла.
Пересборка приложения c новой версией SDK
Мы получили DEX-файлы приложения с изменённой версией SDK. Дело за малым: заменим DEX-файлы в первоначально распакованном APK-файле приложения модифицированными DEX-файлами. И вызовом команды zip получим финальную версию APK, которую осталось лишь подписать. Подпишем отладочным ключом через apksigner, чтобы приложение можно было установить на устройство. Приложение с изменённой логикой SDK готово.
Недостатки
Алгоритм работает для большинства случаев, но иногда подменить реализацию SDK в приложении не получится. Причины этого:
- Обфускация ProGuard. Правила в consumer-файле библиотеки не дают ProGuard перемещать классы SDK. Но если разработчик отменит эти инструкции, то часть классов библиотеки может изменить свой пакет. В этом случае алгоритм не сработает, так как скрипт не найдёт расположение классов старого SDK.
- Ограничение формата DEX. Если весь код в приложении хранится в одном DEX-файле, который почти полностью исчерпал лимит используемых методов. При замене в приложении версии SDK версией с дополнительным логированием количество методов возрастёт. Лимит будет превышен. По формату DEX ограничение равно 2^16.
- Защита приложения от изменения. В приложение встроены механизмы борьбы с модификацией. Например, через валидацию подписи приложения. Изменяя APK, мы, соответственно, изменяем его подпись. При старте приложения оно сверяет подпись с эталонной и выбрасывает исключение. Особенно сложно убрать эту проверку, если она вынесена в нативную часть.
Итоги
Шаги, описанные в статье, мы автоматизировали простым bash-скриптом. У скрипта есть недостатки, однако он сильно ускоряет разбор сложных проблем при интеграции SDK в приложения партнёров. Хотя, мы редко применяем этот подход, так как зачастую находим решение на более ранних стадиях.
Из преобразования в smali можно извлечь дополнительную пользу: smali-файлы позволяют отлаживать приложение без исходников. Для запуска отладки нужно на основе smali-файлов приложения сгенерировать проект в Android Studio и приаттачить дебаггер к интересующему процессу. Подробнее написано в этой статье.