Crear un captcha básico con PHP y GD explicado desde cero

El 16/04/2012, en Captcha, por hecky 12.220 veces visto

Hoy vamos a crear un captcha MUY CASERO y básico en PHP con la ayuda de la libreria GD. Vamos a ir explicando paso a paso cada pedazo de código y al final de la lectura espero que todos se vayan con una sonrisa habiendo aprendido correctamente como se hace.

En varias entradas de mi blog ya hemos usado esta libreria GD. Pero hoy recurriremos de nuevo a ella de una manera muy fácil.

El objetivo sera crear un captcha “presentable”, Legible y de alguna manera “seguro”. Lo dividiremos en 8 etapas (8 captchas) para obtener un unico resultado.

Captcha 1

Lo primero que debemos hacer es crear nuestro recurso que es una imagen, para lo que utilizaremos una función llamada “imagecreatetruecolor” a la cual solo debemos definirle ancho y alto de la imagen deseada.

imagecreatetruecolor nos permite crear una imagen que maneje transparencias y ciertos tipos de fuentes de letra (Posteriormente se utilizaran)
imagecreate Bien podriamos usar esta función que es mas sencilla, pero que no nos da las funcionalidades de imagecreatetruecolor

1
2
3
$ancho=100;
$alto=55;
$image=imagecreatetruecolor($ancho,$alto);

Una vez creado el recurso vamos a pintar esa rectángulo con un color de fondo. Este color de fondo sera aleatorio. Para ello usaremos la función imagecolorallocate la cual define un color del modo RGB.

imagecolorallocate($recurso,$R,$G,$B); De esta manera definiremos los colores deseados.

Como se quiere que sea variable el color, usaremos la funcion rand() (Que de ahora en adelante es nuestra mejor amiga).

rand($MinVal,$MaxVal); A la función rand() se le define el rando de valores que puede tomar. En el caso de los colores RGB solo pueden ir de 0 al 255.

1
2
3
4
$rgb[0] = rand(0,255);
$rgb[1] = rand(0,255);
$rgb[2] = rand(0,255);
$RandomColor=imagecolorallocate($image,$rgb[0],$rgb[1],$rgb[2]);

Ahora solo debemos pintar nuestra imagen. Para esta simple tarea usaremos la funcion imagefill() que rellena con un color

imagefill($recurso,$X,$Y,$color); Pinta la imagen a partir de una cordenada XY de un determinado color.

1
imagefill($image,0,0,$RandomColor);

Al final damos salida a la imagen para que se muestre en el navegador. Para eso usaremos las funciones header() imagepng() e imagedestroy()

header(“Content-type: image/png”); Le diremos al navegador como interpretar el recurso imagen/png
imagepng($recurso); La imagen sera una imagen PNG
imagedestroy($recurso); Destruimos del buffer la imagen

Nuestro codigo final de esta primera imagen es:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//Crear $ancho=100; $alto=55;
$image=imagecreatetruecolor($ancho,$alto);
//Colores
$rgb[0] = rand(0,255);
$rgb[1] = rand(0,255);
$rgb[2] = rand(0,255);
$RandomColor=imagecolorallocate($image,$rgb[0],$rgb[1],$rgb[2]);
//Fondo
imagefill($image,0,0,$RandomColor);
//Salida
header("Content-type: image/png");
imagepng($image);
imagedestroy($image);
?>

Y resulta en la siguiente imagen:

Captcha 2

NOTA: De aquí en adelante los códigos se AGREGARAN a lo ya realizado.

Ahora vamos a crearle a nuestro captcha un marco de color negro. Para esta tarea usaremos la función imageline Y claro definiremos el color negro =)

imageline($recurso,$x1,$y1,$x2,$y2); Crea una linea definiendle el principio y final de la linea.

1
2
3
4
5
6
$negro=imagecolorallocate($image,0,0,0);
//Marco
imageline($image,0,0,$ancho,0,$negro);
imageline($image,0,0,0,$alto,$negro);
imageline($image,$ancho-1,$alto-1,0,$alto-1,$negro);
imageline($image,$ancho-1,$alto-1,$ancho-1,0,$negro);

