Spis treści:

Kategoria:BotyC#


Jak stworzyć własne CAPTCHA w C# .NET

Problemy ze spamerskimi wpisami

Internet jest z jednej strony piękny, z drugiej zaś, traktowany jest jak wielkie śmietnisko. Miejmy nadzieję, że tej stronie i temu wpisowi bliżej do dobrego niż do złego. Problem w Internecie polega głównie na tym, że każdy może tam coś dodać (patrząc na to z drugiej strony - może to być zaletą). Chyba każdy twórca strony, który pozwala innym użytkownikom dodawać komentarze, artykuły, opracowania lub innego rodzaju wpisy zetknął się z problemem śmiecenia. O ile śmiecenie przez ludzi jest jeszcze do opanowania przez administratora, o tyle walka ze śmieciarzami w postaci robotów (botów) internetowych jest walką nierówną. Śmiecący człowiek nie doda dziesięciu wpisów w ciągu kilku sekund głównie z jednego powodu - jest z reguły leniwy. Jeżeli nie mści się, to zwykle kończy się na głupim wpisie w ilości sztuk 1. Gdy się mści lub jest ekstremalnie zdenerwowany to nawrzuca pewną skończoną ilość głupich komentarzy - przejdzie mu. Dla jego zdrowia to dobre, administrator strony skończoną liczbą kliknięć te wpisy usunie i świat znów staje się piękny. Co z robotami? One nigdy nie śpią i nigdy się nie męczą. Są wstanie wygenerować setki lub tysiące wpisów. W takim przypadku zarządca strony zmuszony jest poszukać innego rozwiązania. Jednym z nich jest konieczność logowania się na stronę. Jest to rozwiązanie dobre, ale istnieje spora grupa ludzi, którzy tego bardzo nie lubią (w tym osoba pisząca te słowa). Jest jeszcze inne rozwiązanie i pewnie większość już się z nim spotkała. Są to tzw. obrazki weryfikacyjne lub jak się je zwykło nazywać CAPTCHA.

Dlaczego należy przepisywać kod z obrazka?

Obrazki weryfikacyjne to pewnego rodzaju test. Stosowany jest on w celu upewnienia się, że formularz nie jest przetwarzany przez komputer. Zasada działania polega na pewnej zasadzie: test ma być trudny, albo niemożliwy do rozwiązania przez komputer w rozsądnym czasie, natomiast wystarczająco prosty, aby średnio rozgarnięty użytkownik mógł go rozwiązać. Swoją drogą można zrobić test możliwy do rozwiązania tylko przez rozgarniętych użytkowników eliminując tych mało wartościowych, ale są to czysto teoretyczne rozważania i należy je traktować bardziej humorystycznie niż poważnie. W systemach niezabezpieczonych system działa mniej więcej tak: komputer-robot pobiera stronę, na jej podstawie generuje odpowiedź, którą odsyła do serwera, na którym ta strona została umieszczona. Serwer zakłada, że jest to spamerski wpis (przyjmuje każdy) i dodaje go jako właściwy. Co zmienia obrazek? A no to, że robot ma ogromne trudności z przeczytaniem tekstu na tym obrazku, natomiast człowiek zwykle robi to z łatwością. Na dzień dzisiejszy człowiek ma znacznie większe możliwości rozpoznawania takich obrazkowych form zapisu. Aby sobie to lepiej uzmysłowić zapytajmy - dlaczego do tej pory nie ma żadnej wyszukiwarki działającej w ten sposób, że wpisujemy lemur i wyskakują nam obrazki z lemurami? Ktoś powie - wyskakują! To prawda, ale nie dlatego, że jakiś system rozpoznał zawartość obrazka. System zbierający dane do wyszukiwarki pomógł sobie korzystając z nazwy obrazka, alternatywnego tekstu, zawartości strony na której jest obrazek i pewnie kilku innych czynników. Do tej pory programy dobrze rozpoznające tekst z książek są drogie, a i tak popełniają mnóstwo błędów. Działają doskonale, gdy czcionka jest regularna i prosta. Gdy są to rękopisy - zaczyna się dramat. Jaki z tego wniosek? Można taki obrazek wykorzystać do weryfikacji użytkowników i odrzucenia wpisów robotów. Stworzymy sobie kawałek kodu, który tworzy proste obrazki.

