eFusion 29 сентября 2020

?️ Изображая жертву: как зашить в картинку исполняемый код

Можно ли создать графический файл с исполняемым сценарием JavaScript внутри? Рассказываем как.

Перевод публикуется с сокращениями, автор оригинала: Sebastian Stamm.

***

Картинка, которая одновременно является кодом на JavaScript

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

        <!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      body {
        margin: 0;
        overflow: hidden;
        height: 100vh;
      }

      img {
        width: 100%;
      }
    </style>
    <title>This Image Is Also a Valid Javascript File</title>
  </head>
  <body>
    <!--
      Изображение ниже, является валидным файлом Javascript!

      Пожалуйста, замените тег изображения тегом сценария, чтобы увидеть его в действии.
    -->
    <img src="https://executable-gif.glitch.me/image.gif" />

    <!-- <script src="https://executable-gif.glitch.me/image.gif"></script> -->
  </body>
</html>

    

Выбираем тип изображения

Изображения содержат массу двоичных данных, которые выдадут ошибку, если их интерпретировать как 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.

Выбираем размер изображения

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

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

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

Наименьший пробельный символ – 09 (символ горизонтальной табуляции). Это дает нам ширину изображения 3D09 или 2365.

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

Создаем скрипт

Сейчас наш исполняемый GIF ничего не делает. Он просто присваивает большую строку глобальной переменной GIF89a. Большая часть данных внутри GIF предназначена для кодирования изображения, поэтому, если попытаться добавить туда код JavaScript, то, вероятно, получим поврежденное изображение. Однако формат содержит область, называемую расширением комментария и предназначенную для хранения некоторых метаданных, которые не будут интерпретироваться декодером GIF.

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

Скрипт может выглядеть следующим образом:

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

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

REST OF THE IMAGE */
    

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

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

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

        const fs = require("fs");

const scriptChunks = [
  `\`;document.body.innerHTML='<div style="text-align: center;"><h1 style="margin-top: 45vh; transform: translate(0,-50%); font-family: monospace;">Click Button to play Snake! (Control with WASD)</h1></div>';`,
  `document.querySelector('div').innerHTML += '<button id="play" style="font-size: 32px;">Start Game</button>';`,
  `document.querySelector("#play").addEventListener("click",()=>{const e=document.querySelector("h1");document.querySelector("#play").style.display="none",e.textContent="3",e.style.fontSize="3em";const t=document.createElement("style");`,
  `t.innerHTML=\`@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }\n  @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }\n  @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg);}}\`;`,
  `document.head.appendChild(t),document.body.style.overflow="hidden",setTimeout(()=>{e.textContent="2",e.style.fontSize="4em"},1e3),setTimeout(()=>{e.textContent="1",e.style.fontSize="5em"},2e3);setTimeout(()=>{`,
  `document.body.innerHTML=\`<iframe width="100%" height="100%" src="https://www.youtube.com/embed/dQw4w9WgXcQ?controls=0&autoplay=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"></iframe>\`;`,
  `document.body.innerHTML+='<div id="lol-as-if-i-would-let-you-turn-it-off-blocker-div" style="position: absolute; top: 0; bottom:0; left: 0; right: 0;"></div>';},3e3);`,
  `setTimeout(()=>{const e=document.createElement("div");e.textContent="I was too lazy to write a snake game.";`,
  `e.setAttribute("style","position: absolute;top: 45%;color: white;text-align: center;width: 100%;font-size: 3vw;text-shadow: 2px 2px 2px black; animation:spin 6s linear infinite;"),document.body.appendChild(e)},4500)});`,
  `alert("Script works! Press the Button to play a game of Snake!");`,
];

const chunks = scriptChunks.map((chunk, i) => {
  if (i > 0) {
    chunk = "*/" + chunk;
  }
  chunk += "/*";
  return chunk;
});

const scriptContent = [];
for (let i = 0; i < chunks.length; i++) {
  const chunk = chunks[i];
  scriptContent.push(chunk.length);
  for (let j = 0; j < chunk.length; j++) {
    scriptContent.push(chunk.charCodeAt(j));
  }
}

fs.writeFileSync("out.js", Buffer.from(scriptContent));

fs.readFile("image.gif", function (err, buffer) {
  const data = [...buffer];

  for (let i = 0; i < data.length; i++) {
    if (data[i] === 33 && data[i + 1] === 254) {
      // found comment section
      const currentCommentLength = data[i + 2];

      data.splice.apply(data, [
        i + 2,
        currentCommentLength + 1, // remove current comment
        ...scriptContent, // add script as new comment
      ]);

      fs.writeFileSync("out.gif", Buffer.from(data));
      break;
    }
  }
});
    

Очищаем двоичные данные

Когда у нас есть базовая структура, следует убедиться, что двоичные данные картинки не разрушают синтаксис сценария. Файл состоит из трех разделов: первый – присвоение переменной GIF89a, второй – код JavaScript и третий – многострочный комментарий.

Посмотрим на первую часть:

        GIF89a= ` BINARY DATA `;
    

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

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

В конце кода мы открыли многострочный комментарий. Нужно убедиться, что бинарные данные картинки не мешают синтаксическому анализу:

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

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

В некоторых случаях картинка вообще не могла отрисоваться. Экспериментируя с поворотами битов, я смог свести к минимуму «поломку» изображения. К счастью было только несколько случаев появления вредной комбинации « */ » и в конечном изображении все еще есть немного повреждений, видимых, например, в нижней части строки «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.

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

Браузер в этом случае делает то, что лучше всего соответствует контексту: отображает его картинку в теге <img> или выполняет его JavaScript в теге <script>.

Зачем это все?

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

***

По соображениям безопасности мы не стали публиковать код сервера. С вами была Библиотека программиста. Удачи в хакинге картинок!


Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Middle\Senior .Net разработчик
от 120000 RUB до 165000 RUB
Frontend разработчик
Санкт-Петербург, по итогам собеседования
Программист С++ (Middle)
по итогам собеседования
Tableau developer
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG