AIREITER

¿No encuentras la lógica de firma en el código? Puede que ni siquiera esté ahí

Última actualización: 2026-07-31 07:45:33

Has descargado todo el bundle de webpack, troceado el AST, enumerado las primitivas finales y ya puedes identificar huellas de algoritmos, el procedimiento que conoces. Pero ahora tienes delante una firma en una cabecera de la petición: cambia cada vez y quieres localizar la función que la genera. Buscas operaciones de bits sospechosas, esqueletos de hash y cualquier pista en todos los recursos estáticos. No aparece nada.

No es que hayas buscado mal. La lógica que necesitas no está en el código que descargaste.

Hay una clase de firmas para las que el análisis estático siempre llegará a un callejón sin salida, porque el algoritmo solo existe en tiempo de ejecución. En estos casos no conviene seguir persiguiendo una función inexistente. Hay que cambiar de enfoque: no revertir el algoritmo, sino ejecutarlo o leer su resultado con la mínima intervención posible.

Dos pistas de que el análisis estático no basta

Antes de asumir que estás ante este caso, confirma que no se trata simplemente de algo que se te ha escapado. Hay dos señales bastante claras.

La primera: el valor cambia en cada sesión o petición, pero ningún recurso estático lo genera. Lo ves en una solicitud de red, se renueva con cada recarga, descargas todos los .js, haces búsquedas de texto completo y no encuentras ningún punto donde se construya. El código que lo compone llega en tiempo de ejecución.

La segunda: puedes localizar el valor literal en el bundle, pero el que anotaste ayer deja de funcionar hoy. Es una cadena constante en la salida de compilación y, si la copias ese día, sirve. Unos días después el endpoint da error y descubres que aquella «constante» ha sido sustituida por otro literal nuevo, también localizable. Está codificada en la compilación, pero rota con cada release del frontend.

Ambas situaciones comparten una idea: la vista estática es una fotografía, mientras que la realidad es un flujo que cambia por sesión o con el tiempo. Buscas en el código y no hallas nada, o lo encuentras pero ya ha cambiado. En ambos casos, lo que ha dejado de funcionar es «descargar una vez y copiar». No es el mismo problema que «no reconozco la familia del algoritmo». No es que falle tu identificación por huellas; es que aquello que intentas identificar no está en la copia del código que tienes delante.

Tipo uno: el servidor te entrega el algoritmo para que lo ejecutes

El primer caso funciona así: para acceder a un endpoint, el servidor te entrega antes una pieza de JavaScript de un solo uso. El script se ejecuta en el navegador, genera una cookie o un token, y debes incluirlo para continuar. Su contenido suele variar en cada sesión y, a veces, en cada petición.

Por eso el análisis estático tiene garantizado el fracaso: la lógica llega en tiempo de ejecución y, sencillamente, no forma parte del bundle estático. Un script capturado es solo una instancia concreta. El código que descargaste hoy puede no tener nada que ver con el que se entregue mañana; revertirlo sería perseguir un objetivo móvil.

La estrategia correcta es ejecutarlo como una caja negra. No necesitas entender qué calcula: basta con ofrecerle un entorno suficientemente real para que llegue a producir un resultado y extraerlo. En la práctica:

  • Prepara un shim mínimo de entorno de navegador: stubs vacíos para document, location, navigator, cookie, algunos Observers y temporizadores. Complétalo solo hasta que el script deje de fallar por la ausencia de un objeto global.

  • Ejecuta el código entregado en un sandbox local de Node/V8, mediante node:vm o un proceso iniciado con execjs, y establece un límite de tiempo de ejecución.

  • Al ejecutarse, el script escribe una cookie —o deja un valor en algún global—. Intercepta esa escritura y extrae el token que necesitas.

La comprobación de visitantes de Zhihu encaja en este patrón: un script entregado guarda un identificador de visitante en la cookie del navegador. Simulas el entorno DOM para que crea que se ejecuta en una página real y recoges el valor al terminar. No has revertido ni una línea del algoritmo; solo has montado un escenario suficientemente creíble para que represente su propia función. El coste no está en «entender el algoritmo», sino en «mantener el entorno lo bastante real». El script puede consultar navigator.webdriver, comprobar que existe algún nodo o esperar una respuesta de determinada API. Tu shim debe engañarlo con precisión, sin crecer hasta convertirse en algo inmantenible. Ese equilibrio es justo el que aborda la sección sobre la «superficie mínima de ejecución».

Tipo dos: el identificador estático que cambia en cada release

El segundo caso es el inverso: el valor sí está en el bundle estático como literal, pero se genera durante la compilación y cambia con cada lanzamiento del frontend.