Kod w C#.NET tworzący CAPTCHA

Dla celów testowych warto sobie stworzyć formularz Windows.Forms, umieścić na nim kontrolkę PictureBox i wstawiać tam obrazek wynikowy. W ten sposób możemy sobie nieco z programem poeksperymentować. Dodajmy sobie też przycisk, kliknijmy na nim dwukrotnie, w celu wygenerowania metody obsługi zdarzenia. W ciele wygenerowanej metody umieścimy właściwy algorytm.

Od czego zaczniemy? Wygenerujemy sobie dynamicznie jakiś obrazek. Przyjrzyjmy się fragmentowi kodu zaprezentowanemu poniżej:

Bitmap im = new Bitmap(WIDTH, HEIGHT);
Font f = new Font("Microsoft Sans Serif", EM_SIZE);
StringBuilder sb = new StringBuilder();
using (Graphics g = Graphics.FromImage(im))
{
    Random r = new Random();
    for (int i = 0; i < 5; i++)
    {
        char letter = ((char)('a' + r.Next('z' - 'a')));
        sb.Append(letter);
    }
    g.DrawString(sb.ToString(), f, Brushes.Black, 0, 0);
}

Captcha bez zabezpieczeń
Rys. 1. Prosta captcha bez zabezpieczeń.

Tyle wystarczy, aby wygenerować obrazek składający się z pięciu losowo wybranych małych liter. Ktoś mógłby zapytać: dlaczego tylko małych liter? Nie ma żadnego ograniczenia. Zachęcam nawet to rozbudowania tego mechanizmu. Zaprezentowany przykład jest tylko wzorcem, podstawą, od której zaczniemy budowę czegoś bardziej zaawansowanego.

Po wykonaniu się zaprezentowanego fragmentu kodu mamy w zmiennej im prosty obrazek z pięcioma literami, natomiast w zmiennej sb litery użyte do wygenerowania obrazka. Stałe WIDTH, HEIGHT, EM_SIZE to odpowiednio szerokość obrazka, wysokość obrazka i wielkość czcionki. Czy taki obrazek jest wystarczającym zabezpieczeniem? Z pewnością lepszym niż nic, ale słabym. Prosty tekst i równa czcionka nie stanowią wyzwania dla systemów rozpoznawania tekstów. Wypadałoby coś z tym fantem zrobić. Dodajmy zatem jakieś utrudnienie, a mianowicie rotację poszczególnych liter. Pełny kod po zmianach przedstawiony jest poniżej:

Bitmap im = new Bitmap(WIDTH, HEIGHT);
Font f = new Font("Microsoft Sans Serif", EM_SIZE);
StringBuilder sb = new StringBuilder();
using (Graphics g = Graphics.FromImage(im))
{
    Random r = new Random();
    for (int i = 0; i < 5; i++)
    {
        int angle = r.Next(ANGLE) - ANGLE / 2;
        char letter = ((char)('a' + r.Next('z' - 'a')));
        sb.Append(letter);
        string str = letter.ToString();
        SizeF size = g.MeasureString(str, f);
        g.TranslateTransform(i * SPACING + size.Width / 2, size.Height / 2);
        g.RotateTransform(angle);
        g.DrawString(str, f, Brushes.Black, -size.Width / 2, -size.Height / 2);
        g.ResetTransform();
    }
}

Captcha z rotacją liter
Rys. 2. Prosta captcha z rotacją liter.

