Cómo funciona un CAPTCHA

imagen de recaptcha

CAPTCHA es el acrónimo de Completely Automated Public Turing test to tell Computers and Humans Apart, traducido como prueba de Turing completamente pública y automática para diferenciar los ordenadores y humanos. En las pruebas de formulario de contacto del tema anterior intentaba evitar que el usuario insertara cabeceras no deseadas en el email. Con eso quería que no se utilizara el formulario para enviar múltiples correos, a modo de SPAM. Pero aún así es posible usar un programa que haga un montón de peticiones a nuestro sitio enviando formularios con datos y por tanto nos llenen el buzón de basura. Con la técnica del CAPTCHA intentaré impedir que esto suceda.

El sitio captcha.net ofrece servicio gratuito para incorporar este sistema en un formulario de nuestro sitio. La imagen anterior es como se vería un control de este tipo. El usuario tendrá que introducir las dos palabras que aparecen como una imagen y que verificará el servidor en www.captcha.net. En caso de que le resulte ilegible puede actualizarlas dándole otro conjunto de palabras para intentar descifrarlas.

La base de esta técnica es muy simple. Se trata de que a un ordenador, es decir, a un programa de ordenador le resulta bastante trabajaso extraer las palabras en forma de texto a partir de una imagen de ese texto. Se trata en parte de usar la técnica de reconocimiento de caracteres, OCR Optical character recognition. Aunque explotando aquellos aspectos que suponen más esfuerzo en OCR. Un uso de OCR es para extraer el texto de un documento de papel escaneado, observándose que el reconocimiento de caracteres puede verse alterado por cosas como:

  • Píxeles del fondo que no forman parte de los caracteres pueden alterarlos.
  • El documento contiene muchas fuentes de texto y al sistema le resulta díficil encontrar patrones para algunas fuentes poco comunes.
  • Los caracteres pueden aparecer deformados o le faltan partes.
  • La distancia entre caracteres puede verse alterada produciendo errores.
  • Los caracteres que aparecen juntos o incluso solapados son díficiles de separar.

Actualmente el reconocimiento de caracteres por parte de un programa informático es un problema no totalmente resuelto. Sin embargo a los humanos nos resulta más fácil alcanzar la solución a ese problema. Pero a medida que la informática avanza se va reduciendo esa distancia. Prueba de ello es que los primeros sistemas de CAPTCHA fueron rotos en su día y a medida que esos sistemas van incorporando aspectos más complejos también se van buscando nuevas técnicas para romperlos. Esto es importante de resaltar porque un CAPTCHA sólo tiene una utilidad: "Intentar saber que el usuario es un humano", pero las máquinas cada vez se parecen más a los humanos.

ejemplo captcha no usable Como dice la definición, un CAPTCHA debe ser automático y sobre todo debe ser público. Así el hecho de romper un CAPTCHA debe basarse en técnicas de inteligencia artificial más que en el conocimiento del algoritmo que genera el CAPTCHA. Pero no podemos perder de vista que un CAPTCHA no es invulnerable. Esto quiere decir que de un conjunto de CAPTCHAs podría ser resuelto alguna proporción de casos por una máquina, al menos en un tiempo comparable al que necesitaría un humano. Será más robusto si esa proporción es baja. Pero al mismo tiempo también debe tener la característica de usabilidad. Por ejemplo, la imagen de la izquierda contiene los caracteres aQ3mK y podría ser un buen candidato para cumplir la cualidad de robustez, pero es dificultoso para que un humano lo resuelva. Si cada vez más las máquinas son capaces de resolver los CAPTCHAs, habrá un momento en que ya no podrán ser más robustos pues dejarán de ser usables. Entonces habrá que usar otra cosa como CAPTCHAs basado en imágenes en lugar de texto.

Instalando GD2 de PHP

En el siguiente apartado presento como hice unas pruebas para hacer un sistema de CAPTCHA. Usaré la extensión de PHP GD image. Como dice la introducción del manual de PHP, con esa extensión se pueden crear y manipular archivos de imágenes en una variedad de formatos como GIF, PNG, JPEG entre otros. Algunas funciones de esa extensión como imagechar() que nos permite dibujar un caracter en la imagen nos servirán para crear el CAPTCHA de texto. Otras funciones como imagerotate() nos permitirán girar la imagen. También podemos aplicar filtros con imagefilter() pudiendo hacer cosas como contrastar, difuminar o superficializar la imagen. Con estos u otros filtros conseguiremos aplicar las deformaciones necesarias a la imagen del texto de nuestro CAPTCHA.

