TL;DR

  • Я впервые купил на Amazon электронную книгу.

  • Android-приложение Kindle самой компании Amazon было очень забагованным и часто вылетало.

  • Попробовал скачать мою книгу, чтобы читать её в реально работающем приложении для чтения.

  • Осознал, что Amazon больше не позволяет этого делать.

  • Решил назло выполнить реверс-инжиниринг её системы обфускации.

  • Обнаружил множество слоёв защиты, в том числе рандомизированные алфавиты.

  • Победил их все при помощи колдунства с сопоставлением шрифтов.

Часть 1: Amazon вынудила меня объявить вендетту

Тот раз, когда я решил сделать всё по правилам

Годами я «находил» электронные книги. Но в ЭТОТ раз я решил: «Попробую поддержать автора».

Скачал приложение Kindle для Android. Открыл книгу.

Вылет.

Я просто хотел прочитать книгу

Приложение вылетает. Ну ладно, просто воспользуюсь веб-приложением.

Ой, а я не могу скачать его для офлайн-чтения. Что, если я нахожусь в самолёте?

Постойте-ка, книгу даже нельзя экспортировать в Calibre? Туда, где я храню ВСЕ свои остальные книги?

Подведём итог:

  • Я заплатил деньги за эту книгу.

  • Её можно читать только в поломанном приложении Amazon.

  • Я не могу её скачать.

  • Не могу создать её резервную копию.

  • На самом деле, она мне не принадлежит.

  • Amazon может удалить её, когда пожелает.

Это прокат, а не покупка.

Тут написано «Купить», не «Взять напрокат»
Тут написано «Купить», не «Взять напрокат»

Это уже личное

Я мог бы вернуть за книгу деньги и «найти» её кое-где ещё за полминуты. Так было бы проще.

Но смысл не в этом.

Смысл в том, что Я ЗАПЛАТИЛ ЗА ЭТУ КНИГУ. Она моя. И я собираюсь читать её в Calibre с остальной частью моей библиотеки, даже если для этого мне понадобится выполнить реверс-инжиниринг веб-клиента Amazon.

Время для реверсинга

Kindle Cloud Reader (веб-версия) работает. Изучая сетевые запросы, я наткнулся на следующее:

https://read.amazon.com/renderer/render

Чтобы что-то скачать, нужны:

1. Куки сессии — стандартный логин Amazon.

2. Токен рендеринга — из вызова API startReading.

3. Токен сессии ADP — дополнительный слой аутентификации.

При отправке тех же заголовков и куки браузер возвращает файл TAR.

Что внутри TAR?

page_data_0_4.json   # «Текст» (спойлер: на самом деле, это не текст)
glyphs.json          # SVG-определения для каждого символа
toc.json             # Содержание
metadata.json        # Информация о книге
location_map.json    # Привязки позиций

Часть 3: слои обфускации Amazon из ада электронных книг

Я скачал первые несколько страниц, ожидая увидеть текст, но вместо них получил это:

{
  "type": "TextRun",
  "glyphs": [24, 25, 74, 123, 91, 18, 19, 30, 4, ...],
  "style": "paragraph"
}

Это не буквы, а ID глифов. Символ «T» — это не 84 в Unicode, а его глиф 24.

А глиф 24 — это просто последовательность чисел, определяющая контуры, то есть изображение буквы.

Это подстановочный шифр! Каждый символ сопоставляется с идущими не по порядку ID глифа.

Алфавит меняется. Каждые пять страниц.

Я скачал следующий набор страниц. Та же самая буква «T» теперь имеет глиф 87.

В следующем наборе это уже глиф 142.

Они рандомизируют весь алфавит при КАЖДОМ запросе.

Это означает, что:

  • Можно получать только по пять страниц за раз (жёсткое ограничение API).

  • Каждый запрос получает совершенно новые привязки глифов.

  • ID глифов одного запроса не имеют никакого смысла в других запросах.

  • Невозможно создать единую таблицу сопоставления для всей книги.

Покажу, насколько всё плохо

Для моей 920-страничной книги:

  • Требуется 184 отдельных запроса к API.

  • Нужно взломать 184 разных случайных алфавита.

  • Обнаружен 361 уникальный глиф (a-z, A-Z, знаки пунктуации, лигатуры).

  • Суммарно нужно декодировать 1051745 глифов.

Поддельный хинтинг шрифтов (они заметают следы)

Некоторые контуры SVG содержали вот такой мусор:

M695.068,0 L697.51,-27.954 m3,1 m1,6 m-4,-7 L699.951,-55.908 ...

Мы видим маленькие команды m3,1 m1,6 m-4,-7, это микрооперации MoveTo.

Что в этом коварного:

  • Браузеры обрабатывают их без проблем (нативный Path2D).

  • SVG-библиотеки Python создают ложные соединительные линии.

  • При наивном рендеринге глифы выглядят повреждёнными.

  • Это препятствует методикам сэмплирования контуров.

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

Взгляните

Весело!
Весело!

Со временем я разобрался, что с этим можно бороться заполнением полного контура.

Множественные варианты шрифтов

Он не просто один, их ЧЕТЫРЕ варианта:

  • bookerly_normal (99% глифов);

  • bookerly_italic (выделение);

  • bookerly_bold (заголовки);

  • bookerly_bolditalic (выделенные заголовки).

