Ты, наверное, не раз слышал про стеганографию. В отличии от криптографии, стеганография не шифрует поток данных, а скрывает его в другом потоке. Частным
случаем является сокрытие информации в БМП файлах.
Сегодня мы как раз займемся написанием программы для этого.

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

Итак, начнем с концепции. Я думаю ты в курсе, что изображение есть матрица размером
WxH (где w-ширина картинки, а H-высота), элементами которой являются 24 битовые числа, то есть 3 байтовые: по байту на красный, зеленый и синий цвета (RGB). Как известно в байте 8 бит, и если мы будем менять последние (младшие разряды: 0,1,2 степень двойки) 1-3 бита, то изменение цвета в каждой
составляющей RGB не будет превышать 4, что совсем не значительно, и изменения изображения не будет заметно глазу.
Я думаю тут все понятно, так что не будем грузится теорией и перейдем к кодингу.

Процедуры сделаем для универсальности такими, чтобы они могли записывать и считывать из картинки начиная с указанной точки определенное количество байтов потока, т.о. мы увеличим
безопасность, т.к. неизвестно будет с какой точки нужно начинать раскодировку, а так же просто для удобства
(допустим для записи в файл нескольких, подряд идущих потоков информации).Как ты уже понял, информация будет передаваться в процедуру потоком данных
(тип TStream).Итак описание процедуры выглядит так:

procedure WriteBufToBmp(MS: TStream; StartPos, Count, Val: integer; var Bmp: TBitmap);
procedure ReadBufFromBmp(Bmp: TBitmap; StartPos, Count, Val: integer; var MS:
TStream);

MS - поток данных
StartPos - стартовая позиция
Count - количество записываемых\считываемых байт
Val - какое количество бит записывать\считывать в один байт изображения
Bmp - картинка в которую пишем\считываем

Начнем с записи в файл. Опять немножко теории: рассмотрим число, которое описывает точку изображения. 
Как я уже сказал это 24 битовое число, но как известно дельфи не работает с такими числами, есть только 4 байтовый (32 битовый) тип integer, который нам подходит, в данном
случае (старшие 8 бит нулевые). Итого у нас имеется три байта на цвет, допустим число-цвет 9364709, его двоичное представление выглядит так:

10001110 11100100 11100101

тогда красный цвет это младшие 8 бит: 11100101, зеленый
- средние 8 бит: 11100100, и синий- старшие 8 бит 10001110. В каждой составляющей цвета будем использовать последние биты для себя. Количество используемых битов определяет параметр Val. Оптимальное количество на мой взгляд это два, т.к. не так уж мало как 1 🙂 и нет видимых
искажений изображения. Далее рассмотрим суть записи. Допустим у нас есть байт который надо записать (189 = 10111101) и параметр Val=2, тогда рассмотрим как пройдет запись в выше описанную точку. Для начала разделим записываемый байт на кусочки по два бита 10 11 11 01, соответственно 10 пойдет в красный цвет, 11 - в зеленый, и 11-в синий, а 01 в
следующую точку, наше число после изменений будет выглядеть так: 10001110 11100111 11100111(9365479). Изменения не критические. Так, ну я думаю ты уже наверное понял немножко что к чему, давай кодить.
Для тех кто ничего не понял, я все пояснил.

