Подробнее об этом хаке и особенностях его работы можно узнать из доклада на !!con 2020 «Playing Breakout… inside a PDF!!»
Если вы его не смотрели, то попробуйте открыть файл breakout.pdf в Chrome.
Как и многие из вас, я всегда считал PDF довольно безопасным форматом: автор создаёт текст и графику, после чего он открывается в программе просмотра PDF, больше ничего не делая. Несколько лет назад я мимоходом слышал об уязвимостях Adobe Reader, но особо не задумывался о том, как они могут возникать.
Изначально Adobe сделала PDF именно для этого, но мы уже выяснили, что сегодня это совсем не так. В 1310-страничной спецификации PDF (на самом деле довольно понятном и интересном чтиве) описывается безумное количество возможностей, в том числе:
- Встроенный Flash
- Аннотации в виде звука и видео
- Аннотации в виде 3D-объектов (!)
- Метаданные Web capture
- Произвольные математические функции (в том числе и Тьюринг-неполное подмножество PostScript)
- Формы с поддержкой Rich text, использующие подмножество XHTML и CSS
- Вложения в виде файлов и коллекций файлов
но самое интересное для нас…
- Это скрипты JavaScript на основе стандартной библиотеки, совершенно отличающейся от библиотеки браузера
Разумеется, большинство программ для чтения PDF (кроме Adobe Reader) не реализует основную часть этих возможностей. Однако Chrome реализует JavaScript! Если вы откроете подобный файл PDF в Chrome, то он запустит скрипт. Я выяснил, повторив действия из этого поста о создании PDF с JS.
Однако здесь есть хитрость. Chrome реализует только крошечное подмножество огромной поверхности Acrobat JavaScript API. Реализация API в PDFium браузера Chrome в основном состоит из подобных заглушек:
FX_BOOL Document::addAnnot(IJS_Context* cc,
const CJS_Parameters& params,
CJS_Value& vRet,
CFX_WideString& sError) {
// Not supported.
return TRUE;
}
FX_BOOL Document::addField(IJS_Context* cc,
const CJS_Parameters& params,
CJS_Value& vRet,
CFX_WideString& sError) {
// Not supported.
return TRUE;
}
FX_BOOL Document::exportAsText(IJS_Context* cc,
const CJS_Parameters& params,
CJS_Value& vRet,
CFX_WideString& sError) {
// Unsafe, not supported.
return TRUE;
}
И я понимаю опасения разработчиков — этот Adobe JavaScript API имеет совершенно огромную площадь поверхности. Предположительно, скрипты могут выполнять такие действия, как соединение с произвольными базами данных, распознавание подключенных мониторов, импорт внешних ресурсов и манипулирование 3D-объектами.
Поэтому в Chrome получилась такая странная ситуация: мы можем выполнять произвольные вычисления, однако имеем эту странную, ограниченную поверхность API, при которой ввод-вывод и передачу данных между программой и пользователем реализовывать очень неудобно.
Вероятно, можно встроить в PDF компилятор C, скомпилировав его в JS, например, с помощью Emscripten, но тогда компилятор C должен будет получать ввод из формы простого текста (plain text), а вывод выполнять снова в поле формы.
На самом деле, я заинтересовался PDF пару недель назад из-за PostScript; я читал посты Дона Хопкинса о NeWS — напоминающей AJAX системе, но реализованной в 80-х на PostScript.
Забавно, что PDF стал реакцией на PostScript, который был слишком выразительным (являясь полнофункциональным языком программирования), слишком сложным в анализе и восприятии. Наверно, PDF по-прежнему остаётся в этом плане шагом вперёд, но всё равно смешно, что он разросся всеми этими возможностями.
Ещё один любопытный момент: как любой долгоживущий цифровой формат (лично я испытываю нежные чувства к файловой системе FAT), PDF сам по себе является своего рода историческим документом. Мы можем отследить, как поколения инженеров добавляли нужные им в своё время функции, пытаясь при этом не поломать уже существующие.
Я не совсем понимаю, зачем разработчики Chrome вообще заморачивались поддержкой JS. Они взяли код программы чтения PDF из Foxit; возможно, у Foxit был какой-то клиент, использовавший валидацию форм через JavaScript?
Кроме того, Chrome использует тот же рантайм, что и в браузере, хоть и не раскрывает браузерных API. Насколько я понимаю, это значит, что можно использовать такие возможности ES6, как стрелочные функции и прокси.
Breakout
Так что же мы можем сделать с предоставляемой нам Chrome поверхностью API?
Кстати, должен извиниться за неидеальное распознавание коллизий и непостоянную скорость игры. БОльшую часть игры я содрал с туториала.
Первые доступные пользователю точки ввода-вывода, которые я смог найти в реализации PDF API браузера Chrome, находились в Field.cpp.
Мы не можем менять цвет заливки текстового поля во время выполнения, зато можем менять прямоугольник его границ и задавать стиль границ. Мы не можем считывать точное положение мыши, однако можем при создании PDF привязать к полям скрипты mouse-enter и mouse-leave. Также во время выполнения нельзя добавлять поля: придётся ограничиться тем, что мы поместили в PDF в момент создания. Любопытно, почему разработчики выбрали именно эти методы? Это похоже на какой-то стереотип о программировании на олдскульном FORTRAN: необходимо объявлять все переменные заранее, чтобы компилятор мог статически выделить под них память.
Итак, файл PDF генерируется скриптом, заранее создающим набор текстовых полей, в том числе и игровых элементов:
- Ракетку
- Кирпичи
- Мяч
- Очки
- Жизни
Но для правильной работы игры мы также внесём некоторые хаки.
Во-первых, мы создаём тонкую длинную «полосу» текстового поля для каждого столбца нижней половины экрана. Полосы получают событие mouse-enter, когда игрок перемещает мышь по оси X, поэтому ракетка может перемещаться при движении мыши.
Во-вторых, мы создаём поле под названием «whole», закрывающее всю верхнюю половину экрана. Chrome не ожидает, что отображение PDF будет меняться, поэтому если перемещать поля в JS, то получатся довольно сильные артефакты. Это поле «whole» решает данную проблему — мы включаем/отключаем его во время рендеринга кадра. Этот трюк заставляет Chrome подчищать артефакты.
Кроме того, при перемещении поля, похоже, сбрасывается его поток внешнего отображения. Выбранный вами красивый внешний вид из произвольной PDF-графики «слетает» и заменяется простым залитым прямоугольником с границей. Поэтому в моей игре используется упрощённый словарь характеристик внешнего вида. В самом крайнем случае указанный там цвет заливки при перемещении виджета остаётся неизменным.
Полезные ресурсы
- Справочное руководство по PDF, шестая редакция
- Справочное руководство JavaScript for Acrobat API
- Туториалы Брендана Загерски Minimal PDF и Hand-coded PDF
- В документе PDF Inside and Out есть отличные примеры.
- Библиотека Python pdfrw обладает идеальным для такой работы уровнем абстракции. Многие библиотеки слишком высокоуровневые и раскрывают только графические операторы. Самостоятельная генерация данных PDF возможна, но довольно неудобна, потому что нужно правильно реализовать форматы структур данных и байтовые сдвиги.
На правах рекламы
Закажите сервер и сразу начинайте работать! Создание VDS любой конфигурации в течение минуты, в том числе серверов для хранения большого объёма данных до 4000 ГБ. Эпичненько :)