Steganografia

Steganografia to – podając za wikipedią – nauką o komunikacji w taki sposób by obecność komunikatu nie mogła zostać wykryta. W odróżnieniu od kryptografii (gdzie obecność komunikatu nie jest negowana natomiast jego treść jest niejawna) steganografia próbuje ukryć fakt prowadzenia komunikacji.

Jednym z naszych zadań na Ochronę Danych i Sieci Komputerowych było napisanie programu do ukrywania tekstu w obrazku typu BMP lub też w pliku dźwiękowym typu WAV. Ja wybrałem kodowanie wiadomości w pliku BMP – bo dla mnie jest to łatwiejsze, znam po części ten format i wiem jak to ma w nim działać, dlatego też tutaj przedstawię jak odkodować i zakodować wiadomości właśnie w tym formacie. Najpierw jednak powinniśmy poznać teorie – czyli jak zakodować podane dane w pliku BMP przy pomocy steganografii.

Plik BMP, zapisuje piksele w dość prosty sposób – zapisuje dane w formacie BGR, gdzie na każdy kanał przeznacza 8 bitów, czyli 1 bajt. Jak wiemy z technikum lub też z pierwszego roku studiów – na jednym bajcie możemy zapisać liczbę od 0 do 255. Tak więc jeden piksel jest zapisywany na 3 bajtach*.

By dodać do pliku BMP jakąś wiadomość, skorzystamy z zasady kodowania wiadomości na najmniej znaczącym bicie. Oznacza to tyle, że pobierzemy wartość piksela (albo może raczej jego składowych) i na najmniej znaczących bitach zakodujemy naszą wiadomość. Gwoli wyjaśnienia jeśli ktoś nie pamięta który bit jest najmniej znaczący to mówię że jest to ten bit który koduje 1. Najprościej wyjaśni to rysunek:

najmniej-znaczacy-bit

Teraz by zakodować wiadomość w obrazku musimy przyjąć parę założeń:

  • Musimy wiedzieć które kanały biorą udział w kodowaniu
  • Musimy wiedzieć ile bitów z każdego kanału bierze udział w kodowaniu
  • Musimy wiedzieć od którego piksela i w którą stronę należy odczytywać zakodowane dane

W moim programie – który na samym końcu tej wiadomości zostanie dołączony – wyszedłem z założenia że: użytkownik poda które kanały i ile bitów będzie brało udział w kodowaniu. Zaś sam program będzie będzie zaczynał kodować od współrzędnej (0,0) i po kolei będzie zapełniał wiersz po wierszu aż do wyczerpania pikseli.

Teraz przyszedł czas na sam algorytm działania:

  1. Pobieramy kolor piksela (najlepiej już podzielonego na składowe RGB)
  2. Jeśli mamy kodować wiadomość na kanale czerwonym to przechodzimy do korku 3, a jeśli nie to do korku 5)
  3. Czyścimy r ostatnich bitów na kanale czerwonym
  4. Zapisujemy r bitów wiadomości na ostatnich r bitach kanału czerwonego
  5. Jeśli mamy kodować wiadomość na kanale zielonym to przechodzimy do korku 6, a jeśli nie to do korku 8)
  6. Czyścimy g ostatnich bitów na kanale zielonym
  7. Zapisujemy g bitów wiadomości na ostatnich g bitach kanału zielonego
  8. Jeśli mamy kodować wiadomość na kanale niebieskim to przechodzimy do korku 9, a jeśli nie to do korku 11)
  9. Czyścimy b ostatnich bitów na kanale niebieskim
  10. Zapisujemy b bitów wiadomości na ostatnich b bitach kanału niebieskiego
  11. Zapisujemy piksel ze zmienionym kolorami do wynikowego pliku graficznego.

Przy założeniu że w zmiennych r,g i b jest zapisana informacja ile bitów można zapisać odpowiednio na kanale czerwonym, zielonym i niebieskim.

Poniżej znajduje się moja implementacja algorytmu napisana w C#, znajduje się ona w pliku Encryptor.cs od lini 111:

   // tutaj generujemy stałą która będzie służyła do czyszczenia bitów obrazka,
   // które później będziemy przeznaczać na bity naszej wiadomości
   byte clearR = (byte)~((1 << r) - 1);
   byte clearG = (byte)~((1 << g) - 1);
   byte clearB = (byte)~((1 << b) - 1);


   for ( int y = 0; y < inBmp.Height && data.Count > 0; ++y)
    for (int x = 0; x < inBmp.Width && data.Count > 0; ++x)
    {
     Color cpix = inBmp.GetPixel(x, y); // pobieranie koloru piksela
     byte R = cpix.R, G = cpix.G, B = cpix.B;
     int bf = data.Count;

     if (r > 0) // jeśli chcemy coś zapisać na kanale czerwonym
      R = (byte)((R & clearR) | GetPart(data, r)); // czyścimy bity i zapisujemy dane wiadomości

     if (g > 0) // jeśli chcemy coś zapisać na kanale zielonym
      G = (byte)((G & clearG) | GetPart(data, g)); // czyścimy bity i zapisujemy dane wiadomości

     if (b > 0) // jeśli chcemy coś zapisać na kanale niebieskim
      B = (byte)((B & clearB) | GetPart(data, b)); // czyścimy bity i zapisujemy dane wiadomości

     cpix = Color.FromArgb(R, G, B);
     outBmp.SetPixel(x, y, cpix); // zapisujemy "nowe" kolory

     used += bf - data.Count;

     f1.pbProgressBar.Value = used;
    }

