Стеганография… Этот сравнительно недавно вошедший в компьютерный обиход термин обычно переводится как тайнопись. Совместно с криптографией стеганография (steganography) используется для защиты информации, фундаментального свойства окружающего мира, в таковой вовсе не нуждающейся, как, впрочем, и две другие его категории — материя и мера [1].
Оглавление
Назначение стеганографии
Требует защиты только изображение информации, представленное на материальном носителе, причем под ней понимается создание условий, исключающих либо затрудняющих доступ к носителю, внесение изменений или уничтожение носителя, а также восприятие представленных на нем данных, производимое с помощью методов криптографии и стеганографии. И если, образно говоря, криптография делает понятное непонятным, то стеганография делает видимое невидимым (иногда и в прямом смысле слова). Достигается это «растворением» скрываемой информации среди других данных значительно большего объема. Видимо, значение стеганографии в деле сокрытия информации сегодня помимо спецслужб в полной мере оценили лишь стоящие вне закона взломщики программ и авторы компьютерных вирусов.
Скрываемая информация называется стеганограммой или просто стего. Данные, среди которых она прячется, играют роль информационного контейнера, а потому так же и именуются. Для компьютерного вируса, например, контейнером служит исполняемый файл. Одна и та же стеганограмма может быть упакована в различные контейнеры подобно тому, как одна и та же криптограмма шифруется различными методами или ключами. Если продолжить сравнение с вирусами, то аналогом криптограммы являются обитающие в вычислительной среде компьютерные черви, не нуждающиеся ни в каком носителе.
Тем не менее некоторые авторы не склонны придавать значение собственной информации контейнера, считая ее безразличной как для отправителя, так и для получателя стеганосообщения [2]. Однако это не совсем так, что подтверждается все тем же примером компьютерного вируса.
Контейнером могут служить любые данные (файлы) достаточно большого объема, например графические или звуковые. Их структура проста и, как правило, обладает большой избыточностью, позволяющей вместить значительный объем дополнительной информации. Однако текстовые файлы все же более распространены, и их структура широко известна (всякие новации и навороты в виде закрытых форматов текстовых процессоров типа Microsoft Word и издательских систем, разумеется, не в счет).
Методы текстовой стеганографии
Стеганография, использующая текстовые контейнеры, называется текстовой (text steganography). Далее будет рассмотрено, каким образом можно применять текстовые контейнеры для хранения стего. Достаточно полная классификация подобных методов дана в работе [2]. Удивление вызывает лишь тот факт, что из автоматических методов текстовой стеганографии в этой статье упомянут только один — форматирование (т. е. выравнивание) текста с помощью пробелов.
Суть данного метода состоит в раздвижке строки путем увеличения пробелов между словами, когда один пробел соответствует, например, биту 0, два пробела — биту 1. Однако прямое его применение хотя и возможно, но на практике порождает массу неудобств, в частности, оформление текста становится неряшливым, что позволяет легко заподозрить в нем наличие стего.
В листинге 1 приведена программа, где подобные проблемы уже решены (этот листинг и другие, предложенные в статье, см. на «Мир ПК-диске», раздел «Студия программирования»). Программа попросту перераспределяет пробелы в пределах текущей длины строки, перенося по возможности длинные пробелы в ее конец. В результате строки выходного текста имеют аккуратный вид, затрудняющий выявление стего.
Листинг 1.
Кодирование стего выравниванием строк пробелами.
program StegoShift;
(***************************************************************)
(* Простая стеганографическая программа, использующая *)
(* раздвижку слов в строке для встраивания стего в *)
(* произвольные тексты. мспользует 2 или 3 параметра, которые *)
(* и определяют выполняемую программой функцию. При этом *)
(* первый из параметров всегда представляет файл-контейнер. *)
(* Если параметров два, стеганограмма извлекается из *)
(* контейнера, а результат записывается в заданный вторым *)
(* параметром файл и в виде эхопечати выводится на экран. *)
(* Если параметров три, то стеганографический текст из *)
(* файла заданного вторым параметром, упрятывается в *)
(* файл-контейнер, заданный первым параметром, а результат *)
(* стеганографического преобразования записывается в файл, *)
(* заданный третьим параметром. Совпадение двух имён файлов *)
(* допускается (трёх — абсурд). *)
(***************************************************************)
type
StringType = string [$FF];
const
TempName = ‘$$$$$$$$.$$$’;
Key1 = $1234;
Key2 = $4567;
var
F, G, H : text;
Line, Head, Tail, Body : StringType;
B, I, K, L, N, LenBody, Z : byte;
C : char;
Tab : array [0..255] of byte;
Count : real;
begin
LowVideo;
if not (ParamCount in [1..3])
then
begin
WriteLn (‘Ожидается ввод 2 или 3 параметров:’);
WriteLn (‘2 — вывод стеганограммы в файл;’);
WriteLn (‘3 — запись стеганограммы в файл.’);
Exit
end;
Assign (F, ParamStr (1));
Reset (F);
Count := 0;
MemW [Dseg : $01FE] := Key1;
MemW [Dseg : $01FC] := Key2;
(*———————-ВЫВОД СТЕГАНОГРАММЫ В ФАЙЛ———————*)
if ParamCount = 2
then
begin
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
Rewrite (G);
Z := 0;
L := 0;
while not Eof (F)
do
begin
ReadLn (F, Line);
(* 1. Выделяем тело строки *)
while (Line <> »)
and (Line [1] <= ‘ ‘)
do Delete (Line, 1, 1);
while (Line <> »)
and (Line [Length (Line)] <= ‘ ‘)
do Delete (Line, Length (Line), 1);
(* 2. Заполняем таблицу пробелов *)
FillChar (Tab, SizeOf (Tab), 0);
LenBody := Length (Line);
K := 0;
I := 0;
while I < LenBody
do
begin
while (I < LenBody) and (Line [I] <> ‘ ‘)
do I := Succ (I);
if (I < LenBody) and (Line [I] = ‘ ‘)
then
begin
K := Succ (K);
N := 0;
while (I < LenBody)
and (Line [I] = ‘ ‘)
do
begin
N := Succ (N);
I := Succ (I)
end;
Tab [K] := N
end
end;
if K > 0
then K := Pred (K);
(* 3. Декодируем биты *)
I := 0;
while I < K
do
begin
I := Succ (I);
if Tab [I] > 1
then
begin
Z := Z shr 1;
if Odd (Tab [I])
then Z := Z or $80;
L := Succ (L) mod 8;
if L = 0
then
begin
C := Chr (Z
xor Random (256));
Count := Count + 1;
Write (C);
Write (G, C);
Z := 0
end
end
end;
end;
(* 4. Завершаем работу *)
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
WriteLn (‘Прочитано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end;
(*———————-ЗАПмСЬ СТЕГАНОГРАММЫ В ФАЙЛ———————*)
Assign (G, ParamStr (2));
if Pos (‘:’, ParamStr (3)) <> 2
then Assign (H, TempName)
else Assign (H, Copy (ParamStr (3), 1, 2) + TempName);
Reset (G);
Rewrite (H);
L := 0;
while not Eof (F)
do
begin
(* 1. мнициализируем таблицу раздвижек *)
FillChar (Tab, SizeOf (Tab), 0);
(* 2. Читаем и разделяем строку на части *)
ReadLn (F, Line);
I := 0;
while (I < Length (Line)) and (Line [Succ (I)] <= ‘ ‘)
do I := Succ (I);
Tab [0] := I;
(* начало строки *)
Head := Copy (Line, 1, I);
I := Length (Line);
while (I > 0) and (Line [I] <= ‘ ‘)
do I := Pred (I);
(* конец строки *)
Tail := Copy (Line, Succ (I), Length (Line) — I);
(* тело строки *)
Body := Copy (Line, Succ (Tab [0]), I — Tab [0]);
(* 3. Редуцируем тело строки *)
LenBody := Length (Body);
while Pos (‘ ‘, Body) > 0
do Delete (Body, Pos (‘ ‘, Body), 1);
(* число вставляемых пробелов *)
N := LenBody — Length (Body);
(* 4. Заполняем таблицу раздвижек *)
K := 0;
for I := 1 to Length (Body)
do
if Body [I] = ‘ ‘
then
begin
K := Succ (K);
Tab [K] := 1
end;
(* 5. Распределяем значимые (информационные) пробелы *)
I := 1;
while I < K
do
begin
if L = 0
then (* извлекаем очередной байт *)
begin
if Eof (G)
then C := #00
else Read (G, C);
Count := Count + 1;
Z := Ord (C) xor Random (256);
B := Z and 1;
L := 1;
Write (C);
end;
if N > Succ (B)
then (* запас пробелов не исчерпан *)
begin
(* кодируем бит в таблице *)
Tab [I] := Tab [I] + Succ (B);
(* текущий запас пробелов *)
N := N — Succ (B);
(* указываем следующий бит *)
Z := Z shr 1;
B := Z and 1;
(* счётчик записанных битов *)
L := Succ (L) mod 9
end;
I := Succ (I)
end;
(* 6. Монотонно перераспределяем информационные пробелы *)
if K > 2
then (* число пробелов должно возрастать к концу строки *)
begin
I := 0;
while (Tab [Pred (K)] = 1) and (I < K)
do
begin
Move (Tab [1], Tab [2], K — 2);
Tab [1] := 1;
I := Succ (I)
end
end;
(* 7. Распределяем выравнивающие (незначимые) пробелы *)
while N > 1
do
begin
I := K;
while (I > 0) and (N > 1)
do
begin
if (Tab [I] > 1) or (I = K)
then
begin
Tab [I] := Tab [I] + 2;
N := N — 2
end;
I := Pred (I)
end
end;
Tab [K] := Tab [K] + N;
(* 8. Вставляем пробелы в тело строки *)
N := Length (Body);
while (N > 0) and (K > 0)
do
begin
while (N > 0) and (Body [N] <> ‘ ‘)
do N := Pred (N);
if (Tab [K] > 0) and (N > 0)
then
for I := 1 to Pred (Tab [K])
do Insert (‘ ‘, Body, N);
if K > 0
then K := Pred (K);
if N > 0
then N := Pred (N)
end;
(* 9. Формируем и записываем строку *)
Line := Head + Body + Tail;
WriteLn (H, Line)
end;
(* Заканчиваем обработку *)
Close (F);
Close (G);
Close (H);
Assign (F, ParamStr (3));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (H, ParamStr (3));
if WhereX <> 1
then WriteLn;
if (Count <> 0) and (L <> 0)
then Count := Count — 1;
WriteLn (‘Записано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end.
Одиночные пробелы и пробелы перед последним словом строки не несут информационной нагрузки. В остальных случаях же четное число пробелов кодирует 0, нечетное — 1. Стего при записи предварительно шифруется, а при чтении расшифровывается с использованием операции исключающего «ИЛИ» и встроенного в систему программирования датчика случайных чисел, управляемого константами Key1 и Key2.
Программа работает в двух режимах, определяемых числом параметров вызова. Первым параметром всегда указывается текстовый файл контейнера. Если таких параметров два, программа извлекает стего из контейнера и помещает его в файл, указанный вторым параметром. Параллельно стего распечатывается на экране. При трех параметрах происходит создание стего. Источником стегосообщения является файл, указанный вторым параметром, а результат работы программы помещается в файл, заданный третьим.
Необходимость учета множества нюансов делает данную программу довольно сложной. Поэтому представляют интерес способы встраивания стего, отнесенные в статье [2] к категории «многие другие». Из всех таких способов были выбраны простейшие. Если перечислять их с повышением уровня сложности, то это будет метод изменения порядка следования маркеров конца строки (листинг 2), метод хвостовых пробелов (листинг 3), метод знаков одинакового начертания (листинг 4) и метод двоичных нулей (листинг 5). Теперь кратко рассмотрим их.
Листинг 2.
Кодирование стего перестановкой маркеров концов строк.
program StegoCR_LF;
(***************************************************************)
(* Простая стеганографическая программа, использующая *)
(* замену порядка следования CR/LF в маркерах концов строк для *)
(* встраивания стего в произвольные тексты. мспользует от 1 до *)
(* 3 параметров, которые и определяют выполняемую программой *)
(* функцию. При этом первый из параметров всегда представляет *)
(* файл-контейнер. *)
(* Если этот параметр единственный, то проверяется *)
(* наличие стеганограммы в файле-контейнере с выводом *)
(* результата на экран. По сути это режим стеганодетектора. *)
(* Если таких параметров два, стеганограмма извлекается из *)
(* контейнера, а результат записывается в заданный вторым *)
(* параметром файл. *)
(* Наконец, если используются все три параметра, то *)
(* стеганографический текст из файла заданного вторым *)
(* параметром, упрятывается в файл-контейнер, заданный первым *)
(* параметром, а результат стеганографического преобразования *)
(* записывается в файл, заданный третьим параметром. *)
(* Совпадение двух имён файлов допускается (трёх — абсурд). *)
(***************************************************************)
type
StringType = string [$FF];
const
TempName = ‘$$$$$$$$.$$$’;
Key1 = $1234;
Key2 = $4567;
CR = #$0D;
LF = #$0A;
var
F, G, H : text;
X : StringType;
I : integer;
K, L : byte;
C, CC : char;
Count : real;
begin
LowVideo;
if not (ParamCount in [1..3])
then
begin
WriteLn (‘Ожидается от 1 до 3 параметров:’);
WriteLn (‘1 — вывод стеганограммы на экран;’);
WriteLn (‘2 — вывод стеганограммы в файл;’);
WriteLn (‘3 — запись стеганограммы в файл.’);
Exit
end;
Assign (F, ParamStr (1));
Reset (F);
Count := 0;
MemW [Dseg : $01FE] := Key1;
MemW [Dseg : $01FC] := Key2;
(*———————ВЫВОД СТЕГАНОГРАММЫ НА ЭКРАН———————*)
if ParamCount = 1
then
begin
C := #00;
K := 0;
L := 0;
while not Eof (F)
do
begin
Read (F, CC);
if C = CR
then
if CC = LF
then
begin
L := Succ (L) mod 8;
K := K shr 1;
C := #00;
if L = 0
then
begin
Write (Chr (K));
K := 0;
L := 0
end
end
else C := CC
else
if C = LF
then
if CC = CR
then
begin
L := Succ (L) mod 8;
K := K shr 1 or $80;
C := #00;
if L = 0
then
begin
Write (Chr (K));
K := 0;
L := 0
end
end
else C := CC
else C := CC
end;
Close (F);
if WhereX <> 1
then WriteLn;
WriteLn (‘Ok!’);
Exit
end;
(*———————-ВЫВОД СТЕГАНОГРАММЫ В ФАЙЛ———————*)
if ParamCount = 2
then
begin
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
C := #00;
K := 0;
L := 0;
Assign (G, TempName);
Rewrite (G);
while not Eof (F)
do
begin
Read (F, CC);
if C = CR
then
if CC = LF
then
begin
L := Succ (L) mod 8;
K := K shr 1;
C := #00;
if L = 0
then
begin
K := K xor Random (256);
Count := Count + 1;
Write (G, Chr (K));
Write (Chr (K));
K := 0;
L := 0
end
end
else C := CC
else
if C = LF
then
if CC = CR
then
begin
L := Succ (L) mod 8;
K := K shr 1 or $80;
C := #00;
if L = 0
then
begin
K := K
xor Random (256);
Count := Count + 1;
Write (G, Chr (K));
Write (Chr (K));
K := 0;
L := 0
end
end
else C := CC
else C := CC
end;
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
WriteLn (‘Прочитано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end;
(*———————-ЗАПмСЬ СТЕГАНОГРАММЫ В ФАЙЛ———————*)
Assign (G, ParamStr (2));
if Pos (‘:’, ParamStr (3)) <> 2
then Assign (H, TempName)
else Assign (H, Copy (ParamStr (3), 1, 2) + TempName);
Reset (G);
Rewrite (H);
L := 0;
while not Eof (F)
do
begin
ReadLn (F, X);
Write (H, X);
if L = 0
then
begin
Read (G, C);
Count := Count + 1;
Write (C);
K := Ord (C) xor Random (256)
end;
if K and 1 = 0
then Write (H, CR + LF)
else Write (H, LF + CR);
K := K shr 1;
L := Succ (L) mod 8
end;
Close (F);
Close (G);
Close (H);
Assign (F, ParamStr (3));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (H, ParamStr (3));
if WhereX <> 1
then WriteLn;
if (Count <> 0) and (L <> 0)
then Count := Count — 1;
WriteLn (‘Записано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end.
Листинг 3.
Кодирование стего хвостовыми пробелами.
program StegoBlank;
(***************************************************************)
(* Простая стеганографическая программа, использующая *)
(* хвостовые пробелы для встраивания стего в произвольные *)
(* тексты. мспользует от 1 до 3 параметров, которые и *)
(* определяют выполняемую программой функцию. При этом первый *)
(* из параметров всегда представляет файл-контейнер. *)
(* Если этот параметр единственный, то проверяется *)
(* наличие стеганограммы в файле-контейнере с выводом *)
(* результата на экран. По сути это режим стеганодетектора. *)
(* Если таких параметров два, стеганограмма извлекается из *)
(* контейнера, а результат записывается в заданный вторым *)
(* параметром файл. *)
(* Наконец, если используются все три параметра, то *)
(* стеганографический текст из файла заданного вторым *)
(* параметром, упрятывается в файл-контейнер, заданный первым *)
(* параметром, а результат стеганографического преобразования *)
(* записывается в файл, заданный третьим параметром. *)
(* Совпадение двух имён файлов допускается (трёх — абсурд). *)
(***************************************************************)
type
StringType = string [$FF];
const
TempName = ‘$$$$$$$$.$$$’;
Key1 = $1234;
Key2 = $4567;
Max = 240;
var
F, G, H : text;
Flag : boolean;
X : StringType;
I, K, L : byte;
C : char;
Count : real;
begin
LowVideo;
if not (ParamCount in [1..3])
then
begin
WriteLn (‘Ожидается от 1 до 3 параметров:’);
WriteLn (‘1 — вывод стеганограммы на экран;’);
WriteLn (‘2 — вывод стеганограммы в файл;’);
WriteLn (‘3 — запись стеганограммы в файл.’);
Exit
end;
Assign (F, ParamStr (1));
Reset (F);
Count := 0;
MemW [Dseg : $01FE] := Key1;
MemW [Dseg : $01FC] := Key2;
(*———————ВЫВОД СТЕГАНОГРАММЫ НА ЭКРАН———————*)
if ParamCount = 1
then
begin
Flag := False;
while not Eof (F)
do
begin
ReadLn (F, X);
L := Length (X);
if L < Max
then
begin
while X [L] = ‘ ‘
do L := Pred (L);
L := Length (X) — L;
if not Flag
then K := L
else Write (Chr (K or L shl 4));
Flag := not Flag
end
end;
Close (F);
if WhereX <> 1
then WriteLn;
WriteLn (‘Ok!’);
Exit
end;
(*———————-ВЫВОД СТЕГАНОГРАММЫ В ФАЙЛ———————*)
if ParamCount = 2
then
begin
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
Assign (G, TempName);
Rewrite (G);
Flag := False;
while not Eof (F)
do
begin
ReadLn (F, X);
L := Length (X);
if L < Max
then
begin
while X [L] = ‘ ‘
do L := Pred (L);
L := Length (X) — L;
if not Flag
then K := L
else
begin
C := Chr ((K or L shl 4)
xor Random (256));
Write (G, C);
Write (C);
Count := Count + 1
end;
Flag := not Flag
end
end;
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
if Flag and (Count <> 0)
then Count := Count — 1;
WriteLn (‘Прочитано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end;
(*———————-ЗАПмСЬ СТЕГАНОГРАММЫ В ФАЙЛ———————*)
Assign (G, ParamStr (2));
if Pos (‘:’, ParamStr (3)) <> 2
then Assign (H, TempName)
else Assign (H, Copy (ParamStr (3), 1, 2) + TempName);
Reset (G);
Rewrite (H);
Flag := False;
while not Eof (F)
do
begin
ReadLn (F, X);
while X [Length (X)] = ‘ ‘
do X [0] := Pred (X [0]);
Write (H, X);
if Length (X) < Max — 15
then
begin
if Flag
then
for I := 1 to K shr $04
do Write (H, ‘ ‘)
else
begin
Read (G, C);
Write (C);
Count := Count + 1;
K := Ord (C) xor Random (256);
for I := 1 to K and $0F
do Write (H, ‘ ‘)
end;
Flag := not Flag
end;
WriteLn (H)
end;
Close (F);
Close (G);
Close (H);
Assign (F, ParamStr (3));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (H, ParamStr (3));
if WhereX <> 1
then WriteLn;
if Flag and (Count <> 0)
then Count := Count — 1;
WriteLn (‘Записано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end.
Листинг 4.
Кодирование стего знаками совпадающего начертания.
program StegoChange;
(***************************************************************)
(* Простая стеганографическая программа для работы с *)
(* русскими текстами основанная на частичной замене русских *)
(* символов латинскими одинакового с ними начертания. *)
(* мспользует от 1 до 3 параметров, которые и определяют *)
(* выполняемую программой функцию. При этом первый из *)
(* параметров всегда представляет файл-контейнер. *)
(* Если этот параметр единственный, то проверяется *)
(* наличие стеганограммы в файле-контейнере с выводом *)
(* результата на экран. По сути это режим стеганодетектора. *)
(* Если таких параметров два, стеганограмма извлекается из *)
(* контейнера, а результат записывается в заданный вторым *)
(* параметром файл. *)
(* Наконец, если используются все три параметра, то *)
(* стеганографический текст из файла заданного вторым *)
(* параметром, упрятывается в файл-контейнер, заданный первым *)
(* параметром, а результат стеганографического преобразования *)
(* записывается в файл, заданный третьим параметром. *)
(* Совпадение двух имён файлов допускается (трёх — абсурд). *)
(* Литература: О.Шарапов. Программная русификация *)
(* матричных принтеров. //Монитор. — 1993, #3, стр. 48..57. *)
(***************************************************************)
type
StringType = string [$FF];
Index = (Rus, Lat);
SetOfChar = set of char;
const
TempName = ‘$$$$$$$$.$$$’;
Max = 13;
Key1 = $1234;
Key2 = $4567;
Tab : array [Index, 1..22] of char = (‘ВЕКМНРСТХаеосАОикпрту’#32,
‘BEKMHPCTXaeocAOuknpmy’#00);
var
F, G, H : text;
X : StringType;
I, J : integer;
K, L : byte;
C, CC : char;
LatSet : SetOfChar;
CharSet : SetOfChar;
LargeLat : SetOfChar;
LargeChar : SetOfChar;
Count : real;
begin
LowVideo;
if not (ParamCount in [1..3])
then
begin
WriteLn (‘Ожидается от 1 до 3 параметров:’);
WriteLn (‘1 — вывод стеганограммы на экран;’);
WriteLn (‘2 — вывод стеганограммы в файл;’);
WriteLn (‘3 — запись стеганограммы в файл.’);
Exit
end;
Assign (F, ParamStr (1));
Reset (F);
Count := 0;
MemW [Dseg : $01FE] := Key1;
MemW [Dseg : $01FC] := Key2;
L := 0;
LatSet := [];
CharSet := [];
for I := 1 to Max
do
begin
LatSet := LatSet + [Tab [Lat, I]];
CharSet := CharSet + [Tab [Rus, I]] + [Tab [Lat, I]]end;
LargeLat := LatSet;
LargeChar := CharSet;
for I := Succ (Max) to 21
do
begin
LargeLat := LargeLat + [Tab [Lat, I]];
LargeChar := LargeChar + [Tab [Lat, I]] + [Tab [Rus, I]]end;
(*———————ВЫВОД СТЕГАНОГРАММЫ НА ЭКРАН———————*)
if ParamCount = 1
then
begin
while not Eof (F)
do
begin
ReadLn (F, X);
for I := 1 to Length (X)
do
begin
J := 1;
C := X [I];
if C in LargeChar
then
begin
K := K shl 1;
if C in LargeLat
then K := K or 1;
L := Succ (L) mod 8;
if L = 0
then Write (Chr (K))
end;
end
end;
Close (F);
if WhereX <> 1
then WriteLn;
WriteLn (‘Ok!’);
Exit
end;
(*———————-ВЫВОД СТЕГАНОГРАММЫ В ФАЙЛ———————*)
if ParamCount = 2
then
begin
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
Assign (G, TempName);
Rewrite (G);
while not Eof (F)
do
begin
ReadLn (F, X);
for I := 1 to Length (X)
do
begin
J := 1;
C := X [I];
if C in CharSet
then
begin
K := K shl 1;
if C in LatSet
then K := K or 1;
L := Succ (L) mod 8;
if L = 0
then
begin
Count := Count + 1;
K := K xor Random (256);
Write (Chr (K));
Write (G, Chr (K))
end
end;
end
end;
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
WriteLn (‘Прочитано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end;
(*———————-ЗАПмСЬ СТЕГАНОГРАММЫ В ФАЙЛ———————*)
Assign (G, ParamStr (2));
if Pos (‘:’, ParamStr (3)) <> 2
then Assign (H, TempName)
else Assign (H, Copy (ParamStr (3), 1, 2) + TempName);
Reset (G);
Rewrite (H);
while not Eof (F)
do
begin
ReadLn (F, X);
for I := 1 to Length (X)
do
begin
J := 1;
C := X [I];
while (J <= Max) and (C <> Tab [Rus, J])
and (C <> Tab [Lat, J])
do J := Succ (J);
if J <= Max
then
begin
if L = 0
then
begin
if Eof (G)
then CC := #00
else Read (G, CC);
Count := Count + 1;
Write (CC);
K := Ord (CC) xor Random (256)
end;
if K and $80 <> 0
then X [I] := Tab [Lat, J]else X [I] := Tab [Rus, J];
K := K shl 1;
L := Succ (L) mod 8
end
end;
WriteLn (H, X)
end;
Close (F);
Close (G);
Close (H);
Assign (F, ParamStr (3));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (H, ParamStr (3));
if WhereX <> 1
then WriteLn;
if (Count <> 0) and (L <> 0)
then Count := Count — 1;
WriteLn (‘Записано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end.
Листинг 5.
Кодирование стего двоичными нулями.
program StegoZero;
(***************************************************************)
(* Простая стеганографическая программа для работы с *)
(* текстами основанная на подмене первого пробела в группе из *)
(* двух или более пробелов двоичным нулём. Режим работы *)
(* программы определяется параметрами вызова. Возможны от 1 до *)
(* 3 параметров, из которых первый описывает файл-контейнер. *)
(* Если этот параметр единственный, то проверяется *)
(* наличие стеганограммы в контейнере с выводом результата на *)
(* экран. По сути это режим стеганодетектора. *)
(* Если таких параметров два, стеганограмма извлекается из *)
(* контейнера, а результат записывается в заданный вторым *)
(* параметром файл. *)
(* Наконец, если используются все три параметра, то *)
(* стеганографический текст из файла заданного вторым *)
(* параметром, упрятывается в файл-контейнер, заданный первым *)
(* параметром, а результат стеганографического преобразования *)
(* записывается в файл, заданный третьим параметром. *)
(* Совпадение двух имён файлов допускается (трёх — абсурд). *)
(***************************************************************)
type
StringType = string [$FF];
const
TempName = ‘$$$$$$$$.$$$’;
Key1 = $1234;
Key2 = $4567;
var
F, G, H : text;
Line : StringType;
I, K, L, Z : byte;
C : char;
Count : real;
begin
LowVideo;
if not (ParamCount in [1..3])
then
begin
WriteLn (‘Ожидается от 1 до 3 параметров:’);
WriteLn (‘1 — вывод стеганограммы на экран;’);
WriteLn (‘2 — вывод стеганограммы в файл;’);
WriteLn (‘3 — запись стеганограммы в файл.’);
Exit
end;
Assign (F, ParamStr (1));
Reset (F);
Count := 0;
MemW [Dseg : $01FE] := Key1;
MemW [Dseg : $01FC] := Key2;
(*———————ВЫВОД СТЕГАНОГРАММЫ НА ЭКРАН———————*)
if ParamCount = 1
then
begin
L := 0;
Z := 0;
while not Eof (F)
do
begin
ReadLn (F, Line);
while (Line <> »)
and (Line [1] <= ‘ ‘)
do Delete (Line, 1, 1);
while (Line <> »)
and (Line [Length (Line)] <= ‘ ‘)
do Delete (Line, Length (Line), 1);
while Line <> »
do
begin
while (Line <> ») and (Line [1] > ‘ ‘)
do Delete (Line, 1, 1);
if (Length (Line) > 1)
and ((Copy (Line, 1, 2) = #00′ ‘)
or (Copy (Line, 1, 2) = ‘ ‘))
then
begin
Z := Z shr 1;
if Line [1] = #00
then Z := Z or $80;
L := Succ (L) mod 8;
if L = 0
then Write (Chr (Z))
end;
while (Line <> ») and (Line [1] <= ‘ ‘)
do Delete (Line, 1, 1)
end
end;
Close (F);
if WhereX <> 1
then WriteLn;
WriteLn (‘Ok!’);
Exit
end;
(*———————-ВЫВОД СТЕГАНОГРАММЫ В ФАЙЛ———————*)
if ParamCount = 2
then
begin
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
Rewrite (G);
L := 0;
Z := 0;
while not Eof (F)
do
begin
ReadLn (F, Line);
while (Line <> »)
and (Line [1] <= ‘ ‘)
do Delete (Line, 1, 1);
while (Line <> »)
and (Line [Length (Line)] <= ‘ ‘)
do Delete (Line, Length (Line), 1);
while Line <> »
do
begin
while (Line <> ») and (Line [1] > ‘ ‘)
do Delete (Line, 1, 1);
if (Length (Line) > 1)
and ((Copy (Line, 1, 2) = #00′ ‘)
or (Copy (Line, 1, 2) = ‘ ‘))
then
begin
Z := Z shr 1;
if Line [1] = #00
then Z := Z or $80;
L := Succ (L) mod 8;
if L = 0
then
begin
Count := Count + 1;
C := Chr (Z
xor Random (256));
Write (G, C);
Write (C)
end
end;
while (Line <> ») and (Line [1] <= ‘ ‘)
do Delete (Line, 1, 1)
end
end;
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
WriteLn (‘Прочитано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end;
(*———————-ЗАПмСЬ СТЕГАНОГРАММЫ В ФАЙЛ———————*)
Assign (G, ParamStr (2));
if Pos (‘:’, ParamStr (3)) <> 2
then Assign (H, TempName)
else Assign (H, Copy (ParamStr (3), 1, 2) + TempName);
Reset (G);
Rewrite (H);
L := 0;
while not Eof (F)
do
begin
ReadLn (F, Line);
I := 0; (* последний пробельный символ *)
while (I < Length (Line)) and (Line [Succ (I)] <= ‘ ‘)
do I := Succ (I);
K := Length (Line); (* последний непробельный символ *)
while (K > I) and (Line [K] <= ‘ ‘)
do K := Pred (K);
while I < K
do
begin
while (I < K) and (Line [Succ (I)] > ‘ ‘)
do I := Succ (I);
if (K — I > 2) and (Copy (Line, Succ (I), 2) = ‘ ‘)
then
begin
if L = 0
then
begin
if Eof (G)
then C := #00
else Read (G, C);
Z := Ord (C) xor Random (256)
end;
if Z and 1 = 1
then Line [Succ (I)] := #00;
Z := Z shr 1;
L := Succ (L) mod 8;
I := I + 2;
if L = 0
then
begin
Write (C);
Count := Count + 1
end
end;
while (I < K) and (Line [Succ (I)] = ‘ ‘)
do I := Succ (I)
end;
WriteLn (H, Line)
end;
Close (F);
Close (G);
Close (H);
Assign (F, ParamStr (3));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (H, ParamStr (3));
if WhereX <> 1
then WriteLn;
WriteLn (‘Записано ‘, Count : 0 : 0, ‘ байт стего…’);
Exit
end.
Метод изменения порядка следования маркеров конца строки CR/LF использует индифферентность подавляющего числа средств отображения текстовой информации к порядку следования символов перевода строки (CR) и возврата каретки (LF), ограничивающих строку текста. Традиционный порядок следования CR/LF соответствует 0, а инвертированный LF/CR означает 1.
Метод хвостовых пробелов предполагает дописывание в конце коротких строк (менее 225 символов; значение 225 выбрано достаточно произвольно) от 0 до 15 пробелов, кодирующих значение полубайта.
Метод знаков одинакового начертания предполагает подмену (бит 1) или отказ от такой подмены (бит 0) русского символа латинским того же начертания. Идея этого метода заимствована из статьи [3].
Метод двоичных нулей является разновидностью метода знаков одинакового начертания и предполагает либо замену первого в группе из двух или более внутренних пробелов двоичным нулем (бит 1), либо отказ от нее (бит 0).
Интерфейс программ листингов 2—5 одинаков. Всего возможны три режима их работы, определяемые числом параметров вызова, первый из которых всегда представляет файл стеганоконтейнера. Если этот параметр единственный, то каждая из программ реализует функции стеганодетектора: производится сканирование контейнера с выводом результата на экран. Если таких параметров два, то стеганограмма извлекается из контейнера и расшифровывается. Результат выводится в файл, указанный вторым параметром, а также на экран. При трех параметрах стего выбирается из файла, указанного вторым параметром, шифруется исключающим «ИЛИ» и помещается в файл контейнера, заданного первым. Модифицированной стеганограммой контейнер записывается в файл, заданный третьим параметром.
Для шифрования во всех случаях используется программный датчик случайных чисел, начальное состояние которого определяется константами Key1 и Key2. Всегда производится эхо-печать стего.
Программа листинга 6 является вспомогательной и предназначена для жесткого форматирования таблиц, необходимого для работы с программой листинга 1. Суть жесткого форматирования состоит в замене всех внутренних пробелов строки двоичными нулями. При работе с этой программой в строке вызова необходимо указать имена входного и выходного файлов.
Листинг 6.
Склейка символов таблиц.
program FixTab;
(******************************************)
(* Программа фиксирует внутренние пробелы *)
(* в строке, заменяя их двоичными нулями *)
(******************************************)
type
StringType = string [$FF];
const
TempName = ‘$$$$$$$$.$$$’;
var
F, G : text;
I : byte;
Line, Head, Tail, Body : StringType;
begin
LowVideo;
if ParamCount <> 2
then
begin
WriteLn (‘Укажите в параметре ввода имена входного и’);
WriteLn (‘выходного файлов, которые могут совпадать.’);
Exit
end;
Assign (F, ParamStr (1));
if Pos (‘:’, ParamStr (2)) <> 2
then Assign (G, TempName)
else Assign (G, Copy (ParamStr (2), 1, 2) + TempName);
Reset (F);
Rewrite (G);
while not Eof (F)
do
begin
ReadLn (F, Line);
I := 0;
while (I < Length (Line)) and (Line [Succ (I)] <= ‘ ‘)
do I := Succ (I);
Head := Copy (Line, 1, I);
I := Length (Line);
while (I > 0) and (Line [I] <= ‘ ‘)
do I := Pred (I);
Tail := Copy (Line, Succ (I), Length (Line) — I);
Body := Copy (Line, Succ (Length (Head)), I — Length (Head));
for I := 1 to Length (Body)
do
if Body [I] = ‘ ‘
then Body [I] := #00;
Line := Head + Body + Tail;
WriteLn (G, Line);
WriteLn (Line)
end;
Close (F);
Close (G);
Assign (F, ParamStr (2));
(*$I-*)
Erase (F);
(*$I+*)
I := IoResult;
Rename (G, ParamStr (2));
if WhereX <> 1
then WriteLn;
WriteLn (‘Ok!’);
Exit
end.
Для наглядности все программы написаны на входном языке крайне компактного компилятора Turbo Pascal 3.x компании Borland, почти идеально (если бы не отсутствие встроенного ассемблера) подходящем для исследовательской работы.
Анализ реализации методов
Эффективность описанных методов упаковки стего в контейнере была исследована на переведенном в ASCII-вид тексте главы VI тома I книги «Мертвая вода» [1] объемом 126 729 байт и насчитывающим 2143 строки со строками, выровненными на 65-символьную границу при абзацном отступе в четыре символа. Полученная плотность упаковки (в порядке возрастания) представлена в следующей таблице:
Сравнение методов текстовой стеганографии
Метод | Знаков стего | Плотность, % |
Чередование маркеров конца | 267 | 0,21 |
Выравнивание пробелами | 411 | 0,32 |
Двоичные нули | 740 | 0,58 |
Хвостовые пробелы | 1071 | 0,85 |
Знаки одинакового начертания | 4065 | 3,21 |
Обращает на себя внимание необычно высокая эффективность упаковки стего с использованием подмены символов.
Полученные данные являются лишь оценочными и зависят не только от свойств контейнера, но и от свойств помещаемого в него стего, хотя и в меньшей степени.
Число автоматических методов текстовой стеганографии, естественно, не ограничивается рассмотренными примерами. Пополнить запас примеров можно, в частности, разумной комбинацией уже приведенных.
В завершение стоит упомянуть об одном неожиданном наблюдении, свидетельствующем о том, что текстовых файлов, пригодных для использования в качестве стеганоконтейнера, намного больше, чем это может показаться с первого взгляда. Действительно, таковыми являются и файлы баз данных, символьные поля записей которых фактически представляют собой строки фиксированной длины (естественно, без завершающих символов CR/LF). Надеюсь, читатель по достоинству оценит это наблюдение…
И пусть простят меня коллеги и недоброжелатели за то, что я сейчас выскажу свое безусловно отрицательное отношение к методам «защиты информации», в том числе и к рассмотренным здесь…
От редактора. В развитие темы стоит обратить внимание на работу Карташова Д. В., Чижухина Г. Н. Текстовая стеганография // Труды научно-технической конференции «Безопасность информационных технологий». Пенза, 2003. Примеры изощренных методов текстовой стеганографии приведены, в частности, в работе Xu J., Sung A., Shi P., Liu Q. Text Steganography Using Wavelet Transform // New Mexico Tech Socorro, USA, 2002, выполнявшейся для Министерства обороны США, в котором изложено использование волнового (wavelet) преобразования, а также в публикации Кричевского А. М. Нейронные сети в задачах компьютерной стеганографии // Школа-семинар БИКАМП’03. С.-Петербург, 2003 (http://bicamp.aanet.ru/2003/ papers/sectionIT/ KrichevskyAM.pdf)
Р. Б.
Литература
- Мертвая вода // http://www.dotu.ru, http://www.kpe.ru.
- Беляев А. Стеганограмма: скрытие информации // Программист, 2002, №1 (электронная версия).
- Шарапов О. Программная русификация матричных принтеров // Монитор, 1993, № 3, с. 48—57.
Автор: Василий Текин
Источник: osp.ru