受不了 Burp 几百 MB 还要装 JRE:我用 Rust + GPUI 写了个 16MB 的安全套件

做安全测试的同行,几乎人手一个 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 被动抓网卡」。但做安全测试工作台,被动嗅探当内核是行不通的,三个硬伤:
- 解不开 HTTPS。TLS 1.3 前向保密下,光有服务器私钥都解不开,被动抓只能靠
SSLKEYLOGFILE(仅限你能控制、且愿意吐密钥的客户端); - 只读,改不了包。Repeater / Intruder / Scanner / 拦截改包全都需要在中间「截下来改」,嗅探做不到;
- 裸 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 runtimeblock_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 扩展沙箱)单独抠出来玩一遍,也很值。
如果这篇对你有帮助,欢迎评论区聊聊你心目中「理想的安全工具」是什么样。
更多推荐
所有评论(0)