在这里插入图片描述
做安全测试的同行,几乎人手一个 Burp Suite。功能确实强,但用久了有几件事让我越来越难受:

  • 它是 Java 写的,要拖一个 JRE,安装包含运行时动辄 200MB 起步
  • 空载启动,内存常年几百 MB,开几个标签、跑一轮扫描就上 G;
  • 启动慢、界面在高 DPI 下重绘发钝。

我想要的其实很简单:一个开箱即用、轻、快、原生的安全测试工作台,把 Burp 那四大件(抓包代理、重放、爆破、扫描)以及周边工具该有的都有。于是我用 Rust 做内核、用 gpui(Zed 团队那套 GPU 加速 UI 框架)做界面,从零撸了一个:Scry

先把最有冲击力的数字摆出来——同一台 macOS 上的实测产物:

指标 Burp Suite Scry
运行时依赖 需要 JVM / JRE 无,纯原生二进制
主程序体积 含 JRE 数百 MB 单文件 14MB
打包后 —— .app 15MB / zip 9.9MB
渲染 Java Swing/SWT gpui,macOS 走 Metal
内核语言 Java Rust(18 个 crate 的 workspace)

说明:本文聊的是「安全工具本身怎么造」——架构、体积、性能、Rust 工程实践,以及几个硬核模块的实现原理。文中所有能力都面向授权范围内的安全测试,不涉及任何针对真实目标的攻击教程。

一、16MB 是怎么抠出来的

「功能不少,体积还小」不是玄学,是一连串具体取舍叠加的结果。

1. 根子上选了原生编译

Rust 编译成机器码,不背 JVM、不带解释器、不需要目标机预装运行时。这是和 Java 系工具体积差一个数量级的根本原因。gpui 直接调 GPU(mac 上是 Metal),界面绘制不经过一层厚重的 GUI 中间件。

2. release profile 往死里压

Cargo.toml 里的发布配置是体积的关键开关:

[profile.release]
opt-level = "z"      # 为体积优化(不是为速度的 3)
lto = true           # 链接期跨 crate 内联 + 死代码消除
codegen-units = 1    # 牺牲并行编译,换更彻底的优化
strip = true         # 剥掉符号表
# 注意:千万别加 panic = "abort"
# gpui / Mutex 等依赖栈展开(unwind),abort 会让一次 panic 直接杀进程

opt-level = "z" + lto + codegen-units = 1 + strip 这一套组合拳下来,二进制从几十 MB 量级压到 14MB。最后那条注释是踩出来的:图省事加 panic = "abort" 能再小一点,但 gpui 和锁的实现依赖 unwind,一旦 abort,运行时一个 panic 就是整窗崩溃,得不偿失。

3. 依赖一律挑「纯 Rust、免 C 工具链」的实现

这一条同时省体积、省编译麻烦、还为「零环境交付」铺路:

  • TLS / 证书rustls + tokio-rustls + rcgen 全部统一用 ring 后端,绕开 aws-lc 那条需要 cmake / C 编译器的路;
  • 解压flate2(gzip/deflate) + brotli,纯 Rust;
  • 字符集encoding_rs(GBK/Big5/Shift_JIS → UTF-8,和 Firefox 同款);
  • 哈希 / 对称加解密:RustCrypto 的 md-5/sha2/aes/cbc/ecb,纯 Rust;
  • WASM 扩展运行时wasmtime,但精简了 features——只留 runtime + cranelift + wat,砍掉 component-model、async、cache、并行编译、pooling-allocator 等重头。

全程没有一个动态链接的第三方原生库(otool -L 验证过),交付出去就是「双击即用」。

二、架构:18 个 crate 的纯函数内核 + 一层 GUI

Scry 是一个 Cargo workspace,按职责切成 18 个 crate。核心设计哲学只有一句:把能做成纯函数的引擎全部抽成独立 crate,UI 只是它们的薄壳。

