LIMB
Вход Регистрация

тема

[СТАТЬЯ] Новый вид капчи, основанный на криптографии, на который постепенно переходят сервисы.

Удалённый пользователь 2946 · · 0 ответов · 54 просмотров

Вступление
Капчи - извечная головная боль. Страдают все: люди тыкают по сто раз в какие-то мутные квадраты с автобусами и светофорами, а ботов пачками банят только за то, что они вообще существуют. Казалось бы, прогресс должен был давно нас избавить от этого ужаса, но нет - классические найди велосипед мАкАкИ уже порядком всем надоели, и сайты начали искать новые методы. Один из относительно свежих вариантов - капча, которая не заставляет мозг вылавливать пешеходный переход на пиксельной картинке, а грузит железо. Смысл простой, сайт кидает браузеру задачу перебрать числа от нуля до какого-то лимита, пока не найдется верное. Это число в связке с солью даёт правильный хэш - и вот тогда уже считается, что ты не бот. То есть вместо того, чтобы мучить глаза картинками, заставляют чуть-чуть поработать процессор. 
Не удалось прогрузить медиа

И вот здесь начинается самое интересное. Мы сделаем автосолвер для капчи на cpa.tl и по шагам разберём, как именно это устроено внутри. Важно понимать, что статья написана в разделе криптографии не просто так - ведь суть всей этой проверки завязана именно на криптографические примитивы. Там нет магии и нет "загадочных картинок", есть вполне конкретная задача: перебор в поиске значения, которое после хэширования совпадёт с эталоном. По сути, это упрощённая версия proof-of-work - та же механика, которая используется в блокчейнах, только в масштабах конкретного сайта.
Плюсы и минусы такой капчи
Главный плюс в том, что больше не нужно мучиться с картинками, где половина автобуса спрятана за деревом, а светофор выглядит как кусок пиксельного арта из девяностых. Теперь всё решается автоматом в браузере, и человек даже может не заметить, что что‑то вообще происходило. Пользователь не щурится в монитор и не тратит нервы, а за него тихонько работает процессор. Для ботов же ситуация совсем другая: им приходится тратить ресурсы на перебор и жечь процессорное время, из‑за чего атака становится дороже и менее привлекательной. В этом есть ощущение справедливости - пусть машина трудится ради защиты, а не твой мозг на загадочных квадратиках. Но минусы тоже есть, и игнорировать их не получится. Если у тебя старый ноутбук или телефон, то браузер, решая задачу, может загрузить процессор так, что кулер начнёт шуметь как пылесос, а батарейка таять на глазах. Кроме того, админы могут повысить сложность вычислений, и тогда начнут страдать не только боты, но и обычные пользователи: страницы будут подтормаживать, а простая капча превращаться в маленький стресс‑тест железа. Иногда и вовсе непонятно, почему сайт зависает на несколько секунд, ведь внешне ничего не происходит, а за кулисами браузер тихонько решает головоломку. 
Деобфускация, решение капчи, и прочий бред из недр криптографии.
Пора заканчивать с разогревом и переходить к сути. На сайте cpa.tl вызов капчи срабатывает сразу, как только ты зашёл на страницу логина. То есть ещё до того, как ты вообще что‑то ввёл в поля, браузер первым делом обязан решить задачу. 
Не удалось прогрузить медиа

Там летит самый простой POST-запрос, оставляю его здесь в виде curl'a, чтоб потом можно было вернуться и написать автосолвер такой капчи, начиная от получения, до конечного решения.
Code:
curl 'https://cpa.tl/captcha-altcha/altcha-challenge/' \
  -X 'POST' \
  -H 'Accept: */*' \
  -H 'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' \
  -H 'Connection: keep-alive' \
  -H 'Content-Length: 0' \
  -H 'Origin: https://cpa.tl' \
  -H 'Referer: https://cpa.tl/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
  -H 'X-CSRFToken: null' \
  -H 'X-Requested-With: XMLHttpRequest' \
  -H 'sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"'
