Стеганография. Скрываем текстовую информацию в bmp файле. Практическая реализация на C#

в 16:51, , рубрики: .net, Программирование, Стеганография, метки: ,

Доброго времени суток!

На Хабрахабре уже есть статьи на данную тему и я хочу поделится своим вариантом решения данной задачи. И так, начнем.

Общие сведения

Стеганографическая система (стегосистема) — объединение методов и средств используемых для создания скрытого канала для передачи информации. Стеганография в наше время часто применяется, как правило, для встраивания цифровых водяных знаков, являющееся основой для систем защиты авторских прав и DRM (Digital rights management) систем.

Реализовывать будем метод LSB.
LSB (Least Significant Bit, наименьший значащий бит) — суть этого метода заключается в замене последних значащих битов в контейнере (изображения, аудио или видеозаписи) на биты скрываемого сообщения. Разница между пустым и заполненным контейнерами должна быть не ощутима для органов восприятия человека.

Также будем использовать для шифрования/дешифрования bmp файл, не содержащий палитру. В таком bmp файле каждые 3 байта определяют 3 цвета пикселя.

Подготовка к реализации

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

private BitArray ByteToBit(byte src) {
            BitArray bitArray = new BitArray(8);
            bool st = false;
            for (int i = 0; i < 8; i++)
            {
                if ((src >> i & 1) == 1) {
                    st = true;
                } else st = false;
                bitArray[i] = st;
            }
            return bitArray;
}

private byte BitToByte(BitArray scr) {
            byte num = 0;
            for (int i = 0; i < scr.Count; i++)
                if (scr[i] == true)
                    num += (byte)Math.Pow(2, i);
            return num;
}

Я думаю они понятны и пояснять не стоит.

И так, расположение в bmp информации будет следующим:

  • Пиксель 0,0: признак того, что в файле есть текстовая информация. В качестве признака используется символ /
  • Пиксели 0.1 — 0.3: размер текстовой информации, записанной в файл
  • Пиксели 0.4 и до конца файла: собственно текстовая информация

Для начала рассмотрим код, записывающий в пиксель 0.0 признак зашифрованного файла.

            byte [] Symbol = Encoding.GetEncoding(1251).GetBytes("/");
            BitArray ArrBeginSymbol = ByteToBit(Symbol[0]);
            Color curColor = bPic.GetPixel(0, 0);
            BitArray tempArray = ByteToBit(curColor.R);
            tempArray[0] = ArrBeginSymbol[0];
            tempArray[1] = ArrBeginSymbol[1];
            byte nR = BitToByte(tempArray);

            tempArray = ByteToBit(curColor.G);
            tempArray[0] = ArrBeginSymbol[2];
            tempArray[1] = ArrBeginSymbol[3];
            tempArray[2] = ArrBeginSymbol[4];
            byte nG = BitToByte(tempArray);

            tempArray = ByteToBit(curColor.B);
            tempArray[0] = ArrBeginSymbol[5];
            tempArray[1] = ArrBeginSymbol[6];
            tempArray[2] = ArrBeginSymbol[7];
            byte nB = BitToByte(tempArray);

            Color nColor = Color.FromArgb(nR, nG, nB);
            bPic.SetPixel(0, 0, nColor);

В коде в переменной Symbol хранится код символа "/". Далее этот код преобразуется в массив бит (переменная ArrBeginSymbol). Цвет пикселя 0.0 хранится в переменной curColor. Далее каждый из трех составляющих цветов пикселя преобразуется в массив бит, затем в красном цвете заменяются младшие 2 бита на биты символа "/", в зеленом заменяются младшие 3 бита на биты символа "/" и в синем так же заменяются младшие 3 бита цвета. Из 3 новых полученных цветов создается новый цвет пикселя (nColor) и устанавливается вместо предыдущего цвета. Все, признак того, что в файле есть информация записан в bmp файл.
Способ записи информации, то есть 2 бита, 3 бита и 3 бита выбран для удобства работы, ибо в один пиксель записывается сразу байт информации.

Далее рассмотрим метод проверки признака, описанного выше

private bool isEncryption(Bitmap scr)
        {
            byte[] rez = new byte[1];
            Color color = scr.GetPixel(0, 0);
            BitArray colorArray = ByteToBit(color.R); //получаем байт цвета и преобразуем в массив бит
            BitArray messageArray = ByteToBit(color.R); ;//инициализируем результирующий массив бит
            messageArray[0] = colorArray[0];
            messageArray[1] = colorArray[1];

            colorArray = ByteToBit(color.G);//получаем байт цвета и преобразуем в массив бит
            messageArray[2] = colorArray[0];
            messageArray[3] = colorArray[1];
            messageArray[4] = colorArray[2];

            colorArray = ByteToBit(color.B);//получаем байт цвета и преобразуем в массив бит
            messageArray[5] = colorArray[0];
            messageArray[6] = colorArray[1];
            messageArray[7] = colorArray[2];
            rez[0] = BitToByte(messageArray); //получаем байт символа, записанного в 1 пикселе
            string m = Encoding.GetEncoding(1251).GetString(rez);
            if (m == "/")
            {
                return true;
            }
            else return false;
        }

