Опасный хакер прячет Spotify-код с секретной информацией
Опасный хакер прячет Spotify-код с секретной информацией

Введение

Одной бессонной ночью мы с приятелем разговорились о Spotify-кодах.

Это такие картинки, которые можно отсканировать приложением и попасть на нужный трек/альбом/плейлист. Придуманы они, чтобы люди могли делиться музыкой в социальных сетях или в оффлайне.

Пример. Такая грусть у меня играет прямо сейчас.
Пример. Такая грусть у меня играет прямо сейчас.

Мы решили разобраться, как же Spotify кодирует в них ссылки.

Как работают коды?

Выяснилось, что помимо меню «Поделиться» в приложении, есть официальный сайт Spotify Codes, который генерирует такие коды.

Если скопировать ссылку на картинку с этого сайта, получится что-то такое: https://scannables.scdn.co/uri/plain/jpeg/000000/white/640/spotify:track:5jxN9knH0vlfpN2Ft7a5xi

Прекрасно! Динамическая ссылка, которая принимает на вход ID трека и возвращает изображение с баркодом — самое то для наших экспериментов.

Очень удобно, что генератор может рисовать коды и в SVG. Это позволило без головной боли понять, что столбики бывают восьми разных высот.

Числа в поле height — высоты столбиков
Числа в поле height — высоты столбиков

Первый и последний столбик всегда минимального размера, так что он, видимо, используется для ориентирования при сканировании, как и логотип Spotify, который на картинке всегда слева. Без них ничего не работает.

Важные данные выделены красным
Важные данные выделены красным

Получив число 556205622371746371156, приложение превращает его в 58992959842 с помощью таблицы Грея. Именно это делает распознаванием таким быстрым и устойчивым к ошибкам. Как узнали? К этому моменту мы уже набрели на патент Spotify, описывающий принцип работы таких кодов.

Остался последний вопрос: как же приложение превращает лаконичное 58992959842 в spotify:track:5jxN9knH0vlfpN2Ft7a5xi?

Очевидно, такое число не может вместить в себя все комбинации длинного цифро-буквенного ID, а значит никакого алгоритма нет и соответствие между кодом и ID трека хранится где-то в базе.

Можно проверить, подсунув генератору какой-нибудь очень длинный мусор, который точно не влез бы в число: https://scannables.scdn.co/uri/plain/jpeg/000000/white/640/spotify:track:thisisaverylongidentifierwhichwoulddefinitelyoverflowthatnumericcode

Сработало. Печально, а ведь было интересно научиться кодировать/раскодировать такие картинки полностью самостоятельно.

Решила проверить обмен приложения с сервером в момент распознавания и догадка подтвердилась:

На сервер уходит 58992959842, а возвращается трек 5jxN9knH0vlfpN2Ft7a5xi
На сервер уходит 58992959842, а возвращается трек 5jxN9knH0vlfpN2Ft7a5xi

Скука: дальше всё происходит за ширмой бэкенда. Расходимся?

Оченьдлинныймусор

Стоп, что? Генератор сделал картинку для оченьдлинногомусора?

Вероятно, он не проверяет реальность входных данных и бережно складывает наш мусор в базу, присваивая ему 11-значный числовой ID.

Картинка с мусором из примера выше
Картинка с мусором из примера выше

А что будет, если считать такой код приложением?

Будет ошибка
Будет ошибка

Это ожидаемая реакция. Наверное, сервер уже проверил и инвалидировал некорректный код. Интересно посмотреть, как выглядит такая ошибка, поэтому загляну в трафик ещё раз:

Эм, что?
Эм, что?

Великолепно. Spotify складывает в базу всё, что мы укажем в запросе к генератору, хранит это там, а при сканировании отдаёт обратно в первозданном виде, никак не валидируя.

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

Всё лучше под музыку

Чтобы всё было аккуратно, нужно заставить приложение играть музыку, несмотря на чужеродные данные в ID трека.

Я уже собралась реверсить обфусцированный код Android-приложения, чтобы узнать, как работает парсер, но приятель предложил попробовать разделять музыкальный ID и нашу полезную нагрузку знаком вопроса. Идея сработала, но знак пришлось дважды пропустить через urlencode. Например, spotify:track:2ctvdKmETyOzPb2GiJJT53%253Fhi,habr!, выглядит вот так:

Код с двойным дном
Код с двойным дном

Такой код приложение считает как положено и включит песню, а посмотрев в трафик можно достать спрятанные данные, что и стало корнем моего квеста. Для удобства можно закодировать данные в Base64, влезет в URL их немало.

Выводы

Почему Spotify позволяет хранить в базе мусор? Вероятно, потому что генерацией и распознаванием кодов занимается отдельный микросервис, который должен отвечать моментально. Проверка на существование трека потребовала бы обращения к основному бэкенду, а это ресурсоёмкая задача.

Навредит ли это Spotify? Теоретически эта особенность позволяет заполнить таблицу соответствий кодов и исчерпать всю ёмкость, сломав генерацию картинок для настоящих треков, но на практике это займёт очень много времени. Я посчитала.

Полезно ли это на практике? Не знаю, как это использовать. Можете прятать номера телефонов любовниц в кодах с Егором Летовым и обклеивать ими стены, тогда ваша законная женщина покинет вас, сочтя сумасшедшим, будете свободны. Только имейте в виду, что запрос к API для считывания кода, к сожалению, требует авторизационный токен Spotify-аккаунта, в отличие от запроса для генерации картинки.

Оригинальный квест был здесь, послание было в Base64. Кто-то прошёл его за 42 минуты. На большее я не рассчитывала: имеющие привычку лезть в трафик приложений догадались бы сразу.

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

UPD: утром 15 сентября, то есть спустя всего сутки после публикации, Spotify исправил ошибку.

Больше генератор кодов не принимает мусор, теперь работают только подходящие по формату ID.

Это очень обидно, потому что я думала, что им плевать и они сочтут это фичей. Могла бы в Bug Bounty податься :(