Podemos hacer pruebas con esa extensión en localhost, pero si al final vamos a poner un CAPTCHA en nuestro sitio en producción hemos de saber si tiene activada la extension GD (lo podemos saber viendo el info.php). En las instalaciones de PHP en Windows (como es mi caso en localhost para aprender), necesitamos que esta extensión haya sido instalada inicialmente. Si no es así podemos modificar la instalación tal como comento en este grupo de capturas de pantalla:

Instalando GD2 de PHP
Instalando GD de PHPImagen no disponible
En agregar o quitar programas de Windows podemos ir a la aplicación de PHP para hacer cambios

Un CAPTCHA con GD2 de PHP

captcha

Una forma de aprender cómo funciona un CAPTCHA es hacer un ejemplo. Tras probarlo intentaré ponerlo en el formulario de contacto de este sitio. El ejemplo lo haré primero para ser ejecutado en localhost. Se trata de una página PHP con un formulario que reenvía los datos al script en el mismo documento. En el último apartado de este tema comentaré algunas cosas sobre el script. Por ahora me detengo en los detalles de concepto. En la imagen de la izquierda aparece una captura de pantalla del formulario para hacer pruebas generando CAPTCHAs. El botón "Ver texto" y la cadena adjunta nos ofrece el texto a verificar mientras estamos haciendo pruebas, pero en una versión definitiva el texto a resolver no será enviado bajo ninguna circustancia al navegador del usuario. Sólo la respuesta correcta nos llegará al servidor. Y aún así habría que considerar la posibilidad de que pueda ser interceptada en el camino, pero esa es otra díficil cuestión que ni me atrevo a abordar por ahora. Estos son unos ejemplos de imágenes de CAPTCHA que se obtienen:

captcha captcha captcha

La robustez se basa en generar imágenes que contengan el menor número posible de invariantes. Estas son características que no varían entre una imagen y otra. Serían esas las que podrían tomarse como patrones para resolver el problema. El tema es interesante pero muy complejo y sobrepasa mis conocimientos. Pero hay un par de principios mínimos que tendré en cuenta al generar un texto CAPTCHA:

  • Generar cadenas con caracteres elegidos aleatoriamente.
  • Caracteres posicionados en el eje horizontal tratando de que se peguen entre ellos, buscando el equilibrio adecuado para evitar un exceso de solapamiento que dificulte la usabilidad.
  • Posicionamiento aleatorio de cada caracter en el eje vertical. Con esto se busca que los caracteres estén unidos entre sí no siempre por los mimos sitios.
  • La imagen final ha de tener el menor tamaño original posible, ajustándolo al tamaño de la fuente y aplicando un escalado final suficiente para no perder usabilidad.
  • La imagen se rota un cierto ángulo positivo o negativo, elegido al azar entre un rango predeterminado para deformar los caracteres.
  • La imagen se distorsiona aplicando filtros de difuminado, contraste o superficialización.

En estas dos imágenes se muestra una cadena sin aplicar ninguna deformación y como resulta con ellas:

captcha captcha

Se puede usar un conjunto de caracteres a predeterminar. En este ejemplo uso los rangos 0..9, a..z, A..Z. La cadena de texto se genera seleccionando aleatoriamente 6 caracteres, tamaño que se puede predeterminar también, adaptándose el script automáticamente a ese tamaño. Elegir al azar los caracteres tiene la desventaja de que pueden formarse combinaciones más débiles apareciendo caracteres aislados que no se conectan con los adyacentes. Pero la posibilidad de construir un diccionario de palabras robustas no me parece apropiado. Por un lado supone un mayor coste de recursos, pues habrá que cargar esa lista de palabras en alguna estructura como un array. Además las palabras resueltas podrían ser reutilizadas de alguna forma. Por lo tanto la solución pasa por generarlos aleatoriamente y corregir ciertas deficiencias en el script, como acercando más algunos caracteres como i,l,1 que tienen un ancho efectivo de caracter menor.

Se usa la fuente GD que viene por defecto con la extensión. Sería una mejora usar otras fuentes True Type e incluso poner varias de forma aleatoria. Pero hay que saber donde están instaladas en el servidor en producción. Y el script creo que debe basarse sólo en lo que genere dinámicamente, sin tener que tocar para nada recursos de disco que en principio podemos desconocer. Con eso lo hacemos más rápido y más fiable al no depender de recursos externos. La imagen final ni siquiera se guarda en archivo, como veremos más abajo.