В ответ сервер присылает JSON, который может показаться сперва каким-то бредом, мол как я из этого получу ответ на капчу? 
Code:
{
    "algorithm": "SHA-256",
    "challenge": "c7bd319e0521e2ca86d02bb87b07b8ff25d2749a64fd56db33f601c989e6ee17",
    "maxnumber": 1000000,
    "salt": "03122d63c5d2853ee6e7d8af?expires=1756652362",
    "signature": "66397f14dba003ce706fc3ad2f022cda9b21819b8c355732d95d683acb5d3df7"
}
И вот тут становится видно суть. Сервер указывает алгоритм (SHA‑256), даёт challenge (хэш, с которым нужно сверяться), вываливает соль и ограничение перебора (maxnumber), а также подпись. Никакой магии. Это просто задача на перебор, где твой браузер обязан погонять числа от 0 до миллиона и найти то самое, что совпадёт по хэшу. После этого браузер не сам считает, а подключает worker.js, подтягивая его отсюда.
Code:
https://cpa.tl/static/captcha_altcha/altcha-1.4.0/worker.js
А там уже спрятан весь процесс, воркер запускает перебор, гоняет числа и проверяет хэши, пока одно не совпадёт с challenge. Да, код слегка обфусцирован и подзамаскирован, но на деле это обычный брутфорс, просто вынесенный в отдельный поток, чтобы не вешать страницу.
Не удалось прогрузить медиа


Теперь, когда понятно, что сервер отдаёт нам challenge, соль, лимит перебора и подпись, самое время заглянуть внутрь воркера. На первый взгляд код там выглядит обфусцированным: короткие имена функций, однобуквенные переменные, всё сжато и без понятных комментариев. Но если покопаться он вполне прозрачный и легко делится на логические куски.
Не удалось прогрузить медиа