El ejemplo clásico es un frontend moderno que utiliza consultas preregistradas en lugar de GraphQL en texto plano. Cada operación —cargar el timeline, obtener resultados de búsqueda— se asigna a un id de operación o consulta en tiempo de compilación, que viaja en la ruta del endpoint. Ese id puede encontrarse en el build, pero en cuanto el frontend publica una nueva versión, la misma operación pasa a tener otro valor.

El análisis estático aquí genera una falsa sensación de victoria. Lo encuentras, lo copias, funciona ese día y lo dejas hardcodeado. Dos semanas después el endpoint responde con un 400, y entonces descubres que la «constante» estaba viva. Es más engañoso que el tipo uno porque creías haber resuelto el problema.

No hay ningún algoritmo que revertir: es una constante de compilación. Lo adecuado es leer el valor actual de la página actual en tiempo de petición y guardarlo en caché durante la sesión. Para obtenerlo hay una escalera de opciones, de menor a mayor coste, y conviene probarlas en ese orden:

  1. Revisa los recursos que ya cargó la página. La propia página acaba de enviar una petición con ese identificador; el valor actual está en esa URL y puedes extraerlo del registro del recurso. Es la opción más barata: lees un hecho ya existente, sin tocar una línea de algoritmo.

  2. Si no puedes obtenerlo de ahí, extráelo por patrón desde el código fuente del script. Localiza en el bundle actual la declaración que asocia el nombre de la operación con su id y toma el valor.

  3. Solo si falla lo anterior, explora la tabla de módulos del bundler o descarga y analiza bundles relacionados a partir de pistas. Es la vía más costosa y debe quedar para el final.

Los ids de operación del timeline y la búsqueda de Twitter se obtienen precisamente así: se extraen de la URL de una petición GraphQL que la página ya ha enviado y, una vez localizados, se guardan en caché para toda la sesión.

La versión sistemática de este problema —las distintas formas de obtener una consulta preregistrada, sus costes y cuándo elegir cada una— se trata en el artículo sobre operaciones persistidas. Aquí basta con señalar que pertenece a la categoría más amplia de valores derivados en tiempo de ejecución.

La diferencia práctica entre ambos tipos

Al ponerlos lado a lado, queda claro qué hacer en cada caso.

Tipo uno: desafío

Tipo dos: identificador dinámico

Dónde está la verdad

Se entrega en tiempo de ejecución; nunca está en el código estático

Está en el código estático, pero cambia con cada release

Qué hacer

Ejecutarlo: correr el algoritmo real y recoger su efecto lateral

Leerlo: localizar y extraer una constante, no ejecutar ningún algoritmo

Cómo se manifiesta el fallo

El shim del entorno es insuficiente y el código no llega a terminar

El extractor falla tras una reorganización del bundle y devuelve un valor obsoleto o vacío

Quién provoca el cambio

El servidor, en cualquier momento

Un release del frontend, según la cadencia de despliegue

En una frase: ambos rompen el modelo de «descargar una vez y copiar»; la diferencia es únicamente si necesitas ejecutar el código de verdad. Saber en cuál estás determina si el siguiente paso es montar un sandbox o construir un extractor.

Por qué no conviene forzar la purificación en estos casos

Alguien planteará la objeción: ¿no podrías revertir por completo el algoritmo del script entregado, o reconstruir la regla que genera el identificador dinámico, y crear una implementación nativa desligada del runtime original? Esa es la purificación de la etapa cuatro del flujo de trabajo en cuatro etapas: la forma más limpia, una inversión para lograr independencia a largo plazo y llevarla a CI.

Con estos dos tipos de objetivo, normalmente no compensa. Un modelo aproximado, pero útil, de retorno sería este:

La purificación tiene un coste único C: revertir el algoritmo y validarlo con pruebas diferenciales. Tras purificarlo, ahorra s por unidad de tiempo respecto a «ejecutar o leer cada vez». El periodo de amortización es aproximadamente C / s.

La variable decisiva no es C ni s, sino el ciclo de cambio T del objetivo: cada cuánto se modifica.

  • T < C/s: cambia antes de que recuperes la inversión; la implementación purificada deja de coincidir pocos días después de desplegarla y tienes que rehacerla. El retorno es negativo.

  • T es mucho mayor que C/s: la purificación gana con claridad; una sola inversión dura mucho tiempo y deberías subir por la escalera de purificación hacia el nivel de reescritura nativa.

