AIREITER

Não encontra a lógica de assinatura no código? Talvez ela nem esteja lá

Última Atualização: 2026-07-31 07:45:33

Você desmontou o bundle webpack inteiro, recortou a AST, catalogou as primitivas terminais e já consegue identificar fingerprints — aquela rotina que você domina. Aí sobra um valor de assinatura em um header da requisição: ele muda toda vez, e você quer encontrar a função que o gera. Você procura operações de bit suspeitas e estruturas de hash em todos os assets estáticos, mas não acha nada.

O problema não é falta de cuidado na busca. A lógica simplesmente não está no código que você extraiu.

Há uma categoria de assinaturas para a qual a análise estática sempre vai falhar, porque o algoritmo só existe em tempo de execução. Diante desse tipo de alvo, insistir em encontrar uma função inexistente é perder tempo. A abordagem correta é mudar de estratégia: em vez de reverter o algoritmo, execute-o ou leia o valor, com o mínimo de intervenção possível.

Como saber que a análise estática chegou ao limite

Antes de concluir isso, vale confirmar que você está mesmo diante desse cenário e não apenas deixou algo passar. Há dois sinais fáceis de identificar.

O primeiro: o valor muda a cada sessão ou requisição, mas nenhum asset estático o produz. Você o vê no tráfego de rede, ele é diferente a cada atualização da página, baixa todos os arquivos .js, faz busca textual e não encontra nenhum ponto onde seja montado. Nesse caso, o código responsável chega em tempo de execução.

O segundo: o valor aparece como literal pesquisável no bundle, mas aquilo que funcionava ontem deixa de funcionar hoje. É uma constante de string no resultado do build, válida se você a copiar naquele dia. Alguns dias depois, o endpoint começa a falhar; ao revisar o bundle, aquela “constante” foi substituída por outro literal novo, também pesquisável. Ela é fixada no build, mas muda a cada release do frontend.

Os dois sinais têm algo em comum: a visão estática é uma fotografia, enquanto a realidade é um fluxo que muda com o tempo ou a cada sessão. Você busca no código e não encontra nada, ou encontra algo que está vivo. Em ambos os casos, a estratégia de “extrair uma vez do estático e copiar” deixou de servir. Isso é diferente de não conseguir reconhecer a família do algoritmo. Não significa que seu matching de fingerprints seja fraco; significa que o objeto a ser comparado não está na cópia de código que você tem em mãos.

Tipo um: o servidor entrega o algoritmo para você executar

O primeiro padrão funciona assim: para acessar determinado endpoint, o servidor primeiro entrega um trecho de JS de uso único. O script roda no navegador, gera um cookie ou token, e você precisa carregá-lo para passar pela verificação. Em geral, o conteúdo do script varia por sessão e, às vezes, por requisição.

É por isso que a análise estática inevitavelmente fica vazia: a lógica chega em tempo de execução e simplesmente não existe no bundle estático. Um script que você capturou é apenas uma instância específica. O que você extraiu hoje pode ser completamente diferente do que será entregue amanhã; revertê-lo é perseguir um alvo móvel.

A saída é executá-lo como caixa-preta. Você não precisa entender o cálculo, apenas oferecer um ambiente suficientemente real para o script chegar ao resultado e então capturar esse resultado. Na prática:

  • Monte um shim mínimo de ambiente de navegador: stubs vazios para document, location, navigator, cookie, além de alguns Observers e timers. Complete apenas o necessário até o script parar de lançar erro por falta de algum global.

  • Execute o código entregue em um sandbox local de Node/V8, como node:vm ou um processo iniciado via execjs, com timeout de execução.

  • Quando o script rodar, ele gravará um cookie — ou colocará um valor em algum global. Intercepte essa gravação e extraia o token de que você precisa.

A verificação de visitante do Zhihu segue esse padrão: um script entregue pelo servidor grava um identificador de visitante no cookie do navegador. Você simula o ambiente DOM para fazê-lo acreditar que está em uma página real e, ao terminar, captura o valor. Em todo esse processo, você não reverteu uma única linha do algoritmo; apenas montou um palco convincente o bastante para ele encenar o próprio papel. O custo, portanto, não está em “entender o algoritmo”, mas em “manter o ambiente realista o suficiente”. O script pode sondar navigator.webdriver, verificar se determinado nó existe ou esperar retorno de alguma API; seu shim precisa enganá-lo na medida certa, sem crescer a ponto de se tornar impossível de manter. Esse é o equilíbrio por trás da seção sobre “superfície mínima de execução” mais adiante.

