Рецензия на задачи Script Kiddie 1 и Script Kiddie 2

У picoCTF 2019 было множество интересных задач. Я хочу дать вам краткий обзор того, как я решил две веб-задачи: JS Kiddie 1 (400 баллов) и JS Kiddie 2 (450 баллов). Что вам может показаться интересным, так это то, что вы можете решить две задачи с помощью одного и того же кода.

Эта задача казалась проблемной, поскольку ее решало не так много людей, как другие проблемы. Для сравнения, самый простой из них был решен более чем 21 тыс. Человек, тогда как ~ 570 и 460 игроков решили JS Kiddie 1 и 2. Это дает нам менее 3% всех игроков, которые его решили.

Детский сценарий Java 1

Веб-сайт задачи содержит два элемента - текстовый ввод и кнопку. Исходный код этой страницы показал, что мы должны были предоставить ключ для расшифровки изображения.

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

Понимание исходного кода

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

  • Строки 5–8: он отправляет запрос /bytes конечной точке, которая возвращает числа, разделенные пробелом. Затем он разделяется для создания массива. В переменной bytes хранятся байты, представляющие (искаженное) изображение PNG, которое необходимо расшифровать.
  • Строки 10–29: функция assemble_png(u_in) принимает единственный аргумент, который является ключом (длиной 16 символов), а затем расшифровывает массив bytes для восстановления изображения и отображает его на странице.
  • Строки 18–23: Первый цикл перебирает каждый символ от клавиши (слева направо) и устанавливает так называемое shiftervalue. Код key.charCodeAt(i) — 48 преобразует строку в целое число, т.е. "5" будет 53decimal, как определено в Таблице ASCII, 53 — 48 = 5. Это старый трюк, который был распространен в ассемблере. В результате он преобразует строку 123 в массив [1 2 3] вместо [49 50 51]characters.
  • Строки 20–22: Второй цикл - это дешифрование изображения путем разделения bytesvalues ​​на блоки по 16 байтов (длина ключа). В результате получится 720 байт, что дает 45 блоков (720/16 = 45).
  • Строка 21: Вывод расшифрованного изображения сохраняется в переменной result.

Что нам известно на данный момент?

  • Первый цикл - 16 итераций по столбцам, второй цикл - 45 итераций по строкам.
  • Общее количество комбинаций для 16-байтового ключа составляет 16¹⁰, что на самом деле неосуществимо для атаки полным перебором.

Чтобы лучше понять и визуализировать процесс дешифрования, я создал визуализацию, которая должна помочь вам понять, как дешифруются байты (в этой задаче процесс дешифрования основан на переупорядочении путем добавления значения «сдвигателя» к номеру строки - разному для каждого столбца. ). Первое число в ключе - это наше shifter значение для первого столбца, затем второе число для второго столбца и так далее.

Уменьшение количества комбинаций

Вместо того, чтобы тестировать все комбинации 10¹⁶, что не имеет особого смысла, я сосредоточился на том, чтобы максимально уменьшить количество клавиш. Это поможет легко перебрать определенный набор комбинаций вместо того, чтобы слепо угадывать их ключевые.

Формат файла PNG (первые 8 байтов)

Одна из самых важных улик была спрятана во фрагменте data:image/png. Он описывает изображение PNG, в котором отдельные байты были закодированы с помощью base64.

document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));

Заголовок первые 8 байтов файла PNG уже был известен для этого формата файла и был: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A (байты заголовка файла PNG - константа).

Угадывание следующих 8 байтов

Все предоставленные байты можно проверить с помощью команды hexdump в терминале. Вот пример печати байтов файла PNG:

$ hexdump -C test.png | head -n 2
00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52
00000010  00 00 0c 14 00 00 08 de  08 06 00 00 00 04 0f fc

Кажется, что остальные недостающие байты состоят из 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52. Я написал однострочник bash, чтобы проверить, действительно ли эти байты являются постоянными или, по крайней мере, наиболее распространенными для формата файла PNG. Он просматривает 100 различных файлов PNG и извлекает первые 16 байтов.

Другой способ решения этой проблемы - прочитать RFC 2083, который описывает формат файла PNG. Это позволило бы получить первые 16 байт без каких-либо предположений.

$ for i in `seq 0 15`; do _png_byte=$(for file in *.png; do dd if="$file" skip=$i bs=1 count=1 2> /dev/null | hexdump -C | head -n 1 | awk '{print $2}'; done;); echo -n "Position [${i}]: "; echo -n "${_png_byte}" | sort | uniq -c | sort -rn | awk '{print "0x"$2}' | head -n 1; done;
Position [0]: 0x89
Position [1]: 0x50
Position [2]: 0x4e
Position [3]: 0x47
Position [4]: 0x0d
Position [5]: 0x0a
Position [6]: 0x1a
Position [7]: 0x0a
Position [8]: 0x00
Position [9]: 0x00
Position [10]: 0x00
Position [11]: 0x0d
Position [12]: 0x49
Position [13]: 0x48
Position [14]: 0x44
Position [15]: 0x52

Что будет дальше после расшифровки первых 16 байтов файла PNG?

Следующим шагом будет получение каждого байта из заголовка файла PNG (16 байтов) и его размещение внутри переменной bytes. Это даст значение shifter для каждого байта. В результате у вас должно получиться 16 shifter значений, что равно ключу дешифрования. Самая большая проблема здесь в том, что для одного и того же байта может быть несколько позиций - требуется проверить их все, если их больше одной.

Подбор байтов с несколькими допустимыми позициями

Я написал сценарий грубой силы, чтобы найти и проверить допустимые позиции для каждого байта из заголовка PNG. Оказывается, есть 4 байта на смещениях 2, 8, 9 и 10, где есть более одной допустимой позиции для байта.

More than one valid block for: 78 at 2 with count of 2 
More than one valid block for: 0 at 8 with count of 2 
More than one valid block for: 0 at 9 with count of 3 
More than one valid block for: 0 at 10 with count of 2

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

Я сократил количество комбинаций с 10¹⁶ до 24 возможных ключей, и вот результат:

Для декодирования изображения QR-кода вы можете использовать онлайн-qr-декодер.

key: 4549618526012495
flag: picoCTF{cfbdafe5a65de4f32cce2e81e8c14a39}

Вы можете найти исходный код моего скрипта грубой силы здесь:

Детский JavaScript 2

Задача JS Kiddie 2 не сильно отличается от первой. Тот же самый скрипт грубой силы решит эту проблему. В код второго испытания внесены два основных изменения:

Длина ключевой переменной не 16, а 32 символа:

var key = "00000000000000000000000000000000";

Формула значения переключателя отличается от первой:

shifter = Number(key.slice((i*2),(i*2)+1));

Что он делает, он использует каждую секунду байт ключа в качестве фактического ключа. Это означает, что несмотря на то, что у нас 32 символа, используются только 16.

key: 3738193605318569
flag: picoCTF{3aa9bd64cb6883210ee0224baec2cbb4}

Спасибо за чтение, надеюсь, вам понравилось. Дайте мне знать в комментариях, если у вас есть какие-либо вопросы или темы, о которых вы бы хотели, чтобы я написал.

Не забудьте подписаться на меня в Twitter: https://twitter.com/radekk

Спасибо!