Los dos tipos encajan de forma natural en posiciones distintas:

  • El T del tipo uno lo controla el servidor y puede ser arbitrariamente corto. Pueden cambiar la lógica del script entregado en cualquier momento, y no puedes anticiparlo. Por eso casi siempre cae del lado de «ejecutar». Forzar la purificación de un algoritmo que alguien puede cambiar mañana deja tu destino en manos de su calendario de releases.

  • El T del tipo dos es el ciclo de releases del frontend: de días a meses. Cuanto más robusto sea tu lector —más alternativas, anclas más estables— menor será C/s. Ambas opciones pueden ser razonables, así que aquí sí debes hacer los números.

Ese es el sentido completo del título. Si no encuentras la lógica en el código, no significa necesariamente que te hayas equivocado. Puede que no exista un algoritmo estable que merezca la pena purificar.

Cómo definir una superficie mínima de ejecución

Una vez que eliges «ejecutar» en lugar de «purificar», cambia el objetivo de ingeniería. Ya no buscas limpieza: buscas reducir la superficie de ejecución al mínimo, mantenerla bajo control y poder atribuir cada fallo. Son tres puntos.

Ejecuta solo el fragmento imprescindible. No traslades todo el runtime de la página. Carga únicamente el código del que depende el algoritmo y un shim mínimo. El tamaño del shim tiene un punto óptimo: debe ser lo bastante pequeño para que el código se ejecute sin lanzar excepciones. Cada stub adicional suma mantenimiento —si cambian la comprobación, tendrás que seguirles el ritmo—, y cada stub ausente provoca un fallo inmediato. No metas un entorno completo de navegador por ahorrar trabajo; de lo contrario no estarás manteniendo un firmador, sino medio navegador.

Protege el contexto. Un contexto compilado de V8/Node no es thread-safe. Un desafío de un solo uso abre naturalmente un sandbox nuevo cada vez, por lo que no hay interferencias. Pero en cuanto reutilizas un contexto compilado para ahorrar arranque, o compartes entre peticiones un parser de identificadores dinámicos almacenado en caché, debes serializar las llamadas concurrentes:

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

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

El fallo debe poder atribuirse. Es la parte más olvidada y la que más tiempo ahorra cuando obtienes valores mediante ejecución. Un error debe indicar en qué capa ocurrió:

  • Entorno insuficiente: el código lanza un ReferenceError o se queda bloqueado hasta agotar el tiempo. A tu shim le falta algo que el script espera.

  • Cambio de protocolo: el código termina y devuelve una salida, pero su formato es incorrecto o el JSON no se puede parsear. Han cambiado el formato de salida.

  • Semántica incumplida: obtienes un valor, se puede parsear, pero está incompleto o no supera la validación al usarlo. El propio algoritmo ha cambiado.

Las tres situaciones requieren correcciones totalmente distintas: ajustar el shim, seguir el protocolo o revisar el algoritmo. Un ejecutor que solo informa de que «ha fallado» te obliga a investigar desde cero cada vez. Un ejecutor de desafíos maduro distingue explícitamente entre fallo de ejecución, salida no parseable, resultado incompleto y timeout.

Qué modelo usar en cada fase

El papel del modelo en este flujo es distinto al que tiene en el trabajo de identificación por huellas. Allí identifica familias de algoritmos. Aquí no hay algoritmo que identificar: su tarea principal es ayudarte a tomar la decisión arquitectónica entre «purificar o ejecutar» y atribuir los fallos de ejecución. Los cuatro subpasos exigen capacidades muy diferentes:

Subpaso

Capacidad necesaria

Elección

model id

Decidir entre purificar y ejecutar, defendiendo ambas opciones

Razonamiento sólido y disposición a cuestionar su propia conclusión

Claude Opus 5

claude-opus-5

Localizar el identificador dinámico: encontrar el punto donde se inyecta la constante y leer candidatos en todo el bundle

Contexto largo para leer el bundle completo de una vez

Kimi K3

kimi-k3

Filtrar en masa qué scripts candidatos podrían contener la operación buscada

Coste bajo y cientos de llamadas con alta concurrencia

Claude Sonnet 5

claude-sonnet-5

Ante un fallo de ejecución, leer el log o el stack para identificar la capa que falló

Razonamiento intermedio y explicación basada en un error concreto

GPT-5.6 Sol

gpt-5.6-sol

Conviene destacar la primera fila, porque es el único paso de este artículo en el que cambiar de modelo modifica visiblemente el resultado. Evalúa justo la capacidad de argumentar ambas posiciones y cuestionarse a sí mismo, la misma que se usa en la sección de contraevidencia del trabajo de huellas. Un modelo más débil elige una ruta y acumula razones para justificarla, sin defender de verdad la alternativa. Un modelo de razonamiento sólido lleva «purificar» y «ejecutar» hasta sus últimas consecuencias, expone el mejor argumento y el modo de fallo de cada opción, y concluye comparándolas.