Tipo dois: o identificador está no código, mas muda a cada release

O segundo caso é o inverso: o valor realmente está no bundle estático como literal, mas é gerado no build e muda a cada release do frontend.

O exemplo clássico são frontends modernos que usam queries pré-registradas em vez de GraphQL em texto puro. Cada operação — carregar a timeline, buscar resultados — é mapeada para um operation id ou query id definido no build e enviado no caminho do endpoint. Esse id pode ser pesquisado no bundle, mas assim que o frontend publica uma release, o mesmo operation passa a ter outro valor.

A análise estática cria uma falsa sensação de resolução. Você encontrou o id, copiou, usou no mesmo dia e o deixou hardcoded. Duas semanas depois, o endpoint retorna 400 e só então fica claro que aquela “constante” estava viva. É mais traiçoeiro que o tipo um, justamente porque parecia que você já tinha encontrado a resposta.

A estratégia certa não é revertê-lo — não há algoritmo algum para reverter, é uma constante de build —, mas ler o valor atual da página atual no momento da requisição e mantê-lo em cache durante a sessão. Há uma escada de opções, da mais barata à mais cara, e vale testar as primeiras antes:

  1. Examine os recursos já carregados pela página. Ela acabou de enviar uma requisição contendo esse identificador; o valor atual está naquela URL, e você pode extraí-lo do registro do recurso. É a opção mais barata: ler um fato já existente, sem tocar em uma linha de algoritmo.

  2. Se isso não for possível, extraia por padrão no código-fonte do script. Localize no bundle atual a declaração que mapeia “este nome de operação para este id” e capture o valor.

  3. Somente se as anteriores falharem, investigue a tabela de módulos do bundler ou busque e faça parse de bundles relacionados a partir de pistas. É a alternativa mais cara e deve ficar por último.

Os operation ids de timeline e busca do Twitter são lidos exatamente assim: o id é extraído de uma URL de requisição GraphQL que a própria página já enviou e, depois de obtido, fica em cache durante toda a sessão.

A versão sistemática desse problema — as diversas formas de obter uma query pré-registrada, seus custos e quando usar cada uma — está no artigo sobre operações persistidas. Aqui, o ponto é apenas situá-lo na categoria maior de valores derivados em tempo de execução.

O que separa os dois cenários

Ao colocá-los lado a lado, fica claro o que fazer em cada caso.

Tipo um: desafio

Tipo dois: identificador dinâmico

Onde está a verdade

É entregue em tempo de execução; nunca está no código estático

Está no código estático, mas muda a cada release

O que fazer

Executar: rodar o algoritmo real e capturar o efeito colateral

Ler: localizar e extrair uma constante, sem executar algoritmo

Como a falha aparece

O shim de ambiente é insuficiente e o código não termina

O leitor deixa de encontrar o valor após uma reorganização do bundle e obtém algo vazio ou desatualizado

Quem dispara a mudança

O servidor, a qualquer momento

Uma release do frontend, conforme o ritmo de deploy

Em uma frase: os dois casos quebram a ideia de “extrair uma vez do estático e copiar”; a diferença é apenas se você precisa de fato executar o código. Saber em qual categoria está define imediatamente se o próximo passo é montar um sandbox ou criar um extrator.

Por que não vale forçar a purificação nesses alvos

Alguém pode contestar: não seria possível reverter por completo o algoritmo do script entregue, ou reconstruir integralmente a regra que gera o identificador dinâmico, em uma implementação nativa independente do runtime original? Essa é a purificação da etapa quatro do fluxo de quatro estágios: a forma mais limpa, um investimento único para eliminar dependências no longo prazo, integrada ao CI.

Para esses dois tipos de alvo, em geral isso não compensa. Um modelo simples, mas útil, de retorno é este:

A purificação tem um custo único C — reverter o algoritmo e validá-lo com testes diferenciais. Depois de pronta, ela economiza s por unidade de tempo em relação a “executar ou ler a cada vez”. O prazo de retorno é aproximadamente C / s.

Mas a variável decisiva não é C nem s. É T: o ciclo de mudança do alvo, ou seja, a frequência com que ele muda.

  • T < C/s: o alvo muda antes de o investimento se pagar; a implementação purificada deixa de corresponder poucos dias após o deploy, e você precisa refazê-la. O retorno é negativo.

  • T muito maior que C/s: a purificação é uma vitória clara. Um investimento dura bastante tempo, e faz sentido subir a escada de purificação rumo ao nível de reescrita nativa.

