Incepcja bitów w praktyce

DevDoBlog istnieje już trzy tygodnie, a jeszcze nie pojawił się na nim wpis zawierający kod. Nadszedł czas, aby to zmienić. W moim poprzednim artykule poruszałem tematykę steganologii w technologiach informatycznych. Na końcu tekstu obiecałem omówienie skryptu, który realizuje operację zawierania ukrytej informacji w danych innego typu.

Dlaczego akurat Python

Skrypt będący przedmiotem niniejszego wpisu został przygotowany w języku Python3. Technologia została wybrana z wielu powodów, a najważniejszymi z nich są ekspresja pisanego kodu oraz prostota składni. Dzięki łatwemu dostępowi do wielu dobrych bibliotek, wiele złożonych operacji można było wykonać w pojedynczych liniach kodu.

Składnia Pythona jest łatwiejsza od wielu innych języków programowania i w dużej mierze przypomina język ludzki. Spora w tym zasługa braku typowania zmiennych – dzięki czemu źródła zyskują przejrzystość, naturalność wyrażenia algorytmów i redukcję objętości. Pisanie w Pythonie wymusza też poprawne formatowanie kodu, co wpływa pozytywnie na jego odbiór i klarowność.

Podstawowa algebra Boole’a

Zapisanie tajnej wiadomości w danych w taki sposób, aby przekaz był niezauważalny wymaga od nas wykorzystania nadmiarowości informacji. Wymaganie to oznacza, że będziemy modyfikowali pojedyncze bity w bajtach. Bajt składa się z 8 bitów. Aby tego dokonać będziemy musieli użyć algebry Boole’a do sprawdzania stanu bitów oraz ewentualnego – w razie potrzeby – modyfikowania tego stanu.

https://gist.github.com/devdo-eu/52d87435395db0d3922a5644d42d7b93

Na listingu widoczne są definicje trzech funkcji:
setBit – będziemy używać do zmiany stanu 0 na stan wybranego bitu w bajcie.
clearBit – zapewnia nam funkcję odwrotną. Zamienia stan 1 na 0.
testBit – pozwala na określenie w jakim stanie jest sprawdzany bit.

Spłaszczanie – czyli redukcja wymiarów

Funkcje bitowe będą nam bardzo potrzebne ale zostawmy je na chwilę. Przeskoczmy w kodzie do miejsca, gdzie wczytujemy dane służące nam za nośnik do pamięci komputera.

https://gist.github.com/devdo-eu/fbe4387df96cd7ab57b129b19d76ca59

Za sprawą biblioteki PIL w pierwszej linii wczytujemy obraz z pliku picfile do zmiennej imgIn.
W kolejnej linii za pomocą biblioteki numpy (zwyczajowo skróconej do np) modyfikujemy reprezentację danych w zmiennej imgIn będącą obrazem do postaci tablicy wielowymiarowej, którą będziemy przechowywać w imgArray. Tak utworzona tablica zawsze składa się z trzech wymiarów. Wielkość pierwszych dwóch jest zależna bezpośrednio od rozdzielczości graficznego pliku wejściowego. Ostatni wymiar określa ilość kanałów, w połączeniu których jest wyrażana barwa pojedynczego piksela. Ilość kanałów może się różnić w zależności od rodzaju pliku i zastosowanych algorytmów kompresji.

W linii trzeciej zapisujemy kształt tablicy, czyli wielkości jej kolejnych wymiarów, do zmiennej shape w celu późniejszego odzyskania kształtu pierwotnego danych.
W ostatniej linii następuje spłaszczenie wielowymiarowej tablicy do postaci jednowymiarowej – wektorowej. W ten sposób z obrazu, który w postaci tablicy ma kształt [1024, 768, 3], uzyskujemy wektor postaci [2359296].

