Algorytm najmniej znaczącego bitu (ang. Least Significant Bit) jest najprostszą i najczęściej stosowaną metodą steganograficzną dla obrazów. Przyjmijmy, że chcemy ukryć dowolne dane binarne (tekst, obraz lub inne dane przedstawione jako ciąg zer i jedynek), takie które zmieszczą się w naszym nośniku. Jako nośnik przyjmijmy obraz w formacie PNG w 24-bitowym trybie RGB. Format PNG stosuje bezstratny system kompresji, dzięki temu daje nam bezpośredni dostęp do informacji o poszczególnych pikselach i możemy stworzyć zmodyfikowany obraz bez obaw o “zgubienie” części danych podczas zapisu. Tryb 24-bitowy oznacza, że do dyspozycji mamy 3 kanały, nie używamy kanału Alpha.

Każde dane cyfrowe można przedstawić w postaci binarnej. Wprowadzone informacje przed ukryciem są przetwarzane na tablicę bajtów, a każdy bajt jest zapisywany za pomocą 8 bitów.

string jako ciąg bitów

Zmienna tekstowa String jako ciąg bitów.

Najmniej znaczącymi bitami są bity po prawej stronie w zapisie binarnym. Jeśli w każdej składowej zmienimy ostatni bit, to barwa piksela zmieni się na tyle nieznacznie, że oko ludzkie nie zarejestruje tej zmiany.
Kolor R: 11111110 G: 11111110 B: 11111110, czyli RGB (254, 254, 254), będzie tylko nieznacznie ciemniejszy niż rzeczywisty biały kolor.

Aplikacja przy odczytywaniu wiadomości powinna wiedzieć jak długo ma szukać ukrytych danych, dlatego na początku każdego pliku zostaje ukryta informacja o rozmiarze szyfrogramu. Informacje steganograficzne mają więc dwie części: wielkość komunikatu binarnego oraz samą wiadomość.

Poniżej został przestawiony przykład kodu w języku Java. Chociaż do operacji na bitach lepiej nadawałyby się języki niższego poziomu. Obraz jest wczytywany jako obiekt BufferedImage, co umożliwia pełną kontrolę nad pikselami obrazu. Metoda getRaster(), zwraca obiekt typu WritableRaster, w którym można modyfikować poszczególne piksele obrazu. Długość tablicy bajtów z danymi obrazu jest trzykrotnie większa niż obiektu BufferedImage. Klasa BufferedImage odwołuje się do pikseli obrazu, natomiast dalsza konwersja pliku rozkłada kolejne piksele na trzy kolory składowe RGB. Każdy zapisywany jest na 8 bitach, czyli 1 bajcie. W efekcie do zapisu danych o kolorze z jednego piksela potrzebujemy 3 bajtów. Tablica bajtów imageByte zawiera kolejno występujące po sobie składowe pikseli: RGBRGBRGB… Tablica msg zawiera treść ukrywanej wiadomości. W przypadku próby ukrycia tekstu, tablicę bajtów ze zwykłego typu String można uzyskać poprzez metodę getBytes() z opcjonalnym argumentem formatowania tekstu.

Przetworzenie nośnika i ukrywanej wiadomości na tablice bajtów.

public BufferedImage encryptImage(File file, byte [] text){

      BufferedImage image = bIP.getBufferedImage(file);
      WritableRaster raster = image.getRaster();
      DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer();

      byte[] imageByte = buffer.getData();
      byte msg[] = text;
      byte len[] = BinaryConversion.toByteArray(msg.length);

      LSB.encodeText(imageByte, len, 0);
      LSB.encodeText(imageByte, msg, 32);

      return image;
}

Rozmiar jest przedstawiony jako typ Integer. Do zapisu takiej zmiennej w języku Java używane są 4 bajty, czyli 32 bity. Przyjęte zostało, że w każdym bajcie danych (jednej składowej koloru) zostanie zmieniony tylko jeden najmniej znaczący bit, aby zminimalizować zakłócenia w obrazie. Oznacza to, że ukrycie 1 bajta danych wymaga modyfikacji 8 bajtów obrazu. W efekcie do ukrycia liczby na 32 bitach, użyte zostaną 32 bajty, czyli pierwsze 32 elementy z tablicy reprezentującej obraz.