Code:
(function(){"use strict";const d=new TextEncoder;function p(e){return[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("")}async function b(e,t,r){if(typeof crypto>"u"||!("subtle"in crypto)||!("digest"in crypto.subtle))throw new Error("Web Crypto is not available. Secure context is required (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).");return p(await crypto.subtle.digest(r.toUpperCase(),d.encode(e+t)))}function w(e,t,r="SHA-256",n=1e6,s=0){const o=new AbortController,a=Date.now();return{promise:(async()=>{for(let c=s;c<=n;c+=1){if(o.signal.aborted)return null;if(await b(t,c,r)===e)return{number:c,took:Date.now()-a}}return null})(),controller:o}}function h(e){const t=atob(e),r=new Uint8Array(t.length);for(let n=0;n<t.length;n++)r[n]=t.charCodeAt(n);return r}function g(e,t=12){const r=new Uint8Array(t);for(let n=0;n<t;n++)r[n]=e%256,e=Math.floor(e/256);return r}async function m(e,t="",r=1e6,n=0){const s="AES-GCM",o=new AbortController,a=Date.now(),l=async()=>{for(let u=n;u<=r;u+=1){if(o.signal.aborted||!c||!y)return null;try{const f=await crypto.subtle.decrypt({name:s,iv:g(u)},c,y);if(f)return{clearText:new TextDecoder().decode(f),took:Date.now()-a}}catch{}}return null};let c=null,y=null;try{y=h(e);const u=await crypto.subtle.digest("SHA-256",d.encode(t));c=await crypto.subtle.importKey("raw",u,s,!1,["decrypt"])}catch{return{promise:Promise.reject(),controller:o}}return{promise:l(),controller:o}}let i;onmessage=async e=>{const{type:t,payload:r,start:n,max:s}=e.data;let o=null;if(t==="abort")i==null||i.abort(),i=void 0;else if(t==="work"){if("obfuscated"in r){const{key:a,obfuscated:l}=r||{};o=await m(l,a,s,n)}else{const{algorithm:a,challenge:l,salt:c}=r||{};o=w(l,c,a,s,n)}i=o.controller,o.promise.then(a=>{self.postMessage(a&&{...a,worker:!0})})}}})();
В начале идёт подготовка: подключается TextEncoder, который позволяет превращать строки в массив байтов. Есть функция, которая конвертирует данные в hex‑строку, есть другая - которая считает хэш строки с солью и возвращает его в виде этого самого hex. То есть самое базовое: кодировка и хэширование. Дальше появляется функция перебора - именно она и отвечает за решение «капчи через процессор». Работает она очень просто. Берётся число, конкатенируется с солью, считается SHA‑256, и результат сравнивается с заданным challenge. Если совпало - значит задача решена, если нет - прибавляем единицу и повторяем. Цикл идёт от нуля и до лимита maxnumber.  Есть вспомогательные мелочи: преобразование base64 в массив байтов, генерация вектора для AES и функции для расшифровки обфусцированных блоков. Да, кстати, внутри воркера есть ещё одна ветка с использованием AES‑GCM, где нужно подбирать правильный initialization vector, чтобы расшифровать данные. Это уже усложнённый вариант той же проверки, если challenge приходит не в виде простого хэша, а в виде зашифрованного блока, скрипт пытается перебором найти правильный ключ/IV и получить исходный текст. В конце всё это завязано на onmessage, как только главный скрипт на странице передаёт воркеру задание через postMessage, воркер запускает процесс перебора. Если надо остановить - можно прислать команду abort, тогда текущий цикл прервётся. Если пришла нагрузка с полями challenge и salt, запускается SHA‑256 перебор, если пришёл obfuscated, то подключается AES‑ветка. В обоих случаях принцип одинаков - перебираем, считаем и проверяем. Когда решение найдено, воркер отдает результат обратно в основной поток.
Не удалось прогрузить медиа

Итого что мы имеем: вся эта умная защита по факту сводится к отдельному мини‑скрипту, который тупо перебирает числа до тех пор, пока хэш или расшифровка не совпадёт с эталоном. Код слегка замаскирован для вида, но стоит его разобрать, и он спокойно превращается в понятный пошаговый алгоритм. Значит, дальше задача выглядит прямолинейно. У нас есть серверный ответ с параметрами challenge, соль, лимит, у нас есть рабочая схема из воркера, и у нас есть понимание, что это самая обычная криптография. Следующий шаг - воспроизвести этот алгоритм самостоятельно и написать автосолвер, который будет делать ровно то же самое, получать challenge и соль, перебирать числа и возвращать правильный результат.
Автосолвер на петухоне, реализуем аналог JS-кода.
Ну что, с воркером разобрались, видим, что никакой там магии нет, только тупой перебор. Логично следующим шагом написать свой солвер. По сути, мы просто повторим то, что делал worker.js в браузере, но у себя локально: возьмём SALT + число, погоняем через SHA‑256, сравним с challenge и получим решение. Сервер, как мы уже видели, отдаёт вот такую задачу.
Code:
{
  "algorithm": "SHA-256",
  "challenge": "617ec17c1bf32017d6129dbeee94f72aab07c87377b47bc88c374b1e6eef095f",
  "maxnumber": 1000000,
  "salt": "feb18e87f1083d89f26295d2?expires=1756656059",
  "signature": "ac8f825c82e1ec1ab1967cb34e79012444e2a1c30972514c852057016df669df"
}
Что тут надо? пройтись перебором от 0 до maxnumber, склеить salt + число, хэшнуть это дело и проверить совпадает ли с challenge.
Code:
import hashlib
import base64
import json
import requests
import time

def get_challenge_mock():
    return requests.post('https://cpa.tl/captcha-altcha/altcha-challenge/').json()

def solve_altcha(challenge, salt, maxnumber, algorithm="SHA-256", signature=None):
    start = time.time()
    algo = algorithm.replace("-", "").lower()
    for number in range(maxnumber+1):
        payload = (salt + str(number)).encode()
        digest = hashlib.new(algo, payload).hexdigest()

        if digest == challenge:
            elapsed = round(time.time() - start, 3)
            result = {
                "algorithm": algorithm,
                "challenge": challenge,
                "number": number,
                "salt": salt,
                "signature": signature,
                "took": elapsed
            }
            result_json = json.dumps(result, separators=(",", ":"))
            result_b64 = base64.b64encode(result_json.encode()).decode()
            return result, result_b64

    return None, None

if __name__ == "__main__":
    data = get_challenge_mock()
    print(data)
    res, encoded = solve_altcha(
        data["challenge"],
        data["salt"],
        data["maxnumber"],
        data["algorithm"],
        data["signature"]
    )

    print(encoded)
Не удалось прогрузить медиа

Заключение
И вот тут мы плавно подходим к самой сути. Вся страшная магия капчи, все эти worker.js, challenge, подписи и соли, на самом деле никакого волшебства не содержат. Это самый обычный proof‑of‑work, только масштабированный до уровня входа на сайт. Если в блокчейне майнеры подбирают nonce, чтобы хэш блока удовлетворял условию, то здесь браузер подбирает число, которое вместе с солью даёт правильный SHA‑256.
👍4 🔥1 😢1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.