Pull to refresh

Нестандартная CAPTCHA

Reading time 11 min
Views 6.8K
О создание капчи на PHP+Jquery без задействования графических изображений.




Прелюдия


Сегодня Интернет просто изобилует бесконечным количеством инструкций, в которых различные авторы, рассказывают о своём собственном способе организации защиты от ботов, жаль только, что большинство этих авторов на языке носят типун. Вот, к примеру, плагин jQuery под названием Real Person, он создаёт на странице, нечто подобное:



Причём, стоит обратить внимание на то, что все буквы созданы при помощи всего лишь одного символа — астериска, без использования каких либо изображений. На сайте автора есть примеры, которые показывают, как легко можно изменить длину и набор символов для генерации защитного кода. Также там вы найдёте пример серверного скрипта для проверки правильности ввода символов:
if (rpHash($_POST['realPerson']) == $_POST['realPersonHash']) {

* This source code was highlighted with Source Code Highlighter.

Ё-моё, и вот он Epic Fail! Невиданная досада, автор предлагает нам полностью доверять данным, пришедшим от пользователя. Такая проверка надёжна… он хоть верит в это сам?! С учётом того, что исходный код функции «rpHash()», также описан на сайте автора этого плагина, то с чистой совестью можно было писать и такую проверку:
if ($_POST['In'])== $_POST['Out']) {

* This source code was highlighted with Source Code Highlighter.

На мой взгляд, две строчки кода, описанные выше, полностью идентичны по уровню сложности обхода злоумышленниками. Но всё-таки Я считаю, что такой вид теста Тьюринга достаточно перспективен и поэтому Я решил описать своё видение организации такой защиты.

Метод решения


Данная CAPTCHA представляет собой, набор букв латинского алфавита и арабских цифр. Каждый символ представляет собой матрицу размером 7х7. Любая ячейка матрицы может быть занята или свободна. Занятая ячейка, по умолчанию, имеет тёмный фон, а свободная – прозрачный.


Пример буквы «M».

Любую ячейку можно однозначно определить с помощью двух координат – x и y. Чтобы в целостности воссоздать символ достаточно знать координаты только одного вида ячеек. Разумно хранить координаты занятых ячеек, так как их количество в несколько раз меньше свободных. Для хранения этой информации используется массив:
Array(x1, y1, x2, y2…,xN, yN)
Исходя из вышеизложенного, вот так выглядит буква «M»:
$abc['m'] = array(1,1,7,1,1,2,2,2,6,2,7,2,1,3,3,3,5,3,7,3,1,4,4,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);

* This source code was highlighted with Source Code Highlighter.

При генерации строки символов, надо учитывать смещение координаты x, каждого последующего символа, на n*7 ячеек, где n – количество символов–предшественников:


Соответственно, данную строку можно инициализировать как массив, получившийся в результате слияния двух массивов m(x, y) и a(x+7, y).
Сам генератор массива координат строк выглядит так:
PHP
  1. // весь доступный алфавит
  2. $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
  3. // цикл, генерации символов,
  4. // количество итераций цикла равно количеству символов в строке
  5. for ($i = 0; $i < $the_number_of_letters; ++$i) {
  6.  // случайно выбирается символ
  7.  $letter = $alphanum[intval(mt_rand(0, 33))];
  8.  // создаётся массив символов $array_str
  9.  foreach ($abc[$letter] as $key=>$val)
  10.   // расставляются «правильные» координаты в массиве
  11.   array_push($array_str, ($key%2 == 0)?$val+($i*7):$val);
  12.   // запоминается сама строка
  13.   $di_captcha_str .= $letter;
  14.  }
* This source code was highlighted with Source Code Highlighter.

Визуализируются символы, при помощи тега с заданным выравниванием по левому краю — float:left и чётко заданными размерами длины и высоты. В принципе, для данной цели подойдёт любой блочный элемент, но является одним из самых коротких доступных тегов, поэтому выбор пал на него. Сам код вывода:
JavaScript (jQuery)
  1. // длина блока с тегами
  2. // от этого значения зависит количество тегов в каждой строке
  3. // вычисляется по формуле количество_ячеек+размер_отступа_между_ячейками*7
  4. // +двойной_размер_ячейки – отступ между символами
  5. // и всё это умножается на количество символов n
  6. $('#DICaptchaPic').css('width', ((((cell_size+2)*6)+(3*cell_size)+1)*n));
  7. // переменная для хранения списка тегов 
  8. var html_p_tag = '';
  9. // цикл обхода всего массива с ячейками
  10. for (i = 1; i <= 7*7*n; ++i) { 
  11.  // если ячейка кратна семи, значит она крайняя в символе 
  12.  // и поэтому после неё необходим отступ межсимвольный
  13.  var style = (i%7 == 0)?'margin-right: '+2*cell_size+'px;':'';
  14.  // если ячейка занята, то её фон чёрного цвета
  15.  for (j = 0; j < data[1].length; j += 2) style +=(((i%(data[0]*7)==0)?(data[0]*7):i%(data[0]*7)) == data[1][j] && Math.ceil(i/(data[0]*7)) == data[1][j+1])?'background-color: #000;':'';
  16.  // закрывается тег
  17.  html_p_tag += '<p'+((style=='')?'':' style=\''+style+'\'')+'>'; }
* This source code was highlighted with Source Code Highlighter.

Примечание. Сложность данного алгоритма оценивается как O(n*n), из-за вложенного цикла. Его можно усовершенствовать, добавив конструкцию break во второй цикл, которая будет вызываться при успешном выполнение условия, тогда во втором цикле будет просматриваться только часть массива, а не весь массив, как сейчас. Так же можно вынести внутренний цикл наружу, а в первом присваивать все тегам уникальный id, по которому второй цикл их легко распознает. Хотя это и приведёт к небольшому увеличению кода, но скорость работы заметно увеличиться.
В результате получаются вот такие строки:


Критика и доработка


1. Достаточно перехватить массив с координатами при загрузке его js`ом и сравнить его с шаблонами, чтобы расшифровать строку. Если же перед тем как функция get() вернёт этот массив, в нём перемешать пары, то это усложнит в n раз использование шаблонов.
Поэтому, перед тем как вернуть массив координат, в классе вызывается метод shuffle2, который аккуратно перемешивает массив, не путая пары x и y.
PHP
  1. function shuffle2($array) {
  2.  for ($i = 0; $i < count($array); $i += 2)
  3.   for ($j = count($array)-2; $j > $i; $j -= 2)
  4.    if (mt_rand(0, 1) > 0) {
  5.     $array[$i]+=$array[$j]; $array[$j]=$array[$i]-$array[$j]; $array[$i]-=$array[$j];
  6.     $array[$i+1]+=$array[$j+1]; $array[$j+1]=$array[$i+1]-$array[$j+1]; $array[$i+1]-=$array[$j+1];
  7.    }
  8.  return $array;
  9. }
* This source code was highlighted with Source Code Highlighter.


2. Если добавить в массив координат случайный шум, то это практически сделает невозможным использование шаблонов.
Шум будет двух видов, шум который забивает занятые ячейки, тем самым делая их свободными и шум, распространяющийся на фон, который некоторые свободные ячейки преобразует в занятые.
Для этого был немного модифицирован генератор массива координат строк:
PHP
  1. // $this->noise инициализируется при создание объекта класса
  2. // может принимать значения от 0(нет шума) до 10
  3. $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
  4. // основной цикл
  5. for ($i = 0; $i < $this->the_number_of_letters; ++$i) {
  6.  $letter = $alphanum[intval(mt_rand(0, 33))];
  7.  for ($j = 0; $j < count($this->abc[$letter]); $j += 2)
  8.   // шум внутренний
  9.   if (mt_rand(1, 100) > $this->noise*5)
  10.    array_push($this->array_str, $this->abc[$letter][$j]+($i*7), $this->abc[$letter][$j+1]);
  11.  // шум для фона
  12.  for ($j = 0; $j < 7*7*($this->noise/20); ++$j) {
  13.   array_push($this->array_str, mt_rand(1, 7)+($i*7), mt_rand(1, 7));
  14.  }
  15.  $_SESSION['di_captcha_str'] .= $letter;
  16. }
  17. return $this->shuffle2($this->array_str);
* This source code was highlighted with Source Code Highlighter.

В итоге:


Полностью рабочий пример


Основной класс:
PHP
  1. // В конструкторе задаётся длина строки символов, по умолчанию 6.
  2. // методы класса:
  3. // shuffle2() – перемешивает массив.
  4. // set() – может менять значения некоторых полей класса.
  5. // get() – возвращает массив координат и заносит в сессию саму строку.
  6. // check() – принимает введённый пользователем текст и сравнивает его со строкой, записанной в сессии.
  7. namespace di;
  8. class captcha {
  9. private $str, $array_str = array(), $abc = array(), $the_number_of_letters = 6, $noise = 1;
  10. function __construct($the_number_of_letters = 6) {
  11. $this->the_number_of_letters = $the_number_of_letters;
  12. $this->abc['a'] = array(4,1,3,2,5,2,3,3,5,3,2,4,6,4,2,5,3,5,4,5,5,5,6,5,1,6,7,6,1,7,7,7);
  13. $this->abc['b'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,1,7,2,7,3,7,4,7,5,7,6,7);
  14. $this->abc['c'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,1,4,1,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  15. $this->abc['d'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,1,7,2,7,3,7,4,7,5,7,6,7);
  16. $this->abc['e'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,1,4,2,4,3,4,4,4,1,5,1,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  17. $this->abc['f'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,1,4,2,4,3,4,4,4,1,5,1,6,1,7);
  18. $this->abc['g'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,1,4,1,5,5,5,6,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  19. $this->abc['h'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);
  20. $this->abc['i'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,4,2,4,3,4,4,4,5,4,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  21. $this->abc['j'] = array(7,1,7,2,7,3,7,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  22. $this->abc['k'] = array(1,1,7,1,1,2,5,2,6,2,1,3,3,3,4,3,1,4,2,4,1,5,3,5,4,5,1,6,5,6,6,6,1,7,7,7);
  23. $this->abc['l'] = array(1,1,1,2,1,3,1,4,1,5,1,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  24. $this->abc['m'] = array(1,1,7,1,1,2,2,2,6,2,7,2,1,3,3,3,5,3,7,3,1,4,4,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);
  25. $this->abc['n'] = array(1,1,7,1,1,2,2,2,7,2,1,3,3,3,7,3,1,4,4,4,7,4,1,5,5,5,7,5,1,6,6,6,7,6,1,7,7,7);
  26. $this->abc['o'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  27. $this->abc['p'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,1,6,1,7);
  28. $this->abc['q'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,5,5,7,5,1,6,6,6,2,7,3,7,4,7,5,7,7,7);
  29. $this->abc['r'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,5,5,1,6,6,6,1,7,7,7);
  30. $this->abc['s'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,2,4,3,4,4,4,5,4,6,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  31. $this->abc['t'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,4,2,4,3,4,4,4,5,4,6,4,7);
  32. $this->abc['u'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  33. $this->abc['v'] = array(1,1,7,1,1,2,7,2,2,3,6,3,2,4,6,4,3,5,5,5,3,6,5,6,4,7);
  34. $this->abc['w'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,4,4,7,4,1,5,3,5,5,5,7,5,1,6,2,6,6,6,7,6,1,7,7,7);
  35. $this->abc['x'] = array(1,1,7,1,2,2,6,2,3,3,5,3,4,4,3,5,5,5,2,6,6,6,1,7,7,7);
  36. $this->abc['y'] = array(1,1,7,1,2,2,6,2,3,3,5,3,4,4,4,5,4,6,4,7);
  37. $this->abc['z'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,6,2,5,3,4,4,3,5,2,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  38. $this->abc['0'] = array(3,1,4,1,5,1,2,2,6,2,1,3,5,3,7,3,1,4,4,4,7,4,1,5,3,5,7,5,2,6,6,6,3,7,4,7,5,7);
  39. $this->abc['1'] = array(4,1,3,2,4,2,2,3,4,3,4,4,4,5,4,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  40. $this->abc['2'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,7,3,6,4,4,5,5,5,2,6,3,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
  41. $this->abc['3'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,7,3,5,4,6,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  42. $this->abc['4'] = array(5,1,4,2,5,2,3,3,5,3,2,4,5,4,1,5,2,5,3,5,4,5,5,5,6,5,7,5,5,6,5,7);
  43. $this->abc['5'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,2,3,3,3,4,3,5,3,6,3,7,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  44. $this->abc['6'] = array(3,1,4,1,5,1,6,1,2,2,1,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  45. $this->abc['7'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,6,2,5,3,4,4,3,5,2,6,1,7);
  46. $this->abc['8'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
  47. $this->abc['9'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,2,4,3,4,4,4,5,4,6,4,7,4,7,5,6,6,2,7,3,7,4,7,5,7);
  48. }
  49.  
  50. private function shuffle2($array) {
  51. for ($i = 0; $i < count($array); $i += 2)
  52.  for ($j = count($array)-2; $j > $i; $j -= 2)
  53.   if (mt_rand(0, 1) > 0) {
  54.   $array[$i]+=$array[$j]; $array[$j]=$array[$i]-$array[$j]; $array[$i]-=$array[$j];
  55.   $array[$i+1]+=$array[$j+1]; $array[$j+1]=$array[$i+1]-$array[$j+1]; $array[$i+1]-=$array[$j+1];
  56.   }
  57. return $array;
  58. }
  59.  
  60. function set($name, $val) {
  61. switch ($name) {
  62.  case 'the_number_of_letters':
  63.   $this->the_number_of_letters = (int)$val;
  64.   break;
  65.  case 'noise':
  66.   $this->noise = (int)$val;
  67.   break;
  68.  default:
  69.   return false;
  70. }
  71. return true;
  72. }
  73.  
  74. function get() {
  75. $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
  76. unset($_SESSION['di_captcha_str']);
  77. for ($i = 0; $i < $this->the_number_of_letters; ++$i) {
  78.  $letter = $alphanum[intval(mt_rand(0, 33))];
  79.  //foreach ($this->abc[$letter] as $key=>$val)
  80.  // array_push($this->array_str, ($key%2 == 0)?$val+($i*7):$val);
  81.  for ($j = 0; $j < count($this->abc[$letter]); $j += 2)
  82.   if (mt_rand(1, 100) > $this->noise*5)
  83.   array_push($this->array_str, $this->abc[$letter][$j]+($i*7), $this->abc[$letter][$j+1]);
  84.  for ($j = 0; $j < 7*7*($this->noise/20); ++$j) {
  85.   array_push($this->array_str, mt_rand(1, 7)+($i*7), mt_rand(1, 7));
  86.  }
  87.  $_SESSION['di_captcha_str'] .= $letter;
  88. }
  89. return $this->shuffle2($this->array_str);
  90. }
  91.  
  92. function check($in_string) {
  93. echo $in_string.'|'.$_SESSION['di_captcha_str'];
  94. return (strtolower($in_string) == $_SESSION['di_captcha_str'])?true:false;
  95. }
  96. }
* This source code was highlighted with Source Code Highlighter.

После создания phar и сжатия его, получается файл di_captcha.class.phar.gz весом 3.53кб.
Прример использования класса:
PHP+html
// index.php
// Прример использования класса
session_start();
// THE_NUMBER_OF_LETTERS – константа, количество символов в строке
define('THE_NUMBER_OF_LETTERS', 6);
// При запросе от js о генерации строки, возвращается количество символов и сам массив координат.
if (isset($_POST['action']{14}) && $_POST['action'] == 'captcha_refresh') {
 require 'phar://di_captcha.class.phar.gz/di_captcha.class.php';
 $captcha = new di\captcha();
 $captcha->set('noise', 0);
 echo json_encode(array(THE_NUMBER_OF_LETTERS, $captcha->get()));
} else {

?><br><!DOCTYPE html><br><html><br><head><br> <meta charset='utf-8'><br> <title>Test</title><br> <link rel='stylesheet' media='all' href='style.css'><br> <script type='text/javascript' src='jquery-1.6.1.min.js' charset='utf-8'></script><br> src='script.js' charset='utf-8'></script><br></head><br><body><br> <p id='Title'>Сим-сим, откройся!</p><br> <p id='Msg'><br> <?php<br> if (isset($_POST['action']{11}) &#&&; $_POST['action'] == 'captcha_send') {;<br>  require 'phar://di_captcha.class.phar.gz/di_captcha.class.php';<br>  $captcha = new di\captcha();<br>  echo ($captcha->check($_POST['text_captcha']))?'Сим-сим открылся!':'К сожалению, Вы ошиблись...';<br> }<br> ?><br> </p><br> <form action='index.php' method='post'><br> <div id='DICaptchaPic'></div><br> <p style='padding: 0 10px;'><br>  <input type='text' name='text_captcha' id='text_captcha' value='<?php echo $_POST['text_captcha']; ?>' placeholder='6 символов с картинки'><br><label for='text_captcha'>*aнти-спам</label> <ahref='#' onclick='di_captcha_refresh(); return false;'>Не вижу</a><br> </p><br> <p style='padding: 10px 0;'><br>  <input type='hidden' name='action' value='captcha_send' /><br>  <input type='submit' name='submit' value='Проверить' /><br> </p><br> </form><br></body><br></html><br><?php<br> }<br>* This source code was highlighted with Source Code Highlighter.

При запросе от js о генерации строки, возвращается количество символов и сам массив координат.
И, собственно, js скрипт:
JS
  1. /* script.js */
  2. /* cell_size – размер ячеек в пикселях */
  3. var cell_size = 3;
  4. function di_captcha_refresh() {
  5. $.post('./index.php', {action: 'captcha_refresh'},
  6.  function(data) {
  7.   var data = eval(data);
  8.   $('#DICaptchaPic').css('width', ((((cell_size+2)*6)+(3*cell_size)+1)*data[0]));
  9.   var html_p_tag = '';
  10.   for (i = 1; i <= 7*7*data[0]; ++i) {
  11.   var style = (i%7 == 0)?'margin-right: '+2*cell_size+'px;':'';
  12.   for (j = 0; j < data[1].length; j += 2) style +=(((i%(data[0]*7)==0)?(data[0]*7):i%(data[0]*7)) == data[1][j] && Math.ceil(i/(data[0]*7)) == data[1][j+1])?'background-color: #000;':'';
  13.   html_p_tag += '<p'+((style=='')?'':' style=\''+style+'\'')+'>'
  }  $('#DICaptchaPic').html(html_p_tag); } )} $(document).ready(function() { $('#DICaptchaPic').css('overflow', 'hidden'); $('#DICaptchaPic').css('height', (cell_size+2)*7); di_captcha_refresh(); $('#DICaptchaPic').click(function() {di_captcha_refresh();}); $('#text_captcha').focus()})
* This source code was highlighted with Source Code Highlighter.



P.S.


1. Если в класс генерации строк добавить шаблоны символов, по несколько шаблонов на символ, то это ещё больше усложнит расшифровку посредствам шаблонов.
2. Работники _http://decaptcher.com, _http://captchabot.com и _http://antigate.com/ скажут вам спасибо за такую капчу.
3. Эта статья была опубликована моим другом, в разделе веб разработка несколько дней назад, набрала 16 минусов, 14 плюсов и 11 коментов. Потом была похоронена модераторами по причине «не умеет читать правила сайта (публикует чужие посты и клянчит инвайты)», статья написана специально для сайта, и инвайты ни кто не клянчил, а уточнили, что он не автор статьи. Потом попала в песочницу и заслужила инвайт.
4. Краткий смысл 11 комментариев:
— чтобы обойти капчу надо перехватить post запрос и расшифровать текст по шаблонам
— с шумом при расшифровки будет большой брак
— с шумом буквы «О» и «D», «C» и «G» иногда трудно разборчивы и путаються, их лучше исключить из алфавита
— расшифровать можно любую капчу, главное найти компромисс между читаемостью и сложностью обхода

Скачать исходники

Демо: без шума, с шумом(1)

UPD: nagato разработал скрипт который удачно обходит капчу в 70% случаев.
Tags:
Hubs:
+33
Comments 59
Comments Comments 59

Articles