Pierwsze 32 bajty danych obrazu będą zawsze wykorzystane na osadzenie powyższej informacji. W kolejnych bajtach zawarta zostanie już konkretna wiadomość.

Zmiana obrazu w tablicę bitów

Zmiana obrazu w tablicę bitów

Zamiana liczby typu int na tablicę bajtów.

public class BinaryConversion {
     public static byte[] toByteArray(int length) {
           byte byte3 = (byte)((length & 0xFF000000) >>> 24);
           byte byte2 = (byte)((length & 0x00FF0000) >>> 16);
           byte byte1 = (byte)((length & 0x0000FF00) >>> 8 );
           byte byte0 = (byte)((length & 0x000000FF) );
           return(new byte[]{byte3,byte2,byte1,byte0});
     }
}

W powyższym kodzie liczba całkowita zostaje zamieniona na tablicę czterech bajtów, gdzie każdy bajt jest przedstawiony na 8 bitach. Wykorzystany został operator >>>, który w języku Java służy do przesuwania nieoznaczonych bitów w prawo.

Im więcej najmniej znaczących bitów w danym bajcie zostanie przeznaczonych do zmiany, tym dłuższą wiadomość można ukryć. Wprowadzenie znacznych modyfikacji do nośnika danych powoduje jednak pogorszenie jego jakości, a im bardziej obraz odbiega od oryginału, tym zwiększa się zagrożenie odkrycia wiadomości przez postronnego użytkownika.

Steganogram w najprostszym przypadku może być ukryty sekwencyjnie w kolejnych próbkach sygnału lub w wersji zmodyfikowanej tylko w określonych próbkach według wybranego klucza. Mogą to być przykładowo piksele o dwóch dominujących kolorach w obrazie lub rozmieszczenie w indeksach pikseli o określonej zależności, na przykład w co czwartym pikselu.

Jeżeli bitowo zapisana tajna informacja jest krótsza niż nośnik danych, to znaczy pozostają próbki niezmodyfikowane, dobrą praktyką jest uzupełnienie wiadomości, aby zwiększyć bezpieczeństwo danych. W przeciwnym razie ukryte dane mogłyby zostać łatwo wykryte za pomocą analizy statystycznej sygnału.

Przykładowo za pomocą trzech najmniej znaczących bitów każdej składowej 24-bitowego obrazu o rozdzielczości 800×600 punktów, można ukryć 1,44 mln bitów danych.

Ukrywanie bitów za pomocą podstawowego algorytmu LSB

Ukrywanie bitów za pomocą podstawowego algorytmu LSB

W celu utrudnienia wykrycia tajnej wiadomości w aplikacji zmiana nie powinna nastąpić w kolejno występujących po sobie wartościach tablicy obrazu, ale co
określoną (najlepiej losową) liczbę elementów. Dla ulatwienia w przykładowym kodzie przeskok (ozanaczony jako jump)jest zależny od wielkości obrazu opakowującego oraz długości ukrywanej wiadomości. Dodatkowo przyjęte zostało, że zmianie ulega tylko jeden najmniej znaczący bit w danym bajcie.

Ukrywanie wiadomości.

public class LSB {
      public static byte[] encodeText(byte[] image, byte[] addition, int offset) {
             if(addition.length + offset > image.length){
                  throw new IllegalArgumentException("File_not_long_enough!");
              }
       int jump =1;
       if(offset>0){
              jump = (image.length - offset)/(addition.length*8);
       }
       for (int i=0; i < addition.length; i++) {
             int byteVal = addition[i];
             for (int j=7; j >= 0; j--) {
                  int bitVal = (byteVal >>> j) & 1;
                  image[offset] = (byte)((image[offset]&0xFE) | bitVal);
                  offset+=jump;
             }
      }
      return image;
    }
}