procedure WriteBufToBmp(MS: TStream; StartPos, Count, Val: integer; var Bmp: TBitmap);
var
Ind, Indx, Indy, Indz, Ind1, FreeSpace, Divider, NewVal: integer;
CurVal, MByte, R: byte;
begin
If Val > 8 then
Exit;
{

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

}
If (Val mod 2 <> 0) and (Val <> 1) then
Exit;
{

Проверяем является ли Val кратным двойке, чтобы не замарачиваться с переносами битов.

}
If (MS.Size - MS.Position) < (Count - StartPos) then
Exit;
{

Проверяем хватает ли в потоке информации для записи, чтобы не было ошибки чтения памяти.

}
FreeSpace := (Bmp.Width * Bmp.Height * Val) div 8;
{

Вычисляем свободное место в картинке. Вычисляется вполне по простой формуле: количество байт в картинке Bmp.Width * Bmp.Height и умножить на количество бит, которое можно записать в один байт,
т.о. получаем количество доступных бит, и делим на 8 (для тех кто в танке в байте 8 бит)

}
If (Count - StartPos) > FreeSpace then
Exit;
{

Если опять пошел наглешь (попытка записать больше байт чем доступно в картинке), тоже шлем его 🙂

}
MByte := Round(Power(2, Val) - 1);
{

Возводим двойку в степень Val и вычитаем единицу. Рассмотрим пример: возведем 2 в степень 2, мы получим число 4 в двоичном представлении это 00000100, вычтем
единицу получим 00000011, получаем нули и две единицы
в 2ух младших битах. Это число, как ты догадался, пригодится нам для обнуления младших битов в каждой составляющей цвета и для "расчленения" записываемого байта по частям
(см. далее)

}
Indz := Val;
//тут все ясно.
MS.Read(CurVal, 1);
//считываем из потока один байт
If Bmp.Width > Bmp.Height then
Divider := Bmp.Height
else
Divider := Bmp.Width;
{

Т.к. мы цикл будем заводит по одной переменной, т.е. просто подряд будем перебирать точки в картинке, а картинка
представлена в виде матрицы (см. выше), то нам исходя из значения это переменной-индекса
(в нашем случае будем использовать переменную StartPos т.к. начинаем
не всегда с нуля), надо получить две точки: координаты X и Y. Они будут получаться по формуле X = StartPos div Divider (получаем сколько целых Divider'ов входит в StartPos, т.е. это будет X) Y = StartPos mod Divider (остаток от деления, т.е на сколько мы сдвинулись в текущем X, то есть другая координата - Y). А Divider сам зависит от того какая картинка, он должен быть равен Divider = min(Height, Width), почему, я думаю, понятно).

}
StartPos := StartPos * 8 div Val + 1; 
{

Высчитываем реальную стартовую позицию, дело в том, что мы под стартовой позицией понимаем число равное количеству байт записанных перед текущим потоком информации, соответственно, чтобы получить стартовую позицию в картинке нужно умножить на
8 (сколько потребовалось бы байт для записи при Val = 1) поделенное на Val - на данной точке закончилась запись пред.
потока, соответственно переходим к след. точке +1 :)) 

}
for Ind1 := 0 to (Count * 8 div Val) do 
{

Начинаем цикл с 0 до количества записываемых байт умноженное на 8 и деленное на Val, при умножении на 8 получаем сколько бит надо записать, делим на Val получаем сколько точек в изображении надо использовать

}
begin
NewVal := 0; {в этой переменной будем хранить новое значение (цвет) текущей точки}
Ind := 16; //см.далее
Indx := StartPos div Divider;{вычисляем координату X текущей точки}
Indy := StartPos mod Divider;{вычисляем координату Y текущей точки}
while (Ind >= 0) and (MS.Position <= StartPos + Count) do
begin
{

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

}
R := Byte(Bmp.Canvas.Pixels[Indx, Indy] shr Ind); 