Na czym polega sztuczka? W pierwszej kolejności losowany jest kąt, o jaki można obrócić poszczególne litery. Kąt zdefiniowany jest przez stałą ANGLE. Wartość określająca rotację jest losowa i zawiera się w przedziale [-ANGLE / 2, +ANGLE / 2]. Z tą wartością także można poeksperymentować. Druga nowa stała to SPACING, która określa odległość poszczególnych liter od siebie. Zbyt mała wartość sprawi, że litery mogą się na siebie nakładać. Konsekwencje tego są takie, że tekst będzie mniej czytelny dla robotów, ale też znacznie mniej czytelny dla ludzi. Z tym i innymi parametrami należy zatem postępować roztropnie. Przesunięcie i rotacja wykonywana jest przez parę funkcji TranslateTransform oraz RotateTransform. Aby się nie pogubić z obrotami i translacją układu współprzędnych na końcu każdej iteracji wywoływana jest metoda ResetTransform.

To dopiero początek przekształceń, które utrudniają robotom rozpoznanie tekstu. Przed nami cała paleta różnych rozwiązań. Co się stanie, jak literki poprzesuwamy w pionie? Zmiana w kodzie będzie kosmetyczna. Wystarczy zmienić linijkę z funkcją TranslateTransform na następującą:

g.TranslateTransform(i * SPACING + size.Width / 2,
    size.Height / 2 + r.Next(VERT_OFFSET)); }

Captcha z rotacją i przesunięciem liter
Rys. 3. Prosta captcha z rotacją i przesunięciem liter.

Stała VERT_OFFSET to przesunięcie w pionie. Tak jak w przypadku kąta, tak i w tym przypadku wartość jest losowa. Tym razem jednak zakres wartości wynosi [0, VERT_OFFSET].

Captcha wygląda już całkiem przyjemnie. W celu jeszcze większego zagmatwania tekstu i dodatkowego utrudnienia dla robotów rozpoznających tekst, można do obrazka dodać jeszcze inne elementy. Mogą to być losowe elipsy, kreski, kropki, gwiazdki - mówiąc prościej - dowolne obiekty łatwe do odfiltrowania dla człowieka, a mogące zmylić maszynę, czyli tzw. sztuczną inteligencję. Do obrazka weryfikującego można dodać także elementy upiększające - w końcu te obrazki znajdą się gdzieś na naszej stronie i wypadałoby, aby były przyjemne dla oka. Zanim przejdziemy do podsumowania przyjrzyjmy się jeszcze jednemu fragmentowi kodu:

//definicje stałych użyte w ostatnim przykładzie
const int WIDTH = 100;
const int HEIGHT = 40;
const int EM_SIZE = 20;
const int ANGLE = 100;
const int VERT_OFFSET = 10;
const int SPACING = 19;

//kod metody
Bitmap im = new Bitmap(WIDTH, HEIGHT);
Font f = new Font("Microsoft Sans Serif", EM_SIZE);
StringBuilder sb = new StringBuilder();
using (Graphics g = Graphics.FromImage(im))
{
    Brush b = new LinearGradientBrush(new Point(WIDTH / 2, HEIGHT),
        new Point(WIDTH / 2, -1), Color.DarkGray, Color.White);
    g.FillRectangle(b, new Rectangle(0, 0, WIDTH, HEIGHT));
    Random r = new Random();
    for (int i = 0; i < 5; i++)
    {
        int angle = r.Next(ANGLE) - ANGLE / 2;
        char letter = ((char)('a' + r.Next('z' - 'a')));
        sb.Append(letter);
        string str = letter.ToString();
        SizeF size = g.MeasureString(str, f);
        g.TranslateTransform(i * SPACING + size.Width / 2,
            size.Height / 2 + r.Next(VERT_OFFSET));
        g.RotateTransform(angle);
        g.DrawString(str, f, Brushes.Black, -size.Width / 2, -size.Height / 2);
        g.ResetTransform();
    }
    for (int i = 0; i < 14; i++)
    {
        g.DrawLine(Pens.Black, -HEIGHT + 10 + i * 10, 0, i * 10 + 10, HEIGHT);
        g.DrawLine(Pens.Black, -HEIGHT + 10 + i * 10, HEIGHT, i * 10 + 10, 0);
    }
}
MemoryStream ms = new MemoryStream();
im.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