Плюс специальные лигатуры: ff, fi, fl, ffi, ffl

Больше вариаций = больше уникальных глифов, которые нужно взломать = больше мучений.

Оптическое распознавание справляется так себе (моя неудавшаяся попытка)

Я попробовал выполнить оптическое распознавание символов отрендеренных глифов. Результаты:

  • Распознано 178/348 глифов (51%).

  • С 170 глифами полный провал.

Программы OCR ужасно справляются с отдельными символами без контекста. Они путают «l» с «I» и «1». Не могут обрабатывать пунктуацию. Совершенно отказываются работать с лигатурами.

Вероятно, для хорошей работы OCR требуются слова и предложения.

Часть 4: решение, которое действительно работает

В каждом запросе содержится glyphs.json с определениями SVG-контуров:

{
  "24": {
    "path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...",
    "fontFamily": "bookerly_normal"
  },
  "87": {
    "path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...",
    "fontFamily": "bookerly_normal"
  }
}

ID глифов меняются, но SVG-фигуры остаются прежними.

Почему не удалось сравнивать SVG напрямую

Первая попытка: нормализация и сравнение координат SVG-контуров.

Закончилась неудачей, потому что:

  • Координаты немного варьируются

  • Команды контуров задаются по-разному

Попиксельно-точное сопоставление

К чёрту сравнение координат. Давайте просто отрендерим всё и будем сравнивать пиксели.

1. Рендерим каждый SVG в виде изображения

  • Используем библиотеку cairosvg (она позволяет корректно обрабатывать этот поддельный хинтинг шрифтов).

  • Для обеспечения точности выполняем рендеринг с разрешением 512 x 512px.

2. Генерируем перцептивные хэши

  • Хэшируем каждое отрендеренное изображение.

  • Хэш превращается в уникальный идентификатор.

  • Одинаковая форма = одинаковый хэш, вне зависимости от ID глифа.

3. Создаём пространство нормализованных глифов

  • Сопоставляем все 184 случайных алфавита с ID хэшей.

  • Теперь глиф «a1b2c3d4...» всегда означает букву «T».

4. Выполняем сопоставление с символами

  • Скачиваем TTF-шрифты Bookerly.

  • Рендерим каждый символ (A-Z, a-z, 0-9, пунктуация).

  • Используем SSIM (Structural Similarity Index) для сопоставления.

Почему SSIM идеально подходит для этой задачи

SSIM сравнивает не пиксели напрямую, а структуру изображения. Он справляется с:

  • Незначительными различиями в рендеринге.

  • Вариациями антиалиасинга.

  • Незначительными проблемами масштаба.

Для каждого неизвестного глифа мы находим в TTF символ с наибольшим значением SSIM. Это и будет наша буква.

Обработка пограничных случаев

Лигатуры: ff, fi, fl, ffi, ffl

  • Это одинарные глифы для н��скольких символов

  • Пришлось добавлять их в библиотеку TTF вручную

Особые символы: длинное тире, кавычки, маркеры списков

  • Расширенный набор символов, выходящий за рамки основного ASCII

  • Сопоставление с полным диапазоном Unicode в Bookerly

Варианты шрифтов: полужирный, курсив, полужирный курсив

  • Создание отдельных библиотек для каждого варианта

  • Сопоставление со всеми библиотеками, выбор наилучшей оценки

Часть 5: момент, когда всё заработало

Финальная статистика

=== ЭТАП НОРМАЛИЗАЦИИ ===
Общее количество обработанных наборов: 184
Количество найденных уникальных глифов: 361
Общее количество глифов в книге: 1051745

=== ЭТАП СОПОСТАВЛЕНИЯ ===
Выполнено успешное сопоставление 361/361 уникальных глифов (100,00%)
Неудачное сопоставление: 0 глифов
Средняя оценка SSIM: 0,9527

=== ДЕКОДИРОВАННЫЙ ВЫВОД ===
Общее количество символов: 5623847
Страниц: 920

Идеально. Мы корректно декодировали все символы.

Воссоздание EPUB с идеальным форматированием

В JSON содержится позиционирование для каждого прогона текста:

{
  "glyphs": [24, 25, 74],
  "rect": {"left": 100, "top": 200, "right": 850, "bottom": 220},
  "fontStyle": "italic",
  "fontWeight": 700,
  "fontSize": 12.5,
  "link": {"positionId": 7539}
}

Я воспользовался им, чтобы сохранить:

  • Разрывы параграфов (изменения координат Y)

  • Выравнивание текста (паттерны по координатам X)

  • Стилизацию полужирным/курсивом

  • Размеры шрифтов

  • Внутренние ссылки

Получившийся EPUB почти неотличим от оригинала!

Заключение

Amazon приложила серьёз��ые усилия к своей веб-обфускации.

Оправданы ли были мои мучения?

Для того, чтобы прочитать одну книгу? Нет.

Для того, чтобы отстоять свою точку зрения? Разумеется.

Чтобы узнать о рендеринге SVG, перцептивном хэшировании и метриках шрифтов? Вероятно, да.

Пользуйтесь этим знанием ответственно

Применять его можно только для резервного копирования КУПЛЕННЫХ книг.

Пожалуйста, не подавайте на меня в суд и не оставляйте без штанов.

Если вы каким-то образом связаны с Amazon, то свяжитесь со мной по адресу pixelmelt + at + protonmail.com.