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

发布时间:2026/6/25 21:18:29
受不了 Burp 几百 MB 还要装 JRE:我用 Rust + GPUI 写了个 16MB 的安全套件 做安全测试的同行几乎人手一个 Burp Suite。功能确实强但用久了有几件事让我越来越难受它是 Java 写的要拖一个JRE安装包含运行时动辄200MB 起步空载启动内存常年几百 MB开几个标签、跑一轮扫描就上 G启动慢、界面在高 DPI 下重绘发钝。我想要的其实很简单一个开箱即用、轻、快、原生的安全测试工作台把 Burp 那四大件抓包代理、重放、爆破、扫描以及周边工具该有的都有。于是我用Rust做内核、用gpuiZed 团队那套 GPU 加速 UI 框架做界面从零撸了一个Scry。先把最有冲击力的数字摆出来——同一台 macOS 上的实测产物指标Burp SuiteScry运行时依赖需要 JVM / JRE无纯原生二进制主程序体积含 JRE 数百 MB单文件14MB打包后——.app15MB/ zip9.9MB渲染Java Swing/SWTgpuimacOS 走Metal内核语言JavaRust18 个 crate 的 workspace说明本文聊的是「安全工具本身怎么造」——架构、体积、性能、Rust 工程实践以及几个硬核模块的实现原理。文中所有能力都面向授权范围内的安全测试不涉及任何针对真实目标的攻击教程。一、16MB 是怎么抠出来的「功能不少体积还小」不是玄学是一连串具体取舍叠加的结果。1. 根子上选了原生编译Rust 编译成机器码不背 JVM、不带解释器、不需要目标机预装运行时。这是和 Java 系工具体积差一个数量级的根本原因。gpui 直接调 GPUmac 上是 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 zltocodegen-units 1strip这一套组合拳下来二进制从几十 MB 量级压到 14MB。最后那条注释是踩出来的图省事加panic abort能再小一点但 gpui 和锁的实现依赖 unwind一旦 abort运行时一个 panic 就是整窗崩溃得不偿失。3. 依赖一律挑「纯 Rust、免 C 工具链」的实现这一条同时省体积、省编译麻烦、还为「零环境交付」铺路TLS / 证书rustlstokio-rustlsrcgen全部统一用ring后端绕开aws-lc那条需要 cmake / C 编译器的路解压flate2(gzip/deflate) brotli纯 Rust字符集encoding_rsGBK/Big5/Shift_JIS → UTF-8和 Firefox 同款哈希 / 对称加解密RustCrypto 的md-5/sha2/aes/cbc/ecb纯 RustWASM 扩展运行时wasmtime但精简了 features——只留runtimecraneliftwat砍掉 component-model、async、cache、并行编译、pooling-allocator 等重头。全程没有一个动态链接的第三方原生库otool -L验证过交付出去就是「双击即用」。二、架构18 个 crate 的纯函数内核 一层 GUIScry 是一个 Cargo workspace按职责切成 18 个 crate。核心设计哲学只有一句把能做成纯函数的引擎全部抽成独立 crateUI 只是它们的薄壳。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(bHTTP/1.1 200 Connection Established\r\n\r\n).await?;// 偷看隧道内首字节(peek 不消费 socket)letmutfirst[0u8;1];letlooks_tlsmatches!(client.peek(mutfirst).await,Ok(n)ifn1first[0]0x16// 0x16 TLS handshake record);iflooks_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 私钥为目标域名现签叶子证书letleafsign_leaf_for(ca,example.com);// rcgen ringletserver_configbuild_server_config(leaf);// rustls ServerConfig// 对客户端完成 TLS 握手,从此拿到明文letacceptorTlsAcceptor::from(Arc::new(server_config));lettls_streamacceptor.accept(client).await?;// ← 明文双向流// 对真实服务器,这边再作为 TLS 客户端连上去(可经上游代理)// 两段拼起来 中间人,明文尽在掌握客户端之所以信任这张「冒充」的证书是因为它信任了 Scry 的根 CA。所以工具会提供「一键安装信任」和「导出到其他电脑」的证书分发包。这里有个和「安全」强相关的实现细节Scry 的内置浏览器抓包模式不需要往系统里装 CA而是给 Chromium 传--ignore-certificate-errors-spki-listCA 的 SPKI 哈希。这等于精确告诉浏览器「只放行这一把公钥」既不全局关校验、又能覆盖证书 pinning 的站点——比「一把火关掉所有证书校验」干净得多。save-first抓到先落盘一条铁律抓到请求的第一件事是落盘不是分析。scry_proxy在拿到完整响应后第一步就save_flow()写进 SQLite按method 规范化 URL body 的 sha1去重然后才轮到展示、扩展钩子、改写。进程崩了、窗口关了已抓到的数据都不会丢。四、功能全景Burp 四大件 一堆周边界面是 Burp 式的多页签工作台目前15 个页签全部落地、无占位页。对标关系大致如下能力对标Scry 的实现抓包代理ProxyMITM 内核 虚拟化历史表 报文语法高亮重放Repeater改包重发 美化/原始/Hex/渲染视图爆破IntruderSniper/钳形/集束四模式 载荷生成/处理/Grep 提取 并发限速扫描Scanner被动规则 主动探测 敏感文件发现(Nikto 式)拦截改包Intercept断点队列 MatchReplace 自动改包 范围规则持久化编解码Decoder31 种变换:Base32/58、JWT、XOR/RC4/AES、HMAC…比较ComparerLCS 行/词/字符级 diff Dice 相似度随机性分析Sequencer字符级 比特级香农熵 FIPS 140-2 四项自检越权检测类 Autorize高/低/匿名多身份重放对比SQLi / XSS类 sqlmap / dalfox独立纯函数引擎 专用页站点爬虫SpiderCDP 驱动真实浏览器渲染后抽链扩展BApp三种 Runner(见下)抓包内核本身还补齐了WebSocket 帧抓取、HTTP/2先连上游拿 ALPN两端协议对齐再转发、上游链式解密后把流量交回 sing-box / 机场出网应对受限网络、以及TLS 指纹JA3/JA4的伪装与可视化。下面挑三个我自认为有点意思的工程点展开。五、技术点拆解1. TLS 指纹让 rustls「自己吐」ClientHello很多目标会看JA3/JA4指纹判断「你是不是浏览器」。JA3 的定义是把 ClientHello 的若干字段拼成字符串再取 MD5JA3 MD5( SSLVersion,CipherSuites,Extensions,EllipticCurves,ECPointFormats )要算自己真实的指纹最靠谱的不是「猜我发了什么」而是让 TLS 库真的把 ClientHello 字节吐出来再解析。rustls 允许你把握手数据写进内存缓冲而不真的连网// 让 rustls 把 ClientHello 写进内存(不连网),再解析真实字节算 JA3/JA4letmutconnrustls::ClientConnection::new(config,server_name)?;letmutbufVec::new();conn.write_tls(mutbuf)?;// 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写回结果并触发重绘。爆破 / 扫描这类「流式出结果」的场景则用一个mpscchannel后台串行发包、每出一条结果就send回来前台每 120msdrain一次增量刷进表格。这样无论后台打多少包GPU 主线程永远只做渲染列表用虚拟化只渲视口内那 ~20 行 响应体解码缓存thread_localLRU按堆指针 长度做 key即便单条响应几百 KB、历史几千条也不卡。六、顺手做的一件事把工具开放给 AI既然内核都是规整的纯函数引擎我加了个scry_mcp——一个独立的MCPModel 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 杀手」。平台优先 macOSgpui 在 mac 走 Metal 最成熟。最后照例一句郑重声明⚠️ 本文与该工具仅用于学习研究与获得授权的安全测试。任何对未授权目标的扫描、抓包、改包都可能违法。请务必在合法合规、获得明确授权的前提下使用一切后果由使用者自负。写在最后回头看「16MB 干 Burp 的活」并不是靠某个魔法而是一串朴素选择的叠加原生编译省掉运行时、极致 release profile、纯 Rust 免 C 依赖、纯函数内核可测试、GPU 界面只管渲染、IO 全甩后台。Rust 在系统工具这个赛道真的能同时给你「小、快、稳」三样——这在过去往往要三选二。如果你也受够了某些安全工具的臃肿不妨试试用 Rust 自己造一个趁手的哪怕只是把其中某个引擎MITM 解密、JA3 计算、WASM 扩展沙箱单独抠出来玩一遍也很值。如果这篇对你有帮助欢迎评论区聊聊你心目中「理想的安全工具」是什么样。