Co nam to daje? Postać wektorowa jest o wiele prostsza w dalszym przetwarzaniu – wystarczy jedna pętla, aby „przejść” przez wszystkie dane. Wcześniej do tego samego potrzebowalibyśmy aż trzech pętli. Kwestią do zastanowienia byłby również sposób w jaki umieszczalibyśmy dane w kolejnych kanałach.

Kodowanie informacji w nośniku

Posiadając napisane i sprawne funkcje do operacji na bitach oraz funkcję, która zamieni nam wielowymiarową tablicę na wektor możemy przejść do dalszej pracy. Napiszmy fragment programu umożliwiający nam zapisanie tajnej wiadomości w danych.

https://gist.github.com/devdo-eu/c80a14655bcc08c903505e2664bc885f

Funkcja encryptFlat przyjmuje dwa parametry. Pierwszy z nich: channels  to nasz wektor danych nośnika. Drugi, nazwany information przyjmuje wektor przechowujący informację w postaci znakowej. Na początku deklaracji ciała funkcji są inicjalizowane dwa liczniki: bits oraz chars. Pierwszy z nich przechowuje informację o numerze bitu który aktualnie zapisujemy w nośniku, a drugi – numer znaku tajnej wiadomości do którego należy kodowany bit.

Źródło posiada kilka ciekawych momentów:
-> zmienna byte w pętli for jest nadmiarowa, nieużywana i może zostać usunięta z kodu. Jest to pozostałość po wcześniejszych koncepcjach na rozwiązanie. Zjawisko częste w pracy programisty 😉
-> w liniach 6 i 8, zmieniając ostatnią stałą 0 na inną całkowitą wartość mniejszą od 9, można sprawdzić jak dużej modyfikacji ulegnie nośnik. Stała decyduje który bit w bajcie będzie nadpisywany treścią tajnej wiadomości
-> funkcja zwraca po zakończeniu swojej pracy wartości liczników bits i chars. Do czego może być nam to potrzebne? Jeżeli w przyszłości będziemy chcieli, aby program kodował dużą paczkę tajnych danych w wielu nośnikach – zwrócenie wartości tych liczników pozwoli nam rozpocząć kodowanie w kolejnym nośniku z miejsca w którym skończyliśmy.

Zachęcam do eksperymentów we własnym zakresie nad tym źródłem. Ciekawym, moim zdaniem, rozwinięciem może być dodanie jakiegoś rodzaju szyfrowania do information przed rozpoczęciem procesu zapisywania w nośniku.

Odzyskiwanie informacji z nośnika

Program zaczyna nabierać funkcjonalności, a praca daje pierwsze widoczne efekty. Możemy już umieścić informację w obrazie. Nie mamy jednak napisanego kodu, który umożliwi jej odczytanie. Na tą chwilę program pozwala przygotowywać nam materiały, do wykorzystania w procesie analizy czy ataku pasywnego. Mamy możliwość utworzenia plików zawierających różne ilości ukrytych danych, a następnie porównania oryginału z artefaktami wzbogaconymi ukrytymi danymi.

Napiszmy funkcję do wyłuskiwania utajnionych danych.

https://gist.github.com/devdo-eu/2f6a26cd0f17bfbac275b5e34a49cc4d

Funkcja decryptFlat przyjmuje tylko jeden parametr channels który przechowuje wektor kontenera, naszego wzbogaconego obrazka. Liczniki bits chars wracając ponownie, ponieważ w procesie odczytywania zatajonej wiadomości również będą potrzebne ich role. Zmienna output przechowuje odczytane dane w formie ciągu znakowego, a byteList w postaci kolejnych wartości bajtów. Pierwsza zmienna wykorzystywana jest tylko do określenia granicy do której czytane bity zawierają wprowadzoną transmisję. Granice te oznakowane są za pomocą zmiennych openEmbbed closeEmbbed.