Aquí creamos nuestro marco con cuatro lineas.

Captcha 3

Ya que vimos como hacer lineas, metamos un poco de ruido al captcha. Para esto meteremos una simple rejilla con puras lineas. Y ahora agregaremos un tipo de color gris

1
2
3
4
5
6
7
8
$gray=imagecolorallocate($image,100,100,100);
//Rejilla
imageline($image,25,0,25,$alto,$gray);
imageline($image,50,0,50,$alto,$gray);
imageline($image,75,0,75,$alto,$gray);
imageline($image,0,13,$ancho,13,$gray);
imageline($image,0,26,$ancho,26,$gray);
imageline($image,0,39,$ancho,39,$gray);

Captcha 4

Es hora de agregar una simple cadena de Texto al captcha. Ahora usaremos la función imagestring()

imagestring($recurso,$Tamaño,$X,$Y,$Cadena,$color); El tamaño va de 1 a 5, se define la posicion XY donde se inicia la cadena, posteriormente la cadena y por ultimo el color de la cadena

1
2
//TextoSimple
imagestring($image,2,1,41,"@hecky",$negro);

Captcha 5

Ahora empieza lo divertido. Agregaremos otra cadena que sera el código variable del captcha. Para esto usaremos varias funciones rand(), md5(), strtoupper(), str_replace() y substr().

Explicare por puntos que haremos con cada función:

  1. Crear un numero Aleatorio del 9999 al 99999 con rand(9999,99999);
  2. Usaremos la función md5($recurso); para que el resultado del numero aleatorio nos regrese una cadena mas larga y que nos incluirá letras
  3. Ahora esa cadena la convertiremos a Mayúsculas con la función strtoupper($recurso);
  4. En el siguiente captcha usaremos una fuente de letra especial, y con este tipo de fuente no se distinguen bien los 0 y O, por lo que los remplazare con nada con la funcion str_replace($Acambiar,$Cambio,$Recurso);
  5. Al final no quiero la cadena de 32 caracteres que nos devuelve el md5, por lo que solo obtendre los 5 primeros caracteres con la función substr($recurso,$inicio,$final);

Nuestro código que nos devolverá una cadena variable sera:

1
2
//TextoRandom
$random=substr(str_replace("0","",str_replace("O","",strtoupper(md5(rand(9999,99999))))),0,5);

Y ya al final solo pintamos la cadena el la imagen, de color negro y centrada;

1
imagestring($image,5,30,17,$random,$negro);

Captcha 6

VAMOS A DARLE COLOR A ESA CADENA VARIABLE!!! En el anterior código pintamos la cadena de color negro, pero ese color estático se puede perder cuando nuestro fondo aleatorio tome un color obscuro. Por ello ahora idearemos una forma de que siempre contrasten nuestros colores.

Para esta tarea lo que haremos sera tomar el valor aleatorio del fondo y se lo restaremos a 255 (que es el valor maximo que puede llegar a tomar).

Ej:

Si nuestro fondo es 255,255,255 -> Blanco haremos esto:
255-255,255-255,255-255 -> 0,0,0 (Negro) Aqui nuestro fondo sera blanco y nuestro texto negro.

Por el contrario si:

Nuestro fondo es 0,0,0 -> Negro:
255-0,255-0,255-0 -> 255,255,255 (Blanco) Fondo Negro, letras blancas.

Se esta no es la mejor solución y mas aún cuando los valores se acerquen a la mitad de 255, pero para estos fines si nos ayudara.

Nustro nuevo color lo definiremos entonces de la siguiente manera:

1
$RandomColorInverted=imagecolorallocate($image,255-$rgb[0],255-$rgb[1],255-$rgb[2]);

Y claro cambiaremos al pintar la cadena de $negro a $RandomColorInverted

1
imagestring($image,5,30,17,$random,$RandomColorInverted);

Captcha 7

