Captcha (Features)

by Martin66 ⌂ @, Saturday, February 06, 2016, 21:06 (2995 days ago)

Hallo Entwickler,

großes LOB für das Feature, einzelne IPs zu sperren! Mein Logfile zeigt mir, dass es ein Spammer bei mir nun schon seit Monaten erfolglos versucht. :lol:

Es handelt sich um einen Brute-Force-Angriff, ca. alle 1-2 Minuten. Das Captcha scheint dabei kein Problem zu sein. Deswegen habe ich mir über das Captcha Gedanken gemacht, da ich mein Forum auch weiterhin für unregistrierte User offen halten will:

Ein 5-stelliges Captcha ist für einen Brute-Force-Angriff kein Hindernis. Außerdem ist der Zeichenvorrat derzeit auf 28 Zeichen begrenzt: 28^5 (17 Millionen) Möglichkeiten. Und auch Zeichenerkennung ist einfach: Wenn es eine bestimmte Anzahl fixer Hintergrundbilder gibt, muss man nur mehrfach ein Captcha aufrufen, die Bilder vergleichen, und kann dann den Hintergrund rausrechnen. Deswegen sollte der Hintergrund variabel sein.

Daher habe ich modules/captcha/captcha.php überarbeitet. Jetzt wird ein mal 7-, mal 8-stelliger Code benötigt, und der Zeichenvorrat ist um @%&?# ergänzt. Daraus ergeben sich 33^7 (42.6 Milliarden) bzw. 33^8 (1.4 Billionen) verschiedene Eingabemöglichkeiten. Brute force wird so wohl verdammt schwer. Der Hintergrund wird jedesmal neu erstellt, ohne vorgegebene Bilder, und da man nicht mal weiß, ob 7 oder 8 Zeichen benötigt werden, ist auch das Auslesen aus der Grafik erschwert.

Ich habe keine Ahnung, ob das Ganze was bringt :-D Aber dies als Vorschlag für die nächste mlf-Version. Und da es das erste Mal ist, dass ich etwas mit PHP gemacht habe, würden mich Meinungen und Verbesserungsvorschläge freuen :-) Das Ergebnis kann man unter http://www.access-tutorial.de/forum bewundern.

Meine Änderungen:

modules/captcha/captcha_image.php
---------------------------------
Zeile 8:

  $captcha->generate_image($_SESSION['captcha_session'],'fonts/');


Das backgrounds-Verzeichnis wird obsolet

lang/*.lang
-----------
Da jetzt auch ein paar Sonderzeichen vorkommen können, sollte captcha_expl_image angepasst werden (german.lang: "Bitte die Zeichenfolge des Bildes eingeben:")

modules/captcha/captcha.php
---------------------------

<?php
/*******************************************************************************
* PHP CAPTCHA class (C) 2008 alex@mylittlehomepage.net                         *
* http://mylittlehomepage.net/                                                 *
* rewritten 02/2016 by Martin Asal (asal@gmx.de)                               *
*******************************************************************************/
 
/*******************************************************************************
* This program is free software: you can redistribute it and/or modify         *
* it under the terms of the GNU General Public License as published by         *
* the Free Software Foundation, either version 3 of the License, or            *
* (at your option) any later version.                                          *
*                                                                              *
* This program is distributed in the hope that it will be useful,              *
* but WITHOUT ANY WARRANTY; without even the implied warranty of               *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                *
* GNU General Public License for more details.                                 *
*                                                                              *
* You should have received a copy of the GNU General Public License            *
* along with this program.  If not, see <http://www.gnu.org/licenses/>.        *
*******************************************************************************/
 
