Данная статья является райтапом на задание "128 кассир" из категории "Форензика". В задании рассмотрены внутреннее строение файлов растровых изображений в формате BMP, алгоритм RLE-кодирования и структура штрих кода стандарта Code 128. В качестве инструментов используются hex-редактор ImHex и графический редактор MSPaint.
Ссылка на задание: Игры Кодебай | CTF-платформа
К заданию прилагается архив с файлом task.bmp. Расширение намекает, что данный файл, возможно, предназначен для хранения растрового изображения.
Посмотрим в hex-редакторе что внутри:
Действительно, данные в файле очень похожи на графические данные. Пробуем открыть файл и терпим неудачу.
Вернемся в hex-редактор и более детально проанализируем содержимое файла.
Если в файле всё же растровое изображение, то данные в формате BMP должны состоять из следующих блоков:
1) Заголовок - содержит структуры BITMAPFILEHEADER (всегда первые 14-байт) и BITMAPINFO.
2) Таблица цветов
3) Цветовой профиль
4) Пиксельные данные
Рассмотрим заголовок нашего файла:
В первых двух байтах (адреса 0x0 : 0x1) указана сигнатура файла [00 4D]. Мы же предположили что у нас BMP - меняем на [42 4D].
В следующих четырех байтах (0x2 : 0x5) указан размер файла в байтах [CE B0 02 00] = 0x2B0CE = 176334 байта. Проверим размер нашего файла - все сходится.
После размера 4 байта зарезервированы и должны быть занулены. Далее 4 байта с адресом пиксельных данных (imageData) = 0x48A.
На этом описание структуры BITMAPFILEHEADER завершено, следом идет описание структуры BITMAPINFO:
В четырех байтах по адресу 0xE : 0x11 указан размер данной структуры в байтах (DIB Header Size), согласно которому мы можем определить версию самой структуры: [7C 00 00 00] = 124 байта, значит версия структуры - BITMAPV5HEADER. Количество версий, их описания и различия можно потом легко загуглить, мы же рассмотрим только наиболее важные поля для решения Таска:
С 0x12 по 0x15 и с 0x16 по 0x19 байт должны быть указаны ширина и высота изображения в пикселях: [64 00 00 00] [14 00 00 00] = 100px на 20px.
В двух байтах по адресу 0x1C указана битность изображения - 0x08, т.е. на каждый пиксель приходится 8 бит (1 байт).
В 31-м байте (адрес 0x1E) указан способ хранения пикселей: 0x01 - используется RLE-кодирование.
По адресу 0x22 : 0x25 указан размер пиксельных данных (imageData) в байтах: [24 AA 02 00] = 174628 байт.
Далее идет описание разрешения изображения (0x26 : 0x2D), характеристики таблицы цветов (0x2E : 0x35) [00 01 00 00] - 256 ячеек, описание битовых масок (0x35 : 0x45), цветового пространства (0x46 : 0x79), предпочтения при рендинге (0x7A : 0x7D), указано смещение в байтах цветового профиля от начала BITMAPINFO [A0 AE 02 00] (следовательно его адрес 0x02AEAE) и его размер [20 02 00 00] - 544 байта).
Описание Заголовка bmp-файла завершается четырьмя нулевыми байтами по адресу 0x86 : 0x89 - а это как раз 124 как и было указано в DIBHeaderSize (0x89-0xD=0x7C).
На данном этапе мы изучили заголовок нашего файла и исправили его сигнатуру - давайте попробуем открыть файл:
Видим черный прямоугольник размером 100 на 20 пикселей.
Смотрим в хекс-редакторе что там дальше после заголовка.
А далее в файле, согласно описанию структуры версии BITMAPV5HEADER, по адресу 0x8A должна находиться Таблица цветов, представляющая собой одномерный массив четырехбайтной структуры, в которой указывается цвет в модели RGB. Размер Таблицы 256 ячеек (см. 0x2E : 0x35) - следовательно следующие за Заголовком 1024 байта (256*4) являются Таблицей цветов. Тут ничего интересного - смотрим дальше.
Итак мы на 0x48A - а это согласно заголовку начало пиксельных данных (см. 0xA : 0xD), размер которых 174628 байт (см. 0x22 : 0x25). Помечаем 0x2AA24 байта в хекс-редакторе как ImageData и в конце файла остается 544 байта по адресу 0x02AEAE : 0x02B0CD, которые являются цветовым профилем (см. описание ICC Profile в 0x7E:0x81 и 0x82 : 0x85). Вроде все корректно, но 174628 сжатых байт пиксельных данных для изображения площадью в 2000 пикселей как-то слишком много. Давайте посмотрим данные из ImageData, но прежде разберемся в способах хранения и алгоритме прорисовки пикселей изображения в bmp-формате.
В формате Windows Bitmap хранение пикселей допускается тремя способами:
1) Двумерный массив.
2) RLE - сжатие кодированием повторов.
3) В форматах JPEG или PNG (выходит за рамки данной статьи).
При хранении данных в двумерном массиве, пиксели растра записываются однопиксельными горизонтальными строками, начиная с самого нижнего и строго только от левого пикселя к правому. Для наглядности создадим картинку в формате BMP размером 6px на 5px с произвольными цветами пикселей и посмотрим в hex-редакторе каким образом цвета пикселей хранятся в ImageData:
Как мы видим все просто: перечислены значения пикселей по горизонтали начиная с самой нижней, а строки разделены двумя нулевыми байтами [00 00].
В RLE-кодировании прорисовка производится также по горизонтали, начиная с левого нижнего пикселя и заканчивая правым верхним, при этом дозволено прерывание прорисовки горизонтали и перемещение прорисовки на другую позицию, а формирование изображения осуществляется двухбайтовыми командами:
Код:
Код:
[01..FF][байт] - прорисовать пиксели со значением из второго байта столько раз сколько указано в первом байте
[00][00] - переместить курсор в начало следующей горизонтали
[00][01] - закончить прорисовку
[00][02][XX][YY] - переместить курсор вправо и вверх на значения 0xXX по горизонтали и 0xYY по вертикали, влево и вниз сдвинуть нельзя.
[00][кол-во=3..FF][байты] - прорисовать следующие [кол-во=3..FF] пикселя один раз со значениями из [байты], при этом если количество прорисованных пикселей нечетно, то дописывается дополнительный байт, значение которого неважно.
В нашем файле данные сжаты именно алгоритмом RLE. Изображение 8-ми битное - значит в файле обязательно должна присутствовать таблица цветов из которой и будут браться значения пикселей и она есть (см. 0x8A : 0x489).
Возьмем байты первой горизонтали (т.е. самой нижней) и посмотрим что они прорисовывают - расположены они от начала ImageData до первого разделителя строк [00 00] по адресу 0x48A : 0x497:
Код:
Код:
FF 00 FF 00 FF 00 FF 00 FF 00 05 00 00 00
Первые два байта рисуют 255 [FF] пикселей цветом из ячейки с нулевым индексом [00] таблицы цветов. Следующие четыре пары байтов делают тоже самое. Последние два байта [05 00] дорисовывают 5 таких же пикселей. Байты [00 00] завершают строку и переводят курсор в начало следующей горизонтали.
Таким образом нарисована горизонтальная полоса в 1280 черных пикселей (значение цвета в ячейке таблицы цветов с индексом 00 = #000000 = black). Но как это возможно ведь мы знаем что ширина нашего изображение 100px (см. 0x12 : 0x15)!? Пока не паникуем - в RLE допускается рисовать пиксели за пределами размера растра, анализируем следующие горизонтали.
Далее аналогичным образом прорисовываются еще 44 аналогичных строк - возможно это фон.
Анализируем 46 строку :
Код:
Код:
03 00 01 01 01 11 04 15 01 13 01 03 01 00 01 02 .. 00 00
03 00 - рисуется три пикселя с цветом из ячейки таблицы цветов с индексом "00": #000000
01 01 - рисуется один пиксель с цветом из ячейки таблицы цветов с индексом "01": #010101
01 11 - один пиксель с цветом из ячейки таблицы цветов с индексом "11": #111111
04 15 - четыре пикселя с цветом из ячейки с индексом "15": #151515
01 13 - один пиксель с цветом #131313
и так далее до команды [00 00] всего прорисовывается 1280 пикселей, а ввиду того что пиксели разные, посмеем предположить, что это начало изображения.
Бегло проанализируем остальные горизонтали - везде 1280px. Следовательно ширина картинки не 100px, а 1280px.
Высоту изображения можно вычислить посчитав количество горизонталей - я насчитал 193.
Заменим в заголовке значения ширины и высоты изображения на 0x500 и 0xC1 соответственно:
Сохраняем изменения и пробуем открыть файл:
Видим изображение похожее на штрих код. Попытки распознать его различными сканерами не увенчались успехом, возвращаемся к истокам - к названию и тексту задания.
В названии Таска число 128 наталкивает на мысли, что этот штрих код создан по стандарту Code-128 или GS1-128, ну или какого-то другого стандарта в названии которого фигурирует это число. В тексте описания к заданию что-то про черно-белое или про бело-черное и требования вызвать кассира, что наводит на мысли попробовать инвертировать цвета - инвертируем:
Снова пытаемся прочитать штрих код каким-нибудь сканером и получаем Флаг!
(если не удается отсканировать, следует добавить белые поля в начале и конце штрихкода - они обязательны)
Надеюсь статья была полезна для Вас! Если найдете неточности или ошибки - обязательно напишите об этом в комментариях к статье.