By rozszyfrować dane zapisane w obrazku, należy wykonać operacje odwrotną, czyli przejść przez wszystkie piksele, pobrać odpowiednie bity wszystkich kanałów i złożyć to w całą wiadomość. Pojawia się tutaj jednak pytanie, kiedy kończy się nasza wiadomość, a zaczynają „śmieci”. Mamy tutaj trzy wyjścia:

  1. Możemy zakończyć naszą wiadomość znakiem NULL – spowoduje to że będziemy mogli rozdzielić gdzie kończy się nasza wiadomość a zaczynają się śmieci. Problemem tutaj jest to, że będziemy mogli odczytać „śmieciową” wiadomość – czyli z jakiegoś pliku graficznego do którego nie zapisywaliśmy żadnej wiadomości
  2. Podobnym rozwiązaniem do pierwszego jest zapisanie w wiadomości długości tekstu który ukrywamy. Ma to oczywiście takie same plusy i minusy kończenia naszej wiadomości NULLem.
  3. Rozwiązaniem które ja zastosowałem, to było zapisanie w wiadomości sumy kontrolnej i długości tekstu. Zapisanie długości pozwalało na stwierdzenie ile pikselów trzeba odczytać, a suma kontrolna służyła do sprawdzenia czy dane odczytane z pliku są prawidłowe – czy też nie odczytujemy śmieci.

By nie przedłużać, mój program znajduje się tutaj: https://dl.dropboxusercontent.com/u/35418266/polibuda/zad2-2.rar. Parę uwag na koniec:

  • Szyfrowanie i Deszyfrowanie znajduje się odpowiednio w plikach: Encryptor.cs i Decryptor.cs
  • Metody które będą interesować was najbardziej to: Encryptor.EncryptData(), które przyjmuje jako argumenty ile bitów z danego kanału ma być brane pod uwagę przy zapisywaniu wiadomości, czy dane mają być zapisywane w kodach ascii czy też w utf-16, jaki ciąg znaków ma być zapisany, bitmapę która posłuży za oryginalny obraz, bitmapę do której zostanie zapisana zaszyfrowana wiadomość i ostatni parametr jest wykorzystywany do synchronizacji zapisu z interfejsem
  • Drugą interesującą metodą będzie Decryptor.DecryptData(), która przyjmuje jako argumenty bitmapę wejściową (z której będzie odczytywany ciąg znaków), ile bitów ma być brane pod uwagę z każdego kanału i czy znaki są zapisane w kodach ASCI czy w UTF-16
  • Liczenie sumy kontrolnej jest bardzo proste i znajduje się w metodzie Encryptor.CheckSum i Decryptor.CheckSum (są one identyczne)
  • Dane przed zapisaniem do obrazu czy zaraz po odczytaniu są zapisane jako lista bool (gdzie określone bajty wiadomości są rozbite na bity)
  • Tekst kodowany w ASCI jest kodowany tylko na 7 bitach (czyli wszystkie znaki które wykraczają poza zakres 127 są mieniane na znaki zapytania). Zaś tekst kodowany na UTF-16 jest zapisany na 16 bitach
  • Jak wspomniałem wcześniej, na początku kodowanego tekstu zapisuje sumę kontrolną (na 8 bitach) i później długość tekstu (na 16 bitach)

Kod źródłowy znajduje się na GitHubie: psychob/steganografia

* – mówimy tutaj oczywiście o typie BMP 24 bitowym który jest też wyszczególniony w zadaniu.

3 myśli w temacie “Steganografia

  1. Cześć. Mógłbyś napisać co dokładnie kryje się pod linijką „byte clearR = (byte)~((1 << r) – 1);"? Konkretnie o co chodzi od "~((1 << r) – 1);"?

    1. To są proste operacje bitowe. By zapisać naszą wiadomość do obrazka musimy najpierw usunąć z końca bajtu te bity które posłużą nam do zakodowania wiadomości.

      Dla przypadku gdy chcemy przeznaczyć dwa ostatnie bity na wiadomość.

      • Najpierw przesuwamy 1 na pozycje bitu r ( w naszym przypadku r to jest 2, czyli dostaniemy wartość 0b00000100 )
      • Później odejmujemy od naszej wartości jeden, dzięki czemu dostaniemy stałą która będzie odpowiadała za bity które chcemy przeznaczyć na naszą ukrytą wiadomość (w naszym przypadku da nam to wartość: 0b00000011 )
      • Później negujemy bitowo naszą stałą przy pomocy operatora ~ (co daje nam naszą stałą wynikową 0b11111100)

Dodaj komentarz

Ta witryna wykorzystuje usługę Akismet aby zredukować ilość spamu. Dowiedz się w jaki sposób dane w twoich komentarzach są przetwarzane.