toggle dark theme

iliazeus

илья, иль не я

Как я заставил работать Netflix на Asahi Linux

Год назад я купил макбук. Полгода назад macOS на нем сказала "ой, все", и он окирпичился. Я решил не переустанавливать систему, а попробовать Asahi Linux, и пока что не пожалел об этом. Хотя одна вещь все же раздражала — не работали Netflix и официальное приложение Spotify.

Если честно, Netflix мне не очень-то и нужен — у BitTorrent сейчас намного лучше UX. Но к Spotify я очень привязался, и предпочитаю интерфейс именно у официального клиента, хотя многим это и покажется странным. Но официального клиента Spotify для Linux на архитектуре aarch64 пока что не существует.

Есть, конечно, web-версия. Вернее, была бы, если бы не ошибка:

Playback of protected content is not enabled.

«Воспроизведение защищенного контента отключено», а конкрентее — не установлен модуль Widevine DRM. По этой же причине не работает и Netflix.

Итак, мы начинаем наш челлендж попробуй не нарушить DMCA 2023! Наша задача — понять, как смотреть Netflix на Asahi Linux, не обходя и не ломая DRM. (Без этого условия решения уместится в 280 знаков).

Установка Widevine

one does not simply...

К сожалению, нельзя так просто взять и установить Widevine. Единственный официально поддерживаемый способ запустить Widevine — Chrome + Linux + x86_64. Внимательный читатель, конечно, сразу задаст вопросы: почему оно тогда работает и в Firefox? Почему работает на Android, это же тоже Linux на aarch64? Почему работает на Raspberry Pi?

Давайте разберем по порядку.

Почему работает в Firefox + Linux + x86_64?

Веб-страницы получают доступ к модулям DRM через API Encrypted Media Extensions. В самом Chrome DRM не реализован, он делегирует это одной из библиотек CDM, или Content Decryption Module. В случае Chrome + Linux + x86_64, это библиотека libwedevinecdm.so — проприетарный блоб, заглядывать в который нам запрещено.

К счастью, мы знаем, как с этим блобом общаться: заголовочные файлы для C++ доступны в рамках проекта Chromium. Это позволяет Firefox использовать у себя ровно ту же проприетарную libwidevinecdm.so, взятую в бинарном виде непосредственно из Chrome. К сожалению, для Asahi Linux нельзя сделать так же — готовой библиотеки для Chrome + Linux + aarch64 не существует.

Почему работает в Android + aarch64?

Если вкратце, DRM на Android в целом работает по-другому. API сильно различаются, взять скомпилированный модуль Widevine для Android просто так не получится, а разобрать его мешает DMCA.

Почему работает на Raspberry Pi?

Как я уже сказал, связка Chrome + Linux + aarch64 официально не поддерживается.

Я солгал.

Хромбуки. В хромбуках работает Chrome, установлен плюс-минус Linux, и многие из них на aarch64. Рано или поздно люди это осознали, и написали утилиту, чтобы вытащить libwidevinecdm.so из recovery-образов для хромбуков. Raspberry Pi, насколько я знаю, достает реализацию Widevine именно так, даже упаковывая в .deb-пакет.

К сожалению, есть загвоздка. Хотя в хромбуках есть aarch64-процессоры и aarch64-ядра Linux, весь их userspace все еще скомпилирован для 32-битной armv7l. Для Raspberry Pi это не проблема, но Apple Silicon не способен переварить 32-битный код. Проблема...

...не проблема! Точнее, уже не проблема.

Несколько месяцев назад, когда я только занялся Widevine для Asahi, все было так. Но пару недель назад где-то в Google таки наступил 21 век, и на новых хромбуках userspace компилируется для aarch64. Значит, libwidevinecdm.so для Linux + aarch64 теперь можно вытащить из recovery-образов ChromeOS, что Pi Foundation уже успела сделать.

Итак, все готово для...

Widevine для Arch Linux на ARM

Конечно, не все так просто. ChromeOS — это не совсем Linux; кроме прочего, в его glibc есть не совместимые с Linux патчи. Если просто взять и загрузить libwidevinecdm.so, получим segfault где-то в недрах glibc.