class Captcha
{
function check_captcha($code,$entered_code)
{
 if(strtolower($entered_code) == strtolower($code)) return true;
 else return false;
}
 
function generate_code($letters='abcdefhjkmnpqrstuvwxyz234568@%&?#')
{
 mt_srand((double)microtime()*1000000);
 $clength=mt_rand(7,8);
 $code='';
 for($i=0;$i<$clength;$i++)
 {
  $code.=substr($letters,mt_rand(0,strlen($letters)-1),1);
 }
 return $code;
}
 
function generate_image($code,$fonts_folder='')
{
 $code_length = strlen($code);
 $font_size = 23;
 $font_pos_x = 8 + 36*(8-$code_length)/2;    // centers the code
 $font_pos_y = 35;
 
 // get fonts:
 if($fonts_folder!='')
 {
  $handle=opendir($fonts_folder);
  while($file = readdir($handle))
  {
   if(preg_match('/\.ttf$/i', $file)) $fonts[] = $file;
  }
  closedir($handle);
 }
 
 $im = ImageCreate(290, 50);
 
 // create a color palette. There's a maximum of 255 different colors
 $bgRects = 15;
 $bgGraffity = 100;
 for($i=0; $i<=255; $i++)
 {
  switch($i){
  case 0:        // Primary background color
   $col[$i] = ImageColorAllocate ($im, mt_rand(192,255), mt_rand(192,255), mt_rand(192,255));
   break;
  case($i<=$bgRects):      // colors for background rectangles
   $col[$i] = ImageColorAllocate ($im, mt_rand(128,255), mt_rand(128,255), mt_rand(128,255));
   break;
  case($i<=$bgRects+$bgGraffity):     // colors for background graffities
   $col[$i] = ImageColorAllocate ($im, mt_rand(160,255), mt_rand(160,255), mt_rand(160,255));
   break;
  case($i<=$bgRects+$bgGraffity+$code_length):   // colors for text
   $col[$i] = ImageColorAllocateAlpha ($im, mt_rand(0,96), mt_rand(0,96), mt_rand(0,96), 16);
   break;
  default:       // Colors for dots (I call it "salt")
   $col[$i] = ImageColorAllocate ($im, mt_rand(0,255), mt_rand(0,255), mt_rand(0,255));
  }
 }
 
 // create background
 imageFilledRectangle ( $im, 0,0, 290,50, $col[0] );
 for($i=1;$i<=$bgRects;$i++)      // background Rects
 {
  $x1 = mt_rand(0,290);
  $y1 = mt_rand(0,50);
  $x2 = $x1 + mt_rand(-150,150);
  $y2 = $y2 + mt_rand(-25,25);
  imageFilledRectangle ( $im, $x1,$y1, $x2,$y2, $col[$i] );
 }
 for($i=$bgRects+1; $i<=$bgGraffity; $i++)    // background Arcs
 {
  $x = mt_rand(10,280);
  $y = mt_rand(10,40);
  $w = mt_rand(0,40);
  $h = mt_rand(0,40);
  $s = mt_rand(0,180);
  $e = $s + mt_rand(0,180);
  imageSetThickness($im, mt_rand(1,3));
  $Punkt = imageArc ($im, $x,$y, $w,$h, $s,$e, $col[$i] );
 }
 
 // use fonts, if available:
 if(isset($fonts))
 {
  for($i=0;$i<$code_length;$i++)
  {
   $angle = mt_rand(-25,25);
   $text_color = $col[$bgRects + $bgGraffity + $i + 1];
   ImageTTFText($im, $font_size+mt_rand(-3,4), $angle, $font_pos_x, $font_pos_y, $text_color, $fonts_folder.$fonts[mt_rand(0,count($fonts)-1)], substr($code,$i,1));
   $font_pos_x=$font_pos_x+($font_size+13);
  }
 }
 // if not, use internal font:
 else
 {
  // set text color:
  $x = 50+mt_rand(0,50);
  $y = 10+mt_rand(0,10);
  $text_color = $col[$bgRects + $bgGraffity + 1];
  imageFilledRectangle ( $im, $x-5,$y, $x+80,$y+15, $col[0] );
  ImageString($im, 5, $x, $y, $code, $text_color);
 }
 
 // some "salt" above all
 for($i=0;$i<600;$i++)
 {
  $x = mt_rand(0,290);
  $y = mt_rand(0,50);
  $Punkt = imageSetPixel ($im, $x,$y, $col[mt_rand(0,255)] );
 }
        if(mt_rand(0,1))
        {
                imageFilter($im, IMG_FILTER_NEGATE);
        }
 
 header("Expires: Expires: Mon, 1 Feb 2016 00:00:00 GMT");
 header("Cache-Control: max-age=0");
 header("Content-type: image/png");
 ImagePNG($im);
 exit();
}
 
function generate_dummy_image()
{
 $im = @ImageCreate(180, 40);
 $background_color = ImageColorAllocate ($im, 234, 234, 234);
 $text_color = ImageColorAllocate ($im, 0, 0, 0);
 #ImageString($im, 3, 7, 4, 'CAPTCHA not available', $text_color);
 header("Expires: Expires: Sat, 20 Oct 2007 00:00:00 GMT");
 header("Cache-Control: max-age=0");
 header("Content-type: image/png");
 ImagePNG($im);
}
 
// for math CAPTCHA:
function generate_math_captcha($number1from=1,$number1to=10,$number2from=0,$number2to=10)
{
 $number[0] = rand($number1from,$number1to);
 $number[1] = rand($number2from,$number2to);
 $number[2] = $number[0] + $number[1];
 return $number;
}
 
function check_math_captcha($result, $entered_result)
{
 if(intval($result) == intval($entered_result)) return true;
 else return false;
}
}
?>
 