Ahora hagamos un poco mas agradable a la vista el captcha. Vamos a modificar el tipo de letra predeterminado de PHP y GD y usemos una fuente personalizada. En mi caso elegi la fuente gunplay.ttf y para hacer uso de ella ya no usaremos imagestring(), ahora usaremos imagefttext();

Para poder invocarla necesitaremos algunas cosas:

  1. Que nuestro servidor tenga las librerias de freetype
  2. Que nuestra imagen sea del tipo “truecolor
  3. La fuente .ttf que se desea utilizar.

Y la sintaxis es:

imagefttext($recurso,$tamaño,$angulo,$x,$y,$color,$fuente,$cadena); Lo que cambia aquí es que el Tamaño ya no esta limitado a 5. El ángulo nos permite darle un giro a nuestra letra y la $fuente es la dirección de nuestro ttf

1
2
3
//TextFont
$ttf = "./gunplay.ttf";
imagefttext($image,22,rand(-10,15),12,37,$RandomColorInverted,$ttf,$random);

Nuestra letra tendra un tamaño de 22, en $ttf definiremos la dirección y nombre de la fuente a usar y en el angulo, lo haremos variable, por lo que le metimos un rand(-10,15), para que gire de -10º a 15º, y de esta manera evitamos un texto completamente estático.

Captcha 8

ULTIMO PASO!! Ya que tenemos esto casi terminado, nos falta meter un poco de dificultad para la lectura, para ello agregaremos otro nivel de ruido.

Ahora en vez de lineas meteremos puntos, que sera pixeles aleatorios. Para esta tarea se usa la funcion imagesetpixel();

imagesetpixel($recurso,$x,$y,$color); Solo se le debe indicar la posición del pixel y el color

Como verán es demasiado sencillo. En nuestro caso vamos a meterle 700 pixeles en posiciones aleatorias, con el color que contrarresta. Para eso usaremos un simple bucle para que nos meta de un golpe los 700 pixeles.

1
2
3
4
5
6
//Ruido
for ($i=0;$i $randx=rand(0,100);
$randx=rand(0,100);
$randy=rand(0,55);
imagesetpixel($image,$randx,$randy,$RandomColorInverted);
}

En la parte de arriba claramente se ve como cada vez que se accede al ciclo, se toman nuevos valores aleatorios para posicionarlos y así dar el efecto de ruido.

Por ultimo Ya no queremos ese texto feo de “@hecky” asi que BORRAREMOS ESTA LINEA:

1
2
//TextoSimple
imagestring($image,2,1,41,"@hecky",$negro);

Y ya quedo!!!

CODIGO FINAL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
$ancho=100;
$alto=55;

//Colores
$image=imagecreatetruecolor($ancho,$alto);
$negro=imagecolorallocate($image,0,0,0);
$gray=imagecolorallocate($image,100,100,100);
$rgb[0] = rand(0,255);
$rgb[1] = rand(0,255);
$rgb[2] = rand(0,255);
$RandomColor=imagecolorallocate($image,$rgb[0],$rgb[1],$rgb[2]);
$RandomColorInverted=imagecolorallocate($image,255-$rgb[0],255-$rgb[1],255-$rgb[2]);

//Fondo
imagefill($image,0,0,$RandomColor);

//Marco
imageline($image,0,0,$ancho,0,$negro);
imageline($image,0,0,0,$alto,$negro);
imageline($image,$ancho-1,$alto-1,0,$alto-1,$negro);
imageline($image,$ancho-1,$alto-1,$ancho-1,0,$negro);

//Rejilla
imageline($image,25,0,25,$alto,$gray);
imageline($image,50,0,50,$alto,$gray);
imageline($image,75,0,75,$alto,$gray);
imageline($image,0,13,$ancho,13,$gray);
imageline($image,0,26,$ancho,26,$gray);
imageline($image,0,39,$ancho,39,$gray);

//TextoRandom
$random=substr(str_replace("0","",str_replace("O","",strtoupper(md5(rand(9999,99999))))),0,5);
//TextFont
$ttf = "./gunplay.ttf";
imagefttext($image,22,rand(-10,15),12,37,$RandomColorInverted,$ttf,$random);

