Volver al blog

Ghostwire: Instrumentación de tiempo de ejecución al estilo Frida para JavaScript en navegadores

Michael Lee
Michael Lee

Expert Network Defense Engineer

30-Jul-2026

Resumen:

  • Ghostwire es una instrumentación en tiempo de ejecución para JavaScript en el navegador, en la misma categoría que Frida. Ambos observan un límite de función en vivo en lugar de leer el código fuente y esperar. La diferencia está en el objetivo: Frida instrumenta procesos nativos y móviles, Ghostwire instrumenta JavaScript dentro de Chrome.
  • El transporte es el Protocolo de Herramientas para Desarrolladores de Chrome a través de una tubería de OS. Ghostwire lanza Chrome con --remote-debugging-pipe, por lo que no hay puerto de depuración al que conectarse ni dependencia de terceros que instalar para el núcleo.
  • Los hooks son puntos de ruptura del depurador, no envolturas. El objeto de función en vivo nunca se reemplaza, por lo que fn.toString() sigue devolviendo el código fuente original.
  • La parte interesante es el oráculo. Capturas pares reales (entrada → salida), escribes una reimplementación candidata y dejas que Ghostwire demuestre que está equivocada con un contraejemplo concreto.
  • Una ejecución en vivo rechazó una candidata plausible. Doce pares capturados verificaron la función correcta 12/12; una versión que faltaba un solo +1 coincidió 0/13 y produjo la entrada fallida exacta.
  • Ghostwire es una herramienta de Chrome local hoy. No se conecta a un punto final CDP remoto de WebSocket, por lo que actualmente no puede impulsar la Navegador de Web Scraping Sin Residuos sesión.
  • Úsalo solo donde estés autorizado. Comienza con una cuenta gratuita de Scrapeless para la recolección ordinaria de datos del navegador, que es un trabajo diferente de análisis en tiempo de ejecución.

Leer JavaScript ofuscado te proporciona una hipótesis. Ejecutarlo te da un hecho. Ghostwire existe para convertir el primero en el segundo: observa lo que una función realmente devuelve en tiempo de ejecución, luego verifica tu reimplementación contra esa verdad fundamental e informa cada entrada donde te equivocaste.

Qué es Ghostwire

Ghostwire es una biblioteca de instrumentación en Python para JavaScript que se ejecuta dentro de Chrome. Se adjunta a través del Protocolo de Herramientas para Desarrolladores de Chrome, establece hooks en objetos de función en vivo, registra argumentos y valores de retorno, y compara una implementación candidata con el comportamiento capturado.

La versión utilizada aquí es 0.1.0 en el commit principal a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8. Requiere Python 3.9 o posterior y licencia MIT, y el núcleo no tiene dependencias de terceros porque el protocolo se ejecuta a través de una tubería de OS en lugar de una biblioteca cliente de WebSocket.

Esta guía utiliza una página local sintética en todo momento. La instrumentación en tiempo de ejecución pertenece a la misma categoría de autorización que cualquier otra herramienta de seguridad: analiza sistemas que posees o para los que tienes permiso explícito para probar.

Ghostwire y Frida: Donde la Analogía se Mantiene

Llamar a Ghostwire "Frida para JavaScript en el navegador" es una abreviatura útil, y es precisa sobre el método más que sobre la implementación.

Preocupación Frida Ghostwire 0.1.0
Objetivo principal Procesos nativos, aplicaciones móviles, binarios de escritorio JavaScript ejecutándose en Chrome
Mecanismo de adjunto Inyecta un agente en el proceso objetivo Adjunta una sesión de depuración a través de CDP
Mecanismo de hook Interceptor en direcciones de función nativa Debugger.setBreakpointOnFunctionCall en objetos de función en vivo
Superficie de scripting Agente de JavaScript dentro del proceso Python impulsando el protocolo desde afuera
Dependencia central Tiempo de ejecución y bindings de Frida Ninguna; CDP a través de una tubería de OS

La API de JavaScript de Frida ejecuta un agente dentro del proceso que está inspeccionando. Ghostwire nunca inserta código en la página. Esa diferencia es deliberada: un punto de ruptura en el objeto de función en vivo deja el objeto sin modificar, por lo que la propia verificación fn.toString() de la página ve el código fuente original.

Donde la analogía se mantiene es en el modelo de trabajo. Ambos reemplazan la lectura estática por la observación en vivo, y ambos son para análisis autorizados en lugar de recolección de datos en producción.