Funkcja encodeText jako argumenty przyjmuje tablicę bajtów obrazu, tablicę bajtów osadzanych danych oraz przesunięcie. Wywoływana jest dwukrotnie. Pierwszy raz podczas ukrywania długości wiadomości. Wtedy przesunięcie zawsze wynosi 0, a liczba ukrywana jest w kolejnych 32 bajtach obrazu. Taki zabieg powoduje, że algorytm deszyfrujący wie w jaki sposób odczytać ukrytą wartość. Kolejne wywołanie ma na celu ukrycie treści właściwej tajnego przekazu. Przesunięcie wynosi 32 bajty, aby ominąć przestrzeń z ukrytą długością przekazu. Wiadomość właściwa nie musi już być ukrywana w kolejnych bajtach. Obliczany jest przeskok, nazwany w programie jako jump.

jump = (image.lengthoffset)/(addition.length ∗ 8);

Pozostała liczba bajtów obrazu, w których można ukryć dane, jest dzielona na ilość bitów tajnej informacji. Należy pamiętać, że przyjęta została zasada, że w jednym bajcie obrazu ukryty zostanie jeden bit wiadomości. Dzięki dodaniu przeskoku, wiadomość jest ukryta na obszarze całego pliku, a nie tylko jego początkowym fragmencie. Utrudni to wykrycie faktu zastosowania steganografii.

Zewnętrzna pętla for iteruje po każdym bajcie wiadomości przeznaczonej do ukrycia. Pętla wewnętrzna iteruje po kolejnych ukrywanych bitach wiadomości przy pomocy operatora >>>. W wyniku przesunięcia w prawo najbardziej znaczące bity ukrywanych danych stają się bitami najmniej znaczącymi i mogą być wstawione w miejsce najmłodszych bitów obrazu stanowiącego kontener. Zapis (byte)((image[offset] & 0xF E) | bitVal) przedstawia dany bajt obrazu jako reprezentację bitową a operacja alternatywy z zatajanym bitem pozwala na zmianę najmłodszego bitu na ukrywaną wartość.

Po lewej zdjęcie wejściowe, po prawej zdjęcie z ukrytym obrazem pizzy

Po lewej zdjęcie wejściowe, po prawej zdjęcie z ukrytym obrazem
pizzy

Grafika osadzona w powyższym zdjęciu

Grafika osadzona w powyższym zdjęciu

W powyższym przykładzie jako nośnik danych został użyty obraz PNG o bardzo dużym rozmiarze 5152×3888 pikseli. W tak dużym kontenerze bez problemu można ukryć inny plik – użyte zostało zdjęcie pizzy o wymiarach 1155×684 pikseli. Róźnica między oryginalnym zdjęciem a przetworzonym nie jest widoczna gołym okiem.

Największą wadą metody LSB jest to, że zazwyczaj łatwo wykryć ją przez programy służące do stegoanalizy. Każde przekształcenie nośnika danych powoduje też utratę zapisanych informacji.

Poniżej znajdują się porównanie dwóch wersji ukrytej grafiki z pizzą programem do wykonywania steganoanalizy – Steganabara. Analizie zostały poddane dwa pliki z ukrytą wiadomością. W pierwszym obraz został osadzony tradycyjnym algorytmem LSB, który umieszcza dane w kolejnych bajtach. W drugim użyty został algorytm zmodyfikowany z programu St3g0, używający przeskoku i rozmieszczenia danych w całym nośniku.

Na plikach został zastosowany filtr bitowych masek dla najmniej znaczących bitów każdego kanału koloru. Filtr analizuje warstwy bitowe, oddziela je i łączy w całość. Przy zastosowaniu tej metody dla obrazu z ukrytą wiadomością dla kolejnych składowych bez skoku w algorytmie, wyraźnie widać oddzieloną poziomą linię końca ukrytego obrazu. W przypadku z rozłożeniem ukrywanych bitów na cały plik wynik jest zbliżony do analizy obrazu bez ukrytej wiadomości

naliza filtrem masek bitowyc

analiza filtrem masek bitowych