Martin

Avatar

Captcha

by Auge ⌂, Thursday, February 11, 2016, 15:50 (2990 days ago) @ Martin66

Hallo Martin,

großes LOB für das Feature, einzelne IPs zu sperren! Mein Logfile zeigt mir, dass es ein Spammer bei mir nun schon seit Monaten erfolglos versucht. :lol:

Wenn man die IP oder die typischerweise benutzten IPs kennt und diese auch zumindest über einen längeren Zeitraum genutzt werden, ist das schon ein feines Werkzeug. In mlf1 gab es das ja auch schon, wobei die Speicherung der zu bannenden IPs, Worte und Benutzernamen nicht optimal war. Das waren einfach kommaseparierte Listen in einem Datenfeld. Wie das in mlf2 gelöst ist, weiß ich aus dem Stehgreif nicht.

In der nicht stabilen Version von mlf1 habe ich jedenfalls das Speicherformat geändert, so dass jede IP einen eigenen Eintrag in der Datenbank und einen Zähler bekommt. So wird auch aus einem Datenbestand von mehreren zehn- bis hunderttausend IPs kein Performanceproblem.

Es handelt sich um einen Brute-Force-Angriff, ca. alle 1-2 Minuten.

Du meinst einen Zugriffs- bzw. Eintragsversuch alle ein bis zwei Minuten? Ja, das kenne ich. Dabei habe ich in meinem Testforum mal live zugeschaut. Das war gruselig.

Das Captcha scheint dabei kein Problem zu sein. Deswegen habe ich mir über das Captcha Gedanken gemacht, da ich mein Forum auch weiterhin für unregistrierte User offen halten will:

Dir ist hoffentlich klar, dass der Eintrag im Zweifelsfall ohne den Aufruf des Formulars erfolgen kann?

Ein 5-stelliges Captcha ist für einen Brute-Force-Angriff kein Hindernis. Außerdem ist der Zeichenvorrat derzeit auf 28 Zeichen begrenzt: 28^5 (17 Millionen) Möglichkeiten. Und auch Zeichenerkennung ist einfach: Wenn es eine bestimmte Anzahl fixer Hintergrundbilder gibt, muss man nur mehrfach ein Captcha aufrufen, die Bilder vergleichen, und kann dann den Hintergrund rausrechnen. Deswegen sollte der Hintergrund variabel sein.

Ich habe mir deinen Code bisher nicht im einzelnen angeschaut. Deine Zustandsbeschreibung erscheint aber schlüssig.

Daher habe ich modules/captcha/captcha.php überarbeitet. Jetzt wird ein mal 7-, mal 8-stelliger Code benötigt, und der Zeichenvorrat ist um @%&?# ergänzt. Daraus ergeben sich 33^7 (42.6 Milliarden) bzw. 33^8 (1.4 Billionen) verschiedene Eingabemöglichkeiten. Brute force wird so wohl verdammt schwer.

Mit „verdammt schwer“ wäre ich in Zeiten der Cloud, in der man sich einfach beliebig viel Rechenzeit kaufen kann, verdammt (;-)) vorsichtig. Aber ja, die Anforderungen (in jegliche Richtung) steigen auf diese Art.

Der Hintergrund wird jedesmal neu erstellt, ohne vorgegebene Bilder

Frage dazu: Wie zufällig sind die generierten Hintergründe ohne Vorlagen schlussendlich? Nicht, dass dein "Graffity" auch nur aus ein paar wenigen Variationen besteht.

Ich habe keine Ahnung, ob das Ganze was bringt :-D

Du kannst ja zumindest über deine Beobachtungen deiner Installation berichten.

Aber dies als Vorschlag für die nächste mlf-Version. Und da es das erste Mal ist, dass ich etwas mit PHP gemacht habe, würden mich Meinungen und Verbesserungsvorschläge freuen :-)

Tschö, Auge

--
Trenne niemals Müll, denn er hat nur eine Silbe!

Captcha

