Как решить ALTCHA в C# и вернуть токен без изменений

solve altcha in c# - How to Solve ALTCHA in C# and Post the Token Back Unchanged

Чтобы решить ALTCHA на C#, распознавать нечего. ALTCHA построена на доказательстве работы, а не на распознавании: сайт выдаёт задачу, и клиент обязан перебором найти число, которое ей удовлетворяет. Здесь нет ни картинки, ни звука, ни догадок, поэтому решение получается детерминированным и быстрым. Либо ответ находится, либо сама задача была некорректной или уже истекла. CapSkip добавил ALTCHA в версии 1.2.6, а .NET SDK предоставляет для неё один метод, который принимает URL страницы и саму задачу. По-настоящему подводит людей то, что происходит дальше: token нужно вернуть в форму ровно в том виде, в каком его отдал решатель.

Что понадобится

  • CapSkip 1.2.6 или новее на машине с Windows. Поддержка ALTCHA появилась именно в этом выпуске.
  • Пакет CapSkip для .NET, собранный под .NET Standard 2.0, то есть подходящий для .NET Framework 4.6.1 и выше, .NET Core 2.0 и выше, а также .NET 6 и новее.
  • URL страницы, на которой стоит виджет, и эндпоинт, откуда этот виджет забирает свой challenge.
  • Адрес решателя. Режим Local отвечает на 127.0.0.1 и только для этого устройства; режим Server слушает ваш сетевой адрес или публичный IP, чтобы до него могла достучаться другая машина. Какой из них нужен именно вам, разбирает шаг 4, а задаются оба в одном месте: Настройки подключения.
# dotnet add package CapSkip
dotnet add package CapSkip

Шаг 1: найдите адрес, по которому виджет ALTCHA запрашивает задачу

Всё остальное зависит от одного этого значения, поэтому получите его первым. Откройте DevTools, перейдите на вкладку Network и перезагрузите страницу, на которой стоит виджет. Виджет запрашивает свою задачу, обычно по пути, в котором есть altcha. Именно этот URL запроса вы передаёте решателю, а JSON, который возвращает такой адрес, и есть сам документ задачи: его можно передать вместо URL.

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

Поколение виджетаАтрибут, который задаёт challenge
v1 и v2challengeurl для эндпоинта плюс отдельный атрибут challengejson для встроенного challenge
v3 и новееchallenge, причём этот же атрибут принимает либо URL, либо сами данные challenge

Три стиля отображения, native, checkbox и switch, различаются только внешне. Все они отправляют одну и ту же полезную нагрузку, и разница никогда не доходит до решателя, так что выяснять, какой из них перед вами, не требуется. Атрибуты виджета ALTCHA описывает в своей документации по интеграции.

Шаг 2: вызов решения и два способа передать challenge

Один метод, два аргумента: сначала URL страницы, затем словарь опций, который несёт challenge. Дайте ему эндпоинт, и CapSkip получит challenge за вас.

// dotnet add package CapSkip
using CapSkip;

var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);

// CapSkip fetches the challenge, then brute-forces the counter.
var result = await solver.AltchaAsync(
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["challenge_url"] = "https://example.com/altcha/challenge",
    });

Console.WriteLine(result.Token);   // base64 payload for the form field
Console.WriteLine(result.Number);  // the counter that satisfied it

Два поля результата относятся только к ALTCHA. Token содержит полезную нагрузку в base64, которую ждёт форма, а Number хранит счётчик, решивший challenge. Свойство Code несёт ту же строку, что и Token, так что подходит любое, но Token назван по имени поля, куда он попадает, и в месте вызова читается лучше. Поля GeeTest и user agent для Turnstile здесь остаются null.

Number стоит писать в лог. Он возвращается для обоих поколений ALTCHA, хотя полезные нагрузки у них устроены по-разному: старый token несёт счётчик на верхнем уровне, а token нового поколения, построенного на доказательстве работы v2, его там не держит и прячет внутрь объекта solution. CapSkip достаёт его из объекта solution в собственном ответе API, поэтому оба поколения отдаются одинаково.

Как передать документ challenge вместо эндпоинта

Если ваш код уже получил challenge, передайте сам документ, и тогда сетевого запроса не будет вовсе. Это более быстрый путь, когда вы и так парсите страницу, и именно его стоит выбрать, когда challenge приходит встроенным в HTML, а не с отдельного эндпоинта.

// No fetch happens: the document is already here.
var result = await solver.AltchaAsync(
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["challenge_json"] = new Dictionary<string, object>
        {
            ["algorithm"] = "SHA-256",
            ["challenge"] = "YOUR_CHALLENGE_HASH",
            ["salt"] = "YOUR_SALT",
            ["signature"] = "YOUR_SIGNATURE",
            ["maxnumber"] = 1000000,
        },
    });