Se puede actuar sobre la resolución del trazo de los caracteres, usando un tamaño de fuente más pequeño y luego escalando la imagen al presentarla. Cuanto más pequeña sea la imagen generada más robuto será el CAPTCHA, pues al aplicar luego el escalado la imagen pierde resolución. El ajuste estará en el punto donde no pierda usabilidad. La fuente GD sólo permite tamaños 1 a 5. En los ejemplos uso el tamaño máximo 5 y un escalado (o zoom) de 2. Pero es cuestión de hacer pruebas. Por ejemplo, con fuente 2 y zoom 5 la imagen obtenida tiene poca resolución y en muchos casos resulta ilegible:

captcha

Mejorando la usabilidad del CAPTCHA

Hay que tener en cuenta que el que uno vea bien los caracteres no significa que sea así para otras personas. Cuando hacemos pruebas nos vamos acostumbrando a resolver los CAPTCHAs, por lo que será conveniente hacer un muestreo y comprobar que personas ajenas al desarrollo web pueden resolverlos en una proporción aceptable. El script acompaña una opción para guardar un número determinado de archivos de imágenes. Extraje 25 CAPTCHAs y los inserte en esta página para comprobar la usabilidad. Le he pedido a algunas personas para que me den las soluciones. Aunque no es una demostración muy rigurosa dado el bajo número de encuestados, he podido extraer algunas conclusiones.

Usar un conjunto de letras mayúsculas y minúsculas ofrece mayor robustez pero dificulta la usabilidad. Por ejemplo, a veces no es fácil diferenciar las letras "P" mayúscula y "p" minúscula. No conviene usar minúsculas o mayúsculas solamente, pues hay muchas letras que son diferentes en ambos conjuntos lo que supone una ventaja. Es mejor usar las dos formas y mejorar la usabilidad asimilando letras como las del ejemplo anterior.

Por lo tanto la mejora de la usabilidad se refieren a caracteres que tienen un gran parecido. Son 0, o, O, es decir, el cero y las "oes", el dígito "1" con la letra "l" minúscula, el dígito "5" con la letra "S" mayúscula y algunas cuyas formas minúscula y mayúscula pueden ser confundidas. En estos casos prefiero mantener esos caracteres en la lista de los posibles y admitir el intercambio entre ellos. Por ejemplo, si hay un cero y alguién ve una O mayúscula, o al revés, se dará por bueno.

Hay otra mejora de usabildad relacionada con el posicionamiento. Vea estas imágenes:

captcha captcha

Se generaron sin usar la opción de no cerrar las letras "C, c, G, q". En este caso cuando la letra C es seguida de alguna otra como "H" o "E" la anterior no queda bien definida, pareciéndose a la letra "O" o un cero. Con la opción de no cerrar esas letras obligamos a que la siguiente se desplace verticalmente. Forzando el script para usar la misma cadena de la última imagen "aVsCEL", vemos que en todos los casos desplaza la siguiente letra "E" verticalmente para que la "C" no pierda legibilidad:

captcha

Mejorando la robustez del CAPTCHA

El factor de acercamiento horizontal entre caracteres es clave para dar mayor robustez al CAPTCHA. Lo ideal sería que se solapen, pero se perdería legibilidad. Para posicionar un caracter tomamos la última posición horizontal del anterior, le sumamos un ancho de la fuente y luego reducimos algo para que queden lo más pegados posible:

$x += $ancho_fuente * (1 - $pegarx * $pegarmas);

El factor $pegarx tiene un valor de 1/10. Así el siguiente caracter se separa un ancho de la fuente y se reduce luego una décima parte. La última imagen del apartado anterior se generó con ese factor. Si lo modificamos a 1/5 y forzamos la misma cadena "aVsCEL" (aunque variarán los otros parámetros como posición vertical y deformación) obtenemos una de las imágenes así:

captcha

Los caracteres se pegan más entre sí, pero la "s" minúscula pierde legibilidad. El otro factor de acercamiento es $pegarmas. Es el que ya mencioné para pegar aún más algunos caracteres más estrechos, como "i,l,1". Tiene un valor de 2.5 que al multiplicarlo por el otro factor de 0.1 queda en 0.25, con lo que acercamos más esos caracteres estrechos.

Hay un filtro para deformación llamado superficialización que consiste en aligerar el trazo de los caracteres. Esto deforma aún más cada caracter. Con la misma cadena de antes y a con un factor de pegado de 1/10 tenemos este ejemplo:

captcha

Hay más cosas que podríamos hacer para mejorar la robustez. Como usar otras deformaciones como el tachado u otros filtros. También podríamos utilizar ángulos aleatorios para situar cada caracter con fuentes True Type. Esto lo podríamos hacer con la función de PHP imagettftext().