by Martin66 ⌂ @, Thursday, February 11, 2016, 21:36 (2990 days ago) @ Auge

Dir ist hoffentlich klar, dass der Eintrag im Zweifelsfall ohne den Aufruf des Formulars erfolgen kann?

Oh, nein, das wusste ich nicht. Wie geht das (Antwort darauf notfalls per PN)? Kann man das irgendwie unterbinden, z.B. per .htaccess?

Frage dazu: Wie zufällig sind die generierten Hintergründe ohne Vorlagen schlussendlich? Nicht, dass dein "Graffity" auch nur aus ein paar wenigen Variationen besteht.

Das Captcha wird wie folgt erstellt:

  • Auf den Hintergrund werden 15 Rechtecke gelegt ($bgRects = 15;), deren Größe und Lage zufällig sind.
    Darauf kommen dann 100 Kreisbögen($bgGraffity = 100;), von mir "Graffities" genannt (Herrjeh, im Singular habe ich das ja flasch geschrieben...). Bei jedem einzelnen Kreisbogen werden die Koordinaten, Breite, Höhe sowie Start- und Endpunkt zufällig generiert, und auch die Dicke des Kreisbogens schwankt. Alle Rechtecke und Kreisbögen haben je eine eigene, zufällig generierte Farbe, wobei die Kreisbögen dunkler sind.

  • Nun kommt der eigentliche Text drauf. Jedes Zeichen hat wieder seine eigene, zufällige Farbe - dunkler als die vorher verwendeten. Auch die Schriftgröße variiert etwas. Damit auch die Koordinaten der Zeichen nicht zu leicht errechnet werden können, sind 7-stellige Codes, im Gegensatz zu 8-stelligen, leicht eingerückt. Das bisherige "Wackeln" der einzelnen Zeichen habe ich beibehalten. Aber auch Kippen (skew) wäre alternativ möglich.

    Falls keine Fonts vorhanden (interner Font), wird noch unter dem Code ein Rechteck untergelegt, weil der Text sonst nicht lesbar wäre. Die genauen Koordinaten sind dann aber viel stärker dem Zufall überlassen.

  • Schließlich werden noch beliebige Pixel über alles gestreut (Ich nenne es "Salz"). Die können jede der bisher verwendeten Farben annehmen oder auch irgendeine beliebig andere.

  • Zu guter Letzt wird in 50% der Fälle das ganze Bild noch invertiert. Dann ist die Schrift heller als der Hintergrund.

Übrigens sollte die nächste Version nicht Tahoma mitliefern, finde ich. Vorzugsweise eher seltene Schrift(en), besonders keine typischen Windows-Schrift. Idealerweise mindestens eine Serif und eine Sans-Serif.

Martin

Avatar

Captcha

by Auge ⌂, Friday, February 12, 2016, 17:11 (2989 days ago) @ Martin66

Hallo

Dir ist hoffentlich klar, dass der Eintrag im Zweifelsfall ohne den Aufruf des Formulars erfolgen kann?


Oh, nein, das wusste ich nicht. Wie geht das (Antwort darauf notfalls per PN)?

Ein Request kann grundsätzlich erst einmal von überall kommen. Man muss nur einen GET- oder POST-Request absetzen. Wenn die Struktur stimmt, kann das empfangende Skript ja etwas damit anfangen. Das hiesige Skript prüft das Vorhandensein und den Wert bestimmter Felder. Inwieweit das einen Eintrag am Formular vorbei verhindert, weiß ich aus dem Stehgreif nicht.

Kann man das irgendwie unterbinden, z.B. per .htaccess?

Nein, so geht das (zumindest ohne große Änderungen) nicht.

Das Captcha wird wie folgt erstellt:

Interessant

Übrigens sollte die nächste Version nicht Tahoma mitliefern, finde ich. Vorzugsweise eher seltene Schrift(en), besonders keine typischen Windows-Schrift. Idealerweise mindestens eine Serif und eine Sans-Serif.

Naja, ob die Schrift im realen Leben selten verwendet wird, ist mMn irrelevant. Spätestens wenn die Schrift irgendwoher bezogen werden kann, ist sie auch entzifferbar, egal, wie regelmäßig sie benutzt wird oder nicht. Die mitgelieferten Schriften sollte aber unter einer freien Lizenz stehen, damit es keinen Ärger gibt.

Tschö, Auge

--
Trenne niemals Müll, denn er hat nur eine Silbe!

RSS Feed of thread