Рушим капчу SilkRoad 2.0

в 18:28, , рубрики: ruby, безопасность, капча, Программирование, распознавание

Рушим капчу SilkRoad 2.0

Эта статья является продолжением моего предыдущего топика. Вы просили, и я публикую.

Для начала: я был крайне удивлен, что код из первой статьи действительно побеждал капчу SilkRoad. Люди реально стали интересоваться темным интернетом, и, как Вы знаете, появился SillRoad 2.0 после закрытия первого его товарища (хотя второй, тоже недавно был закрыт). О взломе капчи мы поговорим с Вами под катом.

The Silk Road vs. SilkRoad 2.0

The Silk Road 2 не требует ввода капчи для входа. Она нужна лишь для регистрации. Возможно, ее требуют где-то еще, но я рассматривал ее только на одной странице.

Я узнал, что капча, взломанная нами в прошлой статье, была создана в PHP CMS под названием ExpressionEngine.

SilkRoad 2.0 использует плагин для Rails под названием simple-captcha. Его оригинальная(?) ветвь не поддерживается с 2008 года, но некоторые форки серьезно продвинулись с того времени. Я не уверен, какой из них используется на интересующем нас сайте, но для наших тестов был выбран этот вариант.

Скажем так: капчи SR и SR2 не похожи друг на друга, но вариант от SR2 тоже тривиален. SR2 тоже, вероятно, будет решаться с высокой вероятностью (99%+) без машинного обучения, так как все операции по получению решения обратимы.

Первый взгляд

Капча выглядит вполне неплохо.

Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0

Немного фактов:

  1. Нет заднего фона;
  2. 5 символов, /A[A-Z]{5}z/;
  3. Не «слово», нет трюков со словарями;
  4. Одна строка информации
  5. Отличие от SR: символы не просто повернуты или отражены, но еще и перекошены.

Все перекосы выглядят одинаково, так что давайте взглянем получше.

Деформация, не так ли?

Судя по названиям изображений, скрипт назывался как-то типа «simple_captcha». Это получало получить его исходный код, но на решение задачи была всего пара часов, а не недели. Так как 90% преобразований — лишь искривления ImageMagick, будет нерационально искать алгоритм работы капчи. Тем не менее, имея много примеров, но не зная принципа работы, задача усложняется.

Поэтому заглянем ненадолго сюда и сразу же увидим операции ImageMagick:

params = ImageHelpers.image_params(SimpleCaptcha.image_style).dup
params << "-size #{SimpleCaptcha.image_size}"
params << "-wave #{amplitude}x#{frequency}"
params << "-gravity "Center""
params << "-pointsize #{SimpleCaptcha.point_size}"
params << "-implode 0.2"

Как видим, данная операция вполне откатываема. Они применяют операцию -implode 0.2? Давайте сделаем -implode -0.2!

for i in * ; do
  convert "$i" -implode -0.2 "$i-exploded.png";
done

И взглянем на результаты проделанной работы:

Оригинал Откат
Рушим капчу SilkRoad 2.0 Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0 Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0 Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0 Рушим капчу SilkRoad 2.0
Рушим капчу SilkRoad 2.0 Рушим капчу SilkRoad 2.0

Даже если implode выполнялся бы со случайным параметром, мы могли бы попробовать несколько вариантов и, используя бинарный поиск, определить, какой нам подходит больше всего.

Верхом на волне

Теперь у нас есть текст по оси у. Да, искажение есть только по ней.

Я мог бы прямо сейчас остановиться и сказать, что код моей первой статьи запросто бы решал кучи этих задач, но давайте попробуем кое-что поменять, чтобы добиться успеха на 100%.

Посмотрите сюда:

def distortion(key='low')
  key =
    key == 'random' ?
    DISTORTIONS[rand(DISTORTIONS.length)] :
    DISTORTIONS.include?(key) ? key : 'low'
  case key.to_s
    when 'low' then return [0 + rand(2), 80 + rand(20)]
    when 'medium' then return [2 + rand(2), 50 + rand(20)]
    when 'high' then return [4 + rand(2), 30 + rand(20)]
  end
end

Два случайно генерированных параметра используются в операторе -wave в качестве амплитуды и частоты. Судя по инструкциям ImageMagick, начало волны (по оси х) всегда равняется нулю.

Опираясь на эти два параметра и бинарный поиск, мы можем выстроить эти буквы так же равно, как стоят солдаты в строю.

Так как поиск двух чисел довольно прост, я упущу этот фрагмент и сразу перейду дальше.

Улучшенная сегментация (извлечение объектов)

Заметьте, эта капча отличается от капчи SR1 тем, что пробелы между ее символами не одинаковы. Ощущение, будто задействован кернинг. Взгляните на пробелы между T и J в этом примере, XCUTJ:

Рушим капчу SilkRoad 2.0