No te quedes con mi palabra: pruébalo.

  1. Toma un objetivo real sobre el que ya hayas tomado una decisión como control; sabes intuitivamente si debería ejecutarse o purificarse.

  2. Entrega los hechos observados —si la lógica llega en tiempo de ejecución o es una constante de release, con qué frecuencia cambia y hasta dónde llegan sus dependencias del entorno— a claude-opus-5 y gpt-5.6-sol. Pide a cada uno un informe de decisión «purificar frente a ejecutar».

  3. Revisa dos aspectos: si reconoce que el «ciclo de cambio» es la variable decisiva —si solo compara la dificultad de implementación, pierde—; y si las condiciones que propone para cambiar de opinión son observables. Una condición como «si el identificador deja de cambiar en el próximo release, vuelve a considerar purificar», acompañada de una señal de activación, sí resulta útil.

Una sola ronda basta para distinguir qué modelo está tomando una decisión y cuál se limita a decidir por ti.

El verdadero problema es el coste de cambiar

Cuatro modelos de tres proveedores, tres SDK y tres sistemas de autenticación, además de tres formatos de error. Reescribir el cliente tres veces solo para alternar modelos entre subpasos no merece la pena. Por eso la mayoría usa un único modelo para todo: emplea en «purificar o ejecutar» uno que no se cuestiona a sí mismo, se lanza a purificar un objetivo que la otra parte cambiará mañana y da un rodeo enorme antes de entender que la dirección era errónea desde el principio.

AIReiter elimina esa capa: una clave, una interfaz compatible con OpenAI y los cuatro niveles detrás. Basta con cambiar el campo model del cuerpo de la petición.

# Purificar o ejecutar: el nivel de razonamiento defiende ambas opciones
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": "<hechos observados + defiende el argumento más fuerte tanto para purificar como para ejecutar>"}]
  }'

# Localizar el identificador dinámico en todo el bundle: cambia el campo model y deja el resto igual
#   "model": "kimi-k3"
# Filtrar scripts candidatos en masa:
#   "model": "claude-sonnet-5"
# Atribuir un fallo de ejecución:
#   "model": "gpt-5.6-sol"

Si ya utilizas el SDK de OpenAI, apunta base_url a https://aireiter.com/api/v1 y no cambies nada más. Con el SDK de Anthropic, usa POST /api/v1/messages con la misma clave.

En precio, este flujo concentra el peso de tokens en puntos concretos: Kimi K3 lee el bundle completo del frontend para localizar el identificador dinámico, con unos cientos de miles de tokens por entrada, y Claude Sonnet 5 filtra scripts candidatos en masa, fácilmente mediante cientos de llamadas. Esos dos componentes deciden la mayor parte de la factura. El 30% de descuento de Claude se aplica justo al filtrado en masa de Sonnet y al razonamiento de decisión de Opus; GPT a mitad de precio cubre la atribución de fallos con GPT-5.6 Sol; y la lectura de bundles completos con contexto largo de K3 se puede llamar con la misma clave. El descuento está donde más pesan los tokens y en el nivel de razonamiento más caro, no en una rebaja genérica de bajo coste.

Para terminar

Que el análisis estático no encuentre nada no siempre habla de tu habilidad; a veces describe el objetivo. La firma no está en el código porque llega en tiempo de ejecución o porque cambia en cada release.

Para estos dos casos, deja de buscar una función inexistente. Ante un desafío, ejecútalo de forma mínima: levanta un sandbox apenas lo bastante real para que llegue a producir el resultado. Ante un identificador dinámico, léelo en tiempo de ejecución y guárdalo en caché para la sesión. Ambos caminos exigen primero reconocer que la visión de la «fotografía estática» ha dejado de servir.

Y «purificar o ejecutar» es una decisión puramente arquitectónica que depende de una variable: si el ciclo de cambio del objetivo vence a tu periodo de amortización único. En los objetivos de ciclo corto, especialmente los que el servidor puede entregar en cualquier momento, forzar la purificación ofrece un retorno negativo. Deja ese análisis de ambos lados a un nivel de razonamiento que sea capaz de discutir consigo mismo; no lo decidas de memoria ni permitas que el modelo purifique por ti. La purificación es ingeniería determinista, y su verificación corresponde a las pruebas diferenciales, que cubren las etapas tres y cuatro del flujo de trabajo en cuatro etapas. Aquí el modelo solo te ayuda a pensar una cosa: si realmente merece la pena revertirlo.