Этот параметр принимает словарь, который сериализуется за вас, или уже готовую строку JSON. Передать одновременно и адрес, и сам документ разрешено: побеждает встроенный документ, потому что загрузка лишь повторно получила бы то, что вы только что передали. Под нагрузкой два пути ведут себя тоже по-разному: встроенная задача с истёкшим сроком отклоняется сразу, а не хэшируется впустую, тогда как адрес позволяет решателю забрать свежую задачу, если первая умерла, пока запрос стоял в очереди.

Какие алгоритмы покрывает решатель

Один и тот же метод обслуживает оба поколения. Старая схема закрыта алгоритмами SHA-1, SHA-256, SHA-384 и SHA-512, а доказательство работы v2 закрыто PBKDF2 и итеративным SHA. PBKDF2 стоит по умолчанию и рекомендован самой ALTCHA, так что этим покрыто подавляющее большинство живых сайтов.

Исключения составляют Argon2id и scrypt: их не пытаются считать, а сразу отклоняют. Запрос с таким алгоритмом возвращается примерно за треть секунды с ERROR_CAPTCHA_UNSOLVABLE и никогда не повторяется. Так задумано. Повтор не лечит функцию, требовательную к памяти, поэтому мгновенный отказ лучше имитации бурной деятельности. Для ALTCHA такой результат указывает на алгоритм, а не на нечитаемую картинку, и у самого кода ошибки есть отдельное руководство.

Шаг 3: отправляем токен обратно без изменений, пока он не истёк

Виджет отправляет свою полезную нагрузку в поле формы с именем altcha, значит туда же идёт и ваш токен. Именно на этом шаге всё тихо ломается.

// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["email"] = "[email protected]",
    ["altcha"] = result.Token!,
});

var response = await http.PostAsync("https://example.com/signup", body);

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

Другой способ провалить этот шаг связан со временем. Окна challenge короткие, некоторые сайты закрывают их меньше чем за две минуты, и когда окно истекает, сайт отклоняет ответ пустой ошибкой проверки, которая выглядит в точности как неверный ответ. В самой ошибке нет ничего, что подсказало бы, какой из двух случаев произошёл. Избежать этого помогают три привычки: запрашивать challenge прямо перед решением, а не в начале долгого прогона, отправлять токен в той же единице работы, в которой он был решён, и никогда не держать токен, пока человек заполняет форму.

Ограничивают вас здесь не собственные таймауты опроса клиента, потому что оба они намного длиннее этого двухминутного окна. ALTCHA нагружает процессор, а не держит сессию браузера, поэтому работает на обычном таймауте опроса, а не на удлинённом таймауте для reCAPTCHA.

Параметр конструктораПо умолчаниюЧто он покрывает
defaultTimeout120 секундОпрос ALTCHA и графической капчи
recaptchaTimeout300 секундОпрос reCAPTCHA, Turnstile и GeeTest
pollingIntervalМаксимум 5 секундОпрос начинается с 0,25 секунды и увеличивает интервал до этого значения

Шаг 4: где работает решатель и какой режим подключения для этого нужен

В примерах выше стоит 127.0.0.1, и это верно, пока ваш код и решатель делят одну машину. Как только вызывающий код уезжает в другое место, например в контейнер, на сборочный агент, на VPS или на управляемый хостинг, локальная петля перестаёт указывать на решатель, и первое же решение бросает NetworkException.

Переключите CapSkip в режим Server, и он начнёт слушать адрес в вашей сети или публичный IP, так что любой из этих вариантов сможет обратиться к нему через API. Если трафик идёт через интернет, лучше взять статический публичный IP и добавить правило файрвола, которое пропускает только ожидаемые адреса. Режим Server меняет лишь то, где решатель слушает, и больше ничего: это по-прежнему ваше железо и по-прежнему безлимитная работа. Читайте хост из переменной окружения, чтобы одна и та же сборка работала в обоих случаях. Клиент сам не читает CAPSKIP_HOST, поэтому передайте его в конструктор, как это сделано в полном примере ниже.

Где выполняется код C#Какой режим подключения
На машине с CapSkip, в IDE или консольном приложенииРежим Local. 127.0.0.1 действительно верен
На другой машине в той же сетиРежим Server, по внутреннему адресу этой машины
На хосте контейнеров, VPS или управляемой платформеServer mode со статическим публичным IP и правилом брандмауэра

Одно замечание про прокси, специфичное для ALTCHA. Прокси здесь поддерживается, но используется только при получении challenge. Сессии браузера, которую нужно было бы маршрутизировать, тут нет, поэтому на само доказательство работы прокси не влияет.

Полный рабочий пример

// dotnet add package CapSkip
using CapSkip;

var http = new HttpClient();
var solver = new CapSkipClient(
    host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
    port: 8080);

