幽灵线:用于浏览器JavaScript的Frida类运行时插桩
Expert Network Defense Engineer
TL;DR:
- Ghostwire是用于浏览器JavaScript的运行时插桩,和Frida在同一类别。 两者都观察实时函数边界,而不是读取源代码并寄希望于此。目标不同:Frida插桩本地和移动进程,Ghostwire则插桩Chrome内的JavaScript。
- 传输是通过OS管道的Chrome DevTools协议。 Ghostwire以
--remote-debugging-pipe启动Chrome,因此没有调试端口可供连接,也不需要为核心安装第三方依赖。 - 钩子是调试器断点,而不是封装器。 实时函数对象从不被替换,因此
fn.toString()始终返回原始源代码。 - 有趣的部分是oracle。 你捕获真实的
(输入 → 输出)对,编写候选重实现,并让Ghostwire通过具体的反例来证明它错误。 - 一次实时运行拒绝了一个合理的候选者。 十二个捕获的对验证了正确性12/12;一个缺少单个
+1的版本匹配0/13并产生了确切的失败输入。 - Ghostwire今天是一个本地Chrome工具。 它不连接到远程WebSocket CDP端点,因此目前无法驱动Scrapeless Scraping Browser会话。
- 仅在你被授权的地方使用。 从免费Scrapeless账户开始进行普通的浏览器数据收集,这与运行时分析是不同的工作。
阅读模糊化的JavaScript给你一个假设。运行它则给你一个事实。Ghostwire存在的目的是将第一个变成第二个:它观察一个函数在运行时实际返回的内容,然后将你的重实现与该基本事实进行对比,并报告你出错的每一个输入。
Ghostwire是什么
Ghostwire是一个用于在Chrome中运行JavaScript的Python插桩库。它通过Chrome DevTools协议附加,设置实时函数对象的钩子,记录参数和返回值,并将候选实现与捕获的行为进行比较。
这里使用的版本是0.1.0,上游提交为a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8。它需要Python 3.9或更高版本,采用MIT许可证,核心没有第三方依赖,因为该协议是通过OS管道运行,而不是WebSocket客户端库。
本指南始终使用一个合成的本地页面。运行时插桩属于与任何其他安全工具相同的授权类别:分析你拥有或有明确权限测试的系统。
Ghostwire与Frida:类比在哪里成立
称Ghostwire为“浏览器JavaScript的Frida”是一个有用的简写,它在方法上而不是实现上是准确的。
| 关注点 | Frida | Ghostwire 0.1.0 |
|---|---|---|
| 主要目标 | 本地进程、移动应用、桌面二进制文件 | 在Chrome中执行的JavaScript |
| 附加机制 | 注入一个代理到目标进程 | 通过CDP附加调试会话 |
| 钩子机制 | 本地函数地址上的拦截器 | 在实时函数对象上使用Debugger.setBreakpointOnFunctionCall |
| 脚本表面 | 在被检查进程内的JavaScript代理 | 在外部使用Python驱动协议 |
| 核心依赖 | Frida运行时和绑定 | 无;CDP通过OS管道 |
Frida的JavaScript API在它检查的进程内运行一个代理。Ghostwire从不向页面中插入代码。这个区别是故意的:对实时函数对象的断点不会修改对象,因此页面自身的fn.toString()检查可以看到原始源代码。
类比之所以成立在于工作模型。两者都用实时观察取代静态读取,并且都用于授权分析,而不是生产数据收集。
前提条件
- Python 3.9或更高版本。
- 本地安装Google Chrome或Chromium。
- 在固定提交的Ghostwire仓库中检出代码。
- 仅分析你拥有或被授权分析的目标。
安装
核心没有可安装的包,因此固定检出就是整个设置:
bash
git clone https://github.com/sofianeelhor/ghostwire.git
cd ghostwire
git checkout a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8
在运行任何代码之前,请阅读pyproject.toml。它声明了version = "0.1.0"、requires-python = ">=3.9",以及一个空的dependencies列表,只有mcp>=1.0作为MCP服务器的可选额外部分。
合成目标
这个装置是从本地主机提供的,计算一个故意简单的值。它不包含凭据,不含反机器人逻辑,也没有第三方代码:
python
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>"""
定时器很重要。Ghostwire 会在调用发生时捕获这些调用,所以一个反复被执行的边界会产生一个没有任何交互的语料库。
附加和钩住
ghostwire.attach() 启动 Chrome,连接默认探针,并返回一个 Inspector:
python
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("捕获的对:", len(corpus))
capture_returns=True 记录返回值及其参数,这使得语料库可以作为真实值使用。label 将这些对分组以便后续验证。
准备好收集普通页面数据而不是分析运行时行为了吗?打开一个免费的 Scrapeless 账户,并使用受管浏览器来完成这项工作。
验证候选者与现实的对比
你提供一个重新实现,Ghostwire 会在一个无法看到真实函数的孤立页面中运行它并与捕获的语料库进行比较:
python
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")
第二个候选者是现实中的失败: 语法上没问题,但语义上偏差一个。一个读取压缩源代码的模型会定期产生这种错误,并且因为输出看起来仍然像有效的 base64,所以它能经受住普通审查。
完整可运行脚本
用于验证的容器需要两个调整,这两个是环境问题而不是 Ghostwire 的缺陷:Chrome 在以 root 身份运行时需要 --no-sandbox,并且第一次 HTTP 导航的速度足够慢,以超过默认的 20 秒 CDP 截止时间。
python
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):
"""与 ghostwire.attach() 相同的连接,具有容器安全的 Chrome 标志。"""
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))"
错误 = "(name,value)=>btoa(name+':'+(value*7))"
正确结果 = gw.verify("window.transform", good, label="transform")
错误结果 = gw.verify("window.transform", wrong, label="transform")
新数据 = [["alpha", 1], ["beta", 2], ["", 0]]
新数据结果 = gw.verify("window.transform", good, fresh_inputs=new_inputs)
不匹配 = 错误结果["mismatches"][0]
预期解码 = base64.b64decode(不匹配["expected"]).decode()
得到解码 = base64.b64decode(不匹配["got"]).decode()
assert len(corpus) >= 5
assert 正确结果["verified"] is True
assert 错误结果["verified"] is False
assert 新数据结果["verified"] is True
print("ghostwire 版本: 0.1.0")
print("捕获的配对:", len(corpus))
print("正确候选:", 正确结果["verified"],
"测试:", 正确结果["tested"], "匹配:", 正确结果["matched"])
print("错误候选:", 错误结果["verified"],
"测试:", 错误结果["tested"], "匹配:", 错误结果["matched"])
print("反例输入:", 不匹配["input"])
print("反例预期:", 预期解码)
print("反例得到:", 得到解码)
print("新输入已验证:", 新数据结果["verified"])
print("新输入已测试:", 新数据结果["tested"])
最终:
gw.close()
server.shutdown()
## 运行结果
```text
ghostwire 版本: 0.1.0
捕获的配对: 12
正确候选: True 测试: 12 匹配: 12
错误候选: False 测试: 13 匹配: 0
反例输入: ['sample8', 18]
反例预期: sample8:127
反例得到: sample8:126
新输入已验证: True
新输入已测试: 3
错误候选匹配 0 of 13, 而不是其中一些。因为缺少的 +1 使每个结果都偏移,所以语料库到处拒绝它。一个更微妙的错误会产生部分匹配,这正是语料库善于显示的信号。
反例是具体的:输入 ['sample8', 18] 应生成 sample8:127,因为 18 * 7 + 1 = 127,而该候选生成了 sample8:126。这是一个解码的、可操作的差异,而不是一个布尔失败。
新输入检查验证了 3 个没有先前语料库的输入。Ghostwire 调用了它从未观察过的值的实时函数,涵盖了计时器未生成的边缘案例,包括空字符串。
诚实的限制:Ghostwire 和刮取浏览器
Ghostwire 目前不能驱动无刮取的刮取浏览器会话,其原因是架构上的,而不是配置问题。
Browser.__init__ 启动一个带有 --remote-debugging-pipe=JSON 的本地 Chrome 进程,将两个文件描述符复制到子进程中,并传递给 PipeConnection。每个协议消息通过这些描述符流动。刮取浏览器则暴露一个远程 WebSocket 端点,而 Ghostwire 没有 WebSocket 传输来与之通信。
Engine(browser=...) 接受一个类似浏览器的对象,这是一个真正的扩展接口——本文自己的运行器使用它来传递容器安全的 Chrome 标志。但是让它达到远程端点需要一个兼容 PipeConnection 的 WebSocket 传输以及生命周期更改,因为 Browser.close() 假设它拥有一个它可以终止的本地进程。此项工作在上游尚不存在,因此将桥接处理视为设计草图,而不是支持的路径。
实际的分离是清晰的。使用 Ghostwire 进行授权的运行时分析。使用刮取浏览器进行大规模的基于浏览器的数据收集,这正是 Chrome DevTools 协议概述 所涵盖的。
故障排除
Chrome 立即退出并且管道断开
cdp.py 中的 BrokenPipeError 意味着 Chrome 在启动期间崩溃了。在容器中以 root 身份运行是常见原因;通过 extra_flags 传递 --no-sandbox 和 --disable-dev-shm-usage。
CDPError: timeout: Page.navigate
导航超过了连接截止时间。在创建 Engine 之前,提升 browser.cdp.default_timeout。在这里使用的容器中,第一次导航到 127.0.0.1 花费了大约 15 秒,而 localhost 则约花费 0.1 秒,因此仅主机名的变化改变了结果。
钩子未捕获任何内容
当设置钩子时,该函数未定义。等待页面初始化,并通过先评估 typeof window.yourFunction 来确认表达式是否解析。
验证通过但语料库很小
一小部分配对几乎没有证明任何事情。更多地检验边界,或者提供覆盖未观察到的流量的边界的 fresh_inputs。
结论
功能钩取是简单的一半。改变你工作方式的那一半是oracle,它会检查你的重新实现与记录的行为,并返回出错时的输入。在这一运行中,该输入是 ['sample8', 18],候选者失败是因为它遗漏了一个 +1。
保持范围诚实。Ghostwire 是一个本地 Chrome 研究工具,仅用于你被授权分析的系统,并且目前不与远程浏览器端点连接。
在 Scrapeless 免费计划上开始,用于管理浏览器数据收集,并在工作负载增长时查看 Scrapeless定价。
常见问题
问:Ghostwire 实际上是浏览器上的 Frida 吗?
它承担了相同的角色 — 现场运行时插装而不是静态读取 — 但实现不同。Frida 将一个代理注入本地进程;Ghostwire 将调试会话附加到 Chrome,并且从不将代码放入页面中。
问:为什么 Ghostwire 使用管道而不是 WebSocket?
--remote-debugging-pipe 避免打开一个页面 JavaScript 可能发现的调试端口,并消除了核心中对 WebSocket 依赖的需求。
问:钩取一个函数会改变页面看到的内容吗?
对常规检测检查来说不会。钩子使用 Debugger.setBreakpointOnFunctionCall 设置在实时对象上,因此对象没有被包装,fn.toString() 仍然返回原始源代码。
问:验证 oracle 实际上证明了什么?
证明你的候选者在每个测试输入上都匹配真实函数。它是一个由覆盖率限制的证据,而不是等价的证明,所以通过的结果有 12 对比广泛的边缘案例覆盖率的结果意义更小。
问:Ghostwire 可以连接到 Scrapeless Scraping Browser 吗?
不能。Ghostwire 0.1.0 发起并拥有一个本地 Chrome 通过操作系统管道,而 Scraping Browser 暴露一个远程 WebSocket CDP 端点。要将它们连接起来,需要一个 WebSocket 传输和远程生命周期处理,而上游不提供这些。
问:在任何网站上运行这个安全吗?
不安全。运行时插装应限制在你拥有或已书面授权测试的系统上。在分析第三方代码之前,请查看目标的条款和机器人指令,并获取法律建议。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



