Czasy, w których za pomocą jednego formularza można było zarejestrować stronę we wszystkich katalogach i wyszukiwarkach bezpowrotnie przeminęły. Wszystkiemu winne są niepozorne obrazki zabezpieczające, które zmuszają użytkownika do przepisania ukrytych w nich haseł.
Podczas wysyłania SMS-ów z bramki internetowej, czy przy sprawdzaniu wolnych domen trzeba przepisać kod z grafiki do pola formularza. Powodem tego są nadużycia, które pojawiły się wraz z popularnością bezpłatnych usług.
Przykładem jest bramka jednego z rodzimych operatorów sieci komórkowych. Umożliwiała ona bezpłatne wysyłanie wiadomości SMS na telefony komórkowe z poziomu strony internetowej operatora. Usługa bardzo atrakcyjna. Nie dość, że SMS-y z Internetu są bezpłatne, to (ze względu na klawiaturę komputerową) znacznie wygodniej i szybciej się je pisze. Wysłanie SMS-a sprowadza się do wpisania własnego numeru, treści wiadomości i wciśnięcia przycisku Wyślij.
Taki mechanizm był bardzo wygodny, lecz stwarzał pole do nadużyć. Bez większej znajomości któregoś z języków skryptowych (choćby PHP) można było automatycznie, czyli bez ingerencji człowieka, rozsyłać wiadomości. W ten sposób zrodziła się grupa spamerów, którzy na bezpłatnych bramkach SMS chcieli zarobić. Sieci komórkowe chcą natomiast wypracowywać zysk, więc nie na rękę był im bezpłatny łącznik między Internetem a komórką.
Wprowadzono zatem obrazki zabezpieczające (ang. security images), które blokują roboty-automaty. Z puli kodów-haseł losowo wybierany jest jeden i umieszczany na grafice. Tak przygotowany obrazek trafia na stronę, by zabezpieczyć ją przed automatami.
Człowiekowi przepisanie kodu z obrazka do dodatkowego pola nie sprawia żadnego problemu. Przerasta to jednak możliwości komputera, ponieważ obraz jest dla niego wyłącznie zbiorem różnokolorowych pikseli. Oczywiście możliwe jest wyodrębnienie z obrazu tekstu przy użyciu programów typu OCR (rozpoznającego tekst z obrazu), ale tylko wówczas, gdy różnica kontrastu pomiędzy hasłem a tłem będzie duża różnica. Gdy ta różnica jest niewielka, OCR twierdzi, że strona jest pusta (nie zawiera żadnego tekstu).
Co będzie potrzebne?
Aby stworzyć system do zabezpieczania usług przy użyciu obrazów, wykorzystamy narzędzia ogólnie dostępne na serwerze. Będzie potrzebna obsługa języka PHP z zainstalowaną biblioteką GD (+FreeType) oraz dostęp do MySQL. Nie ma specjalnych wymagań co do wersji biblioteki GD – najlepiej >2.0. Będziemy korzystać z formatu PNG, lecz można go zastąpić GIF-em. Baza danych MySQL będzie nam potrzebna do przechowywania haseł, które trafią później na obrazki. Stworzymy w niej tylko jedną tabelę. Będzie składała się z 5000 rekordów, czyli 5000 różnych kodów, a jej rozmiar to ok. 0,5 MB.
Jak to działa?
Napiszemy trzy skrypty: instalacyjny – pozwoli na stworzenie odpowiedniej tabeli i wygenerowanie haseł do obrazków; graficzny – na poczekaniu wygeneruje odpowiedni obrazek; sterujący – będzie zawierał formularz z naszym obrazkiem zabezpieczającym. Jeżeli do formularza zostanie poprawnie wpisane hasło, skrypt wyświetli podziękowanie. Jeżeli źle, wyświetli komunikat o błędnym haśle.
Baza danych
Rozpoczynamy od stworzenia tabeli do przechowywania wszystkich haseł. Będzie ona składała się z dwóch pól:
- \”hash\”, pole – główny klucz tabeli, typu varchar, 32 znaki, jest to hasło zakodowane algorytmem szyfrującym MD5,
- \”code\”, pole – unikalne, typu varchar, 6 znaków, jest to hasło, które ukaże się na obrazku.
W postaci SQL, kod potrzebny do utworzenia tabeli będzie wyglądał następująco:
CREATE TABLE mi_secimg (
hash VARCHAR(32) NOT NULL,
code VARCHAR(6) NOT NULL,
PRIMARY KEY (hash),
UNIQUE (code)
);
Założyliśmy, że hasło na obrazku będzie składało się z 6 znaków. Tę liczbę można oczywiście rozszerzyć. Zakodowane hasło jest potrzebne ponieważ mamy dwa skrypty: sterujący i generujący obrazek. Skrypt generujący obrazek musi wiedzieć, które hasło ma umieścić na obrazku, a skrypt sterujący musi wiedzieć, czy wpisany przez użytkownika kod jest prawidłowy. W tym celu potrzebna jest baza danych. Jeżeli nie chcemy używać bazy, bo nie mamy do niej dostępu, można zastąpić ją bazą danych opartą o pliki tekstowe. To jednak mniej wygodne rozwiązanie, dlatego w artykule korzystamy z bazy danych.
Pierwsza linia może nieco przytłaczać tych, którzy mają mało do czynienia z wyrażeniami regularnymi. Funkcja {stala}ereg(){/stala} zwraca prawdę, jeżeli element tablicy {stala}$_POST[\’hash\’]{/stala} spełnia szablon {stala}\”[a-z0-9]{32}\”{/stala}. Oznacza to, że ten element tablicy ma się składać z dokładnie 32 znaków. Każdy z tych znaków może przyjąć wartość od a do z i od 0 do 9. Małpa przed całą funkcją jest po to, by funkcja nie zwracała błędu. Ma to miejsce wtedy, gdy wprowadzamy dane do formularza. Wtedy żadne dane nie znajdują się w zmiennej {stala}$_POST[\’hash\’]{/stala} i {stala}ereg(){/stala} zwracałoby błąd.
Instrukcja warunkowa {stala}if{/stala} sprawdza czy kody są zgodne ze sobą. Jeżeli nie, to wyświetla błąd i prosi użytkownika o ponowne wpisanie kodu. W takim przypadku skrypt powinien \”pamiętać\” adres URL, który użytkownik wprowadził. Ten niestety nie ma na tyle rozbudowanej obsługi błędów. Jeżeli oba kody są ze sobą zgodne, skrypt wyświetla podziękowania. W tym momencie należałoby również wykonać czynności związane z dodawaniem strony do katalogu. Najprawdopodobniej podany przez użytkownika adres zostałby zapisany do jakiejś bazy danych, by mógł się nim zająć redaktor odpowiedzialny za dodanie strony do wyszukiwarki.
Generowanie obrazka
W tej chwili skrypt image.php jest jak czarna skrzynka. Wiemy tylko, że tworzy obraz na podstawie hashu dostarczanego w postaci parametru.
Skrypt powinien:
- połączyć się z bazą danych,
- sprawdzić, czy został podany jako parametr hash,
- pobrać hasło z bazy danych, mając do dyspozycji hash,
- sprawdzić, czy hasło jest poprawnej długości (ktoś mógł spróbować podać nieprawidłowy hash),
- rozpocząć tworzenie obrazka poprzez wstawienie tła,
- utworzyć napis na obrazku, czyli nasz kod,
- wyświetlić gotowy obrazek użytkownikowi.
Konfiguracja skryptu:
$passLenght = 6;
$background = \'bg.png\';
$fontLoc = \'font.ttf\';
Ponieważ ten skrypt w rzeczywistości nie wie, jakiej długości jest nasze hasło ({stala}$passLenght{/stala}), jeżeli dokonaliśmy zmiany, należy ją tu uwzględnić. Tworzenie obrazka odbywa się poprzez nałożenie na tło tekstu, czyli naszego hasła (tło to plik bg.png). Może to być dowolny plik graficzny o dowolnych rozmiarach. Musimy wybrać czcionkę z której będziemy korzystać. Może to być dowolna czcionka TTF (w Windows w katalogu {stala}c:/Windows/Fonts{/stala}).
$c = mysql_connect(\'localhost\', \'user\', \'haslo\');
mysql_select_db(\'nazwa_bazy\', $c);
if(!@ereg(\"[a-z0-9]{32}\", $_GET[\'hash\']))
exit();
$password = mysql_result(mysql_query(\'SELECT code FROM mi_secimg WHERE hash=\"\'.$_GET[\'hash\'].\'\"\'), 0);
if(strlen($password) != $passLenght)
exit();
W pierwszych liniach łączymy się z bazą i wykonujemy dokładnie takie samo sprawdzenie przy użyciu funkcji {stala}ereg(){/stala}, jak w skrypcie sterującym. Tym razem całość negujemy. Jeżeli więc szablon nie został spełniony, skrypt kończy definitywnie swoje działanie. Taka sytuacja nie może mieć miejsca. Ze względów bezpieczeństwa trzeba jednak sprawdzać wszystkie dane wejściowe.
Do zmiennej {stala}$password{/stala} trafia kod pobrany na podstawie hasha otrzymanego jako parametr skryptu. Sprawdzamy, czy ilość znaków w pobranym z bazy danych haśle zgadza się z ilością znaków, które powinno mieć hasło. Gdyby te liczby się nie zgadzały, oznaczałoby to, że z bazy zostało pobrane niepoprawne hasło. Na wszelki więc wypadek, gdyby hasła się nie zgadzały, należy zakończyć skrypt.
header(\'content-type: image/png\');
$i = imagecreatefrompng($background);
$color = imagecolorallocate($i, 255, 8, 8);
Wiemy już jak ma wyglądać tabela. Napiszemy więc skrypt instalacyjny, który ma za zadanie:
- połączyć się z bazą danych,
- utworzyć tabelę mi_secimg,
- wygenerować losowo kody 6-znakowe w ilości 5000 i ich zakodowane odpowiedniki,
- wprowadzić wygenerowane kody do bazy danych.
Skrypt rozpoczynamy od zmiennych odpowiedzialnych za konfigurację:
$codeLenght = 6;
$codeQuantity = 5000;
$characters = array(2,3,4,5,6,7,8,9,\'a\',\'b\',\'c\',\'d\',\'e\',\'f\',\'g\',\'h\',\'i\',\'j\',\'k\',\'m\',\'n\',\'o\',\'p\',\'q\',\'r\',\'s\',\'t\',\'u\',\'w\',\'x\',\'y\',\'z\',\'A\',\'B\',\'C\',\'D\',\'E\',\'F\',\'G\',\'H\',\'I\',\'J\',\'K\',\'L\',\'M\',\'N\',\'P\',\'Q\',\'R\',\'S\',\'T\',\'U\',\'W\',\'X\',\'Y\',\'Z\');
Ustaliliśmy, że nasze kody będą składały się z 6 znaków. Lecz równie dobrze mogą to być kody 4- czy 8-znakowe. Ilość znaków, z których będzie zawsze składał się kod, będziemy ustalać w zmiennej {stala}$codeLenght{/stala}.
Kolejny parametr to ilość kodów, które chcemy wygenerować ({stala}$codQuantity{/stala}). Będą one losowo wybierane z bazy i umieszczane na obrazkach. Może to być 5000 kodów, może to być również 1000 kodów. Więcej kodów oznacza, że będą się one rzadziej powtarzały. Jeżeli w tym miejscu umieścilibyśmy tylko 2 kody, istniałoby 50% szansy, że trafimy na poprawny kod. Do takiej sytuacji nie można dopuścić. Ilość kodów musi być na tyle duża, by nie można było przewidzieć, który kod pojawi się jako następny.
Kody, które zostaną wygenerowane przez skrypt, muszą składać się z określonych znaków. Im ich więcej, tym więcej istnieje permutacji 6-elementowego ciągu znaków, czyli im więcej różnych znaków może stanowić jeden element kodu, tym trudniej jest go odgadnąć. Łatwo zauważyć, że w tablicy brakuje cyfr 0 i 1 oraz liter O i I, ponieważ są one często mylone. Podsumowując, z elementów tablicy {stala}$characters{/stala} zostaną losowo wygenerowane hasła składające się z 6 znaków w ilości 5000.
Połączymy się teraz z bazą danych i stworzymy nową tabelę:
$c = mysql_connect(\'localhost\', \'user\', \'haslo\');
mysql_select_db(\'nazwa_bazy\', $c);
mysql_query(\'CREATE TABLE mi_secimg (hash VARCHAR(32) NOT NULL, code VARCHAR(\'.$codeLenght.\') NOT NULL, PRIMARY KEY (hash), UNIQUE (code));\');
Długość zakodowanego hasła będzie zawsze równa 32 znakom. Hasło będzie składało się z tylu znaków, ile ustalimy w zmiennej {stala}$codeLenght{/stala}.
Przyszedł czas na odpowiednie pętle, które wygenerują kody i dodadzą je do bazy danych. Patrząc na poniższy listing, należy go czytać od najbardziej zagnieżdżonej pętli:
for($q=0; $q<$codeQuantity; $q++) {
$code = \'\";
for($p=0; $p<$codeLenght; $p++) {
$code.=$characters[rand(0, count($characters)-1)];
}
mysql_query(\'INSERT INTO mi_secimg VALUES (\"\'.md5($code).\'\", \"\'.$code.\'\")\');
}
echo \'ok!\';
W środkowej pętli \"for\" tworzy się hasło. Pętla zostanie wykonana tyleż razy, ile jest znaków w haśle. Za każdym razem zostanie wygenerowany w niej jeden znak. Za wygenerowanie losowego znaku odpowiedzialna jest funkcja {stala}rand(){/stala}. Wybiera ona losowo liczbę z przedziału 0 do (liczba znaków w tablicy) 1. W ten sposób wylosowany znak trafia do zmiennej {stala}$code{/stala}, która z każdą iteracją poszerza się o jeden znak, by osiągnąć wartość 6 znaków ({stala}$p{/stala} {html}<{/html} {stala}$codeLenght{/stala}).
Gdy kod zostanie wygenerowany, następuje dodanie go do bazy. Ponieważ w zmiennej {stala}$code{/stala} kryje się hasło, można łatwo je przekazać do zapytania, kodując drugie pole algorytmem MD5 przy użyciu funkcji {stala}md5(){/stala}. Duża pętla zatacza koło kolejny raz i resetuje hasło (zmienną {stala}$code{/stala}), aż do wyczerpania puli 5000 haseł.
Skrypt sterujący
Skrypt instalacyjny nie będzie już potrzebny. Zaraz po jego wykonaniu można go usunąć z serwera. Teraz przyszła pora na utworzenie skryptu, który będzie odpowiedzialny za pracę całego mechanizmu. Przykładowy skrypt będzie zabezpieczał usługę \"dodawania strony internetowej do katalogu\". Ze względu na oszczędność miejsca cały formularz będzie składał się jedynie z pola do wprowadzenia adresu strony oraz pola do przepisania hasła z obrazka.
Zadania skryptu:
- połączyć z bazą danych,
- wybrać losowo hasło, które znajdzie się na obrazku,
- wyświetlić formularz z obrazkiem wraz z ukrytym polem zawierającym zakodowane hasło,
- sprawdzić, czy formularz został wysłany (jeżeli tak - sprawdzić, czy hasło jest poprawne),
- wyświetlić podziękowanie i dodać stronę do kolejki oczekujących na dodanie lub wyświetlić informacje o błędnym haśle.
Zaczniemy od połączenia z bazą i utworzenia podstawowego kodu witryny:
Zabezpieczona obrazkiem usługa
Teraz stworzymy formularz, który będzie pozwalał na dodawanie nowych stron do katalogu witryny xyz. Będzie on składał się z pola do wpisywania adresu, obrazka, pola do wpisywania kodu z obrazka oraz ukrytego pola kryjącego hash, czyli zakodowane hasło.
W powyższym kodzie wykorzystujemy wielokrotnie hash, który jest zakodowanym hasłem z brakiem możliwości odzyskania. Pierwszą czynnością jest pobranie z bazy danych losowego rekordu. Odbywa się to poprzez uporządkowanie elementów tabeli {stala}mi_secimg{/stala} w sposób losowy, a następnie pobranie jednego rekordu. Wynikiem zapytania jest nasz losowy hash. W tym momencie nie wiemy oczywiście jak brzmi kod.
Hash przekazujemy do naszego obrazka. Skrypt o nazwie image.php odpowiada bowiem za generowanie obrazu. Jako parametr podajemy mu hash, czyli nasze zakodowane hasło. Gdybyśmy w tym miejscu przesłali rzeczywisty kod, nie mielibyśmy już do czynienia z zabezpieczeniem. I tu jest odpowiedź na pytanie, po co to zakodowane hasło.
Warto zauważyć, że nasz hash trafia również do ukrytego pola. Oznacza to, że po wysłaniu formularza nie stracimy tej zmiennej, będziemy mogli ją później odczytać z tablicy {stala}$_POST[\"hash\"]{/stala}. W ten sposób będziemy w stanie sprawdzić, czy hasło, które podał użytkownik, jest zgodne z hasłem, które jest przydzielone do tego hasha.
Żeby zamienić hash na normalne hasło, będziemy musieli wykonać jedno zapytanie. Pobierze ono rekord, który zawiera właśnie ten hash. Wtedy oba hasła zostaną porównane - to wprowadzone przez użytkownika i to pobrane z bazy danych. Zobaczymy, jak to prezentuje się w praktyce:
if(@ereg(\"[a-z0-9]{32}\", $_POST[\'hash\'])) {
$code = mysql_result(mysql_query(\'SELECT code FROM mi_secimg WHERE hash=\"\'.$_POST[\'hash\'].\'\"\'), 0);
if($code != $_POST[\'code\']) {
echo \'Kod jest niepoprawny! proszę spróbować ponownie\';
} else {
// [..]
echo \'dziękujemy, strona \'.$_POST[\'url\'].\' została dodana do naszego katalogu!\';
}
}
Rozpoczynamy tworzenie obrazka. To jest moment od którego używamy biblioteki GD do manipulacji grafiką. Ponieważ typowy dokument tworzony przez PHP jest plikiem tekstowym, musimy dać przeglądarce znać, że ma do czynienia nie z tekstem, a grafiką. Dlatego też zmieniamy w nagłówku typ danych na \"image/png\", co oznacza, że dane wyjściowe będą w formacie PNG. Analogicznie dla GIF-a typ danych miałby postać: \"image/gif\".
Tworzymy nowy obraz PNG na podstawie tła. Oznacza to, że nowo utworzony obraz będzie miał takie same wymiary, jak obraz bg.png. To zupełnie tak, jakbyśmy poddali obraz bg.png edycji, chcąc dodać do niego tekst. W odpowiedzi otrzymujemy uchwyt do obrazu - {stala}$i{/stala}. Nasz tekst musi mieć jakiś kolor. Musimy więc stworzyć uchwyt do takowego. Służy do tego funkcja {stala}imagecolorallocate(){/stala}, która potrzebuje czterech parametrów. Pierwszym jest uchwyt do pliku, a pozostałe trzy to wartości RGB koloru.
W każdym dostępnym edytorze graficznym można odczytać wartość RGB wybranego z palety koloru. Jeżeli więc chcemy zmienić kolor tekstu, należy wybrać kolor z palety dostępnych, a następnie odczytać poszczególne wartości RGB i podać tej funkcji jako parametry. W naszym przypadku wybrany kolor jest czerwony, aby miał mały kontrast w stosunku do tła.
$box = imagettfbbox (16, 3, $fontLoc, $password);
Obrazek-tło może mieć dowolny rozmiar. Oczywiście tak, lecz z jednym zastrzeżeniem. Obrazek nie może być mniejszy od czcionki. To oczywiste. A w jaki sposób umieścić na obrazku tekst dokładnie na środku? Standardowo tekst rozpoczynałby się do pierwszego piksela i byłby umieszczony w lewym górnym rogu. Rozwiązaniem tego problemu jest funkcja {stala}imagettfbbox(){/stala}, która dokonuje symulacji bloku tekstowego i zwraca współrzędne jego wierzchołków. Posiada 4 parametry wejściowe. Pierwszym jest rozmiar tekstu w pikselach (w naszym przypadku tekst ma wysokość 16 pikseli), drugim - kąt o który obrócony jest tekst. W naszym przypadku są to trzy stopnie.
W rezultacie tekst nie będzie umieszczony poziomo. Trzecim parametrem dla {stala}imagettfbbox(){/stala} jest lokalizacja pliku z czcionką, czyli naszego font.ttf. Na koniec do funkcji wędruje tekst, czyli nasze hasło. W rezultacie otrzymujemy tablicę ze sporą ilością danych na temat wymiarów tak utworzonego bloku tekstowego:
- {stala}$box[0]{/stala} - lewy dolny róg, oś X
- {stala}$box[1]{/stala} - lewy dolny róg, oś Y
- {stala}$box[2]{/stala} - prawy dolny róg, oś X
- {stala}$box[3]{/stala} - prawy dolny róg, oś Y
- {stala}$box[4]{/stala} - prawy górny róg, oś X
- {stala}$box[5]{/stala} - prawy górny róg, oś Y
- {stala}$box[6]{/stala} - lewy górny róg, oś X
- {stala}$box[7]{/stala} - lewy górny róg, oś Y
Mając te dane jesteśmy w stanie obliczyć miejsce, w którym powinien zaczynać się nasz kod, by był dokładnie na środku obrazka.
imagettftext($i, 16, 3, (imagesx($i)-$box[2]+$box[0])/2, (imagesy($i)-$box[7]+$box[1])/2, $color, $fontLoc, $password);
Funkcja {stala}imagettftext(){/stala} wprowadza właściwy tekst na obrazek. Pierwszy parametr to uchwyt obrazka, drugi to rozmiar czcionki wyrażony w pikselach, trzeci to kąt o który obrócony jest tekst, czwarty i piąty to numery pikseli, odpowiednio na osi x i y, od których ma się rozpocząć wyświetlanie tekstu.
Kolejne parametry to kolor tekstu, lokalizacja czcionki oraz tekst, który chcemy wyświetlić. Żeby obliczyć czwarty i piąty parametr, musimy uświadomić sobie jak obliczyć punkt, od którego powinien rozpocząć się tekst. Rysunek pokazuje jak łatwo jest wyliczyć jego koordynaty.
Pozostaje już tylko wyświetlić obrazek. W ten oto sposób stworzyliśmy mechanizm do zabezpieczania usług obrazkami.
Podsumowanie
W artykule została zaprezentowana skuteczna metoda na zabezpieczenie usług przed automatami-robotami. Zaprezentowany mechanizm ochrony jest w pełni skuteczny. Korzystają z niego duże portale, wyszukiwarki (np. Google) oraz operatorzy sieci komórkowych.