try
{
    var result = await solver.AltchaAsync(
        "https://example.com/signup",
        new Dictionary<string, object?>
        {
            ["challenge_url"] = "https://example.com/altcha/challenge",
        });

    // Submit here, while the challenge is still fresh.
    var body = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["email"] = "[email protected]",
        ["altcha"] = result.Token!,
    });
    var response = await http.PostAsync("https://example.com/signup", body);

    Console.WriteLine($"{(int)response.StatusCode} after counter {result.Number}");
}
catch (ApiException ex)
{
    // ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
    Console.WriteLine($"refused: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
    Console.WriteLine("gave up waiting; defaultTimeout is 120 seconds");
}

Остальные типы устроены так же, меняется только метод. RecaptchaAsync принимает sitekey и URL страницы, TurnstileAsync и GeetestAsync работают точно так же, а картинка распознаётся вызовом с base64. Полный список методов собран здесь: странице сервиса распознавания капчи для C#.

Больше, чем один sitekey, нужно только одному типу: Turnstile на странице проверки. Его дополнительные значения описаны в руководстве по challenge-странице для C#.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Пустая ошибка проверки от сайта при токене, который выглядит нормальноChallenge истёк до того, как форма была отправленаПолучайте challenge, решайте и отправляйте в одной единице работы
ERROR_CAPTCHA_UNSOLVABLE внутри ApiException, примерно через треть секундыChallenge использует Argon2id или scryptПовторять нечего. Эти два отклоняются намеренно
ValidationException при вызовеНе передан ни один из двух параметров задачи, либо передан параметр, который ALTCHA не принимаетПередайте эндпоинт challenge или документ challenge, а всё остальное уберите
NetworkException на первом решенииCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server
Свойство Token в результате читается как nullToken заполняется только для ALTCHAВызовите AltchaAsync. В результате ALTCHA свойство Code содержит ту же строку
Сборка падает на неоднозначном TimeoutExceptionЭто короткое имя определено и в CapSkip, и в SystemПишите CapSkip.TimeoutException полностью или перехватывайте CapSkipError
Форма отклоняет токен, который по логам был решёнЧто-то перекодировало, обрезало или переупорядочило полезную нагрузкуПередавайте строку напрямую, не трогая её

FAQ

Нужен ли браузер, чтобы решить ALTCHA в C#?

Нет, и в этом вся польза. ALTCHA выдаёт задачу на хеширование, а не картинку для разглядывания, поэтому работа идёт только на CPU и заканчивается за миллисекунды. Не нужны ни WebDriver, ни headless Chrome, ни user agent. Достаточно консольного приложения с HttpClient, а значит код спокойно живёт внутри worker-сервиса, потребителя очереди или шага сборки, где управлять браузером было бы неудобно.

Может ли приложение .NET на хостинг-платформе достучаться до решателя?

Да. В настройках подключения переведите CapSkip в режим Server, чтобы он слушал сетевой адрес, а не локальную петлю, и направьте CAPSKIP_HOST на этот адрес. Хост контейнеров, VPS, агент CI или управляемый сервис приложений подключаются одинаково, через один и тот же HTTP API. Если трафик идёт через интернет, возьмите статический публичный IP и ограничьте доступ правилом фаервола. Решатель во всех этих случаях остаётся на вашем собственном железе, поэтому ни лицензия, ни счётчик решений не меняются.

Что передавать: эндпоинт или документ challenge?

Передавайте адрес, если у вас ещё нет самого документа. Это одна запись в словаре параметров, она экономит вам запрос, а если задача протухнет, пока запрос стоит в очереди, решатель сам заберёт свежую. Документ передавайте тогда, когда ваш парсер уже считал его со страницы, когда задача вшита прямо в HTML, а не отдаётся отдельным адресом, или когда для загрузки нужны cookie либо заголовки, которые есть у вашего кода и которых нет у решателя. В последнем случае полезно знать про параметр прокси: для ALTCHA он действует на загрузку и только на неё.

Почему счётчик каждый раз получается другой?

Потому что это ответ на конкретную задачу, а не свойство сайта. У каждой задачи свой salt, поэтому подходящее число меняется при каждой выдаче и может оказаться где угодно вплоть до maxnumber, разрешённого этой задачей. Большой счётчик означает лишь, что хэшировать пришлось дольше, и выражается это в нескольких лишних миллисекундах, не более того. В логах он полезен как доказательство того, что работа действительно была сделана, а для кэширования бесполезен.

Коротко

Считайте с виджета адрес задачи, передайте его вместе с URL страницы в единственный метод ALTCHA и отправьте token обратно в поле altcha, ничего в нём не меняя. Держите загрузку, решение и отправку в одном блоке: окно задачи может закрыться меньше чем за две минуты, а истёкшая задача выглядит ровно как неверный ответ. ERROR_CAPTCHA_UNSOLVABLE ждите только от Argon2id и scrypt, которые отклоняются сразу, а не считаются. Переходите в режим Server в тот момент, когда вызывающий код перестаёт делить машину с решателем.

Ещё одно обстоятельство, которое меняет устройство повторных попыток. Поскольку локальный сервис распознавания капчи вычисляет доказательство работы на машине, которой вы и так владеете, повторное решение истёкшего challenge стоит несколько миллисекунд вашего собственного CPU и больше ничего, так что вы вполне можете запросить свежий challenge вместо того, чтобы возиться с устаревшим.