В переменную R записываем текущую компоненту цвета, итак воспользуемся опять нашим примером - 9364709 = 10001110 11100100 11100101b, рассмотрим что делает эта строчка. Как ты видишь используется функция Byte, она
возвращает число равное младшему байту в числе, т.е. в нашем случае функция вернет 11100101, как ты видишь мы передаем Bmp.Canvas.Pixels[Indx,
Indy], т.е. текущую точку изображения, это число логически сдвинуто на Ind вправо (ф-ия shr). Как происходит сдвиг: рассмотрим 1101 0011 т.е. число 211, тогда 211 shr 4 = 0000 1101, как видишь старшие 4 бита сдвинуты в младшие позиции, и старшие позиции обнулены. Тогда рассмотрим что будет на первом шагу цикла: 32-х битовое число - точка
(где старшие(левые) 8 бит обнулены 00000000 10001110 11100111 11100111 сдвинем это число на 16 (Ind = 16) получим 00000000 00000000 00000000
10001110 (т.е. мы вычленили старший значащий байт), после каждого шага будем уменьшать значение Ind на 8 и т.о. вычленим все цвета)

}
R := R and (255 - MByte);
{

Здесь мы обнуляем младшие биты, используемые для записи нашей информации, как все происходит: операция and (конъюнкиция)
возвращает следующие значения: 1 and 1 = 1, 1 and 0 = 0, 0 and 1 = 0, 0 and 0 = 0.
Пример: 1 0 0 1 0 0 1 1 and 0 1 1 1 0 1 0 0 = 0 0 0 1 0 0 0 0. Т.е. для того, чтобы нам обнулить последние биты, не изменяя при этом остальные, нам нужно число, состоящее из
единиц, а в последних битах нули (для определенности будем рассматривать все примеры при Val = 2).Как же мы его получим, да очень просто, возьмем число 255=11111111b и вычтем из него Mbyte = 00000011, получим число 11111100, вот то что нам нужно.

}
R := R or (CurVal and MByte);
{

Теперь производим запись в текущую цветовую
составляющую. CurVal and MByte. Берем два младших бита для записи = 000000?? (? = 1 или 0) складываем c числом R = ??????00, функция OR возращает: 0 or 0 = 0, 0 or 1 = 1, 1 or 0 = 1, 1 or 1 = 1. 

}
NewVal := NewVal shl 8;
{

Сдвигаем NewVal влево. На первом шаге это ноль, на втором шаге (там записан голубой цвет) сдвигается в позицию зеленого (в среднюю) на третьем голубой и зеленый сдвигаются в свои позиции, освобождая место для красного.

}
NewVal := NewVal or R;
{

В младший байт записывается R. Младший байт всегда пуст, до тех пор пока не запишем туда красный, и не сформируем новое 24-х битовое число.
 
}
CurVal := CurVal shr Val;
{

Как ты помнишь, CurVal - это тот байт который мы записываем, берем из него Val битов
(последних) и записываем в байт цвета, тогда, чтобы добираться до старших битов просто будем их сдвигать в младшие позиции.

}
Inc(Indz, Val);
{

Увеличиваем Indz. Indz - это счетчик количества записанных битов из
записываемого байта, то есть он не может превышать 8, если больше то считываем следующий байт).

}
If Indz > 8 then
begin
Indz := Val;
MS.Read(CurVal, 1);
end;
Dec(Ind, 8);
end;
Bmp.Canvas.Pixels[Indx, Indy] := NewVal;
//Записываем новый цвет точки в изображение
Inc(StartPos);
//Сдвигаемся на след. точку
end;
end;

Ну вроде бы я все разъяснил достаточно подробно, вопросов быть не должно, я конечно понимаю, что тем, кто еще не знаком с дискретной математикой, или вообще плохо разбирается в двоичных представлениях чисел понять все сложновато, но тем не менее старайтесь, а мы переходим к процедуре считывания из картинки, она очень похожа на запись, так что так подробно я
пояснять ее не буду!

procedure ReadBufFromBmp(Bmp: TBitmap; StartPos, Count, Val: integer; var MS: TStream);
var
Ind, Indx, Indy, Indz, Ind1, Divider: integer;
CurVal, MByte, R: byte;
begin
If Val > 8 then
Exit;
If (Val mod 2 <> 0) and (Val <> 1) then
Exit;
If MS = nil then
MS := TMemoryStream.Create;
MByte := Round(Power(2, Val) - 1);
Indz := Val;
CurVal := 0;
If Bmp.Width > Bmp.Height then
Divider := Bmp.Height
else
Divider := Bmp.Width;
StartPos := StartPos * 8 div Val + 1;
for Ind1 := 0 to (Count * 8 div Val) do
begin
Ind := 16;
Indx := StartPos div Divider;
Indy := StartPos mod Divider;
while (Ind >= 0) and (MS.Position <= StartPos + Count) do
begin
R := Byte(Bmp.Canvas.Pixels[Indx, Indy] shr Ind);
{
До этого момента все тоже самое рассмотрим что идет далее
}
R := R and MByte;
{

Итак, как ты помнишь, в прошлый раз мы обнуляли младшие Val битов, сейчас же мы обнуляем все кроме старших Val битов т.к. они нам только и нужны!
Т.о. R = Val битам нашей информации, т.е. нам надо считать 8 бит и записать их в поток.

}
CurVal := CurVal or (R shl (Indz - Val));
{

CurVal изначально равно 0. В R хранятся (при Val = 2
(для определенности)) два бита числа, их позиция в байте определяется значением Indz - Val (почему?
я надеюсь вы сами догадаетесь). Тогда R сдвигаем влево на это значение и логически прибавляем к CurVal, т.о. формируя байт

}
Inc(Indz, Val);
If Indz > 8 then
begin
{

Как только байт сформирован, записываем его в поток.

}
Indz := Val;
MS.Write(CurVal, 1);
CurVal := 0;
end;
Dec(Ind, 8);
end;
Inc(StartPos);
end;
end;

