Autor: Karol Bieńkowski, 27 lutego 2001 Spis treści 1. Wstęp 2. Sposób instalacji/użycia 3. Zmiany w jądrze 4. Moduł szyfrujący 1. Wstęp Program składa się z dwóch zasadniczych, dość niezależnych od siebie części. Pierwsza to łata na jądro systemu umożliwiająca korzystanie z dynamicznego wołania funkcji, a druga to moduł wykorzystujący tę nową możliwość. Łata została umieszczona w podkatalogu kernel/, a moduł w podkatalogu modul/. Oprócz tego w podkatalogu testy/ znajduje się skrypt testujący te dwa mechanizmy. 2. Sposób instalacji/użycia W skrócie: - rozpakować do katalogu zad4 (lub innego :)) - cd zad4/kernel - ./zalataj - cd /usr/src/linux - make zImage, ..., lilo - reboot - cd zad4/modul - make - insmod sec_rw.o - teraz można testowac, np. - cd ../testy - ./testuj - rmmod sec_rw - cd zad4/kernel - ./odlataj Najpierw należy zaaplikować łatę do jądra. Jeżeli źródła jądra są w katalogu /usr/src/linux to wystarczy uruchomić skrypt zalataj z podkatalogu kernel/. Jeżeli źródła są gdzieś indziej to trzeba w skrypcie zmienić zmienną CEL. Potem trzeba skompilować jądro. Jeżeli było wcześniej skompilowane, to wystarczy uruchomić make (b)zImage, skopiować obraz jądra a na koncu lilo. Łata działa dla jąder 2.2.17 i 2.2.18. Następnie należy skompilować moduł uruchamiając make w katalogu modul/. Po przeładowaniu systemu można załadować moduł (insmod sec_rw.o) i wszystko powinno działać. Hasło zapisuje się do pliku /proc/haslo, np. echo haslo > /proc/haslo. Istniejące hasło można odczytać z /proc/haslo, np. cat /proc/haslo. UWAGA: Szyfrowanie nie zawsze działa. Nie wszystkie programy używają funkcji sys_read/sys_write do czytania/pisania do pliku. Robią tak cat/echo w mojej dystrybucji Linuksa (Slackware 7.0), ale podobno nie wszystkie. W Midnight Commanderze po naciśnięciu F4 plik jest kodowany, a po naciśnięciu F3 nie. F3 powoduje wywołanie mcedit z opcją -v, a wtedy plik jest mapowany do pamięci (wywołanie systemowe old_mmap) i omija szyfrowanie. (Żeby było bardziej skomplikowanie to mcedit -v dla dużych plików już używa sys_read...). To omijanie szyfrowania jest zgodne ze specyfikacją - pytałem o to autora zadania. Załączam odpowiedź P. Kozieradzkiego. 3. Zmiany w jądrze Dodałem do jądra trzy nowe pliki: - include/linux/simple_hash.h. W pliku simple_hash.h definuje makro CREATE_SIMPLE_HASH. Umieszczenie tego makra w pliku tworzy tablicę haszującą i funcje do jej obsługi (np. nazwa_hash_find, nazwa_hash_remove, gdzie nazwa jest parametrem makra). Taka tablica haszująca jest użyta w pliku dyn_call.c oraz w module (sec_rw.c), dlatego zamiast kodować kod napisałem makro. Korzystam z tablicy haszującej, gdyż zapewnia szybkie wyszukiwanie i łatwo można ją dostosowywać zmieniając jej długość. Założyłem, że w tablicach nie będzie dużo danych :), więc są krótkie (4-elementowa na zarejestrowane funkcje, a 16-elementowa na hasła użytkowników). Kolizje są rozwiązywane przez listy, zaimplementowane w pliku include/linux/list.h. Funkcja nazwa_hash_add() dodaje nowy element, ale gdy element już istnieje to usuwa starą wartość i wpisuje nową. Dlaczego używam kmalloc? Alokowane struktury są małe, więc to się opłaca. Jednostką przydziału dla vmalloc jest cała strona, dlatego używa się go tylko do dużych obszarów, większych od 2 lub 4kB. - include/linux/dyn_call.h - kernel/dyn_call.c Pliki zawierają deklaracje (dyn_call.h) oraz implementację interfejsu dynamicznego wołania funkcji. Oprócz funkcji wyspecyfikowanych w treści zadania (zad4.txt) dodałem dwie funkcje: dyn_call_init(), która musi być wywołana zanim zacznie się korzystać z dynamicznego wołania oraz isreg_dynfun(), która sprawdza czy funkcja o podanej nazwie jest zarejestrowana. Z isreg_dynfun() korzystam w sys_read()/sys_write(). Operacje wstawiania, wyszukiwania, usuwanie funkcji są realizowane za pomocą funkcji utworzonych przez makro CREATE_SIMPLE_HASH. Zmodyfikowałem następujące pliki: - main/inic.c Przy starcie systemu trzeba zainicjować tablicę haszującą funkcji. Właśnie tu to robię. - kernel/ksyms.c W tym pliku eksportuje się symbole używane przez moduły. Dodałem tu funkcje do obsługi dynamicznego wołania funkcji {call|reg|ureg}_dynfun. - fs/read_write.c Tutaj jest modyfikacja umożliwiająca szyfrowanie plików. Parametry i semantyka funkcji są inne niż w przykładzie (zad4.txt), ponieważ wersja z przykładu była niemożliwa do zaimplementowania. Przykład sugeruje, że z wnętrza dynamicznej funkcji (o nazwie sec_read) będzie wołana funkcja read systemu plików (odczytana z filp->f_op->read). Jednak system cały czas zasypia w funcji read (tej z f_op), robi to np. konsola. Po usunięciu modułu pierwsza próba skorzystania z konsoli spowoduje błąd: po wyjściu z funcji read nastąpi skok pod adres, gdzie wcześniej znajdował się moduł. No i segmentation fault. Można się przed tym zabezpieczyć umieszczając w module MOD_INC_USECOUNT przed read, a MOD_DEC_USECOUNT po read. Wtedy modułu nie da się usunąć. U mnie funkcje wołane dynamiczne mają za zadanie tylko zaszyfrować bufor. Przy czytaniu jest to proste - funkcja dostaje bufor przeczytany przez read i modyfikuje go przed przesłaniem go do użytkownika. W przypadku pisania jest gorzej: otrzymany bufor (argument sys_write) jest tylko do odczytu, więc trzeba zaalokować nowy. Musi to być bufor w przestrzeni adresowej użytkownika, bo takiego bufora oczekuje write. Robię to funkcją do_mmap. Uznałem, że alokowanie bufora lepiej umieścić w sys_write, a nie w module - wtedy funkcje szyfrujące (read_encrypt/write_encrypt) są lepiej wyspecyfikowane. Po prostu zmieniają bufor, więc łatwo tworzyć ich nowe wersje nie wnikając w problemy alokowanie pamięci. Żeby uniknąć alokowania pamięci i przygotowania parametrów w przypadku, gdy moduł szyfrujący nie jest załadowany sprawdzam na początku czy funkcje szyfrujące są zarejestrowane. 4. Moduł szyfrujący Po pierwsze zawiera interfejs do systemu /proc. Tworzy plik /proc/haslo do którego każdy użytkownik może zapisać swoje hasło i gdzie może je zobaczyć. Do przechowywania haseł jest używana tablica haszująca utworzona makrem CREATE_SIMPLE_HASH. Funcje do obsługi pliku /proc/haslo: - write_proc() Umożliwia zapisywanie hasła fragmentami od dowolnego miejsca. Implementacja nie jest wydajna (przy zapisie każdego kawałka na nowo alokuje się bufor), ale dobrze działa gdy hasło zapisuje się na raz. A poza tym wydajność pisania do /proca nie decyduje o wydajności systemu. Pamięć na hasło jest alokowana przez kmalloc, ponieważ P. Kozieradzki uznał, że ograniczenie długości hasła poniżej wielkości ramki nie wpłynie na ocenę. - read_proc() Czytanie z /proc/haslo, począwszy od file->f_pos. - open_proc(), close_proc() Wiadomo. Druga część modułu to funkcje do szyfrowania/deszyfrowania plików - to tak naprawdę ta sama funkcja rw_encrypt(), ale rejestrowana pod różnymi nazwami: read_encrypt i write_encrypt. Jest tylko jedna funkcja szyfrująca/deszyfrująca, bo zadany algorytm szyfrowania jest symetryczny, ale to nie jest reguła - interfejs modułu jest tak określony, że może być używany z szyfrowaniem niesymetrycznym. Przed szyfrowaniem oprócz sprawdzania właściciela (to było w zadaniu) sprawdzam czy szyfrowany plik jest zwykły (S_ISREG). Szyfrowanie innych plików: katalogów, linków, urządzeń, łączy nie bardzo ma sens. Nie działa wtedy np. konsola. Nie pozwalam, żeby były szyfrowane pliki roota. Gdyby nie to sprawdzenie to po wpisaniu przez roota hasła do /proc/haslo system przestałby działać - bibliteka glibc jest czytana sys_readem. W funkcji szyfrującej używam rzutowania z loff_t na unsigned long, bo bez tego pojawia się błąd przy insmodzie: undefined reference to __moddi3().