Estructura de la página de prueba del CAPTCHA

La página de prueba contiene un script PHP con un formulario que se remite al mismo script para validar el CAPTCHA. No voy a exponer todo el código aquí, pues si lo desea puede ver el código completo. Presentaré la parte del script PHP que va antes del HTML, aunque omitiendo algunas cosas para abreviar:

session_start();    
//Declaramos variables para configurar el CAPTCHA
...
//Declaramos variables para recibir el formulario
$texto = "";
$texto_verif = "";
$nombre = "";
$email = "";
$mensaje = "";
$verificado = false;
$primera_sesion = true;
if (isset($_SESSION["texto"]) && ($_SESSION["texto"]!="") && 
    isset($_GET) && isset($_GET["envio"]) && 
    ($_GET["envio"]=="Enviar")){
    foreach($_GET as $campo=>$valor){
        switch ($campo) {
            case "nombre": $nombre = $valor; break;
            case "email": $email = $valor; break;
            case "texto-verif": 
                $texto_verif = preg_replace("/[ \s]+/", "", $valor); 
            break;            
        }
    }
    $texto1 = $texto_verif;
    $texto2 = $_SESSION["texto"];
    ...
    if ($texto1 == $texto2) {
        //Si se verifica destruimos sesión
        $verificado = true;
        $_SESSION = array();
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }
        session_destroy();
        //Aquí va el proceso del formulario, enviar por email
        //por ejemplo o remitir a otra página
    } else {
        $texto_verif = "";
        $texto = "";
    }
}
if (!$verificado){
    //Aquí construimos el CAPTCHA
    ...
    //La cadena de texto generada se guarda en la
    variable de sesión
    if (!isset($_SESSION["texto"])) {
        $primera_sesion = true;
    } else {
        $primera_sesion = false;
    }    
    $_SESSION["texto"] = $texto;    
    ...
    //Creamos imagen GD y posicionamos los caracteres. 
    Luego aplicamos filtros para deformar
    $imagen = imagecreate($ancho_imagen, $alto_imagen);
    ...
    //Luego abrimos un búfer para extraer la imagen
    codificada en base64
    ob_start();
    imagepng($imagen);
    imagedestroy($imagen);     
    $buffer = ob_get_clean();
    $imagen_data = base64_encode($buffer);    
}

Abrimos una sesión con session_start(). Establecemos los valores iniciales de las variables y si ya existe una sesión, buscamos en el GET si hay algo recibido del formulario. El texto a verificar viene en el campo texto-verif y le quitamos todos los espacios. Comprobamos que es igual que el texto que tenemos almacenado en $_SESSION["texto"]. Si se verifica destruimos completamente esa sesión. En ese punto podemos remitir el proceso a otro destino, por ejemplo ejecutar el envío por email o redireccionar a otra página. En este ejemplo sacamos el resultado en la misma página controlando que $verificado = true.

Esa página de prueba no tiene ninguna medida de seguridad para formularios, pues su único propósito es probar la extensión GD en localhost para generar CAPTCHA. Para una versión definitiva se debería tener en cuenta cosas como lo expuesto en formularios seguros.

Si !$verificado (será también la situación inicial) pasamos a construir el CAPTCHA. Generamos aleatoriamente la cadena de texto y la guardamos en $_SESSION["texto"]. Luego creamos una imagen GD con la función imagecreate() para posicionar en ella los caracteres y aplicar los filtros de deformación con imagerotate() e imagefilter(). Finalmente tenemos la imagen en la variable $imagen, pero aún no podemos usarla en el HTML que iría a continuación.

La función imagepng($imagen, $archivo) convierte esa imagen en un archivo con formato png. Pero si no queremos usar recursos de disco, es mejor abrir un búfer y lanzar la imagen en él. Con ob_start() lo abrimos y entonces con imagepng($imagen) lo que hacemos es enviarlo a ese búfer. Liberamos la memoria destruyendo la variable y luego volcamos todo lo que hay en el búfer con $buffer=ob_get_clean(). Finalmente lo codificamos en base 64 con $imagen_data=base64_encode($buffer). Así tenemos la imagen en forma de texto plano que luego insertaremos en el elemento <img> quedando su atributo como src="data:image/png;base64,<?php echo $imagen_data;?>. La imagen no supera los 2KB, por lo que codificarlas en base64 y enviarlas con la página no supone un coste excesivo. Y además no hay que tocar recursos de disco en operaciones intermedias.