Os dois tipos de alvo naturalmente caem em pontos diferentes:

  • No tipo um, T é controlado pelo servidor e pode ser arbitrariamente curto. A lógica do script entregue pode mudar a qualquer momento, sem previsibilidade. Por isso, quase sempre ele fica no lado de “executar”. Forçar a purificação de um algoritmo que alguém pode mudar amanhã significa atrelar seu destino ao ritmo de release dessa outra parte.

  • No tipo dois, T acompanha o ciclo de releases do frontend, de dias a meses. Quanto mais robusto for seu leitor — com alguns fallbacks adicionais e âncoras mais estáveis — menor fica C/s. Os dois lados podem fazer sentido, então aqui vale realmente calcular.

Esse é o sentido completo do título. Não encontrar algo no código não quer dizer necessariamente que você deixou passar. Talvez não exista ali um algoritmo estável que valha a pena purificar.

Como definir a superfície mínima de execução

Depois que você escolhe “executar” em vez de “purificar”, o objetivo de engenharia muda. Você não busca uma solução elegante; busca reduzir a superfície de execução ao mínimo, mantê-la controlada e tornar cada falha atribuível. Há três pontos.

Execute apenas o fragmento necessário. Não leve todo o runtime da página para dentro do processo. Forneça somente o código de que o algoritmo depende, mais um shim mínimo. Existe um ponto ideal para o tamanho do shim: ele deve ser pequeno o bastante para apenas permitir que o código rode sem lançar exceção. Cada stub extra vira mais manutenção — eles alteram uma sondagem, e você precisa acompanhar —, enquanto cada stub ausente derruba a execução imediatamente. Não coloque um ambiente inteiro de navegador só por conveniência; nesse ponto, você não estará mantendo um signer, mas metade de um navegador.

Proteja o contexto. Um contexto V8/Node compilado não é thread-safe. Um desafio de uso único naturalmente abre um sandbox novo a cada execução, sem interferência entre chamadas. Mas, no momento em que você reutiliza um contexto compilado para economizar inicialização, ou compartilha em cache um parser de identificador dinâmico entre requisições, as chamadas concorrentes precisam ser serializadas:

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)

Toda falha precisa ser atribuível. Este é o aspecto mais ignorado — e o que mais economiza tempo — ao buscar valores via execução. Uma falha precisa indicar em qual camada ocorreu:

  • Ambiente insuficiente: o código lança um ReferenceError ou trava até o timeout. Falta no shim algo de que ele precisa.

  • Protocolo alterado: o código termina e gera saída, mas o formato está errado ou o JSON não faz parse. O formato de saída mudou.

  • Semântica não atendida: você recebeu um valor, ele faz parse, mas está incompleto ou falha na validação ao ser usado. O próprio algoritmo foi alterado.

Esses três casos exigem correções completamente diferentes: ajustar o shim, acompanhar o protocolo ou verificar o algoritmo. Um executor que apenas informa “falhou” obriga você a investigar do zero a cada incidente. Um executor de desafio maduro os distingue explicitamente: falha de execução, saída não parseável, resultado incompleto e timeout devem ser erros separados.

Qual modelo usar em cada etapa

O papel do modelo nesse fluxo é diferente do trabalho de fingerprinting. Lá, ele ajuda a identificar a família do algoritmo. Aqui, não há algoritmo para identificar; sua principal função é ajudar na decisão arquitetural entre “purificar ou executar” e atribuir falhas de execução. As quatro subetapas pedem capacidades bem diferentes:

Subetapa

Capacidade necessária

Escolha

model id

Decidir entre purificar e executar, argumentando os dois lados

Raciocínio forte, disposto a contestar a própria conclusão

Claude Opus 5

claude-opus-5

Descobrir onde o identificador dinâmico está escondido: localizar o ponto de injeção da constante e ler candidatos no bundle

Contexto longo, capaz de ler o bundle inteiro de uma vez

Kimi K3

kimi-k3

Filtrar em massa quais scripts candidatos podem conter a operação procurada

Baixo custo, centenas de chamadas com alta concorrência

Claude Sonnet 5

claude-sonnet-5

Em uma falha de execução, ler o log ou stack e identificar a camada que falhou

Raciocínio intermediário, com explicação baseada em um erro específico

GPT-5.6 Sol

gpt-5.6-sol