scry_app      ← gpui 界面(唯一有副作用/状态的层)
  │  复用
  ├─ scry_proxy    HTTP/S MITM 抓包内核 + 重放 + 上游 + WS + HTTP/2 + TLS 指纹
  ├─ scry_ca       CA 生成 + 按域名动态签叶子证书(+缓存)
  ├─ scry_storage  SQLite 落盘 + 去重(save-first)
  ├─ scry_decode   Content-Encoding 解压 + charset + MIME 分类
  ├─ scry_analyze  参数/Cookie/摘要提取 + 过滤 + 导出 curl
  ├─ scry_scan     被动规则 + 主动探测 + 敏感文件发现(Nikto 式)
  ├─ scry_sqli     SQLi 检测引擎(sqlmap 式,纯函数)
  ├─ scry_xss      XSS 上下文感知引擎(dalfox 式,纯函数)
  ├─ scry_codec    31 种编解码 / 加解密 / 哈希
  ├─ scry_diff     LCS 比较(Comparer)
  ├─ scry_seq      令牌随机性分析(香农熵 + FIPS 140-2)
  ├─ scry_crawl    站点爬虫(BFS 调度)
  ├─ scry_ext_api / scry_ext_host   扩展契约 + 三种 Runner
  ├─ scry_mcp      MCP 服务(给 AI 调度引擎)
  └─ scry_core / scry_sniff …       共享类型 / 被动嗅探

这么切的好处非常实在:每个引擎 crate 都零 IO、零网络、可单测。比如 SQLi 的「生成探测载荷」「判定响应是否命中」是纯函数,XSS 的「识别反射上下文」「按上下文合成载荷」也是纯函数——它们不发包,发包这件有副作用的事统一交给 scry_proxy::replay

结果就是全工作区累计 300 多个单元 / 集成测试clippy 零警告。安全工具最怕「自己就有 bug、误报漏报一团乱」,纯函数 + 高覆盖测试是把质量焊死的最实在办法。

三、抓包内核:为什么是 MITM,而不是抓网卡

这是动手前我认真核对过的架构决策,也是很多人会搞混的地方。

一种直觉是「像 Wireshark 那样用 libpcap 被动抓网卡」。但做安全测试工作台,被动嗅探当内核是行不通的,三个硬伤:

  1. 解不开 HTTPS。TLS 1.3 前向保密下,光有服务器私钥都解不开,被动抓只能靠 SSLKEYLOGFILE(仅限你能控制、且愿意吐密钥的客户端);
  2. 只读,改不了包。Repeater / Intruder / Scanner / 拦截改包全都需要在中间「截下来改」,嗅探做不到;
  3. 裸 TCP 重组又脏又苦,乱序、重传处理起来事倍功半。

Burp、mitmproxy、Charles、Fiddler、Reqable——没有一个拿 libpcap 当内核。它们都是 TLS 终止式 MITM 代理:自己作为「中间人」,对客户端扮演服务器、对服务器扮演客户端,在中间拿到明文。Scry 走的就是这条路(scry_proxy::mitm)。

CONNECT 之后,偷看一个字节

代理收到 CONNECT host:443 时,怎么知道隧道里是 TLS 还是明文?我的做法是先回 200,再用 MSG_PEEK 偷看首字节而不消费它

// 回 200,客户端才会在隧道里发后续字节
client.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;

// 偷看隧道内首字节(peek 不消费 socket)
let mut first = [0u8; 1];
let looks_tls = matches!(
    client.peek(&mut first).await,
    Ok(n) if n >= 1 && first[0] == 0x16   // 0x16 = TLS handshake record
);

if looks_tls {
    // 终止 TLS、解密
    mitm::intercept_https(client, host, port, /* ... */).await
} else {
    // 隧道里其实是明文 HTTP(有些代理把 80 端口也走 CONNECT)
    capture_tunneled_http(client, host, port, /* ... */).await
}

0x16 是 TLS 握手记录的第一个字节。靠这一个字节自适应,一个标准 CONNECT 代理就能同时正确处理 HTTPS 和「被强行套进 CONNECT 的明文 80」,不会再因为「对明文强行做 TLS 握手」而断连。

动态签证书:终止 TLS 的核心

要对客户端「扮演服务器」,就得给它一张目标域名的证书。Scry 启动时生成一个根 CA(落在 ~/.scry/ca.pem),之后为每个访问到的域名用 CA 私钥现场签一张叶子证书,按域名缓存(签发是 CPU 大头,keep-alive / 同域多连接直接命中缓存):

// 概念示意:用 CA 私钥为目标域名现签叶子证书
let leaf = sign_leaf_for(&ca, "example.com");        // rcgen + ring
let server_config = build_server_config(leaf);        // rustls ServerConfig

// 对客户端完成 TLS 握手,从此拿到明文
let acceptor = TlsAcceptor::from(Arc::new(server_config));
let tls_stream = acceptor.accept(client).await?;      // ← 明文双向流

// 对真实服务器,这边再作为 TLS 客户端连上去(可经上游代理)
// 两段拼起来 = 中间人,明文尽在掌握

客户端之所以信任这张「冒充」的证书,是因为它信任了 Scry 的根 CA。所以工具会提供「一键安装信任」和「导出到其他电脑」的证书分发包。

