Все посты
7 мин чтенияglizzykingdreko

Ежедневные челленджи hCaptcha — Глава 2

Если вы ещё не читали Главу 1, настоятельно рекомендую начать с неё. В этой главе мы разберём другой динамический челлендж hCaptcha.

Ежедневные челленджи hCaptcha — Глава 2

Если вы ещё не читали Главу 1, настоятельно рекомендую начать с неё. В этой главе мы разберём другой динамический челлендж hCaptcha.

Челлендж с недостающей дугой

В этом челлендже показана фигура в форме пончика, у которой отсутствует один сегмент, или «дуга», и располагается он всегда с правой стороны. Наша основная задача проста: точно определить координаты середины этого недостающего сегмента. Именно эти координаты и есть точка сброса (drop-out), которую требует payload.

Давайте разберём всё по шагам — с концептуальными объяснениями и практическими фрагментами кода.

Подход

Имея в качестве единственных данных от hCaptcha только png-картинку челленджа, я действовал так:

  1. Предобработать изображение
  2. Определить форму пончика
  3. Визуализировать его для отладки на декартовой плоскости*****
  4. Определить недостающую дугу по непрерывности контура
  5. Применить это к исходному изображению и определить координаты

На практике проще, чем в теории.
Приступим к челленджу:

*******Этот шаг нужен исключительно для лучшего понимания ситуации; для итогового решения он не потребуется

Предобработка

Прежде чем начинать любые геометрические рассуждения, изображение нужно «очистить», чтобы остались только существенные детали.

  • Преобразование в оттенки серого и размытие по Гауссу:
    Перевод изображения в оттенки серого упрощает данные, сокращая число цветовых каналов, а размытие по Гауссу помогает подавить шум — благодаря этому мелкие неровности или артефакты не приводят к ложным срабатываниям при поиске границ.

  • Обнаружение границ (алгоритм Canny):
    Детектор границ Canny используется, чтобы выделить контуры внутри изображения. Этот шаг критически важен: он изолирует очертания фигуры-пончика, делая последующий поиск контуров и геометрический анализ более надёжными.

Много слов, а на деле всего 3 очень эффективные строки кода.

# 1. Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 2. Blur
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 3. Identify edges
edges = cv2.Canny(blurred, 50, 150)

Определение формы пончика

Когда у нас есть чёткая карта границ, следующий шаг — отделить форму пончика от фона. Достигается это так:

  • Маскирование области пончика:
    Я создаю две маски: одну для внешней окружности (radius + 5), другую для внутренней (radius — 5). Пересечение (или разность) этих масок эффективно изолирует кольцо, задающее пончик.
  • Анализ контуров:
    Найдя контуры на маскированном изображении, я затем выбираю самый крупный контур. Этот шаг отсеивает посторонний шум и гарантирует, что анализируется только основная форма пончика.


circles = cv2.HoughCircles(
   edges,
    cv2.HOUGH_GRADIENT,
    dp=1,
    minDist=50,
    param1=50,
    param2=30,
    minRadius=20,
    maxRadius=100
)
circle_vis = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
h, w = edges.shape[:2]

circles = np.uint16(np.around(circles))
largest_circle = max(circles[0], key=lambda x: x[2])
x_center, y_center, r = largest_circle

# Draw circles for debug.
cv2.circle(circle_vis, (x_center, y_center), r, (0, 255, 0), 2)
cv2.circle(circle_vis, (x_center, y_center), r + 5, (255, 0, 0), 2)
cv2.circle(circle_vis, (x_center, y_center), r - 5, (255, 0, 255), 2)

В результате получается очищенное изображение, содержащее только точки границы пончика.

Визуализация в декартовых координатах

Чтобы упростить отладку и убедиться, что данные о границах интерпретируются правильно, я проецирую периметр пончика на декартову плоскость. Этот шаг включает:

  • Задание опорных окружностей:
    Я накладываю три окружности на пустой холст:
  • Зелёная окружность обозначает обнаруженную границу пончика.
  • Синяя окружность обозначает внешний ориентир (radius + 5).
  • Фиолетовая окружность очерчивает внутреннюю границу (radius — 5)
  • Отрисовка валидных точек границы:
    Красные точки представляют валидные точки границы, извлечённые из обработанного изображения. Эти точки показывают непрерывность (или разрыв) границы пончика, позволяя визуально определить недостающую дугу.