Вот вроде бы и все, очень просто! Для тех кто все таки не понял, я приготовил пример использования этих процедур.
Вообще я сделал отдельный модуль в который записал эти две процедуры, и программу которая использует этот модуль.
Давай посмотрим как, например, создать программу, пользуясь этими ф-ями, для записи нескольких файлов в изображение, и соответственно, для чтения из изображения, записанных файлов.
Программа будет простой, только для демонстрации того как работать с ф-ями, можно реализовать и другими способами, но я не стал особо
заморачиваться с улучшением наворотами и т.д., это уж соизвольте сделать сами! Итак создай форму и кинь на нее 4 кнопки, один ТЭдит, и один лабел, один OpenImageDialog, OpenDialog и SaveDialog со вкладки
Dialogs.

Опиши две переменные вот так:

public
Bmp: TBitmap; //изображение в которое будем писать
List: TStringList; //список файлов которые будем записывать
end;

Так, теперь на событие онклик Button1(открыть), добавь вот такой код.

If OpenPictureDialog1.Execute then
begin
Caption := OpenPictureDialog1.FileName; //просто так
Bmp := TBitmap.Create; //создаем изображение
Bmp.LoadFromFile(Caption);//загружаем файл в битмап
List := TStringList.Create;//создаем список записываемых файлов
Label1.Caption := 'Свободно: ' + IntToStr((Bmp.Width * Bmp.Height * 2) div 8) + 'байт'; {вычисляем свободное место и пишем об это в label1}
end;

В событии OnClick Button2 она же добавить файл, имеется вот что

If OpenDialog1.Execute then
List.Add(OpenDialog1.FileName); //добавляем имя открытого файла в список записываемых

На клик буттон3 aka сохранить повесим процедуру записи в изображение: Сейчас опять надо втыкать, итак как мы будем хранить
информацию в файле, надо точно определиться со структурой! Для начала мы запишем нашу подпись, или как это сейчас модно называть Сигнатуру, чтобы точно знать, в этом файле
содержится наша информация, я записал туда свой ник. Затем мы запишем размер потока, и наконец сам поток. В потоке будет по очереди хранится информация о файлах, а именно длина названия, название, размер файла, файл.
Обрати внимание, что мы записываем имя файла без пути. При
извлечении файлов из картинки будем записывать их в папку, которая указана в Edit1.Text.

Вот сама процедура записи:

procedure TForm1.Button3Click(Sender: TObject);
var
FS: TFileStream;
MS, MS1: TMemoryStream;
Buf: shortstring;
BufI, Ind: integer;
CurPos: integer;
begin
MS := TMemoryStream.Create;//создаем поток
Buf := 'Stexen';{записываем свою подпись} 
MS.Write(Buf, 7);//непосредственно запись в поток 
MS.Position := 0;{перемащаем позицию в потоке на начало(если не понял зачем, ознакомься с потоками)}
WriteBufToBmp(MS, 0, 7, 2, Bmp);//записываем подпись в картинку
MS.Clear;//очищаем поток
BufI := List.Count;//записываем в BufI количество записываемых файлов
Ms.Write(BufI, SizeOf(integer)); {записываем в поток количество файлов}
{начинаем запись в поток иноформации о всех файлах}
for Ind := 0 to List.Count - 1 do 
begin
{открываем на чтение текущий записываемый файл}
FS := TFileStream.Create(List.Strings[Ind], fmOpenRead); 
BufI := Length(ExtractFileName(List.Strings[Ind])+1);{присваиваем BufI количество символов в имени файла} 
Buf := ExtractFileName(List.Strings[Ind]);{присваиваем Buf имя файла без пути}
MS.Write(BufI, SizeOf(Integer));{записываем в поток BufI, т.е. количество символов в имени файла}
MS.Write(Buf, BufI);{записываем в поток имя файла}

BufI := FS.Size;
MS.Write(BufI, SizeOf(Integer));{записываем в поток размер файла}