这里有个和「安全」强相关的实现细节:Scry 的内置浏览器抓包模式,不需要往系统里装 CA,而是给 Chromium 传 --ignore-certificate-errors-spki-list=<CA 的 SPKI 哈希>。这等于精确告诉浏览器「只放行这一把公钥」,既不全局关校验、又能覆盖证书 pinning 的站点——比「一把火关掉所有证书校验」干净得多。

save-first:抓到先落盘

一条铁律:抓到请求的第一件事是落盘,不是分析scry_proxy 在拿到完整响应后第一步就 save_flow() 写进 SQLite(按 method + 规范化 URL + body 的 sha1 去重),然后才轮到展示、扩展钩子、改写。进程崩了、窗口关了,已抓到的数据都不会丢。

四、功能全景:Burp 四大件 + 一堆周边

界面是 Burp 式的多页签工作台,目前 15 个页签全部落地、无占位页。对标关系大致如下:

能力 对标 Scry 的实现
抓包代理 Proxy MITM 内核 + 虚拟化历史表 + 报文语法高亮
重放 Repeater 改包重发 + 美化/原始/Hex/渲染视图
爆破 Intruder Sniper/钳形/集束四模式 + 载荷生成/处理/Grep 提取 + 并发限速
扫描 Scanner 被动规则 + 主动探测 + 敏感文件发现(Nikto 式)
拦截改包 Intercept 断点队列 + Match&Replace 自动改包 + 范围规则持久化
编解码 Decoder 31 种变换:Base32/58、JWT、XOR/RC4/AES、HMAC…
比较 Comparer LCS 行/词/字符级 diff + Dice 相似度
随机性分析 Sequencer 字符级 + 比特级香农熵 + FIPS 140-2 四项自检
越权检测 (类 Autorize) 高/低/匿名多身份重放对比
SQLi / XSS (类 sqlmap / dalfox) 独立纯函数引擎 + 专用页
站点爬虫 Spider CDP 驱动真实浏览器渲染后抽链
扩展 BApp 三种 Runner(见下)

抓包内核本身还补齐了 WebSocket 帧抓取HTTP/2(先连上游拿 ALPN,两端协议对齐再转发)、上游链式(解密后把流量交回 sing-box / 机场出网,应对受限网络)、以及 TLS 指纹(JA3/JA4) 的伪装与可视化。

下面挑三个我自认为有点意思的工程点展开。

五、技术点拆解

1. TLS 指纹:让 rustls「自己吐」ClientHello

很多目标会看 JA3/JA4 指纹判断「你是不是浏览器」。JA3 的定义是把 ClientHello 的若干字段拼成字符串再取 MD5:

JA3 = MD5( SSLVersion,CipherSuites,Extensions,EllipticCurves,ECPointFormats )

要算自己真实的指纹,最靠谱的不是「猜我发了什么」,而是让 TLS 库真的把 ClientHello 字节吐出来再解析。rustls 允许你把握手数据写进内存缓冲而不真的连网:

// 让 rustls 把 ClientHello 写进内存(不连网),再解析真实字节算 JA3/JA4
let mut conn = rustls::ClientConnection::new(config, server_name)?;
let mut buf = Vec::new();
conn.write_tls(&mut buf)?;          // buf 里就是真实的 ClientHello 记录
let (ja3, ja4) = parse_client_hello(&buf);

踩坑实录:rustls 0.23 每次握手会随机化扩展顺序(一种抗指纹固化的设计),导致 JA3 逐连接都在变、不稳定;而 JA4 会先排序再哈希,所以稳定。所以界面上以 JA4 为准展示。这也是「读文档不如让库自己吐字节」的一个生动例子。

2. 扩展系统:一个契约,三种 Runner

要支持用户写扩展,又不想破坏「14MB、零依赖」这条线,我把扩展做成一套钩子契约 + 三种运行后端,按信任度和语言分流:

Runner 适合 取舍
内置 / Native dylib 可信、要极致性能 快,但和宿主同进程
WASM 沙箱(wasmtime) 第三方扩展(默认) 无任何宿主 import = 无能力逃逸;fuel 限死循环、内存上限防炸弹
外部进程(Python) 想用 Python 写 stdio 上跑 JSON-RPC,崩溃隔离、不嵌 CPython

钩子契约就三个:on_request / on_response / on_flow_complete,配一个 manifest.json 自述身份与权限:

{
  "name": "passive-secret-scan",
  "version": "0.1.0",
  "hooks": ["on_flow_complete"],
  "permissions": ["read_flow"],
  "wasm": "extension.wasm",
  "fuel": 50000000
}