Эту проблему решает пакет glibc-widevine. Он патчит glibc специально для совместимости с Widevine. Похожие патчи есть и в glibc для Raspbian, разве что там они идут из коробки.

Также нужно пересобрать Chromium с поддержкой Widevine — на Linux + aarch64 это официально не поддерживается, поэтому при стандартной сборке он отключен. Для этого также есть патч.

Итого:

Проблема

Asahi Linux собирается с поддержкой страниц памяти по 16K. Блоб Widevine поддерживает только 4K. Пересобрать ядро под другой размер страниц, конечно, можно, но сейчас для этого нужны костыли и много времени. А просто дизассемблировать и поправить проприетарный блоб нельзя.

Программист заглянул внутрь блоба Widevine. Фото в цвете.

Чтобы понять, в чем именно проблема, посмотрим на то, как libwidevinecdm.so загружается в память. Как и другие .so-библиотеки, внутри это ELF — Executable and Linkable Format — который разбирается загрузчиком — ядром или ld.so — и сообщает ему, как именно загрузить код и данные в память и подготовить их к исполнению.

Внутри файлов ELF есть Program Header Table — таблица заголовков, описывающих сегменты программы. Для сегментов с типом LOAD там описано, как загрузить этот сегмент в память, и разрешить ли эту память читать/писать/исполнять.

С выравниванием этих сегментов и есть проблема. Они загружаются в память вызовами mmap(), который требует:

Загрузчик проверяет эти ограничения:

case PT_LOAD:
    /* A load command tells us to map in part of the file.
       We record the load commands and process them all later.  */
    if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset)
         & (GLRO(dl_pagesize) - 1)) != 0))
      {
        errstring
    = N_("ELF load command address/offset not page-aligned");
        goto lose;
      }

Чтобы не стать goto loserом, необходимо убедиться, что (vaddr - offset) % pagesize == 0, где vaddr — Virtual (memory) Address — адрес в памяти, куда загрузить сегмент, а offset — смещение данных в файле библиотеки.

Вот Program Header Table для моей копии libwidevinecdm.so:

Type             Offset     VAddr      FileSize   MemSize    Align      Prot
PT_PHDR          0x00000040 0x00000040 0x00000230 0x00000230 0x00000008 r--

PT_LOAD          0x00000000 0x00000000 0x00904290 0x00904290 0x00001000 r-x
PT_LOAD          0x00904290 0x00905290 0x00007500 0x00007500 0x00001000 rw-
PT_LOAD          0x0090b790 0x0090d790 0x00000df0 0x00c36698 0x00001000 rw-

PT_TLS           0x00904290 0x00905290 0x00000018 0x00000018 0x00000008 r--
PT_DYNAMIC       0x00909618 0x0090a618 0x00000220 0x00000220 0x00000008 rw-
PT_GNU_RELRO     0x00904290 0x00905290 0x00007500 0x00007d70 0x00000001 r--
PT_GNU_EH_FRAME  0x00524a24 0x00524a24 0x000010fc 0x000010fc 0x00000004 r--
PT_GNU_STACK     0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 rw-
PT_NOTE          0x00000270 0x00000270 0x00000024 0x00000024 0x00000004 r--

Я выделил пустыми строками три сегмента PT_LOAD.

Если pagesize == 0x1000 (4 КБ), то ограничения соблюдаются для всех сегментов. Но стоит увеличить pagesize до 0x4000 (16 КБ), как в Asahi Linux, как второй и третий сегменты PT_LOAD станут его нарушать. Для других сегментов это не так важно — они не загружаются напрямую через mmap().

Решение

Менять положение сегментов относительно друг друга в памяти нельзя — это сломает в коде относительные смещения из одного сегмента в другой. Более того, это библиотека DRM — она злится на любые изменения себя в памяти. А на копание в коде этой библиотеки злится DMCA.

Посмотрим еще раз на наше злополучное условие (vaddr - offset) % pagesize == 0. Менять vaddr мы не можем по причинам выше. Но мы можем поменять offset, если переместим сегменты в самом файле библиотеки.

Для первого PT_LOAD ничего делать не нужно, но вот для второго получаем vaddr - offset == 0x00905290 - 0x00904290 == 0x1000. Исправим это, добавив 0x1000 байт паддинга между первым и вторым сегментами в файле, не забыв поправить offset. Теперь vaddr - offset == 0x00904290 - 0x00904290 == 0. С третьим сегментом поступим аналогично.