Метод аналогичен коду приведенному выше с точностью до наоборот. Если в пикселе 0.0 записан символ "/", то функция возвращает true, иначе — false.

Далее в файл записывается размер текстовой информации. Рассмотрим метод подробнее:

        private void WriteCountText(int count, Bitmap src) {
            byte[] CountSymbols = Encoding.GetEncoding(1251).GetBytes(count.ToString());
            for (int i = 0; i < 3; i++)
            {
                BitArray bitCount = ByteToBit(CountSymbols[i]); //биты количества символов
                Color pColor = src.GetPixel(0, i + 1); //1, 2, 3 пикселы
                BitArray bitsCurColor = ByteToBit(pColor.R); //бит цветов текущего пикселя
                bitsCurColor[0] = bitCount[0];
                bitsCurColor[1] = bitCount[1];
                byte nR = BitToByte(bitsCurColor); //новый бит цвета пиксея

                bitsCurColor = ByteToBit(pColor.G);//бит бит цветов текущего пикселя
                bitsCurColor[0] = bitCount[2];
                bitsCurColor[1] = bitCount[3];
                bitsCurColor[2] = bitCount[4];
                byte nG = BitToByte(bitsCurColor);//новый цвет пиксея

                bitsCurColor = ByteToBit(pColor.B);//бит бит цветов текущего пикселя
                bitsCurColor[0] = bitCount[5];
                bitsCurColor[1] = bitCount[6];
                bitsCurColor[2] = bitCount[7];
                byte nB = BitToByte(bitsCurColor);//новый цвет пиксея

                Color nColor = Color.FromArgb(nR, nG, nB); //новый цвет из полученных битов
                src.SetPixel(0, i + 1, nColor); //записали полученный цвет в картинку
            }
        }

В CountSymbols записывается количество символов исходного текста. Каждая цифра занимает один байт, поэтому максимальная длина исходного текста — 999 — 4 = 995 символов(4 — это один пиксель на признак присутствия информации в файле и три пикселя на размер текстовой информации). При необходимости можно увеличить, взяв пиксели не с 0.1 по 0.3, а с 0.1 по 0.4 например, и так далее. В цикле for каждая цифра количества исходного текста преобразуется в массив бит и записывается в младшие пиксели цвета по принципу, описанному выше.

Метод чтения размера текстовой информации:

 private int ReadCountText(Bitmap src) {
            byte[] rez = new byte[3]; //массив на 3 элемента, т.е. максимум 999 символов шифруется
            for (int i = 0; i < 3; i++)
            { 
                Color color = src.GetPixel(0, i + 1); //цвет 1, 2, 3 пикселей 
                BitArray colorArray = ByteToBit(color.R); //биты цвета
                BitArray bitCount = ByteToBit(color.R); ; //инициализация результирующего массива бит
                bitCount[0] = colorArray[0];
                bitCount[1] = colorArray[1];

                colorArray = ByteToBit(color.G);
                bitCount[2] = colorArray[0];
                bitCount[3] = colorArray[1];
                bitCount[4] = colorArray[2];

                colorArray = ByteToBit(color.B);
                bitCount[5] = colorArray[0];
                bitCount[6] = colorArray[1];
                bitCount[7] = colorArray[2];
                rez[i] = BitToByte(bitCount);
            }
            string m = Encoding.GetEncoding(1251).GetString(rez);
            return Convert.ToInt32(m, 10);
        }

Метод обратный функции WriteCountText. Пояснять, я думаю, не стоит.

Реализация