//Ruido
for ($i=0;$i<=700;$i++){
$randx=rand(0,100);
$randy=rand(0,55);
imagesetpixel($image,$randx,$randy,$RandomColorInverted);}
//Salida
header("Content-type: image/png");
imagepng($image);
imagedestroy($image);
?>

Captcha Final

Bonito ¿verdad? Se que no es el gran catpcha, pero mínimo es agradable a la vista, en tres pruebas de ataque de OCR resistió (después veremos esto), esta mejor que muchos captchas que ofrecen por ahí, y lo mas importante de todo es que LO HICIMOS NOSOTROS MISMOS.

Después de explicar todo esto, no creo quede alguna duda, pero en todo caso pueden preguntar. Hoy tuvimos un gran acercamiento al manejo de Imágenes con PHP y GD y creamos nuestro primer captcha casero. Lo demás ya solo es cosa de obtener el parámetro y validar sesión (Pero eso ya se los dejo a ustedes).

Sin mas espero les agradara el articulo.

Saludos ;)

hecky@neobits.org
Sigueme en Twitter: http://twitter.com/hecky

 
  • http://twitter.com/_jcvg_ Julio Vidal

    excelente tutorial, pero con algunos puntos que podrían provocar que sea crackeable.

    Ante todo utilizar una semilla fiable para la ejecución del script y evitar que por algún error en otro script vallan a predecir los rand que genera el servidor, un fallo un poco difícil mas no imposible ya que si se logra ubicar algún otro script en la cual se conozca o controle la semilla y peticiones con keep-live controlarías todos los rand subsecuentes.

    Por otra parte el ruido extra al ser del mismo color que las letras te causan una debilidad extra en el captcha ya que al ser pixeles dispersos y del mismo color que las letras seria relativamente sencillo ubicar el color de los mismos y limpiar todos los pixeles diferentes de ese color a blanco y de ahí proceder a limpiar un poco mas.

    Como medida extra estaría bien dibujar letra por letra con diferente color y diferente inclinación.

    • http://neobits.org hecky

      Hola Julio Vidal

      Muchas gracias por tus comentarios, Tienes mucha razón en cuanto a que la imagen es muy fácil de limpiar, no se si ver tanta vulnerabilidad como tu que el color de los pixeles dispersos sea igual al de las letras. Ya que si por ejemplo eliminas esos pixeles de ese color borrarias las letras. Pero se puede hacer por localización y limpiarla bastante aceptable. En cuanto a la semilla yo no uso fuera del script, pero estoy de acuerdo que la manera que hago la cadena variable es algo muy burdo.

      Recuerda que el tutorial es para los que se inician, no quería meter tantas cosas y funciones que resultaran un poco difíciles de entender, al final esta es la guia y ustedes la mejorar tanto como gusten =)

      Y concuerdo en que lo mejor seria, letra por letra, posición aleatoria, con angulo aleatorio y color variable =) (y que tal con fuente aleatoria?)

      Saludos ;)

      • http://twitter.com/_jcvg_ Julio Vidal

        A lo del rand lo que te comentaba era lo siguiente, 

        Si en el sitio hubiera otro script N con una semilla predecible o controlable por decir

        //dummy srand ( $ _ GET [ 'wever' ] ) rand (0,100)

        Al no tener en tu script captcha.php un srand( blablabla_awesome_blablabla).

        El atacante podria hacer 
        GET /dummy.php?wever=5 –> keep alive –> procesa respuesta –>GET /captcha.php

        en alguna maquina controlada simulas los scripts con la misma cantidad de rands y obtienes el mismo resultado ya que el rand inicializa la semilla por conexion no por script y con un keep alive y controlando o prediciendo la semilla solo te faltaria saber cuantos rands se ejecutan en el primer script lo cual podrias simular a prueba y error hasta obtener un resultado equiparable y deduces el captcha en tu maquina controlada simulando los valores del servidor objetivo.

        “ no se si ver tanta vulnerabilidad como tu que el color de los pixeles dispersos sea igual al de las letras”

        No me refiero a eliminarlos sino todo lo contrario. Debido a que son pixeles dispersos son mas faciles de localizar por q mas de alguno quedo totalmente aislado y sacando el color de los pixeles aislados limpias toda la imagen sin importar el demas ruido, todo lo que sea diferente a ese color lo vuelves blanco y listo, con una funcion que recorra la imagen y en base a lo aislado del pixel, a prueba y error para determinar el mejor valor de aislamiento, borrarias la mayoria de pixeles aislados dejando casi limpia la imagen.

        Saludos,

        • http://neobits.org hecky

          Compañero el ataque que usted propone es muy bueno, de hecho hace ya tiempo se mostró que no están random y es predecible.

          Pero en este caso a comparación de su ataque propuesto, no son las mismas condiciones, no se envía nada por GET (de hecho ni envió nada) ni se estan imprimiendo los valores rand…si no que se saca el md5sum. Y se toma una sola parte. Eso para un atacante es mas dificil, por que tendria que ver de que valor a que valor es el rand, luego sacar el md5sum de eso, y ver que solo se coje una sola parte.Un atacante no puede adivinar facilmente ese intervalo, tampoco puede adivinar de que manera se esta sacando la cadena (Md5, SHA, crc32, de un array,etc…) y tampoco puede saber que posiciones de la cadena estoy sacando.Como vera su ataque es muy hipotetico pero este no es el caso.

          Lo de limpiar la imagen es aun mas facil de lo que usted plantea, e inclusive seria mas facil si uno adivina el tipo de tipografia, y con eso el ataque se vuelve pan comido.

          La funcion del post es solo mostrar el manejo de la libreria GD para la creacion de imagenes dinamicas, no de la seguridad de un captcha con validaciones. Por eso es que en este momento no meti validaciones, para enfocarnos nada mas en la creacion de la imagen.

  • http://www.facebook.com/profile.php?id=100001087066779 Jack Skeleton

    Tengo un par de dudas en tu algoritmo para crear la cadena aleatoria:

    $random=substr(str_replace(“0″,”",str_replace(“O”,”",strtoupper(md5(rand(9999,99999))))),0,5);

    ¿Por qué reemplazas la “O” del md5? Si se supone que md5 no tiene O, tiene numeros de 0 al 9 y letras de la ‘a’ a la ‘f’ (valores hexadecimales).

    ¿Por qué reemplazas el 0? Supongo que es para evitar que las personas lo confundan con una ‘o’, no estoy seguro.

    Propongo usar mt_rand() en vez de rand() porque se supone que es más rápido (4 veces más rápido), aun que en la actualidad no se ve mucha diferencia real.

    También propongo que se evite usar md5 ya que de los 32 caracteres solo tomaras 5, usa algo más rápido y corto como crc32 o crc32b que brindan 8 caracteres, son más rápidos.

    De esa forma, la propuesta que tengo para generar la cadena aleatoria es:

    $random = substr(strtoupper(hash(‘crc32′,uniqid(mt_rand() . mt_rand(), true),false)),0,5);

    Aquí te dejo una breve demostración de velocidad, y buen trabajo, me gustó tu publicación.

    <?
    $start = startTime();
    $random1=substr(str_replace(“0″,”",str_replace(“O”,”",strtoupper(md5(rand(9999,99999))))),0,5);
    echo $random1 .” – “.number_format(endTime($start), 10, ‘.’, ‘,’);

    echo “”;

    $start = startTime();
    $random2=substr(strtoupper(hash(‘crc32′,uniqid(mt_rand() . mt_rand(), true),false)),0,5);
    echo $random2 .” – “.number_format(endTime($start), 10, ‘.’, ‘,’);

    function startTime() {
    $mtime = microtime();
    $mtime = explode(” “,$mtime);
    $mtime = $mtime[1] + $mtime[0];
    return $mtime;
    }

    function endTime($starttime) {
    $mtime = microtime();
    $mtime = explode(” “,$mtime);
    $mtime = $mtime[1] + $mtime[0];
    $endtime = $mtime;
    return $totaltime = ($endtime – $starttime);
    }
    ?>

    http://pastebin.com/2Ruqkpwq

    • http://neobits.org hecky

      Hola!!

      Pff la verdad lo del str_replace de la O se me paso por completo xDDDDDDTodo lo demas ya es cuestion de gustos, y cada quien puede hacerlo de la mejor manera. En lo personal yo no lo haría así xD

      • http://www.facebook.com/profile.php?id=100001087066779 Jack Skeleton

         Sí, es cosa de gustos, ¿podrías poner el ejemplo completo de uso con su formulario de verificación?

        • http://neobits.org hecky

          Hola @facebook-100001087066779:disqus  creo hare una serie de entradas sobre captchas, la validacion la dejamos para otra vale?

  • http://twitter.com/jantonioCalles Juan Antonio Calles

    Muy bueno @neobits:disqus ! lo voy a poner en práctica, muchas gracias :)

    • http://neobits.org hecky

      Gracias Juanan y mas aun por la visita ;)

  • Javi

    Gran tutorial, yo acabo de hacerme un sitio web para escribir cualquier frase como si fuera un captcha http://yourcaptcha.com y me sirvió de mucha ayuda la librería opensource que encontré, cool php captcha, en la que utiliza unas ecuaciones más complicadas para generar los típicos captcha que devuelven herramientas como reCaptcha.

    Sobre el tema de seguridad, aquí nos estamos centrando más en generar la imagen no??

    • http://neobits.org hecky

      Claro, aqui me centro nada mas en la generación de la imagen dinamica con la libreria GD de PHP =)

      Solo son ejemplos, de la imagen. Se tendría que meter mucha mas seguridad desde la generación de cadena, la aleatoriedad, la sesión, el tiempo, y la validación.
      Saludos ;)

  • ncw2233

    Buen tutorial, sencillo y claro.
    Saludos.

    • http://neobits.org hecky

      Gracias!!

  • Pingback: Nueva versión de Flu b0.5.2

  • Elena4

    no te imaginas cuantos emails recibo sobre usuarios que no pueden resolver el captcha de mi blog

    • http://neobits.org hecky

      =P Y este se le hace mas legible?

  • Pepon2391

    Joder, aún no he empezado la carrera y sólo de leerte ya me entran ganas de programar, eres un auténtico maestro. Gracias por TODA LA PÁGINA y porque tus “tutoriales” son auténticas obras de arte.

    • http://neobits.org hecky

      Hola Pepon2391

      Nada de maestro =S y me alegra se pase por acá, y sus palabras son inmerecidas.

      Si me permite un consejo, NO NECESITA ENTRAR A LA CARRERA PARA EMPEZAR A ESTUDIAR, yo empece cerca de 2 años antes de entrar a la carrera =)

  • http://twitter.com/acruzgomez/status/259212414476763136/ @acruzgomez

    Cómo crear un c#aptcha MUY CASERO, en PHP con la ayuda de la libreria #GD. http://t.co/tEtM0pNC

  • http://www.facebook.com/swtsecuritty Systcm Swt

    Estimado empece a tener dudas que significa $rgb 

    • http://neobits.org hecky

      $rgb es un valor entero aleatorio que puede ir del 0 al 255…

      y rgb significa Red-Green-Blue =P

  • John Carlos

    Amigo muy practica tu explicacion, pero yo quiero colocar la imagen en una posicion x, y tu me prodrias ayudar, yo estoy trabajando con imagefilledarc e imagepng

    • http://neobits.org hecky

      No entendi exactamente que quiere hacer??

  • Fer

    Excelente tutorial te felicito!. Tengo un Captcha muy elemental, que no se le puede modificar el tamaño, ni tipo de fuente. Muy didáctico y estéticamente bonito. Espero saber como probarlo, no sé mucho sobre cómo lamar a la función y sesiones. ¡Muchas gracias por tu aporte y nuevamente..Felicitaciones!.

    • http://neobits.org hecky

      Muchas Gracias Fer!!!

  • http://www.facebook.com/remyrivera Remy Rivera

    Muy bueno…….

    • http://neobits.org hecky

      Gracias Remy