Requisitos Previos

  • Python 3.9 o posterior.
  • Google Chrome o Chromium instalado localmente.
  • Un checkout del repositorio de Ghostwire en un commit fijado.
  • Solo objetivos que poseas o a los que estés autorizado a analizar.

Instalación

El núcleo no tiene paquetes que instalar, por lo que un checkout fijado es toda la configuración:

bash Copy
git clone https://github.com/sofianeelhor/ghostwire.git
cd ghostwire
git checkout a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8

Lee pyproject.toml antes de ejecutar cualquier cosa. Declara version = "0.1.0", requires-python = ">=3.9", y una lista de dependencies vacía, con mcp>=1.0 solo como un extra opcional para el servidor MCP.

El Objetivo Sintético

Este dispositivo se sirve desde localhost y calcula un valor deliberadamente simple. No contiene credenciales, lógica anti-bot ni código de terceros:

python Copy
PAGE = b"""<!doctype html><html><body><script>
function transform(name, value) {
  return btoa(name + ':' + (value * 7 + 1));
}
window.transform = transform;
let i = 0;
setInterval(function () {
  window.lastResult = transform('sample' + i, 10 + i);
  i += 1;
}, 120);
</script></body></html>"""

El temporizador importa. Ghostwire captura llamadas a medida que suceden, por lo que un límite que se utiliza repetidamente produce un corpus sin ninguna interacción.

Adjuntar y Enganchar

ghostwire.attach() lanza Chrome, conecta las sondas predeterminadas y devuelve un Inspector:

python Copy
import ghostwire

with ghostwire.attach("http://localhost:8000/", headless=True) as gw:
    gw.wait(1.0)
    gw.hook("window.transform", capture_returns=True, label="transform")
    gw.wait(1.5)
    corpus = gw.corpus("transform")
    print("pares capturados:", len(corpus))

capture_returns=True registra el valor de retorno junto a los argumentos, lo que hace que el corpus sea utilizable como verdad fundamental. La label agrupa esos pares para su verificación posterior.

¿Listo para recopilar datos de página ordinarios en lugar de analizar el comportamiento en tiempo de ejecución? Abre una cuenta gratuita de Scrapeless y utiliza un navegador gestionado para ese trabajo.

Verificar un Candidato Contra la Realidad

Proporcionas una reimplementación, y Ghostwire la ejecuta contra el corpus capturado en una página aislada que no puede ver la función real:

python Copy
good = "(name,value)=>btoa(name+':'+(value*7+1))"
wrong = "(name,value)=>btoa(name+':'+(value*7))"

good_result = gw.verify("window.transform", good, label="transform")
wrong_result = gw.verify("window.transform", wrong, label="transform")

El segundo candidato es el fallo realista: sintácticamente correcto, semánticamente incorrecto por uno. Un modelo que lee código fuente minificado produce este tipo de error con regularidad, y sobrevive a una revisión casual porque la salida todavía parece una base64 válida.

Script Ejecutable Completo

El contenedor utilizado para la verificación necesitaba dos ajustes, ambos ambientales en lugar de defectos de Ghostwire: Chrome requiere --no-sandbox cuando se ejecuta como root, y la primera navegación HTTP es lo suficientemente lenta como para exceder el plazo de CDP de 20 segundos.

python Copy
import base64
import http.server
import socketserver
import sys
import threading
from pathlib import Path

HERE = Path(__file__).resolve().parent
UPSTREAM = next(
    p / "upstream" for p in (HERE, *HERE.parents)
    if (p / "upstream" / "ghostwire").is_dir()
)
sys.path.insert(0, str(UPSTREAM))
from ghostwire import Browser, Engine, Tracer
from ghostwire.api import Inspector
from ghostwire.crypto import CryptoLogger
from ghostwire.dataflow import DataflowTracer
from ghostwire.objects import LiveObjects
from ghostwire.oracle import Oracle
from ghostwire.origin import OriginTracer
from ghostwire.probes import NetLog, ScriptWatcher


def attach_local(url, extra_flags):
    """El mismo cableado que ghostwire.attach(), con flags de Chrome seguros para contenedores."""
    browser = Browser(headless=True, extra_flags=extra_flags)
    browser.cdp.default_timeout = 90.0
    engine = Engine(browser=browser)
    scripts, net, tracer = ScriptWatcher(), NetLog(), Tracer()
    for probe in (scripts, net, tracer):
        engine.add_probe(probe)
    oracle = Oracle(engine, tracer)
    engine.start()
    engine.navigate(url)
    return Inspector(
        engine, scripts, net, tracer, oracle,
        OriginTracer(engine), DataflowTracer(engine),
        LiveObjects(engine), CryptoLogger(engine),
    )


