200) { // not a plausible character width - probably not a GD font at all error_log("freeCap: implausible char width in font file: ".$font_locations[$i]); continue; } $usable_fonts[] = $font_locations[$i]; $font_widths[] = $header['width']; } $font_locations = $usable_fonts; // without a font we can't draw the word at all, so say so plainly // instead of emitting a mangled image if(sizeof($font_locations)==0) { error_log("freeCap: no usable GD font files found - check paths/permissions"); $im = ImageCreate(200,50); $bg = ImageColorAllocate($im,255,255,255); $red = ImageColorAllocate($im,255,0,0); ImageString($im,3,10,18,"CAPTCHA config error",$red); sendImage($im); } // modify image width depending on maximum possible length of word // you shouldn't need to use words > 6 chars in length really. // intval: the average font width is fractional, and GD image dimensions // must be ints on PHP8 $width = intval($max_word_length*(array_sum($font_widths)/sizeof($font_widths))+75); $height = 90; $im = ImageCreate($width, $height); $im2 = ImageCreate($width, $height); ////////////////////////////////////////////////////// ////// Avoid Brute Force Attacks: ////////////////////////////////////////////////////// if(empty($_SESSION['freecap_attempts'])) { $_SESSION['freecap_attempts'] = 1; } else { $_SESSION['freecap_attempts']++; // if more than ($max_attempts) refreshes, block further refreshes // can be negated by connecting with new session id // could get round this by storing num attempts in database against IP // could get round that by connecting with different IP (eg, using proxy servers) // in short, there's little point trying to avoid brute forcing // the best way to protect against BF attacks is to ensure the dictionary is not // accessible via the web or use random string option if($_SESSION['freecap_attempts']>$max_attempts) { $_SESSION['freecap_word_hash'] = false; $bg = ImageColorAllocate($im,255,255,255); ImageColorTransparent($im,$bg); $red = ImageColorAllocate($im, 255, 0, 0); // depending on how rude you want to be :-) //ImageString($im,5,0,20,"bugger off you spamming bastards!",$red); ImageString($im,5,15,20,"service no longer available",$red); sendImage($im); } } ////////////////////////////////////////////////////// ////// Functions: ////////////////////////////////////////////////////// function make_seed() { // from http://php.net/srand // cast to int: the seed functions take an int, and passing a float is // deprecated on PHP8 (which emits a notice that corrupts the image output) list($usec, $sec) = explode(' ', microtime()); return (int) ((float) $sec + ((float) $usec * 100000)); } function rand_color() { global $bg_type,$rand_func; if($bg_type==3) { // needs darker colour.. return $rand_func(10,100); } else { return $rand_func(60,170); } } function myImageBlur($im) { // w00t. my very own blur function // in GD2, there's a gaussian blur function. bunch of bloody show-offs... :-) $width = imagesx($im); $height = imagesy($im); $temp_im = ImageCreateTrueColor($width,$height); $bg = ImageColorAllocate($temp_im,150,150,150); // preserves transparency if in orig image ImageColorTransparent($temp_im,$bg); // fill bg ImageFill($temp_im,0,0,$bg); // anything higher than 3 makes it totally unreadable // might be useful in a 'real' blur function, though (ie blurring pictures not text) $distance = 1; // use $distance=30 to have multiple copies of the word. not sure if this is useful. // blur by merging with itself at different x/y offsets: ImageCopyMerge($temp_im, $im, 0, 0, 0, $distance, $width, $height-$distance, 70); ImageCopyMerge($im, $temp_im, 0, 0, $distance, 0, $width-$distance, $height, 70); ImageCopyMerge($temp_im, $im, 0, $distance, 0, 0, $width, $height, 70); ImageCopyMerge($im, $temp_im, $distance, 0, 0, 0, $width, $height, 70); // remove temp image ImageDestroy($temp_im); return $im; } function sendImage($pic) { // output image with appropriate headers global $output,$im,$im2,$im3; header(base64_decode("WC1DYXB0Y2hhOiBmcmVlQ2FwIDEuNCAtIHd3dy5wdXJlbWFuZ28uY28udWs=")); switch($output) { // add other cases as desired case "jpg": header("Content-Type: image/jpeg"); ImageJPEG($pic); break; case "gif": header("Content-Type: image/gif"); ImageGIF($pic); break; case "png": default: header("Content-Type: image/png"); ImagePNG($pic); break; } // kill GD images (removes from memory) // these may not all exist yet if we bail out early (eg config error), // and passing a non-image to ImageDestroy is fatal on PHP8 if(!empty($im)) { ImageDestroy($im); } if(!empty($im2)) { ImageDestroy($im2); } ImageDestroy($pic); if(!empty($im3)) { ImageDestroy($im3); } exit(); } ////////////////////////////////////////////////////// ////// Choose Word: ////////////////////////////////////////////////////// $word = ""; if($use_dict==1) { // load dictionary and choose random word // file() returns false if the dictionary is missing/unreadable, which on // PHP8 makes sizeof() and mt_rand() throw - so check before using it $words = @file($dict_location, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if(is_array($words) && sizeof($words)>0) { $word = strtolower($words[$rand_func(0,sizeof($words)-1)]); // cut off line endings/other possible odd chars $word = preg_replace("/[^a-z]/","",$word); } else { error_log("freeCap: dictionary missing or empty (".$dict_location.") - falling back to random word"); } // might be large file so forget it now (frees memory) $words = ""; unset($words); } // covers both $use_dict==0 and a dictionary that gave us nothing usable // (eg a word that was entirely non-alphabetic, which would render as blank) if($word=="") { // based on code originally by breakzero at hotmail dot com // (http://uk.php.net/manual/en/function.rand.php) // generate pseudo-random string // doesn't use ijtf as are easily mistaken // I'm not using numbers because the custom fonts I've created don't support anything other than // lowercase or space (but you can download new fonts or create your own using my GD fontmaker script) $consonants = 'bcdghklmnpqrsvwxyz'; $vowels = 'aeuo'; $word = ""; // max() guards against $max_word_length<5, which would make mt_rand throw // on PHP8 (min must be <= max) $wordlen = $rand_func(5,max(5,$max_word_length)); for($i=0 ; $i<$wordlen ; $i++) { // don't allow to start with 'vowel' if($rand_func(0,4)>=2 && $i!=0) { $word .= $vowels[$rand_func(0,strlen($vowels)-1)]; } else { $word .= $consonants[$rand_func(0,strlen($consonants)-1)]; } } } // save hash of word for comparison // using hash so that if there's an insecurity elsewhere (eg on the form processor), // an attacker could only get the hash // also, shared servers usually give all users access to the session files // echo `ls /tmp`; and echo `more /tmp/someone_elses_session_file`; usually work // so even if your site is 100% secure, someone else's site on your server might not be // hence, even if attackers can read the session file, they can't get the freeCap word // (though most hashes are easy to brute force for simple strings) $_SESSION['freecap_word_hash'] = $hash_func($word); ////////////////////////////////////////////////////// ////// Fill BGs and Allocate Colours: ////////////////////////////////////////////////////// // set tag colour // have to do this before any distortion // (otherwise colour allocation fails when bg type is 1) $tag_col = ImageColorAllocate($im,10,10,10); $site_tag_col2 = ImageColorAllocate($im2,0,0,0); // set debug colours (text colours are set later) $debug = ImageColorAllocate($im, 255, 0, 0); $debug2 = ImageColorAllocate($im2, 255, 0, 0); // set background colour (can change to any colour not in possible $text_col range) // it doesn't matter as it'll be transparent or coloured over. // if you're using bg_type 3, you might want to try to ensure that the color chosen // below doesn't appear too much in any of your background images. $bg = ImageColorAllocate($im, 254, 254, 254); $bg2 = ImageColorAllocate($im2, 254, 254, 254); // set transparencies ImageColorTransparent($im,$bg); // im2 transparent to allow characters to overlap slightly while morphing ImageColorTransparent($im2,$bg2); // fill backgrounds ImageFill($im,0,0,$bg); ImageFill($im2,0,0,$bg2); if($bg_type!=0) { // generate noisy background, to be merged with CAPTCHA later // any suggestions on how best to do this much appreciated // sample code would be even better! // I'm not an OCR expert (hell, I'm not even an image expert; puremango.co.uk was designed in MsPaint) // so the noise models are based around my -guesswork- as to what would make it hard for an OCR prog // ideally, the character obfuscation would be strong enough not to need additional background noise // in any case, I hope at least one of the options given here provide some extra security! $im3 = ImageCreateTrueColor($width,$height); $temp_bg = ImageCreateTrueColor(intval($width*1.5),intval($height*1.5)); $bg3 = ImageColorAllocate($im3,255,255,255); ImageFill($im3,0,0,$bg3); $temp_bg_col = ImageColorAllocate($temp_bg,255,255,255); ImageFill($temp_bg,0,0,$temp_bg_col); // we draw all noise onto temp_bg // then if we're morphing, merge from temp_bg to im3 // or if not, just copy a $widthx$height portion of $temp_bg to $im3 // temp_bg is much larger so that when morphing, the edges retain the noise. if($bg_type==1) { // grid bg: // draw grid on x for($i=$rand_func(6,20) ; $i<$width*2 ; $i+=$rand_func(10,25)) { ImageSetThickness($temp_bg,$rand_func(2,6)); $text_r = $rand_func(100,150); $text_g = $rand_func(100,150); $text_b = $rand_func(100,150); $text_colour3 = ImageColorAllocate($temp_bg, $text_r, $text_g, $text_b); ImageLine($temp_bg,$i,0,$i,$height*2,$text_colour3); } // draw grid on y for($i=$rand_func(6,20) ; $i<$height*2 ; $i+=$rand_func(10,25)) { ImageSetThickness($temp_bg,$rand_func(2,6)); $text_r = $rand_func(100,150); $text_g = $rand_func(100,150); $text_b = $rand_func(100,150); $text_colour3 = ImageColorAllocate($temp_bg, $text_r, $text_g, $text_b); ImageLine($temp_bg,0,$i,$width*2, $i ,$text_colour3); } } else if($bg_type==2) { // draw squiggles! $bg3 = ImageColorAllocate($im3,255,255,255); ImageFill($im3,0,0,$bg3); ImageSetThickness($temp_bg,4); for($i=0 ; $i= 80000) { ImagePolygon($temp_bg,$points,$text_colour3); } else { ImagePolygon($temp_bg,$points,intval(sizeof($points)/2),$text_colour3); } } } else if($bg_type==3) { // take random chunks of $bg_images and paste them onto the background for($i=0 ; $i$prev_y-2); ImageCopy($im, $im2, $i, $y_pos, $i, 0, $font_pixelwidth, $height); // for debug: //ImageRectangle($im,$i,$y_pos+10,$i+$font_pixelwidth,$y_pos+70,$debug); } // for debug: //sendImage($im); ImageFilledRectangle($im2,0,0,$width,$height,$bg2); // randomly morph each character individually on x-axis // this is where the main distortion happens // massively improved since v1.2 $y_chunk = 1; $morph_factor = 1; $morph_x = 0; for($j=0 ; $j would be sent before the image headers