# Prepare blank images
img1 = np.ones((size, size, 3), dtype=np.uint8) * 255
img2 = img1.copy()
img3 = img1.copy()

# Draw coordinate axes on each
for img in (img1, img2, img3):
    cv2.line(img, (0, size//2), (size, size//2), (0, 0, 0), 1)
    cv2.line(img, (size//2, 0), (size//2, size), (0, 0, 0), 1)

# Compute scale factor and center in Cartesian space
scale = (size * 0.8) / (2 * radius)
center_pt = (size // 2, size // 2)

# Mask the donut region
outer_mask = np.zeros_like(edges)
inner_mask = np.zeros_like(edges)
cv2.circle(outer_mask, (center_x, center_y), radius + 5, 255, -1)
cv2.circle(inner_mask, (center_x, center_y), radius - 5, 255, -1)
donut_region = cv2.bitwise_and(edges, cv2.bitwise_and(outer_mask, cv2.bitwise_not(inner_mask)))

# Keep only the largest contour
contours, _ = cv2.findContours(donut_region, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
if contours:
    largest_contour = max(contours, key=cv2.contourArea)
    cleaned = np.zeros_like(donut_region)
    cv2.drawContours(cleaned, [largest_contour], -1, 255, thickness=cv2.FILLED)
    donut_region = cleaned

# Collect valid points and compute angles
white_pixels = np.argwhere(donut_region > 0)
white_pixels = [(p[1], p[0]) for p in white_pixels]  # (x, y)

margin = 5
angle_points = {}
for (px, py) in white_pixels:
    dist = math.hypot(px - center_x, py - center_y)
    if (radius - margin) <= dist <= (radius + margin):
        angle_deg = math.degrees(math.atan2(py - center_y, px - center_x))
        if angle_deg < 0:
            angle_deg += 360
        angle_deg = int(round(angle_deg))
        angle_points.setdefault(angle_deg, []).append((px, py))

# Group angles into raw arcs
sorted_angles = sorted(angle_points.keys())
raw_arcs = []
current_arc = []
gap_thresh = 5
min_arc_len = 5
if sorted_angles:
    current_arc = [sorted_angles[0]]
    for i in range(1, len(sorted_angles)):
        curr_angle = sorted_angles[i]
        prev_angle = sorted_angles[i - 1]
        if (curr_angle - prev_angle) <= gap_thresh:
            current_arc.append(curr_angle)
        else:
            if len(current_arc) >= min_arc_len:
                raw_arcs.append((min(current_arc), max(current_arc)))
            current_arc = [curr_angle]
    if len(current_arc) >= min_arc_len:
        raw_arcs.append((min(current_arc), max(current_arc)))

# Merge raw arcs to get present arcs, then compute missing arcs
present_arcs = merge_arcs(raw_arcs, gap_threshold=5)
missing_arcs = find_missing_arcs(present_arcs)

# Draw red dots for all valid points on each image
for (px, py) in white_pixels:
    dx = px - center_x
    dy = py - center_y
    proj_x = int(center_pt[0] + dx * scale)
    proj_y = int(center_pt[1] + dy * scale)
    if 0 <= proj_x < size and 0 <= proj_y < size:
        for img in (img1, img2, img3):
            cv2.circle(img, (proj_x, proj_y), 1, (0, 0, 255), -1)

# Draw reference circles on each image
for img in (img1, img2, img3):
    cv2.circle(img, center_pt, int(radius * scale), (0, 255, 0), 1)       # Middle (green)
    cv2.circle(img, center_pt, int((radius + 5) * scale), (255, 0, 0), 1)   # Outer (blue)
    cv2.circle(img, center_pt, int((radius - 5) * scale), (255, 0, 255), 1) # Inner (purple)

Извлечение недостающей дуги

Имея на руках данные о границах, следующая задача — определить, где именно граница пончика прерывается.

  • Группировка и объединение углов:
    Я вычисляю угол каждой точки границы относительно центра пончика. Затем эти углы группируются в «дуги» на основе их непрерывности. Небольшие разрывы (в пределах порога) объединяются, так что остаются только значимые разрывы.

# Draw present arcs in pink on all three images
for (arc_start, arc_end) in present_arcs:
    for img in (img1, img2, img3):
        cv2.ellipse(
            img,
            center_pt,
            (int(radius * scale), int(radius * scale)),
            0,
            arc_start,
            arc_end,
            (255, 0, 255),  # Pink
            2
        )
  • Определение недостающих участков:
    Взяв дополнение к объединённым «присутствующим» дугам, можно вычислить разрывы (недостающие дуги). Затем самая большая недостающая дуга выбирается в качестве цели для решения челленджа.


# Draw missing arcs in red on img2 and img3
for (arc_start, arc_end) in missing_arcs:
    for img in (img2, img3):
            cv2.ellipse(
                img,
                center_pt,
                (int(radius * scale), int(radius * scale)),
                0,
                arc_start,
                arc_end,
                (0, 0, 255),  # Red
                2
            )
  • Вычисление точки сброса (drop-out):
    Вычисляется середина этой недостающей дуги. Эта точка после соответствующего преобразования из декартовой проекции обратно в систему координат исходного изображения и есть та самая критически важная точка сброса (drop-out), которую нужно отправить в payload челленджа.

# We draw a green line and dot for the largest missing arc
largest_missing_arc = max(missing_arcs, key=lambda arc: arc[1] - arc[0])
arc_start, arc_end = largest_missing_arc
mid_angle = (arc_start + arc_end) / 2.0
mid_rad = math.radians(mid_angle)
scaled_r = int(radius * scale)
line_end_x = center_pt[0] + int(scaled_r * math.cos(mid_rad))
line_end_y = center_pt[1] + int(scaled_r * math.sin(mid_rad))
cv2.line(img3, center_pt, (line_end_x, line_end_y), (0, 255, 0), 2)
cv2.circle(img3, (line_end_x, line_end_y), 5, (0, 255, 0), -1)

Обратное отображение на исходное изображение

Финальный шаг — перевести вычисленную середину из декартовой плоскости обратно в координатное пространство исходного изображения. Для повышения точности середина отмечается на «воображаемой» окружности, отступающей на 7 пикселей внутрь от обнаруженной внешней границы. Эта поправка компенсирует любые неточности границы и даёт более надёжную координату для payload hCaptcha.

# Mark the largest missing arc midpoint on the original image.
original_with_missing_dot = image.copy()

largest_missing_arc = max(missing_arcs, key=lambda arc: arc[1] - arc[0])
arc_start, arc_end = largest_missing_arc
mid_angle = (arc_start + arc_end) / 2.0
mid_rad = math.radians(mid_angle)
dot_x = int(x_center + (r - 7) * math.cos(mid_rad))
dot_y = int(y_center + (r - 7) * math.sin(mid_rad))
cv2.circle(original_with_missing_dot, (dot_x, dot_y), 5, (0, 255, 0), -1)
coord_text = f"({dot_x},{dot_y})"
cv2.putText(original_with_missing_dot, coord_text, (dot_x+5, dot_y-5),
    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

Заключение

Ключ к эффективному решению «челленджа с недостающей дугой» — в точной предобработке изображения, систематическом анализе контуров и аккуратных геометрических вычислениях. Отладочные визуализации, хотя и необязательны, существенно упрощают разработку.

Полную оптимизированную реализацию солвера можно найти в моём репозитории на GitHub. А ещё загляните на TakionAPI — мой сервис для обхода антибот-защиты, предоставляющий API для hCaptcha, Datadome, PerimeterX, Queue-IT и кастомные решения для веб-скрейпинга. Присоединяйтесь к нашему сообществу в Discord, чтобы получить пробный доступ к API и поддержку!

Понравилась статья? Вы можете поддержать меня через BuyMeACoffee.

Подписывайтесь и пишите мне

hcaptchaocranti-botbypasspython

Пропустите реверс-инжиниринг.

Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.