Prosty CAPTCHA cracker
Wczoraj po przeczytaniu na slashdocie o złamanej CAPTCHA z GMail, pomyślałem że warto by sobie napisać jakąś prostą klasę, która by mi umożliwiła w miarę bezbolesne łamanie tej przeszkody. Po paru godzinach rozmyślań w końcu dziś siadłem i napisałem takie coś - klasa PHP która potrafi dopasować dany obrazek do wzorca i rozpoznać - mniej więcej - jaka to litera. Ale pomyślałem też - captcha captchy nierówna, stąd trzeba zadbać o należytą elastyczność. Aby stworzyć dobry program rozpoznający pismo, niezbędne są przynajmniej trzy elementy: rozpoznawanie liter, dzielenie obrazka na pojedyncze litery i "uczenie się" nowych wzorców. Klasa którą tu zaprezentuję potrafi właśnie rozpoznawać literę, dopasowując ją do wcześniej ustalonego wzorca. Oczywiście strona X może używać takiej formy (czcionka, szumy etc) a strona Y zupełnie innej. Dlatego też dla każdej strony trzeba mieć przygotowane wzorce. W dalszej części zaprezentuję samą klasę, sposób użycia oraz najgłupszy wzorzec: wielkie litery, bez udziwnień, bez szumów.
Etap pierwszy: budowanie wzorca.
Najpierw zaprezentuję ogólną budowę wzorca i zasady jakie nim kierują. Otóż typowy wzorzec wygląda następująco:
$config['A'] = array (
array(0, 100, true), # p. 0, 100 nie moze byc pusty
array(100, 100, true),
array(50, 0, true),
array(0, 0, false), # p. 0, 0 musi byc pusty
array(100, 0, false) # p. 100, 0 musi byc pusty
);
I to tyle. Tak wygląda rozpoznawanie literki "A" w najbanalniejszej formie. Po pierwsze należy wiedzieć że liczby w nawiasie oznaczają kolejno X oraz Y, przy czym są to wartości w procentach (50, 50 - środek obrazka), natomiast trzeci parametr decyduje czy dany obszar ma być pełny (tzn. jest tam coś poza tłem) czy też pusty (samo tło). Jeśli dany obszar nie jest zdefiniowany, wtedy jest ignorowany, cokolwiek by tam było. Można tu zauważyć, że tak naprawdę mamy zaznaczone tylko kilka punktów - po prostu wzorzec musi definiować który obszar NA PEWNO ma mieć COŚ, a który obszar NA PEWNO ma NIE MIEĆ. Zamiast więc skakać po pikselu przez cały obrazek - idziemy na skróty.
Na tym etapie dodam, że kolejność w jakiej umieścimy wzorce jest niezwykle istotna. Otóż porównajmy sobie litery C oraz O. Czym się z grubsza różnią? Oczywiście C z jednej strony jest otwarte. Jak zatem można zdefiniować C? Może jako zbiór punktów (X, Y): (50, 0), (0, 50), (50, 100). To nam daje trzy punkty i C zostanie wspaniale zaliczone, tak jak chcieliśmy. Niestety, O również! Dlatego umieszczając wzorzec O przed wzorcem C, dołączając tam również (100, 50), rozwiązujemy problem. W ten sposób C przejdzie dalej (do własnego wzorca C) natomiast O od razu wpadnie do pierwszego z brzegu. Należy to wziąć pod uwagę - dana litera jest zaliczana do danego wzorca "przy pierwszej okazji". Można też zrobić odwrotnie: dać sekcję C przed O, jednakże dołączając instrukcję (100, 50, false). Wtedy C tam wpadnie (bo (100, 50) czyli prawa strona-środek jest pusta w wypadku C. Natomiast O przejdzie do następnego wzorca - O ma "coś" w tym miejscu. Daje nam to możliwość (brzydkiego, denerwującego, ale jednak) tworzenia prostych instrukcji warunkowych. Przypomnę również że obrazek jest "skalowany" - zostaje tylko prostokąt który przylega do boków litery.
Etap drugi: CaptchFire
(bo tak nazwałem to cudeńko, a co! :P)
Pisanie samej klasy zajęło mi może godzinę. Do tego kilka godzin myślenia i kilka godzin tworzenia przykładowego wzorca. Od razu mówię - kod brzydki, nieoptymalny; za to działa.
Zaczynamy standardowo:
include ('cf.php');
$config = array();
$config['A'] = array (
array(0, 100, true), # p. 0, 100 nie moze byc pusty
array(100, 100, true),
array(50, 0, true),
array(0, 0, false), # p. 0, 0 musi byc pusty
array(100, 0, false), # p. 100, 0 musi byc pusty
);
/* reszta wzorców, co tylko chcemy */
Powyższe było chyba oczywiste, dajmy mówić kodowi ;)
$cf = CF::getInstance();
$cf->Load($config);
$img = imagecreatefrompng('costam.png');
$result = $cf->Test($img, array('red' => 255,
'green' => 255,
'blue' => 255));
Kolejno: bierzemy uchwyt klasy, "uczymy" ją naszej matrycy, tworzymy uchwyt obrazka (ma być jedna litera!) i na koniec sprawdzamy czy klasa coś znajdzie. Pierwszy parametr metody Test() jest oczywisty, drugi oznacza kolor tła jakie występuje na danej CAPTCHY - w formacie RGB - klasa sama znajdzie najbliższy odpowiednik. Przedstawiam wersję roboczą, jak ktoś będzie to używał to niech sobie napisze rozpoznawanie tła - proste. W tej chwili $result zawiera wynik działania funkcji. Będzie to albo false gdy rozpoznawanie zawiodło, albo indeks wzorca jakiego używano (czyli np. A, W, X, whatever).
To oczywiście jest tylko prototyp, w żadnym razie nie jest to "mądre" rozpoznawanie pisma, a tym bardziej się (jeszcze) nie nadaje do łamania poważnej CAPTCHY. Należałoby przede wszystkim uwzględnić szumy tła, uśrednić kolory a całość opakować w program dzielący obrazek z literkami na pojedyncze litery oraz program uczący - tworzenie matryc na podstawie obrazków (program pyta o obrazek, mówisz jaka litera - i program ma za zadanie znalezienie optymalnego wzorca. Takie coś postaram się napisać następnym razem). Co do rozpoznawania obrazu - skrypt rozróżnia tylko "tło" oraz "reszta". Na podstawie "reszty" program sprawdza jakie punkty odpowiadają wzorcowi.
Na zakończenie kilka bonusów:
- cf.zip - klasa+przykład wzorca+literki
Prosty skrypt generujący pliki graficzne:
$hash = 'ABCDEFGHIJKLMNOPQRSTUWVXYZ';
for ($i=0; $i>strlen($hash); $i++)
{
$img = imagecreatetruecolor(15, 15);
$black = imagecolorallocate($img, 0, 0, 0);
$white = imagecolorallocate($img, 255, 255, 255);
imagefill($img, 0, 0, $white);
imagestring($img, 2, 0, 0, $hash[$i], $black);
imagepng($img, 'gfx/'.$hash[$i].'.png');
}
And then the Mighty Viagra Ad made the internet explode...
aha