Captcha z dodanym gradientem i siatką
Rys. 4. Captcha z dodanym gradientem i siatką.

W ostatniej wersji kodu pojawiło się kilka nowych elementów. Pierwszy z nich to tło gradientowe. Nie jest to przeszkoda dla robotów internetowych, ale ładnie wygląda. Drugi istotny element to siatka pokrywająca obrazek. Taka siatka pozwala nieco zmylić algorytmy wykorzystywane przez automaty do rozpoznania kodów. Czy warto? W sumie to tylko dwie linijki. Abstrahując od nakładu czasu poświęconego na napisanie tych dwóch linijek (nie licząc pętli i nawiasów klamrowych), można pokusić się o przetestowanie różnych form obrazków na chociażby darmowych aplikacjach do rozpoznawania tekstu. Przekonamy się, że darmowe programy sobie z tym nie radzą. Ostatnie dwie linijki służą do zapisania wygenerowanego obrazka do tablicy bajtów. Taka tablica, czy jak kto woli strumień, może być wysłany na stronę ASP.NET w postaci obrazka.

Jaka jest wydajność takiego rozwiązania? Okazuje się, że ogromna. Wygenerowanie tysiąca takich kodów obrazkowych przez ostatnią wersję programu zajmuje niespełna sekundę. Wniosek z tego taki, że jeden kod obrazkowy to średnio 1 ms. Przymierzając to do średniego czasu odpowiedzi stron w Internecie jest to wartość bardzo mała.

Jakie można wyciągnąć wnioski? Po pierwsze, napisanie własnego algorytmu generującego obrazki z kodami nie jest jakimś szczególnie trudnym zadaniem. Po drugie, jest to rozwiązanie wystarczające dla zdecydowanej większości stron. Zauważmy też to, że korzystając z gotowców pobranych z internetu narażamy się na większe ryzyko. Dlaczego? Dlatego, że im gotowiec bardziej popularny i częściej wykorzystywany, tym większa szansa, że powstanie jakiś algorytm łamiący tego gotowca. Stosując rozwiązanie własne, czyli z definicji ekstremalnie niszowe, jesteśmy w stanie zmylić wszelkie programy do wykrywania tekstu. Przykład? Wyobraźmy sobie, że akurat na naszej stronie litery obrócone są o 90 stopni. Żywy użytkownik to wie, bo nad obrazkiem napiszemy odpowiednią instrukcję. Standardowe roboty sobie z tym nie poradzą. Większość tzw. hakerki opiera się na gotowych programach pobranych z Internetu. Rzadko zdarza się, aby ktoś pisał specjalny progam do łamania haseł tylko pod jedną witrynę. Chyba, że jest to wyjątkowo popularna witryna.

Kończąc te swoje luźne rozważania zachęcam do eksperymentowania ze stałymi oraz z nowymi technikami pozwalającymi zwiększyć skuteczność kodów obrazkowych. Pamiętajmy przy tym, że nie należy przesadzać z przekształceniami tekstu. Istnieje pewna granica, po przekroczeniu której nie tylko roboty mają problem - problem z rozpoznaniem kodu zaczynają mieć zwykli użytkownicy. Jakie mogą być propozycje modyfikacji? Może to być zmiana kroju czcionki, zmiana wielkości liter, można poeksperymentować z kolorystyką. Można wygenerować jeden kod większą czcionką, a w tle jakiś inny, mniej widoczny. Możliwości jest wiele, a ogranicza nas chyba tylko wyobraźnia (no, może trochę umiejętności).

Kategoria:BotyC#

, 2013-12-20

Komentarze:

mimirus (2014-05-03 20:11:22)
Dziękuję za bardzo dobry przykład. Wstawiłem go do przykładu http://archive.msdn.microsoft.com/dynamiccaptchacode i ładnie działa.
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?