Данная статья является райтапом на задание "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-кодировании прорисовка производится также по горизонтали, начиная с левого нижнего пикселя и заканчивая правым верхним, при этом дозволено прерывание прорисовки горизонтали и перемещение прорисовки на другую позицию, а формирование изображения осуществляется двухбайтовыми командами:
В нашем файле данные сжаты именно алгоритмом RLE. Изображение 8-ми битное - значит в файле обязательно должна присутствовать таблица цветов из которой и будут браться значения пикселей и она есть (см. 0x8A : 0x489).
Возьмем байты первой горизонтали (т.е. самой нижней) и посмотрим что они прорисовывают - расположены они от начала ImageData до первого разделителя строк [00 00] по адресу 0x48A : 0x497:
Первые два байта рисуют 255 [FF] пикселей цветом из ячейки с нулевым индексом [00] таблицы цветов. Следующие четыре пары байтов делают тоже самое. Последние два байта [05 00] дорисовывают 5 таких же пикселей. Байты [00 00] завершают строку и переводят курсор в начало следующей горизонтали.
Таким образом нарисована горизонтальная полоса в 1280 черных пикселей (значение цвета в ячейке таблицы цветов с индексом 00 = #000000 = black). Но как это возможно ведь мы знаем что ширина нашего изображение 100px (см. 0x12 : 0x15)!? Пока не паникуем - в RLE допускается рисовать пиксели за пределами размера растра, анализируем следующие горизонтали.
Далее аналогичным образом прорисовываются еще 44 аналогичных строк - возможно это фон.
Анализируем 46 строку :
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, ну или какого-то другого стандарта в названии которого фигурирует это число. В тексте описания к заданию что-то про черно-белое или про бело-черное и требования вызвать кассира, что наводит на мысли попробовать инвертировать цвета - инвертируем:
Снова пытаемся прочитать штрих код каким-нибудь сканером и получаем Флаг!
(если не удается отсканировать, следует добавить белые поля в начале и конце штрихкода - они обязательны)
Надеюсь статья была полезна для Вас! Если найдете неточности или ошибки - обязательно напишите об этом в комментариях к статье.
Ссылка на задание: Игры Кодебай | 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
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, ну или какого-то другого стандарта в названии которого фигурирует это число. В тексте описания к заданию что-то про черно-белое или про бело-черное и требования вызвать кассира, что наводит на мысли попробовать инвертировать цвета - инвертируем:
Снова пытаемся прочитать штрих код каким-нибудь сканером и получаем Флаг!
(если не удается отсканировать, следует добавить белые поля в начале и конце штрихкода - они обязательны)
Стандарт штрихового кода Code 128 в России регламентируется ГОСТ ISO/IEC 15417-2013 «Автоматическая идентификация. Кодирование штриховое. Спецификация символики Code 128 (Код 128)».
Code-128 имеет три уникальных кодируемых набора знаков данных, выбор которого зависит от знака "Start" или использования одного из знаков "Code А", "Code В", "Code С" или знака "Shift". Каждый символ кодируется в шаблон состоящий из 11 модулей, образующих шесть элементов шириной от одного до четырех модулей: три штриха и три пробела. Знак Stop имеет семь элементов, включающих четыре штриха и три пробела, и состоит из 13-ти модулей. Перед знаком Stop всегда должен присутствовать контрольный знак, который не указывается в визуальном представлении.
Рассмотрим структуру Code-128 на следующем примере:
Code-128 имеет три уникальных кодируемых набора знаков данных, выбор которого зависит от знака "Start" или использования одного из знаков "Code А", "Code В", "Code С" или знака "Shift". Каждый символ кодируется в шаблон состоящий из 11 модулей, образующих шесть элементов шириной от одного до четырех модулей: три штриха и три пробела. Знак Stop имеет семь элементов, включающих четыре штриха и три пробела, и состоит из 13-ти модулей. Перед знаком Stop всегда должен присутствовать контрольный знак, который не указывается в визуальном представлении.
Рассмотрим структуру Code-128 на следующем примере:
Надеюсь статья была полезна для Вас! Если найдете неточности или ошибки - обязательно напишите об этом в комментариях к статье.
Последнее редактирование: