AIREITER

Не можете найти логику подписи в коде? Возможно, её там вообще нет

Последнее обновление: 2026-07-31 07:47:31

Вы выгрузили весь webpack-бандл, нарезали AST, выписали листовые примитивы и уже умеете читать отпечатки — ту самую процедуру, которую освоили раньше. Но теперь в заголовке запроса торчит значение подписи. При каждом запросе оно новое, а функции, которая его считает, найти не удаётся: ни подозрительные битовые операции, ни заготовки хешей во всех статических ассетах ничего не дают.

Проблема не в том, что вы плохо искали. В выгруженном коде этой логики действительно нет.

Есть целый класс подписей, для которых статический анализ обречён на пустой результат: сам алгоритм появляется только во время выполнения. В такой ситуации не нужно продолжать искать несуществующую функцию. Правильнее сменить подход: не реверсить алгоритм, а минимально выполнить его или прочитать готовое значение.

Как понять, что статический анализ здесь бессилен

Сначала стоит убедиться, что перед вами именно такой случай, а не просто пропущенный фрагмент кода. Обычно его выдают два признака.

Первый: значение меняется от сессии к сессии или от запроса к запросу, но в статических ассетах его никто не генерирует. В сетевом запросе вы его видите, после каждого обновления страницы оно другое, однако скачиваете все .js, ищете по полному тексту — и нигде не находите место, где оно собирается. Значит, код приходит во время выполнения.

Второй: значение находится в бандле как литерал, но вчерашняя строка сегодня уже не работает. В build-выводе лежит обычная строковая константа, её можно скопировать и использовать в тот же день. Через пару дней endpoint начинает отвечать ошибкой, а прежняя «константа» оказывается заменена новым литералом, который тоже легко найти. Значение зашито на этапе сборки, но обновляется с каждым релизом фронтенда.

У обоих признаков одна причина: статический код — это снимок, тогда как реальное значение живёт во времени или зависит от сессии. В одном случае поиск ничего не находит, в другом — находит, но значение уже успело устареть. Перестал работать именно подход «один раз выгрузить статику и скопировать». Это не проблема вида «не удалось определить семейство алгоритма». Дело не в слабом сопоставлении отпечатков: объекта для сопоставления просто нет в той копии кода, которая у вас на руках.

Первый тип: сервер присылает алгоритм, который нужно выполнить

Схема первого типа выглядит так: прежде чем пустить вас к endpoint, сервер отдаёт одноразовый JS-фрагмент. Скрипт исполняется в браузере, создаёт cookie или token, и без этого значения запрос не пройдёт. Содержимое скрипта обычно уникально для сессии, а иногда и для каждого запроса.

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

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

  • Собрать минимальную заглушку браузерной среды: пустые объекты для document, location, navigator, cookie, а также несколько Observers и таймеров. Дополняйте её только до тех пор, пока скрипт не перестанет падать из-за отсутствующих глобальных объектов.

  • Запускать полученный исходник в локальной песочнице Node/V8 — через node:vm или процесс, стартующий из execjs, — и обязательно ограничивать время выполнения.

  • При работе скрипт запишет cookie либо положит значение в глобальный объект. Перехватите эту запись и извлеките нужный token.

Проверка посетителя у Zhihu устроена именно так: полученный скрипт записывает идентификатор посетителя в browser cookie, а вы подменяете DOM-среду, чтобы он считал, будто работает на реальной странице, после чего забираете значение. Ни одной строки алгоритма при этом реверсить не пришлось: вы лишь подготовили достаточно убедительную сцену, на которой скрипт сам сыграл свою роль. Цена решения заключается не в «понимании алгоритма», а в том, чтобы «поддерживать достаточно правдоподобную среду». Скрипт может проверять navigator.webdriver, искать определённый node, ожидать ответ от API — и ваша заглушка должна обманывать его ровно в нужной степени, не разрастаясь до неподдерживаемого монстра. Этому компромиссу посвящён раздел ниже о «минимальной поверхности выполнения».

Второй тип: динамический идентификатор, который обновляется с релизом

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

Классический пример — современный фронтенд с заранее зарегистрированными запросами вместо открытого GraphQL-текста. Каждая операция — получение ленты, результатов поиска и так далее — сопоставлена с operation id или query id, заданным на этапе сборки и передаваемым в пути endpoint. Этот id можно найти в build-выводе, но после нового релиза фронтенда та же операция получает другое значение.

