I administer a music related forum and registrations from ‘bots’ were getting a little out of hand. On the forum’s registration page, I was using a CAPTCHA scheme that I made a very long time ago. Today I was reading up on how easily OCR technology can crack CAPTCHAs found on forms all over the Web. I realized that perhaps that this was one reason why I was getting so many spammy registrations. I thought that my CAPTCHA may need some updating so I came up with an entirely new scheme.
The basic idea is to present the user with a group of images of items with random numbers superimposed over each of the images.
The user has to enter the number that corresponds to one of the randomly chosen images to authenticate. Hopefully, a machine will not be able to discern a ‘boat’ from a ‘dog’.
It turned out to be relatively simple to implement. First, I grabbed a bunch of images of ‘things’… a bird, a dog, an umbrella, a banana, a car, a cat, etc. from around the Web. I wrote a PHP class that generates a composite image from 9 smaller ones and derives a 3 digit code and the name of the thing in the photo.
So, here’s the code I came up with:
<?php // Written by Bob Scott // //www.negatron.org/ class confirmationImage { // Class constructor function confirmationImage() { $this->photoDir = './things/'; // with trailing slash - directory where the images are stored $this->textAngle = 25; $this->textPosX = 56; $this->textPosY = 96; $this->textFontSize = 20; $this->randFont = './fnt/arialbi.ttf'; // True Type font to use $this->writeDir = $_SERVER['DOCUMENT_ROOT'] . '/cImages'; // directory must be readable, writable and executable by web server user $this->writeURL = '/cImages'; } function makeImage() { $randomNumbers = array(); // Get all available photos if ($handle = opendir($this->photoDir)) { while (false !== ($file = readdir($handle))) { if ($file != "." && $file != ".." && !strstr($file,'php')) { $photos[] = $this->photoDir . $file; } } closedir($handle); } // Randomize photos shuffle($photos); // Get the first 9 random images. // Create a 3 digit number for each of the images // also derive the name of the image from the file name for ($i = 1 ; $i <= 9 ; $i++) { $im[$i] = @imagecreatefrompng($photos[$i]); $fontColor[$i] = imagecolorallocate($im[$i], rand(188,255), rand(188,255), rand(188,255)); $fontColorB[$i] = imagecolorallocate($im[$i], rand(0,120), rand(0,120), rand(0,120)); $rNum = rand(100,999); // never have duplicate random numbers while (!in_array($rNum, $randomNumbers)) { $randomNumbers[$i] = $rNum; $rNum = rand(100,999); } // get photo name from file name $photoNames[$i] = strtoupper(str_replace('.png', '', str_replace($this->photoDir, '', $photos[$i]))); // put numbers on each photo imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX-1, $this->textPosY-1, $fontColor[$i], $this->randFont, $randomNumbers[$i]); imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX+1, $this->textPosY+1, $fontColor[$i], $this->randFont, $randomNumbers[$i]); imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX-2, $this->textPosY-2, $fontColor[$i], $this->randFont, $randomNumbers[$i]); imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX+2, $this->textPosY+2, $fontColor[$i], $this->randFont, $randomNumbers[$i]); imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX, $this->textPosY, $fontColorB[$i], $this->randFont, $randomNumbers[$i]); } // Make final composite image $imf = imagecreatetruecolor(300, 300); $cc = 1; for ($x = 0 ; $x < 300 ; $x += 100) { for ($y = 0 ; $y < 300 ; $y += 100) { imagecopy ($imf, $im[$cc], $x, $y, 0, 0, 100, 100); $cc++; } } // What number / photo combination shall we use?? $crn = rand(1,9); $this->photo_name = $photoNames[$crn]; $this->photo_number = $randomNumbers[$crn]; $ffn = uniqid('cpt_') . '.png'; $fFile = $this->writeDir . '/' . $ffn; $this->fURL = $this->writeURL . '/' . $ffn; imagepng ($imf, $fFile); imagedestroy($imf); for ($i = 1 ; $i <= 9 ; $i++) { imagedestroy($im[$i]); } return TRUE; } } ?>
To access this class, just do something like this in your PHP script that generates your registration page:
// make confirmation string include_once('inc_class_image_confirmation.php'); $cm = new confirmationImage; $cm->makeImage(); $code = $cm->photo_number; $confirm_image = '<img src="' . $cm->fURL . '" alt="" />'; $picName = $cm->photo_name;
You will want to store the value of the variable $code on the backend… either in a cookie variable, a session variable or even a database for later access. You will the want to display $confirm_image and $picName to the user. The user’s input must match $code when error checking your registration page.
When setting up everything, make a directory that is readable, writable and executable by the Web server user toy hold the confirmation images. On *nix machines, a simple
chmod 777 cImages
should suffice. I made the individual images all PNG format and resized them to 100×100 pixels. Name the individual files with the name of the object in the photo. For example, the photo of the bird is ‘bird.png’. I purposely choose photos of items that have one word in their name… though ‘rubber ducky’ was awfully tempting. Also, your PHP installation must have GD with PNG support built in or this will not work.
Here’s a directory listing of the images I wound up choosing:
$ ls -1 ball.png banana.png bird.png boat.png bread.png car.png cat.png clock.png cloud.png computer.png cup.png dog.png drum.png flower.png guitar.png hat.png key.png knife.png lamp.png pencil.png phone.png shirt.png shoe.png spoon.png tree.png umbrella.png
A few thoughts while building this. I realize that users that are visually impaired will not be able to use this CAPTCHA scheme. If you decide to implement something like this for your Web site, please take note of this important fact. I really tried to make all the items in the photos as simple as possible in hopes that non-native English speakers will still be able to use the scheme with a super basic understanding of English. My forum is in English anyways.
Also, the directory with the confirmation images will fill up over time. You might want to run a cron job that cleans those up from time to time:
# remove stale confirmation images 0 20 * * * /usr/bin/find /usr/local/apache/htdocs/cImages -type f -mmin +360 -exec rm -rf {} \; >/dev/null 2>&1
This sort of CAPTCHA alternative has possibly been done before but this is my take on it. Let me know what you think!