WASM 这条尤其香:扩展模块没有任何宿主导入函数,意味着它默认对系统毫无能力(连读文件都做不到),fuel 配额防死循环、StoreLimits 封顶内存防炸弹,每次钩子新建实例、互相零共享,&self 钩子天然并发安全无需加锁。第三方扩展跑在这种沙箱里,心里踏实得多。关键是——这一切都静态链接进那个 14MB 的二进制,不需要装 Python、不需要装 wasmtime。

3. 不卡 UI 的异步桥接

gpui 是 GPU 渲染的,主线程要稳定冲 120 帧,绝不能在上面阻塞做网络 IO。但发包是异步的 Future。我的桥接套路是:

把发包 Future 丢到 gpui 的 background_executor 后台线程,在那条线程上建一个 current-thread tokio runtime block_on 驱动它(只阻塞这条后台线程);完成后用 cx.spawn 回到主线程,通过 WeakEntity::update 写回结果并触发重绘。

爆破 / 扫描这类「流式出结果」的场景,则用一个 mpsc channel:后台串行发包、每出一条结果就 send 回来,前台每 120ms drain 一次增量刷进表格。这样无论后台打多少包,GPU 主线程永远只做渲染,列表用虚拟化(只渲视口内那 ~20 行)+ 响应体解码缓存(thread_local LRU,按堆指针 + 长度做 key),即便单条响应几百 KB、历史几千条也不卡。

六、顺手做的一件事:把工具开放给 AI

既然内核都是规整的纯函数引擎,我加了个 scry_mcp——一个独立的 MCP(Model Context Protocol)服务,让 Cursor / Claude 这类 AI 客户端能直接调度 Scry 的引擎:列流量、重放请求、跑被动 / 主动扫描、敏感文件发现、越权检测、编解码……

它走 stdio 上的行分隔 JSON-RPC,和 GUI 共用同一个 ~/.scry/scry.sqlite,所以可以和界面同时跑、互不抢端口:

// AI 客户端请求工具清单 → 服务返回(节选)
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

{"jsonrpc":"2.0","id":1,"result":{"tools":[
  {"name":"list_flows",      "description":"列出抓到的流量(可按 host 过滤)"},
  {"name":"send_request",    "description":"发一个请求(= Repeater)"},
  {"name":"passive_scan",    "description":"对历史流量跑被动规则"},
  {"name":"authz_test",      "description":"多身份重放做越权检测"}
]}}

注册进客户端配置即可被发现:

// ~/.cursor/mcp.json
{
  "mcpServers": {
    "scry": { "command": "/path/to/target/release/scry-mcp" }
  }
}

效果是:你可以让 AI「把刚抓到的那条登录请求重放一遍、换个身份看看有没有越权」,它真的会去调引擎执行。安全工具 + AI 助手,比想象中顺。

七、适用与边界(不吹银弹)

把话说清楚,免得期待错位:

  • 适合:macOS 上做授权范围内的 Web 安全测试 / 流量分析、想要轻量原生替代、Rust 技术栈、想给 AI 接安全能力的人。
  • 暂不如 Burp 的地方:商业版那种深度自动扫描、庞大的 BApp 生态、团队协作 / 报告流水线——这些是多年积累,短期补不齐。Scry 的定位是「轻、快、能改、能扩、能被 AI 调度」的工作台,不是「Burp 杀手」。
  • 平台:优先 macOS(gpui 在 mac 走 Metal 最成熟)。

最后照例一句郑重声明:

⚠️ 本文与该工具仅用于学习研究获得授权的安全测试。任何对未授权目标的扫描、抓包、改包都可能违法。请务必在合法合规、获得明确授权的前提下使用,一切后果由使用者自负。

写在最后

回头看,「16MB 干 Burp 的活」并不是靠某个魔法,而是一串朴素选择的叠加:原生编译省掉运行时、极致 release profile、纯 Rust 免 C 依赖、纯函数内核可测试、GPU 界面只管渲染、IO 全甩后台

Rust 在系统工具这个赛道,真的能同时给你「小、快、稳」三样——这在过去往往要三选二。如果你也受够了某些安全工具的臃肿,不妨试试用 Rust 自己造一个趁手的;哪怕只是把其中某个引擎(MITM 解密、JA3 计算、WASM 扩展沙箱)单独抠出来玩一遍,也很值。

如果这篇对你有帮助,欢迎评论区聊聊你心目中「理想的安全工具」是什么样。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