[Перевод] Картинка, которая одновременно является кодом на Javascript

smnn729ml93ktwej8pmv9h2tfnk.gif


Изображения обычно хранятся как двоичные файлы, а файл Javascript по сути является обычным текстом. Оба типа файлов должны следовать собственным правилам: изображения имеют конкретный формат файла, определённым образом кодирующий данные. Для того, чтобы файлы Javascript можно было исполнять, они должны следовать определённому синтаксису. Я задался вопросом: можно ли создать файл изображения, одновременно являющийся допустимым синтаксисом Javascript, чтобы его можно было исполнять?

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

https://codesandbox.io/s/executable-gif-8yq0j? file=/index.html

Если вы хотите посмотреть изображение и изучить его самостоятельно, то скачать его можно отсюда:

https://executable-gif.glitch.me/image.gif


К сожалению, изображения содержат множество двоичных данных, которые при интерпретации в качестве Javascript будут выдавать ошибку. Поэтому моя первая мысль заключалась в следующем: что если просто поместить все данные изображения в большой комментарий, примерно так:

/*ALL OF THE BINARY IMAGE DATA*/


Это будет допустимый файл Javascript. Однако файлы изображений должны начинаться с определённой последовательности байтов, заголовка файла, специфичного для формата изображения. Например, файлы PNG всегда должны начинаться с последовательности байтов 89 50 4E 47 0D 0A 1A 0A. Если изображение будет начинаться с /*, то файл перестанет быть файлом изображения.

Этот заголовок файла привёл меня к следующей идее: что если использовать эту последовательность байтов как имя переменной и присвоить ей значение длинной строки:

PNG=`ALL OF THE BINARY IMAGE DATA`;


Мы используем шаблонные строки вместо обычных строк " или ', потому что двоичные данные могут содержать разрывы строк, а шаблонные строки с ними справляются лучше.

К сожалению, большинство последовательностей байтов в заголовках файлов изображений содержат непечатаемые символы, которые нельзя использовать в именах переменных. Но есть один формат, который мы можем использовать: GIF. Блок заголовка GIF имеет вид 47 49 46 38 39 61, что удобно преобразуется в ASCII в строку GIF89a — абсолютно допустимое имя переменной!


Теперь, когда мы нашли формат изображения, начинающийся с допустимого имени переменной, нам нужно добавить символы знака равенства и обратного апострофа (backtick). Следовательно следующими четырьмя байтами файла будут: 3D 09 60 04

m9nifb5wdim8wduwjhcvkrcc0q8.png


Первые байты изображения

В формате GIF четыре байта после заголовка определяют размеры изображения. Нам нужно уместить в них 3D (знак равенства) и 60 (обратный апостроф, открывающий строку). В GIF используется порядок little endian, поэтому второй и четвёртый символы имеют огромное влияние на размеры изображения. Они должны быть как можно меньше, чтобы изображение не получилось шириной и высотой в десятки тысяч пикселей. Следовательно, нам нужно хранить большие байты 3D и 60 в наименее значимых байтах.

Второй байт ширины изображения должен быть допустимым пробельным символом (whitespace), потому что он будет пробелом между знаком равенства и началом строки GIF89a= `.... Стоит также помнить, что шестнадцатеричный код символов должен быть как можно меньше, иначе изображение окажется огромным.

Наименьший пробельный символ — это 09 (символ горизонтальной табуляции). Он даёт нам ширину изображения 3D 09, что в little endian равно 2365; немного шире, чем бы мне хотелось, но всё равно вполне приемлемо.

Для второго байта высоты можно выбрать значение, дающее хорошее соотношение сторон. Я выбрал 04, что даёт нам высоту 60 04, или 1120 пикселей.


Пока наш исполняемый GIF почти ничего не делает. Он просто присваивает глобальной переменной GIF89a длинную строку. Мы хотим, чтобы происходило что-нибудь интересное! Основная часть данных внутри GIF используется для кодирования изображения, поэтому если мы попробуем вставить туда Javascript, то изображение, вероятно, будет сильно искажённым. Но по какой-то причине формат GIF содержит нечто под названием Comment Extension. Это место для хранения метаданных, которые не интерпретируются декодером GIF — идеальное место для нашей Javascript-логики.

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

В конечном итоге наш файл может выглядеть следующим образом:

GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:

`;alert("Javascript!");/*

REST OF THE IMAGE */