MS.CopyFrom(FS, FS.Size); {зписываем в поток сам файл}
FS.Free;{закрываем файл}
end;
MS1 := TMemoryStream.Create;{создаем вспомогательный поток и записываем в него размер основного потока}
BufI := MS.Size;
MS1.Write(BufI, 4);

MS1.Position := 0;{переводим позиции в потоках на начало}
MS.Position := 0;

WriteBufToBmp(MS1, 7, 4, 2, Bmp);{записываем в картинку,размер потока}
WriteBufToBmp(MS, 11, MS.Size, 2, Bmp);{записываем сам поток}
IF SaveDialog1.Execute then
begin
Bmp.SaveToFile(SaveDialog1.FileName);{сохраняем новый файл-картинку}
end;
MS.Free;//очищаем память
MS1.Free;
end;

Итак, чтобы записать в картинку несколько файлов с помощью нашей программы, нужно нажать открыть
(button1) и выбрать файл в который будем записывать, затем нужно нажать буттон2 и выбрать записываемый файл
(столько раз нажать, сколько нужно записать файлов), и ткнуть сохранить
(буттон3).

Перейдем к чтению из картинки, для этого нужно ткнуть открыть, затем буттон4(назови ее извлечь).
Так, а вот и сама процедура считывания из картинки.

procedure TForm1.Button4Click(Sender: TObject);
var
FS: TFileStream;
MS: TMemoryStream;
Buf: shortstring;
BufI, Ind, Indx: integer;
CurPos: integer;
FName: string;
begin
MS := TMemoryStream.Create;
ReadBufFromBmp(Bmp, 0, 7, 2, TStream(MS));{считываем из картинки нашу подпись(считываем в поток)}
MS.Position := 0;
MS.Read(Buf, 7);(считываем из потока 7 байт)
MS.Clear;
If Buf <> 'Stexen' then {проверяем являются ли семь байтов нашей подписью, конечно же если нет, то выходим из процедуры c матюгами!}
begin
Messagedlg('Ничего не записанно', mtConfirmation, [mbOK], 0);
Exit;
end;
ReadBufFromBmp(Bmp, 7, 4, 2, TStream(MS)); {считываем ращмер потока}
MS.Position := 0;
MS.Read(BufI, 4);
MS.Clear;

ReadBufFromBmp(Bmp, 11, BufI, 2, TStream(MS)); {считываем весь поток из картинки}
MS.Position := 0;
MS.Read(BufI, 4);{считываем из потока количество записанных файлов}
Indx := BufI;
for Ind := 0 to Indx - 1 do
begin
MS.Read(BufI, 4); {считываем длину имени}
MS.Read(Buf, BufI); {считываем имя файла}

FName := Edit1.Text + '\' + Buf; {с переди к имени файла прибавляем путь из Edit1.Text}
FS := TFileStream.Create(FName, fmCreate); {создаем файл считанный из картинки}

MS.Read(BufI, 4);{считываем размер файла}

FS.CopyFrom(MS, BufI);{записываем в файл содержимое}
FS.Free;//закрываем файл
end;
MS.Free; //освобождаем память
end;

Вот собственно и все, что я хотел рассказать тебе в этой статье.
Конечно же сами процедуры не очень оптимизированы
и работают достаточно медленно, но я особо и не заморачивался по поводу оптимизации, я лишь передал суть того как можно записать в картинку стороннюю информацию, без видимых изменений файла и без изменения размера!
Сам же я писал на ассеблере все это дело, но это уже
тренировка вашего ума, пишите сами, не ленитесь 🙂
А да еще не надо меня закидывать письмами с предложениями оптимизировать, 
улучшить и изменить все это, делайте сами! И еще советую вам все прочитать, просмотреть и самому все это реализовать, вот тогда точно будет толк от статьи, ну а ленивые
(такие как я и еще процентов 90%) просто не
заморачиваясь могут скачнуть все исходники.
Удачи!

Исходники

Check Also

Компания Sophos открывает исходные коды утилиты Sandboxie

Утилита для Windows Sandboxie, позволяющая запускать любые приложения в защищенной песочни…

1 комментарий

  1. Аватар

    Aqel

    11.09.2017 at 20:57

    Хорошо описано, вот бы ещё по JPG такое — знаю. что там сложнее алгоритм, но тоже возможно.

Оставить мнение