16
POCZĄTKI
HAKIN9 6/2008
P
odczas wypełniania różnorodnych formularzy
na stronach WWW bardzo często musimy
udowodnić, że nie jesteśmy botami. Zwykle
służą do tego specjalnie spreparowane obrazki, z
których przeciętny człowiek powinien bez problemu
odczytać nieco zniekształcony tekst (Rysunek 1).
Taki rodzaj zabezpieczeń przydaje się
szczególnie przy różnego rodzaju rejestracjach,
np. darmowych kont e-mail, lub wypowiedziach
na forach internetowych. Chroni on przed
automatycznym tworzeniem kont przez automaty
(np. roboty spamerskie). Technika ta nosi nazwę
CAPTCHA, co jest skrótem angielskiego określenia
Completely Automated Public Turing Test to Tell
Computers and Humans Apart. Luis von Ahn,
Manuel Blum, Nicholas Hopper i John Langford z
Carnegie Mellon University stworzyli ją na potrzeby
Yahoo. Chcieli w ten sposób ukrócić automatyczne
tworzenie spamerskich kont e-mail na tym portalu.
Na początku CAPTCHA sprawdzała się wyśmienicie.
Jednak – jak to w przyrodzie bywa – każdej akcji
towarzyszy reakcja. Opracowanych zostało kilka
typów ataków, które w mniejszym bądź większym
stopniu umożliwiały obejście tego zabezpieczenia.
Najprostszy atak polega na zatrudnieniu
podstawionych osób, które zakładały konta e-mail
chronione przez system CAPTCHA. W końcu osoby te
były w stanie odczytać zniekształcony tekst. Problem
w tym, że nikt nie będzie chciał robić czegoś takiego
za darmo – należy za to zapłacić, co może okazać
się mało opłacalne dla spamera. Drugi problem to
niska efektywność rozwiązania. Inne, dosyć ciekawe
SŁAWOMIR ORŁOWSKI
Z ARTYKUŁU
DOWIESZ SIĘ
co to jest test CAPTCHA,
jakie są wady i zalety tego
rozwiązania,
jak z poziomu kodu C#
wygenerować obrazek
CAPTCHA,
o klasach platformy .NET
obsługujących grafikę 2D.
CO POWINIENEŚ
WIEDZIEĆ
znać podstawy języka C#
i technologii ASP.NET.
rozwiązanie polega na użyciu podstawionych stron
– najlepiej pornograficznych. Jeśli użytkownik takiego
serwisu WWW zechce obejrzeć jakąś jego część
(zdjęcie), musi wpisać kod z obrazka. Wystarczy, że
obrazek zabezpieczający do takiej strony zostanie
wklejony z formularza znajdującego się na stronie,
którą chcemy zaatakować. Nieświadomy użytkownik
sam rozwiąże za nas zagadkę. Jest to kolejny rodzaj
ataku, w którym wykorzystujemy człowieka. Sprytne,
bardziej efektywne, tańsze – ale czy wystarczająco
skuteczne? Ludzie odpowiedzialni za projekt
CAPTCHA twierdzą, że nie. Spamerzy twierdzą
oczywiście, że tak. Jednak proszę sobie wyobrazić
serwis WWW, w którym za każdym razem, kiedy
chcemy zobaczyć coś nowego, musimy wpisywać
jakiś kod. Wątpię, żeby taka strona przyciągała
wielu użytkowników. Lepszym rozwiązaniem są
programy OCR (ang. Optical Character Recognition),
które za pomocą pewnych algorytmów potrafią
odczytać tekst z obrazka. Oczywiście standardowe
aplikacje OCR nie poradzą sobie z przeciętnym
obrazkiem CAPTCHA. Potrzebne są narzędzia
bardziej wyspecjalizowane, stworzone specjalnie na
potrzeby łamania akurat tego typu zabezpieczeń.
Jednym z takich projektów jest program PWNtcha
(ang. Pretend We’re Not a Turing Computer but a
Human Antagonist). Potrafi on prawdopodobnie
zdekodować tekst pochodzący ze stosunkowo
mało skomplikowanych obrazów. Nie należy jednak
oczekiwać, że poradzi sobie z każdym obrazkiem.
Aby w miarę dobrze działał, należy go dostroić do
konkretnego problemu. Napisałem prawdopodobnie,
Stopień trudności
Test CAPTCHA
Jak odróżnić człowieka od automatu w Internecie? Istnieje prosty
test, który może to sprawdzić. Wystarczy wygenerować obrazek
ze zniekształconym tekstem i kazać użytkownikowi go odczytać.
17
TEST CAPTCHA
HAKIN9
6/2008
ponieważ autor PWNtcha nie udostępnia
tego programu do testów. Jest to dosyć
logiczne, ponieważ narzędzie mogłoby być
użyte przez spamerów. Innym projektem jest
aiCAPTCHA. Używa on algorytmów sztucznej
inteligencji. Nie sądzę jednak, że zostanie
stworzony program, który będzie sobie radził
ze wszystkimi rodzajami obrazków CAPTCHA.
Ostatnio na świecie pojawiło się coraz więcej
przeciwników tego zabezpieczenia. Osoby
niedowidzące mają poważne problemy przy
odczytaniu tekstu. Obrazki te są również
coraz trudniejsze do odczytania przez
dobrze widzące osoby. Taki system wymaga
również od użytkownika pewnej fatygi, a to
może skutecznie zniechęcić użytkowników.
Przecież korzystanie z Internetu powinno być
lekkie, łatwe i przyjemne. Istnieje też sporo
innych, darmowych i komercyjnych rozwiązań
przeciwko botom, jednak test CAPTCHA
nadal jest – i pewnie jeszcze długo będzie
– wykorzystywany.
W tym artykule chciałbym zaproponować
stworzenie własnej klasy, która będzie miała
możliwość generowania obrazków CAPTCHA.
Klasa będzie napisana w języku C#, w
związku z tym będzie ją można używać w
projektach ASP.NET. Użyjemy standardu C#
2.0 i .NET 2.0. Program napisany będzie przy
użyciu Visual C# 2008 Express Edition. Niech
klasa nosi nazwę Captcha i będzie częścią
przestrzeni nazw Hakin9. Na początku
zadeklarujemy pola i konstruktory klasy
(Listing 1.).
Pola
width
i
height
będą
przechowywały odpowiednio szerokość
i wysokość obrazka. Pole
fontSize
odpowiada za wielkość czcionki. Pole
text
zawierać będzie tekst, jaki ma
być umieszczony na obrazku. Pole
random
będzie pomocne przy generacji
liczb pseudolosowych, potrzebnych
do losowego dodawania pewnych
elementów utrudniających programom
OCR odczytanie tekstu. Pora teraz napisać
główną metodę klasy, która będzie
generowała obrazek (Listing 2.).
Metoda ta zwracać będzie obiekt
typu
Bitmap
, czyli mapę bitową. Ponieważ
w definicji klasy zadeklarowaliśmy trzy
konstruktory, na początku metody
Generate
musimy sprawdzić, czy wszystkie wymagane
pola są wypełnione danymi. Jeśli nie,
generowany jest wyjątek. Dalej deklarujemy
egzemplarz klasy
Bitmap
, który zawierać
będzie obrazek. Na jego podstawie budujemy
obiekt klasy
Graphics
, który umożliwi nam
rysowanie. Pole, które będziemy wypełniać
grafiką, definiujemy za pomocą klasy
Rectangle
.
Płótno, na którym będziemy malować,
jest już przygotowane – pora teraz na pędzel.
Użyjemy do tego klasy
HatchBrush
. W
konstruktorze tej klasy przekazujemy styl, kolor
rysowania i kolor podkładu. Zbiór styli zawiera
typ wyliczeniowy
HatchStyle
– mamy
do wyboru aż 56 rodzajów wypełnienia.
Teraz za pomocą referencji
g
wypełniamy
całość obrazka używając do tego metody
FillRectangle
. W tym przypadku wybrany
został styl
SmallConfetti
, ale nic nie stoi
na przeszkodzie, aby Czytelnik przeciążył
metodę
Generate
z argumentem wywołania,
który umożliwi własny wybór stylu. Jest to
Rysunek 2.
Przykładowe użycie klasy
Captcha
Listing 1.
Pola i konstruktory klasy Captcha
using
System
;
using
System
.
Text
;
using
System
.
Drawing
;
using
System
.
Drawing
.
Imaging
;
using
System
.
Drawing
.
Drawing2D
;
namespace
Rysunki
{
public
class
RPicture
{
private
int
width
;
private
int
height
;
private
int
fontSize
;
private
string
text
;
private
Random
random
;
public
RPicture
()
{
random
=
new
Random
();
}
public
RPicture
(
int
_width
,
int
_height
,
int
_fontSize
)
{
random
=
new
Random
();
width
=
_width
;
height
=
_height
;
fontSize
=
_fontSize
;
}
public
RPicture
(
int
_width
,
int
_height
,
int
_fontSize
,
string
_text
)
{
random
=
new
Random
();
width
=
_width
;
height
=
_height
;
fontSize
=
_fontSize
;
text
=
_text
;
}
}
Rysunek 1.
Przykładowe obrazki
zabezpieczające
18
POCZATKI
HAKIN9 6/2008
TEST CAPTCHA
19
HAKIN9
6/2008
bardzo proste ćwiczenie, które może wykonać
Czytelnik samodzielnie.
W kolejnym kroku przygotowujemy napis,
który zostanie umieszczony na obrazku. Klasa
StringFormat
przechowuje informacje
dotyczące formatowania napisu. Z kolei
egzemplarz klasy
GraphicsPath
posłuży
nam do umieszczenia tekstu na obrazku.
Jest to bardzo ciekawa klasa, niezwykle
użyteczna przy tworzeniu fragmentów
aplikacji, które obsługują obiekty graficzne.
Za jej pomocą możemy rysować różnorodne
kształty, wypełniać wnętrza figur itd. Kształty,
w tym również tekst, to skończona sekwencja
połączonych ze sobą linii i łuków. Metodą
AddString
dodajemy napis do obiektu
path
. Jej trzeci argument odpowiada za
styl czcionki. Można wybierać pomiędzy
kursywą, pogrubieniem, przekreśleniem i
podkreśleniem. Jak widać na Listingu 2., style
można ze sobą łączyć. Myślę, że pozostałe
argumenty wywołania nie wymagają opisu.
Dla tekstu tworzymy jeszcze nowy
pędzel. Wybrany został na stałe styl
LargeConfetti
– tak, jak w poprzednim
przypadku można przeciążyć metodę
Generate
, aby przyjmowała jako argument
również ten styl. Tekst powinien być
nieznacznie zaburzony. Posłuży do tego
metoda
Warp
klasy
GraphicsPath
. Jedna
z jej wersji przyjmuje cztery argumenty.
Pierwszym z nich są punkty definiujące
czworościan, który będzie zawierał
zmieniony obrazek. Drugi argument
to obrazek źródłowy, trzeci to macierz
przekształcenia – użyta została macierz
zerowa. Ostatnim argumentem jest typ
wyliczeniowy zawierający dwa sposoby
przekształcania:
Bilinear
i
Perspective
.
Proponuję samemu poeksperymentować
z możliwymi transformacjami. Tutaj zostało
użyte dosyć proste przekształcenie z
elementami losowości.
Po tych czynnościach pozostaje nam
tylko wyświetlić przygotowany tekst za
pomocą metody
FillPath
. Do narysowania
tekstu celowo użyty został nieregularny styl
pędzla oraz zaburzenie tekstu. Utrudni to
programom próbującym zdekodować tekst
odnalezienie liter. W kolejnych dwóch pętlach
dodajemy do obrazka losowe zakłócenia. Jest
to konieczne, ponieważ obrazek generowany
zawsze w ten sam sposób byłby łatwy do
oczytania przez programy, które wystarczyłoby
odpowiednio dostosować. Pierwsza pętla
dodaje wypełnione elipsy umieszczone
w przypadkowych miejscach (metoda
FillEllipse
). Za sprawą odpowiednio
dobranych współczynników elipsy pojawią
się na obrazku jako kropki, które nie będą
utrudniały człowiekowi odczytania tekstu z
Listing 2.
Metoda generująca obrazek CAPTCHA
public
Bitmap
Generate
()
{
if
(
width
==
0
||
height
==
0
||
text
==
null
)
throw
new
NullReferenceException
(
"Please select width, height and text
for image"
);
Bitmap
picture
=
new
Bitmap
(
width
,
height
,
PixelFormat
.
Format32bppArgb
);
Graphics
g
=
Graphics
.
FromImage
(
picture
);
g
.
SmoothingMode
=
SmoothingMode
.
AntiAlias
;
Rectangle
r
=
new
Rectangle
(
0
,
0
,
width
,
height
);
HatchBrush
brush
=
new
HatchBrush
(
HatchStyle
.
SmallConfetti
,
Color
.
LightGray
,
Color
.
White
);
g
.
FillRectangle
(
brush
,
r
);
StringFormat
format
=
new
StringFormat
();
format
.
Alignment
=
StringAlignment
.
Center
;
format
.
LineAlignment
=
StringAlignment
.
Center
;
GraphicsPath
path
=
new
GraphicsPath
();
path
.
AddString
(
text
,
FontFamily
.
GenericSerif
,
(
int
)
FontStyle
.
Bold
+
(
int
)
FontStyle
.
Italic
,
fontSize
,
r
,
format
);
brush
=
new
HatchBrush
(
HatchStyle
.
LargeConfetti
,
Color
.
LightGray
,
Color
.
DarkGray
);
float
r1
=
random
.
Next
(
width
/
8
);
float
r2
=
random
.
Next
(
height
/
4
);
PointF
p1
=
new
PointF
(
0
,
0
);
PointF
p2
=
new
PointF
(
width
,
r2
);
PointF
p3
=
new
PointF
(
r1
,
height
);
PointF
p4
=
new
PointF
(
width
,
height
-
r2
);
PointF
[]
points
=
{
p1
,
p2
,
p3
,
p4
}
;
Matrix
m
=
new
Matrix
();
path
.
Warp
(
points
,
r
,
m
,
WarpMode
.
Perspective
);
g
.
FillPath
(
brush
,
path
);
for
(
int
i
=
0
;
i
<
width
/
3
;
i
++)
g
.
FillEllipse
(
brush
,
new
Rectangle
(
random
.
Next
(
0
,
width
)
,
random
.
Next
(
0
,
height
)
,
random
.
Next
(
1
,
width
/
20
)
,
random
.
Next
(
1
,
width
/
20
)));
for
(
int
i
=
0
;
i
<
width
/
22
;
i
++)
g
.
DrawEllipse
(
new
Pen
(
brush
)
,
(
float
)
random
.
Next
(
0
,
width
)
,
(
float
)
random
.
Next
(
0
,
height
)
,
(
float
)
random
.
Next
(
0
,
width
)
,
(
float
)
random
.
Next
(
0
,
height
));
return
picture
;
}
Listing 3.
Strona obrazek.aspx, generująca testowy obrazek
protected
void
Page_Load
(
object
sender
,
EventArgs
e
)
{
string
tekst
=
"Hakin9"
;
Captcha
image
=
new
Captcha
(
150
,
50
,
38
,
tekst
);
image
.
Generate
()
.
Save
(
Response
.
OutputStream
,
System
.
Drawing
.
Imaging
.
ImageForm
at
.
Jpeg
);
Session
.
Add
(
"tekst"
,
tekst
);
}
Rysunek 3
. Wynik skanowania obrazka
CAPTCHA przy pomocy FineReader
18
POCZATKI
HAKIN9 6/2008
TEST CAPTCHA
19
HAKIN9
6/2008
obrazka. Dobrym pomysłem jest również
umieszczenie pewnych nieregularnych
linii, które nachodzą na napis. Jest to duże
utrudnienie dla botów, oczywiście pod
warunkiem, że linie są rysowane losowo. To
zabezpieczenie realizowane jest w drugiej
pętli, która rysuje losowe elipsy z odpowiednio
dobranymi współczynnikami. Na końcu
metody zwracany jest cały obrazek. I to
wszystko. Klasa do generowania obrazków
CAPTCHA jest już gotowa. Oczywiście można
do niej wprowadzić szereg modyfikacji.
Najlepiej umieścić ją w projekcie aplikacji
Windows, gdzie z łatwością możemy ją
przetestować.
Kolejne zadanie to stworzenie testowego
serwisu WWW, w którym klasa Captcha
zostanie użyta praktycznie. Umieszczamy ją w
projekcie strony ASP.NET. Przypomnę tylko, ze
pliki klas najlepiej umieszczać w podkatalogu
App_Code katalogu projektu. Oczywiście
na potrzeby artykułu serwis będzie bardzo
prosty. Będzie składał się z dwóch stron:
Default.aspx i obrazek.aspx. Pierwsza z nich
jest standardową stroną WWW, natomiast
za pomocą drugiej wygenerujemy testowy
obrazek. Obrazek zwracany będzie jako
odpowiedź. Innym rozwiązaniem może być
tymczasowy zapis obrazka na dysku twardym
serwera, jednak przy sporym ruchu mogłoby
się okazać, że na dysku brakuje miejsca do
zapisu. Strona obrazek.aspx musi zawierać
kod obrazka testowego. Odpowiedni kod
umieścimy w metodzie zdarzeniowej
Page _
Load
(Listing 3.). Jest to jedynie przykład i
pod zmienną
tekst
powinno się podstawić
automatycznie wygenerowany ciąg o danej
długości. W artykule Automatyczna generacja
ciągów (Hakin9 5/2008) przedstawiłem jeden
ze sposobów otrzymania takiego ciągu.
Dalej generowany jest obiekt klasy
Captcha
.
Wygenerowany obrazek od razu zamieniamy
na strumień wyjściowy i pakujemy do formatu
JPEG. Na koniec dodajemy jeszcze zmienną
sesji, która przechowywać będzie tekst
umieszczony na obrazku. Strona Default.aspx
zawierać będzie jedynie cztery kontrolki:
Image1
,
TextBox1
,
Button1
i
Label1
(Listing 4.). Pierwsza z nich wyświetlać będzie
obrazek CAPTCHA. Jej własność
ImageUrl
ustawiamy na obrazek.aspx. Po kliknięciu
przycisku Button1 nastąpi sprawdzenie, czy
tekst wprowadzony do kontrolki
TextBox1
jest
taki sam, jak ten przechowywany w zmiennej
sesji
tekst
(Listing 5.). Serwis WWW w akcji
przedstawia Rysunek 2. Przeprowadźmy
jeszcze prosty test wygenerowanego obrazka.
Użyjemy do tego aplikacji FineReader 9.0 w
wersji Professional. Jest to jeden z najbardziej
popularnych programów typu OCR. Jedna z
jego opcji umożliwia skanowanie obrazków
zapisanych w najpopularniejszych formatach.
Obrazek CAPTCHA wygenerowany za
pomocą stworzonej klasy został zapisany na
dysku jako mapa bitowa. Został następnie
wczytany za pomocą programu FineReader,
po czym dokonano próby odczytania tekstu.
Wynik przedstawia Rysunek 3.
Program nie jest w stanie odczytać
tekstu z obrazka. Oczywiście ten test
należy traktować ostrożnie, ponieważ
FineReader jest zoptymalizowany specjalnie
do pracy biurowej. Pokazuje on jednak,
że standardowe algorytmy OCR w tym
przypadku zawodzą.
Problem odpowiedniego zabezpieczenia
wszelakiego rodzaju formularzy
rejestracyjnych przed automatami używanymi
przez spamerów to poważna sprawa.
Podsumowanie
W zeszłym roku ponad 97% wszystkich
wysłanych maili to niechciana poczta.
Automatyczne wpisy reklamowe na
forach internetowych nie przysparzają
ich administratorom chwały. Sondaże
przeprowadzane w Internecie mogą paść
ofiarą botów. Naturalnie test CAPTCHA
nie rozwiąże tych wszystkich problemów,
może jednak zmniejszyć ich skalę.
Dodatkowo możemy używać odpowiednich
filtrów. Przyznam się, że czasem mam
problemy z prawidłowym odczytaniem
tekstu z obrazka. Potrafi to zirytować wielu
użytkowników. Jednak jeśli może to pomóc
w ograniczeniu działalności spamerów,
warto poświęcić temu kilka sekund i nieco
wysilić swoje zmysły.
Listing 4.
Strona Default.aspx
<%
@
Page
Language
=
"C#"
AutoEventWireup
=
"true"
CodeFile
=
"Default.aspx.cs"
Inherits
=
"_
Default"
%>
<!
DOCTYPE
html
PUBLIC
"
-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/
xhtml1/DTD/xhtml1-transitional.dtd">
<
html
xmlns
=
"http://www.w3.org/1999/xhtml"
>
<
head
runat
=
"server"
>
<
title
>
Untitled
Page
<
/
title
>
<
/
head
>
<
body
>
<
form
id
=
"form1"
runat
=
"server"
>
<
div
>
<
asp
:
Image
ID
=
"Image1"
runat
=
"server"
ImageUrl
=
"obrazek.aspx"
/
>&
nbsp
;<
br
/
>
<
asp
:
TextBox
ID
=
"TextBox1"
runat
=
"server"
><
/
asp
:
TextBox
>
<
asp
:
Button
ID
=
"Button1"
runat
=
"server"
OnClick
=
"Button1_Click"
Text
=
"Sprawdź!"
/
>
<
br
/
>
<
asp
:
Label
ID
=
"Label1"
runat
=
"server"
BackColor
=
"White"
Font
-
Bold
=
"True"
Font
-
Size
=
"Larger"
Text
=
"Label"
><
/
asp
:
Label
><
/
div
>
<
/
form
>
<
/
body
>
<
/
html
>
Listing 5.
Metoda zdarzeniowa Button1_Click
protected
void
Button1_Click
(
object
sender
,
EventArgs
e
)
{
string
pass
=
Session
[
"tekst"
]
.
ToString
();
if
(
TextBox1
.
Text
==
pass
)
Label1
.
Text
=
"Nie jesteś botem !"
;
else
Label1
.
Text
=
"Jesteś botem !!!"
;
}
Sławomir Orłowski
Z wykształcenia fizyk. Obecnie jest doktorantem na
Wydziale Fizyki, Astronomii i Informatyki Stosowanej
Uniwersytetu Mikołaja Kopernika w Toruniu. Zajmuje się
symulacjami komputerowymi układów biologicznych
(dynamika molekularna) oraz bioinformatyką.
Programowanie jest nieodzowną częścią jego
pracy naukowej i dydaktycznej. Ma doświadczenie w
programowaniu w językach C, C++, Delphi, Fortran, Java,
C# i Tcl. Współzałożyciel i koordynator grupy .NET WFAiIS.
Jest autorem artykułów i książek informatycznych.
Strona domowa: http://www.fizyka.umk.pl/~bigman/.
Kontakt z autorem: bigman@fizyka.umk.pl