W przypadku informacji będącej tekstem, ograniczenie odzyskiwania nie ma dużego znaczenia, ponieważ osoba odczytująca wiadomość bez trudu zorientuje się gdzie kończy się transmisja. Sprawa wygląda jednak zupełnie inaczej w przypadku danych, które muszą zostać zinterpretowane przez program pośredni nim odbiorca wiadomości będzie mógł ją odczytać. Taka sytuacja wystąpi gdy wiadomość ukryta będzie obrazem, dźwiękiem lub aplikacją.

Skazy w ciele funkcji dekodującej

Funkcja w aktualnej formie pozostawia szerokie pole do optymalizacji, daleko jej do doskonałości i zawiera trochę kodu pozostałego z poprzednich pomysłów na rozwiązanie problemu. Zmienna output służy tylko do znalezienia granicy wprowadzonych danych – jest jednak nadmiarowa, ponieważ te same dane, tylko w innej formie są przechowywane w byteList. Usunięcie ze źródła zmiennej output jest dobrym pomysłem na pierwsze modyfikacje tego fragmentu.

Najgorszą linią kodu pod względem obciążenia obliczeniowego jest linia 13 zawierająca instrukcję warunkową if wyszukującą fragment tekstu w tekście. Po dokładniejszej obserwacji, można dostrzec, że operacja jest powtarzana po dodaniu każdego znaku do zmiennych wynikowych. Ten fragment również jest doskonałym miejscem aby rozpocząć refaktoryzację – do czego zachęcam.

Europejska strona kodowa

Samo odczytanie z nośnika, czy zapisanie informacji do nośnika wystarczy nam na jakiś czas. Chwilę potem będziemy chcieli zapisać wyniki tych operacji do plików wynikowych. Napiszmy fragment kodu, który zrobi to dla nas.

https://gist.github.com/devdo-eu/c7cbe19c32439267861191a52538d97b

Gotowe! Skrypt ma za zadanie umieszczać wiadomości tekstowe w języku polskim w plikach graficznych i z tego względu, niezbędna jest linia 18, w której tekst jest dekodowany do postaci bajtowej za pomocą strony kodowej cp1250.

Z powodu braku (jeszcze!) implementacji funkcjonalności umożliwiającej ukrycie paczki danych o znacznej wielkości w wielu plikach-kontenerach, potrzebne jest zabezpieczenie w liniach 20-24. Sprawdza ono czy ilość danych, które zamierzamy ukryć jest mniejsza niż pojemność wybranego pliku-nośnika.

Ciekawsze fragmenty w tym źródle to operacje w liniach 26-29. Za pomocą zmiennej shape w której zapamiętaliśmy pierwotny kształt tablicy imgArray, konwertujemy ją do postaci wielowymiarowej, potem ta tablica konwertowana jest na grafikę, a następnie zapisywana do pliku „encrypted.png”.

Widoki na przyszłość

Skrypt w aktualnej formie spełnia swoje zadanie: umożliwia ukrycie tekstu w danych będących plikiem graficznym. Jak można rozwinąć ten projekt?
-> na dobry początek umożliwić schowanie danych binarnych
-> dodać szyfrowanie danych oraz sumę kontrolną, która określi spójność tych danych po wyłuskaniu
-> funkcjonalność sprawdzająca parametry statystyczne nośnika do określenia jak bardzo widoczna jest nasza transmisja

Następnie można pokusić się o dodanie:
-> pakowania danych w celu zmniejszenia objętości
-> umożliwić wybranie innego nośnika niż plik graficzny
-> dodania innych niż LSB, bardziej wyszukanych metod zawierania tajnych danych w nośniku
-> napisać moduł rozpraszający dane po nośniku
-> umożliwienia zawarcia jednej transmisji w wielu nośnikach. Będzie się to prawdopodobnie wiązało z zaprojektowaniem nagłówka, który pomoże w określeniu kolejności w jakiej trzeba dekodować nośniki

Repozytorium zawierające opisywany projekt jest dostępne na GitHub. Dodatkowo, zamieszczam źródła w formie, która została przedstawiona w artykule.

Dodaj komentarz