PAGE = b"""<!doctype html><html><body><script>
function transform(name, value) {
  return btoa(name + ':' + (value * 7 + 1));
}
window.transform = transform;
let i = 0;
setInterval(function () {
  window.lastResult = transform('sample' + i, 10 + i);
  i += 1;
}, 120);
</script></body></html>"""


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(PAGE)))
        self.end_headers()
        self.wfile.write(PAGE)

    def log_message(self, *args):
        pass


class QuietServer(socketserver.TCPServer):
    allow_reuse_address = True

    def handle_error(self, request, client_address):
        pass


server = QuietServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()

gw = attach_local(
    f"http://localhost:{port}/",
    ["--no-sandbox", "--disable-dev-shm-usage"],
)
try:
    gw.wait(1.0)
    gw.hook("window.transform", capture_returns=True, label="transform")
    gw.wait(1.5)

    corpus = gw.corpus("transform")
    good = "(name,value)=>btoa(name+':'+(value*7+1))"
wrong = "(nombre, valor) => btoa(nombre + ':' + (valor * 7))"
    good_result = gw.verify("window.transform", good, label="transform")
    wrong_result = gw.verify("window.transform", wrong, label="transform")
    fresh = [["alpha", 1], ["beta", 2], ["", 0]]
    fresh_result = gw.verify("window.transform", good, fresh_inputs=fresh)

    mismatch = wrong_result["mismatches"][0]
    expected_decoded = base64.b64decode(mismatch["expected"]).decode()
    got_decoded = base64.b64decode(mismatch["got"]).decode()

    assert len(corpus) >= 5
    assert good_result["verified"] is True
    assert wrong_result["verified"] is False
    assert fresh_result["verified"] is True

    print("versión de ghostwire: 0.1.0")
    print("pares capturados:", len(corpus))
    print("candidato correcto:", good_result["verified"],
          "probado:", good_result["tested"], "coincidido:", good_result["matched"])
    print("candidato incorrecto:", wrong_result["verified"],
          "probado:", wrong_result["tested"], "coincidido:", wrong_result["matched"])
    print("entrada contraejemplo:", mismatch["input"])
    print("contraejemplo esperado:", expected_decoded)
    print("contraejemplo obtenido:", got_decoded)
    print("entradas frescas verificadas:", fresh_result["verified"])
    print("entradas frescas probadas:", fresh_result["tested"])
finally:
    gw.close()
    server.shutdown()

## Lo que devolvió la ejecución