Однако существует небольшое ограничение: хотя сам блок комментария может иметь любой размер, он состоит из нескольких подблоков, и максимальный размер каждого из них составляет 255. Между подблоками есть один байт, определяющий длину следующего подблока. Поэтому чтобы уместить туда большой скрипт, его нужно разделить на мелкие фрагменты, примерно вот так:

alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...


Шестнадцатеричные коды в комментариях — это байты, определяющие размер следующего подблока. Они не относятся к Javascript, но обязательны для формата файла GIF. Чтобы они не мешали остальной части кода, их нужно поместить в комментарии. Я написал небольшой скрипт, обрабатывающий фрагменты скрипта и добавляющий их в файл изображения:

https://gist.github.com/SebastianStamm/c2433819cb9e2e5af84df0904aa43cb8


Теперь, когда у нас есть базовая структура, нам нужно сделать так, чтобы двоичные данные изображения не испортили синтаксис кода. Как говорилось в предыдущем разделе, файл состоит из трёх разделов: в первом выполняется присваивание значения переменной GIF89a, второй — это код на Javascript, а третий — комментарий из нескольких строк.

Давайте взглянем на первую часть с присвоением значения переменной:

GIF89a= ` BINARY DATA `;


Если двоичные данные будут содержать символ ` или сочетание символов ${, то у нас возникнет проблема, ведь они или завершат шаблонную строку, или создадут недопустимое выражение. Исправить это довольно просто: достаточно изменить двоичные данные! Например, вместо символа ` (шестнадцатеричный код 60) можно использовать символ a (шестнадцатеричный код 61). Так как эта часть файла содержит палитру цветов, то это приведёт к незначительному изменению некоторых цветов, например, к использованию цвета #286148 вместо #286048. Маловероятно, что кто-нибудь заметит разницу.

Боремся с искажениями


В конце Javascript-кода мы открыли многострочный комментарий, чтобы двоичные данные изображения не влияли на парсинг Javascript:

alert("Script done");/*BINARY IMAGE DATA ...


Если данные изображения будут содержать последовательность символов */, то комментарий закончится преждевременно, из-за чего файл Javascript окажется недопустимым. Здесь мы снова можем вручную изменять один из двух символов, чтобы они не завершали комментарий. Однако поскольку теперь мы находимся в разделе закодированного изображения, то в результате получим повреждённое изображение, например такое:

f_0gwirbbteg23j_jpzph_0ictm.gif


Повреждённое изображение

В самых неблагоприятных случаях изображение может вообще не отображаться. Аккуратно выбирая бит, который нужно инвертировать, мне удалось минимизировать искажения. К счастью, было всего несколько случаев повреждающих сочетаний*/. На готовом изображении всё равно заметны небольшие искажения, например, в нижней части строки «Valid Javascript File», но в целом я вполне доволен результатом.

Завершаем файл


Нам осталась последняя операция — завершение файла. Файл должен завершаться байтами 00 3B, поэтому нам нужно завершить комментарий раньше. Поскольку это конец файла и любые потенциальные повреждения будут не особо заметны, я просто завершил комментарий из блоков и добавил однострочный комментарий, чтобы конец файла не вызывал проблем при парсинге:

/* BINARY DATA*/// 00 3B


Теперь, после всего этого, у нас, наконец, есть файла, являющийся одновременно и изображением, и правильным файлом Javascript. Однако нам нужно преодолеть последнее препятствие: если мы загрузим изображение на сервер и попытаемся использовать его в теге script, то, скорее всего, получим подобную ошибку:

Refused to execute script from 'http://localhost:8080/image.gif' because its MIME type ('image/gif') is not executable. [Отказ от исполнения скрипта из 'http://localhost:8080/image.gif', потому что его MIME-тип не является исполняемым.]


То есть браузер справедливо говорит: «Это изображение, я не буду его исполнять!». И в большинстве случаев это вполне уместно. Но мы всё равно хотим его исполнить. Решение заключается в том, чтобы просто не говорить браузеру, что это изображение. Для этого я написал небольшой сервер, передающий изображение без информации заголовка.

Без информации о MIME-типе из заголовка браузер не знает, что это изображение и делает именно то, что лучше всего подходит в контексте: отображает его как изображение в теге или исполняет как Javascript в теге