При добавлении паддинга в ELF нужно поправить и некоторые другие поля. Но мы меняем только сам ELF-файл — загруженный в память код будет идентичным оригиналу, запущенному на системе со страницами памяти по 4K. Поэтому библиотека при самопроврке ничего не заподозрит и не разозлится.

Гранулярность разрешений

В системах с 4K-страницами каждые 4КБ памяти могут иметь свой набор разрешений на чтение/запись/исполнение. Библиотка была скомпилирована с учетом этой возможности. Но на системах с 16K-страницами гранулярность таких разрешений 16КБ. Это порождает две проблемы.

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

Во-вторых, по той же причине пришлось отключить RELRO — Relocation Read-Only — еще одну меру безопасности, которая отмечает некоторые секции как read-only после загрузки.

Само по себе это — не уязвимости, но это ослабление защиты против потенциально уже существующих. Злоумышленник, теоретически, может записать в такую страницу произвольный код, а затем исполнить его. На практике, ему нужно для начала будет найти уязвимость в браузере. Если это вас пугает, можно использовать для Netflix отдельный браузер.

Патчинг ELF

Сначала я пытался использовать LIEF, но то ли из-за багов, то ли из-за кривости рук у меня не вышло. В конце концов, я в кофеиновом трансе расчехлил hexedit и поправил все руками. К моему удивлению, это сработало!

Не уверен, что я могу легально распространять патченый ELF, но я написал скрипт на питоне, которым вы можете пропатчить его сами. Достаточно запустить этот скрипт, и вот у вас уже есть libwedevinecdm.so, которую может загрузить Firefox под Asahi Linux!

Финальные штрихи

Из-за странностей в glibc на ChromeOS, о которых я говорил ранее, мне пришлось написать библиотечку с функциями __aarch64_ldadd4_acq_rel и __aarch64_swp4_acq_rel, которую я загружал через LD_PRELOAD. Это выглядело не очень красиво, и я стал думать, как можно добавить эти функции в сам libwidevinecdm.so.

Еще помните, что нам нужно было добавить туда 0x1000 байтов для выравнивания? Они попадают в исполняемую память, поэтому я засунул эти функции туда! Я боялся, что библиотеке это не понравится, но, кажется, она их не заметила при своих проверках. Программа получает их адреса через Global Offset Table — таблицу смещений функций, которая заполняется загрузчиком из данных в самом ELF. Я заменил эти данные так, чтобы они указывали на место, куда я дописал новые функции.

Все это я включил в свой Python-скрипт, который, с одобрения мейнтейнера, добавил в пакет widevine-aarch64. Теперь достаточно установить widevine-aarch64 из AUR, и с Widevine на Asahi Linux будет готов к работе!

Особенности Netflix

Spotify у меня заработал, но Netflix все равно отказывался что-либо показывать. Дело было в проверке User-Agent. В конце-концов, я поменял его на взятый из ChromeOS:

Mozilla/5.0 (X11; CrOS aarch64 15236.80.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.125 Safari/537.36

Версия Widevine, которую мы получили, называется L3 — наименее защищенный уровень. Более высокие уровни защиты требуют аппаратной поддержки. На Apple Silicon есть нужные чипы, но библиотека не родная, и их поддержка там нет.

Большинство сервисов не дают смотреть 4K-контент на таком уровне защиты — максимум 1080p. Но Netflix и тут отличился: по умолчанию он отдает таким клиентам 720p, а 1080p включает, только если попросить его особым образом на уровне самого протокола. Для этого есть браузерные расширения. Не уверен, зачем они так сделали; возможно, у кого-то из клиентов были проблемы из-за отсутствия поддержки L3-версией "железного" декодирования видео?

Заключение

Меня забавляет, что все это я делал не чтобы обойти DRM, а наоборот, чтобы оно наконец заработало нормально. Это неправильно! Из того, что я смог легально посмотреть контент, за который я заплатил, в нормальном мире не должно получаться детективной статьи!

Дорогой Гугл, пожалуйста, добавь в матрицу сборки хотя бы Ubuntu на aarch64. Я знаю, тебе не сложно.