Опустим код, который открывает/закрывает файл, проверяет на ошибки, его можно будет посмотреть, скачав проект. Приведем код, который собственно и записывает информацию в файл. Некоторый код уже был приведен выше.
Bitmap bPic — открытый файл с картинкой.

            BinaryReader bText = new BinaryReader(rText, Encoding.ASCII);

            List<byte> bList = new List<byte>();
            while (bText.PeekChar() != -1) { //считали весь текстовый файл для шифрования в лист байт
                bList.Add(bText.ReadByte());
            }
            int CountText = bList.Count; // в CountText - количество в байтах текста, который нужно закодировать
            bText.Close();
            rFile.Close();

            //проверяем, поместиться ли исходный текст в картинке
            if (CountText > ((bPic.Width * bPic.Height)) - 4 ) {
                MessageBox.Show("Выбранная картинка мала для размещения выбранного текста", "Информация", MessageBoxButtons.OK);
                return;
            }

            //проверяем, может быть картинка уже зашифрована
            if (isEncryption(bPic))
            {
                MessageBox.Show("Файл уже зашифрован", "Информация", MessageBoxButtons.OK);
                return;
            }

            byte [] Symbol = Encoding.GetEncoding(1251).GetBytes("/");
            BitArray ArrBeginSymbol = ByteToBit(Symbol[0]);
            Color curColor = bPic.GetPixel(0, 0);
            BitArray tempArray = ByteToBit(curColor.R);
            tempArray[0] = ArrBeginSymbol[0];
            tempArray[1] = ArrBeginSymbol[1];
            byte nR = BitToByte(tempArray);

            tempArray = ByteToBit(curColor.G);
            tempArray[0] = ArrBeginSymbol[2];
            tempArray[1] = ArrBeginSymbol[3];
            tempArray[2] = ArrBeginSymbol[4];
            byte nG = BitToByte(tempArray);

            tempArray = ByteToBit(curColor.B);
            tempArray[0] = ArrBeginSymbol[5];
            tempArray[1] = ArrBeginSymbol[6];
            tempArray[2] = ArrBeginSymbol[7];
            byte nB = BitToByte(tempArray);

            Color nColor = Color.FromArgb(nR, nG, nB);
            bPic.SetPixel(0, 0, nColor);

            WriteCountText(CountText, bPic); //записываем количество символов исходного текста

            int index = 0;
            bool st = false;
            for (int i = 4; i < bPic.Width; i++) { 
                for (int j = 0; j < bPic.Height; j++) {
                    Color pixelColor = bPic.GetPixel(i, j);
                    if (index == bList.Count) {
                        st = true;
                        break;
                    }
                    BitArray colorArray = ByteToBit(pixelColor.R);
                    BitArray messageArray = ByteToBit(bList[index]);
                    colorArray[0] = messageArray[0]; //меняем
                    colorArray[1] = messageArray[1]; // в нашем цвете биты
                    byte newR = BitToByte(colorArray);

                    colorArray = ByteToBit(pixelColor.G);
                    colorArray[0] = messageArray[2];
                    colorArray[1] = messageArray[3];
                    colorArray[2] = messageArray[4];
                    byte newG = BitToByte(colorArray);

                    colorArray = ByteToBit(pixelColor.B);
                    colorArray[0] = messageArray[5];
                    colorArray[1] = messageArray[6];
                    colorArray[2] = messageArray[7];
                    byte newB = BitToByte(colorArray);

                    Color newColor = Color.FromArgb(newR, newG, newB);
                    bPic.SetPixel(i, j, newColor);
                    index ++;
                }
                if (st) {
                    break;
                }
            }

И, соответственно код, который считывает информацию из bmp файла

Bitmap bPic = new Bitmap(rFile);
            if (!isEncryption(bPic)) {
                MessageBox.Show("В файле нет зашифрованной информации", "Информация", MessageBoxButtons.OK);
                return;
            }

            int countSymbol = ReadCountText(bPic); //считали количество  символов
            byte[] message = new byte[countSymbol];
            int index = 0;
            bool st = false;
            for (int i = 4; i < bPic.Width; i++) {
                for (int j = 0; j < bPic.Height; j++) {
                    Color pixelColor = bPic.GetPixel(i, j);
                    if (index == message.Length) {
                        st = true;
                        break;
                    }
                    BitArray colorArray = ByteToBit(pixelColor.R);
                    BitArray messageArray = ByteToBit(pixelColor.R); ;
                    messageArray[0] = colorArray[0];
                    messageArray[1] = colorArray[1];

                    colorArray = ByteToBit(pixelColor.G);
                    messageArray[2] = colorArray[0];
                    messageArray[3] = colorArray[1];
                    messageArray[4] = colorArray[2];

                    colorArray = ByteToBit(pixelColor.B);
                    messageArray[5] = colorArray[0];
                    messageArray[6] = colorArray[1];
                    messageArray[7] = colorArray[2];
                    message[index] = BitToByte(messageArray);
                    index++;
                }
                if (st) {
                    break;
                }
            }
            string strMessage = Encoding.GetEncoding(1251).GetString(message);

Заключение

Метод LSB не сложен в реализации, и его можно использовать для сокрытия нужной информации в bmp файле. Но существенным недостатком метода является то, что размер bmp имеет большой размер, что делает этот метод нежизнеспособным для передачи конфиденциальной информации по сети интернет.
Сам проект: rghost.ru/37128613.
В архиве 2 картинки, одна с текстом, вторая без.

Автор: Fenja


  1. Марина:

    Спасибо! Как раз сейчас занимаюсь это темой. Содержательная статья, очень помогла.

  2. Гость:

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js