```text
versión de ghostwire: 0.1.0
pares capturados: 12
candidato correcto: True probado: 12 coincidido: 12
candidato incorrecto: False probado: 13 coincidido: 0
entrada contraejemplo: ['sample8', 18]
contraejemplo esperado: sample8:127
contraejemplo obtenido: sample8:126
entradas frescas verificadas: True
entradas frescas probadas: 3

El candidato incorrecto coincidió 0 de 13, no algunos de ellos. Debido al +1 que falta, desplaza cada resultado, el corpus lo rechaza en todas partes. Un error más sutil produciría una coincidencia parcial, que es exactamente la señal que un corpus es bueno para destacar.

El contraejemplo es concreto: la entrada ['sample8', 18] debería producir sample8:127 porque 18 * 7 + 1 = 127, y el candidato produjo sample8:126. Esa es una diferencia decodificada y accionable en lugar de un fallo booleano.

La verificación de entradas frescas verificó 3 entradas sin corpus previo. Ghostwire llamó a la función en vivo para valores que nunca había observado, lo que cubre casos límite que el temporizador nunca generó, incluida la cadena vacía.

Ghostwire actualmente no puede conducir una sesión de Navegador de Scraping Sin Scrapeless, y la razón es arquitectónica más que un problema de configuración.

Browser.__init__ lanza un proceso Chrome local con --remote-debugging-pipe=JSON, duplica dos descriptores de archivo en el hijo, y se los entrega a PipeConnection. Cada mensaje de protocolo fluye a través de esos descriptores. El Navegador de Scraping expone un endpoint WebSocket remoto en su lugar, y Ghostwire no tiene un transporte WebSocket para comunicarse con él.

Engine(browser=...) acepta un objeto similar a un navegador, que es una auténtica costura de extensión — el propio ejecutor de este artículo lo utiliza para pasar flags de Chrome seguros para contenedores. Pero hacer que alcance un endpoint remoto requeriría un transporte WebSocket compatible con PipeConnection más cambios en el ciclo de vida, ya que Browser.close() asume que posee un proceso local que puede terminar. Ese trabajo no existe upstream, así que considera un puente como un boceto de diseño en lugar de un camino apoyado.

La división práctica es clara. Usa Ghostwire localmente para análisis de tiempo de ejecución autorizados. Usa el Navegador de Scraping para la recolección de datos basada en navegador a escala, que es lo que cubre el resumen del Protocolo DevTools de Chrome.

Solución de Problemas

Chrome sale inmediatamente y el tubo se rompe

Un BrokenPipeError de cdp.py significa que Chrome murió durante el lanzamiento. Ejecutar como root en un contenedor es la causa habitual; pasa --no-sandbox y --disable-dev-shm-usage a través de extra_flags.

CDPError: timeout: Page.navigate

La navegación superó el plazo de conexión. Aumenta browser.cdp.default_timeout antes de crear el Engine. En el contenedor utilizado aquí, la primera navegación a 127.0.0.1 tomó alrededor de 15 segundos, mientras que localhost se resolvió en aproximadamente 0.1 segundos, por lo que solo el nombre del host cambió el resultado.

El gancho no captura nada

La función no estaba definida cuando se configuró el gancho. Espera a que la página se inicialice y confirma que la expresión se resuelve evaluando typeof window.yourFunction primero.

La verificación pasa pero el corpus es pequeño

Un puñado de pares demuestra muy poco. Ejercita más el límite, o suministra fresh_inputs cubriendo bordes que el tráfico observado nunca alcanzó.

Conclusión

El enganche de funciones es la mitad fácil. La mitad que cambia cómo trabajas es el oráculo, que verifica tu reimplementación contra el comportamiento registrado y devuelve la entrada donde falló. En esta ejecución, esa entrada fue ['sample8', 18], y el candidato falló porque omidió un solo +1.

Mantén el alcance honesto. Ghostwire es un instrumento de investigación de Chrome local para sistemas que estás autorizado a analizar, y actualmente no se comunica con puntos finales de navegador remotos.

Empieza en el plan gratuito de Scrapeless para la recolección de datos de navegador gestionada, y revisa los precios de Scrapeless cuando esa carga de trabajo crezca.

Preguntas Frecuentes

Q: ¿Es Ghostwire realmente Frida para el navegador?

Ocupa el mismo papel: instrumentación en tiempo real en lugar de lectura estática, pero las implementaciones difieren. Frida inyecta un agente en un proceso nativo; Ghostwire adjunta una sesión de depuración a Chrome y nunca coloca código en la página.

Q: ¿Por qué Ghostwire usa un tubo en lugar de un WebSocket?

--remote-debugging-pipe evita abrir un puerto de depuración que el JavaScript de la página podría descubrir, y elimina la necesidad de una dependencia de WebSocket en el núcleo.

Q: ¿Enganchar una función cambia lo que ve la página?

No para las comprobaciones de detección habituales. Los hooks se establecen con Debugger.setBreakpointOnFunctionCall en el objeto en vivo, por lo que el objeto no está envuelto y fn.toString() todavía devuelve la fuente original.

Q: ¿Qué prueba realmente el oráculo de verificación?

Que tu candidato coincidió con la función real en cada entrada probada. Es evidencia limitada por la cobertura, no una prueba de equivalencia, por lo que un resultado aprobado con 12 pares significa menos que uno con una amplia cobertura de casos extremos.

Q: ¿Puede Ghostwire conectarse al Scraping Browser de Scrapeless?

No. Ghostwire 0.1.0 lanza y controla un Chrome local a través de un tubo de OS, mientras que el Scraping Browser expone un punto final CDP remoto de WebSocket. Conectarlos requiere un transporte de WebSocket y manejo de ciclo de vida remoto que upstream no proporciona.

Q: ¿Es seguro ejecutar esto contra cualquier sitio web?

No. La instrumentación en tiempo de ejecución debe limitarse a sistemas que posees o que tienes autorización por escrito para probar. Revisa los términos del objetivo y directivas de robots, y obtén asesoría legal antes de analizar código de terceros.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar