sreeharipj/winnow
GitHub: sreeharipj/winnow
winnow 是一款能够从符号被剥离的 Rust 恶意软件二进制文件中提取作者专属特征,并自动生成高特异性 YARA-X 规则的安全工具。
Stars: 1 | Forks: 0
# winnow

为剥离过的 x86-64 Rust 恶意软件二进制文件生成 YARA-X 规则。输入一个二进制文件,输出专属于该文件的规则。基于 [unhusk](https://github.com/sreeharipj/unhusk) 构建,该工具能从 panic 元数据中,将剥离过的 Rust 二进制文件中由作者编写的函数分离出来。
unhusk 回答的是“这个剥离过的 Rust 二进制文件中,哪些字节属于作者”。winnow 将这个答案转化为特征签名。因为 unhusk 的输入在构建时就已归属于作者——这是基于 panic 元数据的溯源,而非启发式推测——winnow 据此构建规则的字节和字符串都源自作者本人,既非标准库也非依赖 crate。仅基于作者独有材料构建的规则,不应会在不相关的软件上触发;这一主张是经过测量的,而非假设的(在一个包含 76 个二进制文件的保留语料库上,三条规则做到了零误报——详见 [`docs/validation.md`](docs/validation.md))。
## 安装
```
cargo build --release # Rust 1.70+
```
winnow 会通过 shell 调用 `unhusk`,后者必须存在于 `PATH` 中(或通过 `--unhusk-bin ` 传入);请查看 [unhusk](https://github.com/sreeharipj/unhusk) 仓库以进行构建。
## 使用方法
```
# Tier 2 (workhorse):为剥离了符号的 Rust 恶意软件二进制文件生成 YARA-X 规则
winnow # emits .yar
# 同时尝试 Tier 1 (masked-hex + independent behavioral string),取决于
# 本项目衡量自身的 benign corpus:
winnow --tier1 --corpus-dir corpus/bin
# emits .yar and, only if
# earned, _tier1.yar
```
## 示例输出
使用 `--tier1` 对真实的 Akira 勒索软件样本运行:
```
wrote akira_v2_x.yar (7 STRONG fns, 4 panic strings, 21 code atoms)
tier1 — measuring against benign corpus (78 files)
tier1 — masked atom for fn 0xc805c reduced 186B -> 64B window at +0x2b (64 exact bytes, 0 corpus collisions)
tier1 — behavioral string "/tmp/stop_vms.sh" (fn 0xdc434) is rare — kept
tier1 — code factor: 7 atom(s) reduced & kept, 0 dropped as non-discriminative
wrote akira_v2_x_tier1.yar (TIER 1 EARNED — 3 masked-code atoms, 22 independent behavioral strings)
```
其编写的规则为:[`examples/akira_v2_x_tier1.yar`](examples/akira_v2_x_tier1.yar) —— 一条真实的 Tier 1 规则,包含掩码过的代码 atom 以及从互不相交的函数中提取的独立行为字符串,且未对该工具的产出做任何修改。
## 一个二进制文件,一条规则(这是设计理念,而非权宜之计)
winnow 不会在不同恶意软件家族或不同版本之间进行泛化。一个样本产出一条用于指纹识别该样本的规则。如果该规则也恰好捕获了后续版本,仅仅是因为作者从未修改过被指纹识别的函数——这是一份额外的收获,绝不是原定目标,也绝不是靠放宽规则标准来强行实现的。
这就是应对误报的总体策略。泛化正是误报的根源:签名向匹配整个家族迈出的每一步,也就是向匹配某些良性程序靠近的一步。winnow 从保证为某位作者独有的材料出发,其唯一的工作就是将这种独特性完好无损地保留到最终规则中。追逐下一个版本无异于用现实存在的现有保证去交换 speculative 的未来命中,因此它绝不这么做。
## 测量指导构建
该项目的核心法则:在运行了可能证伪其存在理由的实验之前,任何组件都不会发布。
其设计主张——基于作者独有输入实现近乎零误报——终究只是一种论点,而这个论点存在漏洞。“这个字符串很罕见”、“这个模式不会发生碰撞”、“这些字节具有区分度”,这些全都是关于良性代码背景分布的假设。如果不建立关于“什么是常见的”模型(即良性语料库),你无法知道一个字符串是否罕见。因此,诚实的主张绝不是“不需要良性语料库”,而是 **无需针对每个家族单独验证**:良性误报率只需测量一次,随后每条规则的特异性便依赖于作者归因,而不是针对每个家族重新通过负样本数据进行验证。每一个阶段的顺序安排,都是为了尽早得出该测量结果,并由它来决定下一步构建什么。
## 良性语料库
包含 154 个良性的 x86-64 ELF Rust 二进制文件,均为 release 模式构建且已剥离,涵盖 CLI、系统层、异步和并行工具——也就是恶意软件集合同样会呈现的形态。其中一个子集被移除了 `.eh_frame`,以模拟边界退化的情况。它最初只有 75 个,后来增长到了 154 个;这种增长唯一带来的好处是缩小了误报的置信区间,因此在 pipeline 中持续扩充它几乎不产生额外成本。
该语料库被划分为作为过滤器的一半(A)和互不相交的保留一半(B)。规则是基于 A 构建的,而它们的误报仅会在 B 上统计——关于这种划分为何重要以及完整的测量细节,请参见 [`docs/validation.md`](docs/validation.md)。
## 层级
winnow 会输出其证据所能支撑的最强规则,并为每条规则标注其依赖的依据。证据的可用性并不统一——这是从 unhusk 继承的特点,panic 字符串在经历极深的剥离后依然存活的概率远高于函数边界——因此,生成器会选择性能降级而不是直接彻底失败。
- **Tier 1,多因子。** 来自 STRONG 作者函数的掩码十六进制 AND 一个独立的行为字符串。这是最强的规则;要求作者独有的代码与作者独有的数据同时存在,且两者不随彼此协同变化。
- **Tier 2,字符串主导。** 作者的 panic 路径字符串加上无边界的代码信号。这是现实中真正的主力。
- **Tier 0,拒绝。** 被加壳、无文件头或归因失效的输入将不会生成规则,并会给出明确的原因。一个拒绝生成不安全规则的生成器,远比一个总是强行生成内容的生成器更有价值。
### 独立性,以及设计上必须做出的一项修正
Tier 1 将两个因子的小概率相乘,但这仅在两个因子相互独立时才成立。然而 panic 路径字符串与代码并不独立——它们正是 unhusk 最初找到代码的线索来源。如果一条规则将“这个作者函数”与“该函数引用的 panic 字符串”进行 AND 操作,其实是对同一份证据计算了两次。因此,Tier 1 的第二个因子必须来自真正独立的证据:一条作者出于行为目的(而非为了 panic 报告)选择的行为字符串(例如主机名、互斥锁名或格式化字符串)。
同样的非独立性产生了一个后果,这在最初的设计中被忽略了,后来通过测量才得以纠正。最初的层级模型包含一个针对字符串特征微弱情况的“纯代码”降级方案。该层级在逻辑上是行不通的:代码因子在因果上处于字符串归因的下游,因此当作为归因线索的字符串消失时,根本不存在可供降级的代码因子。使用 `--remap-path-prefix` 构建的二进制文件没有作者专属的 panic 路径,因此没有可归因的函数,因此也就没有任何可用于签名的内容——无论是代码还是其他形式。这种情况属于 Tier 0,而不是一个纯代码层级。这种错误标记从设计阶段起就一直存在于架构中,直到一个真实的 `--remap-path-prefix` 样本产生了零个可归因函数时才被发现。
## 针对真实恶意软件的测试结果
选取了六个野生 Rust 恶意软件样本作为可签名对象。其中三个不可用,原因各不相同:`blackcat_x`(Windows PE,超出处理范围)、`01flip_x`(`--remap-path-prefix` 移除了 panic 路径——Tier 0),以及 `p2pinfect_x`(无 ELF section headers,属于原始/不完整的 dump 文件)。三个可用的样本——`krusty_x`(KrustyLoader,Tier 2)、`akira_v2_x`(Akira,Tier 1)和 `blackcat_sphynx_x`(BlackCat Sphynx,Tier 2)——各自生成了一条规则,在自身样本上成功自触发,并产生了 **零误报(横跨 76 个保留的良性二进制文件)**(95% 置信上限的“三法则”约为 ~3.9%)。完整的表格、保留集测试方法以及 Tier 1 的独立性测量详见 [`docs/validation.md`](docs/validation.md)。
三个样本中仅有一个(Akira)达到了 Tier 1:它包含 3 个掩码代码 atom 和 22 个罕见的行为字符串(`/tmp/stop_vms.sh`、`/akiranew.txt`、一个硬编码的 token 以及 ESXi 路径),且均提取自互不相交的函数。BlackCat Sphynx 只有一个 STRONG 函数,因此其代码 atom 和唯一的行为字符串同属于该函数——不存在不相交的配对,而单个函数无法自我佐证,因此它降级到了 Tier 2。KrustyLoader 仅提取到了语料库中常见的通用 std panic 字符串,因此没有任何字符串能通过稀有度过滤,它同样也降级到了 Tier 2。这正是设计本身做出的预测:恶意软件作者的代码量很少,unhusk 的召回率有限,且 `.rodata` 中干净独立的行为字符串往往缺失——因为恶意软件通常恰好会加密或运行时解密这些字符串。Tier 2 才是现实中真正的主力;Tier 1 只是一种幸运的特例。
可签名集合中有一半不可用,而且这三个失败案例并非无意义的噪音——它们是对应用边界的清晰刻画。格式错误(PE)、归因失效(`--remap-path-prefix`)以及无 section headers(原始 dump)是三种截然不同的机制,每一种都精确对应着工具停止工作的位置及其原因。部署后的召回率受限于 unhusk 的脆弱性,而不是 winnow 的逻辑:winnow 只能对 unhusk 能够归因的内容进行签名。
## 主张的范围与限制
- **已主张:** 在三个可用样本中,在包含 76 个二进制文件的保留良性测试集上实现了零误报(95% 置信上限的“三法则”约为 ~3.9%),且误报率只需测量一次,而非按家族重复测量。其中一个样本(Akira)获得了双因子的 Tier 1 规则;另外两个保持在 Tier 2。
- **未主张:** 具备代表真实环境的误报率。面对 154 个精心筛选的语料库得出三条规则,只是一个开端,并非具备代表性的研究。样本量 n 较小,且语料库向常见 crate 倾斜。
- **未主张:** 家族或版本覆盖。根据设计,每条规则仅用于指纹识别一个二进制文件。
## 复现该测量结果
上述数据不需要凭信仰接受。
```
scripts/build_corpus.sh # git-clone + cargo build the benign corpus into corpus/bin/
# (manifest committed at corpus/manifest.csv)
scripts/measure_holdout.sh # split the corpus into filter half A and held-out half B,
# build rules against A, count false positives only on B
scripts/measure_independence.sh # decompose each earned Tier 1 rule into code-only and
# string-only variants, scan each against B
```
两个脚本都会将它们的原始输出写入 `results/` 目录(已被 gitignored);[`docs/validation.md`](docs/validation.md) 是该输出经过提交和筛选后的快照。`corpus/manifest.csv` 列出了每一个良性二进制文件的源码仓库、commit 以及构建参数——语料库本身并未提交,仅提交了重新构建它的配方。
## 局限性
- 结果依赖于 n=3 个可用样本。那三个不可用的样本则展示了输入端有多么容易崩溃。
- Tier 1 由三者之一(Akira)获得,另外两个均被降级。多因子规则现在确实能在真实恶意软件上触发,但基础薄弱——仅有一个达标样本。它所需的独立行为字符串通常不存在或被加密,因此大多数样本只能达到 Tier 2,而非 Tier 1。
- winnow 继承了 unhusk 的所有限制:仅支持 x86-64 ELF;无法处理加壳、使用了 `--remap-path-prefix` 和 `panic_immediate_abort` 的情况;召回率有限;对异步繁重的代码精度较低,而恶意软件往往偏向这种代码。
- 良性语料库由 154 个偏向常见 crate 的二进制文件组成,并非从具备代表性的 Rust 软件总体中抽样得出。误报数据的可靠性,仅取决于该语料库的代表性有多强。
## 与 unhusk 的关系
unhusk 是后端;winnow 是其背后的基于 JSON 契约的规则生成器。winnow 使用 `unhusk --precision --json` 获取函数边界、层级和 panic 路径归因,并自行重新解析 ELF 文件以获取原始字节和非 panic 字符串。这种二次解析构成了两者之间的接缝:当前的契约既不携带原始字节,也不包含非 panic 的作者字符串,因此消费者必须重新推导生产者已经看到过的内容。最彻底的修复方法是采用 v2 契约,让 unhusk 直接输出每个函数的字节和行为字符串——这是一次重构,而不是重新设计,且并非该工具正常工作所必需的。
## 许可证
基于 Apache License, Version 2.0 授权。详见 `LICENSE`。
标签:DNS 反向解析, Rust, YARA, 云资产可视化, 可视化界面, 网络流量审计, 逆向分析, 通知系统