Здесь статический анализ создаёт ложное чувство победы. Вы нашли id, скопировали его, проверили в тот же день и захардкодили. Через две недели endpoint начинает возвращать 400 — и только тогда выясняется, что «константа» всё это время была живой. Этот вариант коварнее первого именно потому, что кажется уже решённым.

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

  1. Посмотреть ресурсы, которые страница уже загрузила. Страница только что отправила запрос с этим идентификатором, значит актуальное значение уже есть в URL и его можно извлечь из записи ресурса. Это самый дешёвый вариант: вы считываете существующий факт, а не трогаете алгоритм.

  2. Если не получается взять его оттуда, извлечь значение по паттерну из исходника скрипта. Найдите в актуальном бандле декларацию вида «этому имени операции соответствует этот id» и прочитайте его.

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

Именно так читаются operation id для ленты и поиска Twitter: id берут из URL GraphQL-запроса, который страница уже отправила, затем кешируют на время сессии и используют дальше.

Системный разбор этого подхода — сколько существует способов получить заранее зарегистрированный запрос, во что каждый обходится и какой выбирать в разных условиях — есть в материале о persisted operations. Здесь важно лишь отметить, что это часть более широкого класса значений, получаемых во время выполнения.

Чем эти два случая отличаются на практике

Если поставить оба типа рядом, выбор действия становится очевидным.

Первый тип: challenge

Второй тип: динамический идентификатор

Где находится актуальное значение

Приходит во время выполнения, в статическом коде его нет

Есть в статическом коде, но обновляется с каждым релизом

Что делать

Выполнять: запускать реальный алгоритм и забирать побочный эффект

Читать: находить и считывать константу, а не запускать алгоритм

Как выглядит сбой

Слишком бедная заглушка среды, код не доходит до конца

Читалка ломается после перестановки в бандле и возвращает устаревшее либо пустое значение

Кто запускает изменения

Сервер, в любой момент

Релиз фронтенда, по графику деплоев

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

Почему не стоит любой ценой очищать такую реализацию

Здесь обычно возникает возражение: разве нельзя полностью отреверсить алгоритм присланного скрипта или восстановить правило генерации динамического идентификатора и написать нативную реализацию, не зависящую от исходного рантайма? Это и есть очистка на четвёртом этапе четырёхэтапного процесса: самый чистый вариант, одна инвестиция ради долгосрочной независимости, затем всё уходит в CI.

Для этих двух типов целей ответ чаще всего один: затраты не окупаются. Грубая, но рабочая модель окупаемости выглядит так:

Очистка требует разовых затрат C: отреверсить алгоритм и проверить его дифференциальным тестированием. После этого она экономит s за единицу времени по сравнению с постоянным выполнением или чтением. Срок окупаемости примерно равен C / s.

Решающая переменная здесь не C и не s, а цикл изменений цели T — то есть частота, с которой она меняется.

  • T < C/s: цель изменится раньше, чем окупится работа. Очищенная реализация перестанет совпадать уже через несколько дней после выпуска, и всё придётся делать заново. Отдача отрицательная.

  • T намного больше C/s: очистка гарантированно выгодна. Одна инвестиция работает долго, и стоит подниматься по лестнице очистки к уровню нативной переписи.

Эти два типа естественным образом попадают в разные зоны:

  • Цикл T у первого типа контролирует сервер, и он может быть сколь угодно коротким. Логику выдаваемого скрипта могут изменить в любой момент, а предсказать это невозможно. Поэтому почти всегда разумнее выполнять код. Пытаться очистить алгоритм, который могут поменять завтра, — значит поставить свою судьбу в зависимость от чужого релизного ритма.

  • У второго типа T равен циклу релизов фронтенда — от дней до месяцев. Чем надёжнее ваша читалка, чем больше у неё запасных вариантов и устойчивых якорей, тем ниже C/s. Здесь могут быть разумны оба решения, так что расчёт действительно имеет смысл.

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

Как ограничить поверхность выполнения минимумом

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

Запускайте только необходимый фрагмент. Не тащите в решение весь рантайм страницы. Передавайте лишь код, от которого действительно зависит алгоритм, и минимальную заглушку. У размера заглушки есть оптимум: она должна быть достаточно маленькой, чтобы код просто перестал падать. Каждая дополнительная подмена — это новая точка поддержки: поменяют проверку, придётся менять и её. Но недостающая заглушка сразу обрушит выполнение. Не переносите целую браузерную среду только ради экономии усилий — иначе вам придётся поддерживать не signer, а половину браузера.

Изолируйте контекст. Скомпилированный контекст V8/Node не потокобезопасен. Одноразовый challenge естественным образом открывает новую песочницу для каждого запуска, и они не мешают друг другу. Но как только вы начинаете переиспользовать скомпилированный контекст ради экономии на старте или кешировать парсер динамического идентификатора между запросами, параллельные вызовы нужно сериализовать:

class RuntimeSigner:
    def __init__(self, source: Path) -> None:
        self._context = execjs.get("Node").compile(source.read_text())
        self._lock = threading.Lock()  # V8 context is not thread-safe

    def call(self, fn: str, *args):
        with self._lock:
            return self._context.call(fn, *args)

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

  • Среда слишком бедная: код выбрасывает ReferenceError или зависает до таймаута. В заглушке не хватает чего-то, что ему требуется.

  • Протокол изменился: выполнение завершилось, результат есть, но его форма неверна или JSON не парсится. Значит, поменялся формат вывода.

  • Не совпадает семантика: значение получено и парсится, но оно неполное либо не проходит валидацию при использовании. Изменился сам алгоритм.

Для этих трёх случаев нужны совершенно разные исправления: дополнить заглушку, проследить изменения протокола или проверить алгоритм. Исполнитель, который сообщает лишь общее «не сработало», заставляет каждый раз расследовать проблему с нуля. Зрелый executor явно различает ошибку выполнения, непарсируемый вывод, неполный результат и таймаут.

Какую модель использовать на каждом этапе

Роль модели в этом процессе не та же, что при работе с отпечатками. Там модель определяет семейство алгоритма. Здесь определять нечего: её основная задача — помочь принять архитектурное решение «очищать или выполнять» и установить причину сбоя при выполнении. Четыре подзадачи требуют от модели совершенно разных качеств:

Подзадача

Нужная способность

Выбор

model id

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

Сильное рассуждение и готовность спорить с собственным выводом

Claude Opus 5

claude-opus-5

Найти динамический идентификатор: определить место внедрения константы и кандидатов по всему бандлу

Длинный контекст, чтение всего бандла целиком

Kimi K3

kimi-k3

Массово отфильтровать скрипты-кандидаты, которые могут содержать нужную операцию

Низкая стоимость, сотни вызовов при высокой параллельности

Claude Sonnet 5

claude-sonnet-5

По логу или stack при ошибке выполнения определить сломавшийся слой

Средний уровень рассуждений, объяснение на основе конкретной ошибки

GPT-5.6 Sol

gpt-5.6-sol

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

Не верьте на слово — проверьте:

  1. Возьмите реальную цель, по которой решение уже принято, в качестве контрольного примера: вы интуитивно знаете, нужно её выполнять или очищать.

  2. Передайте наблюдаемые факты — логика приходит в рантайме или значение является релизной константой, как часто оно меняется, насколько глубока зависимость от окружения — в claude-opus-5 и gpt-5.6-sol. Пусть каждая модель подготовит memo с решением «очищать или выполнять».

  3. Проверьте два пункта: распознала ли модель цикл изменений как решающую переменную. Если она сравнивает только сложность реализации, она проиграла. И второе: можно ли наблюдать условия, при которых она изменит вывод. Формулировка вроде «если идентификатор перестанет меняться в следующем релизе, вернитесь к очистке» с понятным триггером пригодна для работы.

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

Главная проблема — цена переключения

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

AIReiter убирает этот слой: один ключ, один OpenAI-совместимый интерфейс, все четыре уровня доступны через него, а модель меняется заменой поля model в теле запроса.

# Purify or execute: the reasoning tier arguing both sides
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<observed facts + argue the strongest case for both purify and execute>"}]
  }'

# Locate the dynamic identifier across the bundle: change the model field, leave the rest
#   "model": "kimi-k3"
# Bulk-filter candidate scripts:
#   "model": "claude-sonnet-5"
# Attribute an execution failure:
#   "model": "gpt-5.6-sol"

Если вы уже используете OpenAI SDK, укажите для base_url значение https://aireiter.com/api/v1 и больше ничего не меняйте. Для Anthropic SDK используйте POST /api/v1/messages с тем же ключом.

По стоимости нагрузка в этом процессе распределена неравномерно. Kimi K3 читает весь фронтенд-бандл в поисках динамического идентификатора — это несколько сотен тысяч токенов на вход. Claude Sonnet 5 массово фильтрует скрипты-кандидаты, и количество вызовов легко уходит за сотни. Именно эти два шага формируют основную часть счёта. Скидка 30% на Claude приходится прямо на массовую фильтрацию Sonnet и аргументацию решения Opus, GPT за полцены — на диагностику сбоев через GPT-5.6 Sol, а чтение целого бандла в длинном контексте K3 доступно по тому же ключу. Скидка действует на наиболее токеноёмкую пакетную работу и самый дорогой reasoning-уровень, а не на абстрактно дешёвую модель.

  • Получить API key

  • Попробовать без регистрации: сначала вручную запустите несколько memo «очищать или выполнять», сравните две модели по тому, распознают ли они цикл изменений, и только затем решайте, стоит ли встраивать их в процесс.

Итоги

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

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

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