A primeira linha merece atenção porque é a etapa deste artigo em que a troca de modelo altera visivelmente o resultado. O teste é justamente a capacidade de argumentar os dois lados e contestar a si mesmo, a mesma habilidade usada na seção de evidências contrárias do trabalho de fingerprinting. Um modelo mais fraco escolhe um caminho e acumula argumentos para confirmá-lo, sem considerar seriamente a alternativa. Um modelo de raciocínio forte leva “purificar” e “executar” até as últimas consequências, registra o argumento mais forte e o modo de falha de cada opção e chega a uma conclusão comparativa.

Não aceite isso apenas pela minha palavra. Teste:

  1. Escolha como controle um alvo real sobre o qual você já tomou essa decisão — um caso em que sua intuição já sabe se deve executar ou purificar.

  2. Forneça os fatos observados — lógica entregue em runtime ou constante de release, frequência de mudança e profundidade das dependências de ambiente — a claude-opus-5 e gpt-5.6-sol. Peça a cada um um memorando de decisão sobre “purificar versus executar”.

  3. Observe duas coisas: se o modelo reconheceu o “ciclo de mudança” como variável decisiva — se ele apenas comparar dificuldade de implementação, perdeu —; e se as condições para mudar de posição são observáveis. Algo como “se o identificador parar de mudar na próxima release, volte para purificar”, acompanhado de um sinal de gatilho, é utilizável.

Uma rodada basta para mostrar qual modelo realmente toma uma decisão e qual só decide por você.

O verdadeiro problema é o custo de trocar de modelo

Quatro modelos de três fornecedores, três SDKs, três esquemas de autenticação e três formatos de erro. Reescrever o cliente três vezes apenas para alternar modelos entre subetapas não compensa. Por isso, muita gente usa um único modelo em todo o fluxo, escolhe um modelo que não questiona a si mesmo na decisão entre “purificar ou executar”, mergulha na purificação de um alvo que o outro lado mudará amanhã e só percebe muito tarde que seguiu na direção errada desde o início.

AIReiter elimina essa camada: uma chave, uma interface compatível com OpenAI e as quatro categorias por trás dela. Para trocar de modelo, basta mudar o campo model no corpo da requisição.

# 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"

Se você já usa o SDK da OpenAI, aponte base_url para https://aireiter.com/api/v1 e não mude mais nada. No SDK da Anthropic, use POST /api/v1/messages com a mesma chave.

Em preço, o peso de tokens deste fluxo está concentrado: o Kimi K3 lê todo o bundle do frontend para localizar o identificador dinâmico, com algumas centenas de milhares de tokens por entrada, e o Claude Sonnet 5 filtra scripts candidatos em massa, facilmente em centenas de chamadas. São esses dois que definem a maior parte da conta. O desconto de 30% no Claude incide diretamente sobre a filtragem em massa com Sonnet e a argumentação decisória com Opus; o GPT pela metade do preço atende à atribuição de falhas com GPT-5.6 Sol; e a leitura de bundle inteiro em contexto longo do K3 pode ser chamada com a mesma chave. O desconto está justamente no lote mais pesado em tokens e na categoria de raciocínio mais cara, não em uma economia genérica.

Conclusão

Quando a análise estática não encontra nada, nem sempre o problema é sua habilidade. Às vezes, é a natureza do alvo: a assinatura não está no código porque chega em runtime ou porque muda a cada release.

Para esses dois cenários, pare de procurar uma função que não existe. No caso do desafio, execute o script de forma mínima, montando um sandbox apenas realista o suficiente para ele chegar a um resultado. No caso do identificador dinâmico, leia o valor em runtime e mantenha-o em cache durante a sessão. Antes de tudo, ambas as abordagens exigem reconhecer que a visão de “fotografia estática” deixou de funcionar.

E “purificar ou executar” é uma decisão puramente arquitetural, definida por uma variável: se o ciclo de mudança do alvo vence ou não seu prazo de retorno do investimento inicial. Nos casos de ciclo curto, especialmente aqueles que o servidor pode alterar a qualquer momento, forçar a purificação gera retorno negativo. Entregue essa análise dos dois lados a uma categoria de raciocínio capaz de contestar a própria conclusão; não decida só de cabeça e não deixe o modelo purificar por você. Purificação é engenharia determinística, e sua validação pertence aos testes diferenciais, que correspondem às etapas três e quatro do fluxo de quatro estágios. Aqui, o modelo serve apenas para ajudar você a pensar em uma coisa: se isso sequer vale a pena reverter.