Метод, который мы использовали в первой статье, успешно бы провалился сейчас, так как он ищет лишь пробелы по вертикали. Мы получали бы неверное решение примерно в 50% всех случаев. Необходим более четкий алгоритм.

Помимо всего: Движущиеся квадраты

Данный алогритм может разделить наши объекты. (Здесь показаны примеры для Ruby и C++, написанные мною очень давно.)

Филипп Шписс создал самый лучший и подходящий для нас пример, который я когда-то видел. Я взял его анимацию:

Рушим капчу SilkRoad 2.0

Суть в том, что квадрат будет идти вдоль первого попавшегося ему объекта и возвращает массив с найденными точками. Если объединить это с чем-то типа алгоритма Дугласа Пукера, то получится многоугольник. (А вот и применение данного алгоритма в другом проекте.)

Проблема заключается в том, что нужно сразу убирать только что найденные символы без применения еще каких-либо методов.

Значит, мы хотим убрать символ, чтобы при повторном запуске алгоритма движущихся квадратов был найден другой символ, следующий за только что убранным. Либо мы можем записывать координаты найденного объекта. чтобы в следующий раз начинать поиски «за ним», что сложнее в реализации.

Это довольно затруднительно без какой-либо библиотеки. Между прочим, попиксельные операции в Ruby очень (очень-очень) медленно выполняются. Давайте искать более простой путь.

Метод заливки

Так будет умнее, быстрее и легче.

  1. Дублируем изображение;
  2. Находим первый черный пиксель, чтобы оказаться «внутри» символа;
  3. Выполняем его заливку белым цветом, чтобы сделать его невидимым в рабочей зоне;
  4. Находим различия между оригиналом и изображением с удаленным символом;
  5. Повторяем до обнаружения всех симолов.

Выглядит это как-то так:

def each_extracted_object(im)
  return enum_for(__method__, im) unless block_given?

  loop do
    xy = first_black_pixel(im)
    break if xy.nil?

    # Save the original
    copy = im.clone

    # Erase it from our working image
    im = im.color_floodfill(xy[0], xy[1], 'white')

    # Exclusion to get the difference, trim and yield
    copy.composite!(im, 0, 0, Magick::ExclusionCompositeOp)
    copy = copy.negate.trim('white')

    # This stuff creates a bit of garbage
    GC.start

    yield copy
  end
end

Рассмотрим пошаговые визуальные преобразования:

Действие Пример
Оригинальное изображение (монохромное для простоты) Рушим капчу SilkRoad 2.0
Выполняем заливку белым цветом для первого найденного слева пикселя Рушим капчу SilkRoad 2.0
Находим различия между оригиналом и изображением со стертым символом Рушим капчу SilkRoad 2.0
Откатываемся назад Рушим капчу SilkRoad 2.0

Результатом второго шага является изображение, с которым мы продолжим работу, а результат четвертого — изолированный символ.

Все это выполняется на относительно хорошей скорости.

Заметим, что данный метод не будет работать, если на одной прямой по оси y будут два объекта для заливки. Для примера смотрите изображение с T и J выше.

Совпадение рисунков

На капче SR1 в целях отделения символов от заднего фона мы должны быди применять фильтры. Это превратило Рушим капчу SilkRoad 2.0 в Рушим капчу SilkRoad 2.0

С такой капчей мы получили набор красивых букв. Собрав информацию из 40 капч, мы получили вот такой набор:
Рушим капчу SilkRoad 2.0

Он был получен, скажем, взятием буквы M и сравнением ее прозрачности со всеми остальными М: 1 / number_of_m_examples.

Вместо применения нейронной сети, просто найдем символ с наибольшим количеством совпадений (а также с учетом волны) с полученным нами ранее набором.

def font_match(im, candidate)
  score = 0
  (0...FONT_HEIGHT).each do |y|
    (0...FONT_WIDTH).each do |x|
      if black?(im.pixel_color(x, y)) == black?(candidate.pixel_color(x, y))
        score += 1
      end
    end
  end

  return score.to_f / (FONT_WIDTH * FONT_HEIGHT)
end

0.96 ** 5 — примерно 81% пройденных капч.

Для сравнения, с 40 примерами и 3 часами тренировок нейронная сеть победила лишь 45%.

Подведем итоги

Решение плохих и некачественных капч — легко дело. Используя некоторые фрагменты из статьи о SR1, эта несвязанная с прошлой капча была побеждена за 3 часа. Я уверен, что с более умной работой процент решаемости будет более 95.

Я все еще не хочу публиковать полный код скрипта, так как он может быть использован для взлома других приложений, а именно тех, которые работают на simple_captcha gem в Ruby.

Мне также любопытна полезность капч в 2014 году. Я слышал, что в сети Tor вам могут решить до тысячи капч за 1$.

Несмотря на это, я узнал о капчах столько, что мне хватит этих знаний на всю жизнь :)

За идею серии статей спасибо ilusha_sergeevich

Автор: alexandfox

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js