Перед вами SDK для подписи, выплюнутый webpack: переменные называются a и _0x3f2b, поток выполнения намеренно запутан, а строковые литералы спрятаны в массиве и достаются по индексу. Задача понятна: получить самостоятельную реализацию, которая будет выдавать в точности те же байты, что и оригинал.
Сегодня в такой момент почти все делают одно и то же: вставляют весь бандл в чат с LLM и спрашивают: «Что делает этот код?» Обычно ответ выглядит убедительно. Вы пишете реализацию по его описанию — и она промахивается мимо целевого результата буквально на один байт.
Дело не в том, что модель бесполезна. Просто ей часто отдают не ту часть работы. При деобфускации LLM хорошо справляется с распознаванием паттернов и построением гипотез, но плохо годится для фактической верификации. Она увидит каркас криптографического примитива среди битовых операций, но не сможет достоверно подтвердить, что распознала его верно. Это должны решать ваши тесты.
Ниже — процесс из четырёх этапов, построенный вокруг этого разделения ролей, и три задачи, которые я модели не доверяю никогда.
Этап 1. Разбивайте бандл скриптом, а не силами LLM
Первая мысль обычно такая: «Контекстное окно огромное — загружу туда весь файл». Не стоит. И причин две.
Во-первых, это пустая трата контекста. Основную часть обфусцированного бандла составляют полифилы, runtime-обвязка и бизнес-модули, не относящиеся к интересующему вас коду. Вы оплачиваете их передачу модели, а взамен получаете рассеянное внимание.
Во-вторых, и это важнее: чем больше контекст, тем больше мест, куда может встроиться галлюцинация. Модель способна склеить признаки двух несвязанных модулей и выдать внутренне непротиворечивый вывод, которого в коде в действительности нет. Такие ошибки ловить гораздо сложнее, чем явную бессмыслицу.
Разбиение — детерминированная задача. Автоматизируйте её:
С помощью AST-инструмента —
@babel/parserилиacorn— разделите бандл на модули и функции, затем проиндексируйте их по областям видимости;Извлеките все числовые литералы и строковые константы, сгруппировав их по частоте и разрядности;
Постройте граф вызовов и отметьте узлы с in-degree 0 — точки входа — и out-degree 0, то есть листовые примитивы;
Найдите функции с аномально высокой плотностью битовых операций: сочетание
^,>>>,<<и&внутри одной функции обычно указывает на алгоритмическое ядро.
Именно листовые примитивы стоит показывать модели. Как правило, это несколько десятков строк с минимальной зависимостью от состояния верхних уровней и чёткими границами входа и выхода. Функция на 40 строк вместе с используемыми константами — размер, с которым модель работает надёжно.
После этого этапа у вас должен появиться список кандидатов на роль примитивов: тело функции, все задействованные ею константы и места вызова. Дальше каждый запрос к модели работает ровно с одним элементом этого списка.
Этап 2. Поручите модели определить семейство алгоритма
Здесь LLM действительно трудно заменить.
У криптографических алгоритмов и кодировок есть узнаваемые отпечатки: характерные константы, сочетания величин сдвигов, структура циклов. Человек распознаёт их по мере накопления опыта. Модель же видела публичные реализации практически всего этого, поэтому такая задача для неё естественна.
Вот несколько публичных примеров того, как выглядят такие отпечатки:
Пара
0x811c9dc5и0x01000193— начальное значение и простое число 32-битного хеша FNV-1a. Это стандартные значения со справочной страницы FNV, которую ведёт один из соавторов алгоритма, Landon Curt Noll; там они приведены в десятичном виде: 2166136261 и 16777619;0x61707865, 0x3320646e, 0x79622d32, 0x6b206574— little-endian-представление ASCII-строки"expand 32-byte k". Это константы начального состояния ChaCha20 из RFC 8439 §2.3;Последовательность вида
a += b; d ^= a; d = rotl(d, 16); c += d; b ^= c; b = rotl(b, 12);с четырьмя сдвигами 16/12/8/7 — сигнатура quarter round в ChaCha20. Родственный Salsa20 использует 7/9/13/18, и уже этих четырёх чисел достаточно, чтобы отличить их друг от друга;Таблица из 64 констант, начинающаяся с
0xd76aa478, — это T-таблица MD5 из RFC 1321 §3.4;Таблица на 256 байт, начинающаяся с
0x63, 0x7c, 0x77, 0x7b, — S-box AES, приведённый в FIPS 197, таблица 4;0xEDB88320— отражённый полином CRC-32, используемый в спецификации gzip, RFC 1952.
Всё решает формулировка запроса. Вопрос «Что делает этот код?» даст вам связный текст. Нужен не он, а структурированный и проверяемый вывод. Поэтому промпт должен требовать три вещи: кандидаты, доказательства и контрдоводы.
Below is a leaf function extracted from an obfuscated bundle, together with
every numeric constant it references.
<function>
{{function body}}
</function>
<constants>
{{constant list, with locations}}
</constants>
Answer in the following structure. Do not write prose.
1. Candidate algorithm families (at most 3, ranked by likelihood)
For each: the name, and which class it belongs to
(hash / stream cipher / block cipher / encoding / compression / checksum)
2. Supporting evidence
Every piece of evidence must point to a specific constant value or a
specific line above. "Structurally similar" and other unverifiable
phrasing is not allowed.
3. Counter-evidence and deviation points
If this were the standard implementation of that algorithm, what should
appear here but doesn't? What appears that a standard implementation
would never contain? Are these deviations a "variant," or "I got it wrong"?
4. The minimal test to decide this hypothesis
Give 3 concrete inputs and, if the hypothesis holds, the shape of the
output each should produce. Include boundary cases.
Третий раздел — главная ценность этого промпта. Отклонения от стандартной реализации как раз показывают, что было изменено в конкретном коде: собственный алфавит, заменённая константа, другое число раундов. Именно эти отличия потребуют работы при переписывании; всё остальное можно взять из публичной реализации. О том, как заставить модель найти каждое такое отклонение, подробно рассказано в материале об идентификации алгоритмов по отпечаткам.
Четвёртый раздел сразу превращает вывод модели в следующие тесты и избавляет от лишнего обмена сообщениями.
На этом же этапе часто встречаются нестандартные кодировки. Проверка здесь механическая: алфавит из 65 символов — 64 символа и один знак заполнения, группировка по 6 бит, длина результата ceil(n/3)*4 — перед вами семейство Base64; переставленный алфавит означает собственную таблицу. Аналогично, если словарь начинается с 256 и растёт, а ширина выходного кода увеличивается по мере его заполнения, это LZW. Всё это можно подтвердить только по соотношениям длины входа и выхода, не читая ни одной строки кода.
Этап 3. Любую гипотезу проверяйте дифференциальным тестом
Модель выдвинула гипотезу. Пока вы не написали тест, это всего лишь утверждение.
Обойти этот этап нельзя: это единственный барьер во всём процессе, который останавливает галлюцинации. Почему он единственный, разобрано в материале о дифференциальном тестировании. Рассматривайте исходную реализацию как чёрный ящик и по очереди сравнивайте с ней свой переписанный вариант:
# Differential-test skeleton: original as black box, rewrite compared case by case
CASES = [
b"", # empty input: exposes initial state and padding logic
b"\x00", # single zero byte
b"\xff", # single high byte: checks sign-bit handling
b"a" * 63, # one below the block boundary
b"a" * 64, # exactly one block
b"a" * 65, # one above: checks padding and carry
bytes(range(256)), # full byte coverage: checks alphabet mapping
]
for case in CASES:
assert rewritten(case) == blackbox(case), case.hex()
Несколько практических правил:
Больше всего информации дают переходы через границу. У блочных алгоритмов логика padding лучше всего проявляется на 64n и 64n±1. Если во всём наборе не проходит только один случай, длина входа сразу подскажет, на каком уровне ошибка.
Запускайте один и тот же вход дважды. Если результаты отличаются, реализация примешивает случайное число или временную метку. Нужно найти точку их добавления и дать возможность подменять значение извне — иначе дифференциальное сравнение в принципе невозможно. Это самая частая проблема при переписывании логики подписи: алгоритм верен, просто источник энтропии ещё не вынесен наружу.
Снимайте слои по одному, не сравнивайте итог целиком. Сначала добейтесь совпадения внутреннего хеша, затем проверьте слой кодирования, а после — сборку результата. Когда расходится весь вывод, источник ошибки неизвестен; при послойной проверке виноват первый не прошедший слой.
Зафиксируйте тестовые векторы как fixture. Таблица «известный вход → известный выход» позволяет быстро понять после обновления внешней реализации: «я ошибся в коде или они что-то поменяли?» Со временем ценность такого набора только растёт.
На этом этапе модель может предложить тестовые случаи и помочь интерпретировать различия, но не должна решать, кто прав. Решает assert.
Этап 4. Доводите реализацию до продакшена по уровням изоляции
Когда все гипотезы подтверждены, их нужно превратить в решение, пригодное для долгой эксплуатации. Здесь есть приоритетный порядок: чем выше уровень, тем настойчивее стоит за него бороться.
Нативная реализация на целевом языке. Полностью отделена от исходного runtime и зависит только от стандартной библиотеки. Только этот вариант обходится без отдельного процесса, дополнительных зависимостей и безболезненно встраивается в CI.
Локальный JS-движок с минимальным фрагментом. Некоторые части логики слишком дорого очищать в краткосрочной перспективе, поэтому можно сохранить небольшой фрагмент оригинального JS и запускать его локально в Node/V8. Но контекст JS-движка не потокобезопасен: вызовы одного скомпилированного контекста из нескольких потоков нужно защищать блокировкой:
class Signer:
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)
Пассивный мост к браузеру. Если нужное состояние можно получить только из runtime реальной страницы, браузер пока остаётся единственным вариантом. Это временное решение: явно зафиксируйте его в документации интерфейса и не позволяйте ему стать реализацией по умолчанию.
Этот порядок деградации стоит закрепить в правилах проекта. С каждым переходом вниз на уровень на порядок растут поверхность зависимостей, число сценариев отказа и цена развёртывания: на первом уровне у вас чистая функция, на третьем — внешний процесс, которому нужен человек, чтобы поддерживать сессию. По умолчанию добивайтесь первого уровня — так вы избежите большей части долгосрочных затрат, которые незаметно накапливает подход «лишь бы заработало». О трёх уровнях и о том, как не дать пассивному мосту разрастись в неконтролируемую систему, читайте в материале о лестнице очистки протокола.
Для ориентира: обфусцированный SDK для подписи после прохождения всех четырёх этапов превращается в самостоятельную реализацию менее чем на 600 строк, зависящую только от встроенного crypto runtime. Такое сжатие не означает, что модель «поняла» исходный файл: когда семейство алгоритма распознано правильно, большую часть кода можно просто взять из публичной реализации.
Какую модель использовать на каждом этапе
Четыре этапа требуют принципиально разных способностей. Одна модель на весь процесс означает либо лишние расходы, либо потерю точности:
Этап | Что ей действительно нужно уметь | Мой выбор | model id |
|---|---|---|---|
Структурная карта после разбиения | Длинный контекст, чтение графа вызовов целого модуля за один проход | Kimi K3 |
|
Определение семейства алгоритма и контрдоводы | Сильное рассуждение: находит отклонения и умеет спорить с собственным выводом | Claude Opus 5 |
|
Массовое переименование символов и восстановление комментариев | Низкая стоимость, сотни вызовов с высокой параллельностью | Claude Sonnet 5 |
|
Атрибуция различий при падении теста | Средний уровень рассуждений, объяснение расхождений на уровне конкретных байтов | GPT-5.6 Sol |
|
Особенно важен второй этап. Определение семейства алгоритма — единственный шаг, на котором смена модели заметно меняет результат. Здесь проверяется сразу два качества: сколько публичных реализаций модель видела и готова ли она критиковать собственный ответ. На одной и той же листовой функции слабая модель уверенно назовёт неправильный алгоритм, а сильная сама вычеркнет своего кандидата в разделе с контрдоводами.
Не верьте мне на слово — проверьте сами. Вот протокол:
Возьмите 3 листовые функции из своего обфусцированного бандла; ответ хотя бы для одной из них вы уже должны знать — это будет контрольный пример.
Используя трёхчастный промпт из этапа 2, отдельно отправьте один и тот же вход в
claude-opus-5иgpt-5.6-sol.Смотрите только на две вещи: угадала ли модель семейство кандидата и действительно ли раздел с контрдоводами возражает самому выводу, а не пересказывает его другими словами.
Качество контрдоводов и будет критерием выбора: от него напрямую зависит, сколько бесполезных тестов вы напишете на этапе 3.
На этапах 3 и 4 выбор модели почти неважен — подойдёт любая работающая. На первом этапе нужна лишь модель с длинным контекстом, чтобы не писать собственный механизм извлечения фрагментов.
Главная проблема — не выбор модели, а цена переключения
Четыре модели от трёх поставщиков — это три SDK, три схемы авторизации и три формата ошибок. Переписывать клиент ради небольшой экономии не хочется, и именно поэтому большинство в итоге запускает одну модель для всех задач.
AIReiter убирает этот слой: один ключ, единый OpenAI-совместимый интерфейс, все четыре модели за ним, а переключение сводится к замене поля model в теле запроса.
# Algorithm-family ID: the reasoning tier
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": "<Stage 2 prompt + leaf function + constants>"}]
}'
# Bulk symbol renaming: change the model field, leave everything else
# "model": "claude-sonnet-5"
Если вы уже используете OpenAI SDK, укажите для base_url значение https://aireiter.com/api/v1 — больше ничего менять не нужно. Для Anthropic SDK отправляйте запрос на POST /api/v1/messages, используя тот же ключ.
По цене модели Claude доступны со скидкой 30% от прайса, а GPT — за половину цены. Для этого процесса разница существенна: массовое переименование символов на этапе 3 легко требует сотен вызовов, а один проход с длинным контекстом на этапе 1 занимает несколько сотен тысяч токенов на каждый вход. Именно на эти два этапа приходится основная стоимость, и скидка применяется как раз к самой дорогой части работы.
Попробовать без регистрации — сначала вручную запустите несколько промптов, сравните разделы с контрдоводами у двух моделей и автоматизируйте процесс только после выбора.
Три вещи, которые не стоит поручать модели
Первое: не просите её сразу написать финальную реализацию. На запрос «перепиши всё целиком» модель выдаст код, который выглядит завершённым и запускается, но содержит незаметные отклонения. А найти их будет трудно, потому что вы не проверяли эту реализацию слой за слоем. Правильнее получать от модели гипотезу для каждого примитива, проверять их по одному и собирать решение самостоятельно. Это медленнее, зато вы понимаете, почему каждая строка написана именно так.
Второе: не доверяйте ей решение о корректности результата. Вопрос «Можешь подтвердить, что эта реализация верна?» бесполезен: модель склонна соглашаться с пользователем. Единственный источник истины — дифференциальный тест. Модель считает результат правильным, а тест падает? Прав тест. Модель говорит, что код неверен, но проходят все тесты? И здесь прав тест.
Третье: не перекладывайте на неё оценку правомерности. Можно ли исследовать целевую систему, публиковать выводы и использовать полученные данные — зависит от вашей юрисдикции, условий целевого сервиса и конкретной цели. У модели нет фактической основы, чтобы рассуждать об этом: её ответ лишь имитирует тон знакомых ей дисклеймеров. Решение должны принять вы или ваш реальный юрист.
Итог
Роль модели в такой работе вполне конкретна: это распознаватель паттернов, видевший публичные реализации алгоритмов и способный за секунды предложить правдоподобные гипотезы. Она не является источником окончательных ответов и не выступает верификатором.
Каркас процесса не зависит от использования ИИ: механическое разбиение, гипотеза, проверка, очистка. Эти четыре шага существовали задолго до языковых моделей. LLM ускоряет только этап гипотезы: вместо дней поиска по справочникам он занимает минуты. Остальные три этапа требуют ровно столько же работы, сколько и раньше.
Когда вызовы моделей для этих четырёх этапов закреплены в скриптах, весь процесс становится быстрым. Остаётся только трение при переключении между моделями — инфраструктурная проблема, которую решает выбор модели поверх единого интерфейса.
