sreeharipj/unhusk
GitHub: sreeharipj/unhusk
利用 Rust panic 元数据从剥离符号的 release 二进制文件中识别用户编写函数的逆向分析工具。
Stars: 0 | Forks: 0
# unhusk
利用 panic 元数据在剥离了符号的 Rust release 二进制文件中识别用户编写的函数 —— 无需反汇编,无需符号表,无需签名数据库。
## 功能介绍
一个经过 strip 处理和 LTO 优化的 Rust release 二进制文件主要由标准库和 Cargo 依赖构成。作者编写的代码只占很小一部分。`unhusk` 通过利用 Rust panic 机制的结构特性来查找用户编写的函数。
**主要输出 —— `certain`(确定)函数:**指那些直接引用了 `.data.rel.ro` 中 `core::panic::Location` 结构体的函数,且这些结构体的源码路径指向用户代码(`src/`、`tests/`、`examples/`)。每一个这样的函数都包含用户路径的 panic 元数据。
**在 60 个真实的 Rust 二进制文件上测得的精确度(ripgrep, bat, fd, just, dust, broot, topgrade, …):**
| 真值方法 | 精确率中位数 | 平均值 | 真正的 FP 率 |
|---|---|---|---|
| 符号名称(crate 归属) | **93.8%** | 88.1% | **6.2%** |
| DWARF `decl_file` | 86.6% | 77.5% | — |
符号名称数据 (93.8%) 是更客观真实的测量结果。DWARF 的数据较低,是因为即使闭包体完全属于用户代码,DWARF 也会将 `FnOnce`/`FnMut` 闭包垫片归类到 `core/src/ops/function.rs`;这占据了约 67% 的 DWARF 误报。不可避免的那 6.2% 误差来自于使用用户类型单态化的 std/dep 泛型函数 —— 在剥离符号的二进制文件中,unhusk 无法将它们与真正的用户函数区分开来。
**测得的召回率(以 DWARF 为分母 —— 上限):**`certain` 在真实二进制文件中大约能捕获 15% 的用户函数(中位数 15.8%,范围 0.4%–45.5%)。在包含 13 个二进制文件的 realval 集中,总体召回率(certain+inferred,d=∞)**按 DWARF 分母计算为 46.2%**(上限),**按符号分母计算为 19.0%**(下限):DWARF 因为丢弃了 FnOnce/FnMut 闭包垫片和未映射的单态化而导致统计偏低;而符号统计则因为包含了该工具无法找到的 `::method` 样板代码而导致统计偏高。真实的用户逻辑召回率介于两者之间。推导过程请参见 `realval/BACKTRACE_SWEEP.md`。结构性上限将在下文的“局限性”中解释。
**调用闭包(非用户代码):**`inferred`(推断)函数可以通过调用边从用户代码到达。在真实二进制文件中 DWARF 精确度约为 5%(主要是从用户 panic 位置传递调用的 std/dep 胶水代码)。单独进行标记。
## 工作原理
对于每个可达的 `panic!`、`assert!`、边界检查和 `.unwrap()` 调用位置,Rust 都会在 `.data.rel.ro` 中嵌入一个 `core::panic::Location` 结构体。每个结构体都持有一个指向 `.rodata` 中源码路径字符串的胖 `&'static str` 指针(通过 `.rela.dyn` 中的 PIE 重定位实现),外加行号和列号。
**阶段 1** —— 重构每个 Location 结构体并对其源码路径进行分类:
```
.rela.dyn R_X86_64_RELATIVE { offset, addend }
│ offset → slot in .data.rel.ro (the file-ptr field of the Location)
└── addend → string in .rodata ("src/main.rs", "/rustc/…", …)
.data.rel.ro [ ptr(reloc) | len(u64) | line(u32) | col(u32) ]
```
源码路径分类(确定性,无启发式算法):
| 模式 | 归属 |
|---|---|
| `src/*.rs`, `tests/*.rs`, `examples/*.rs` | **用户代码** |
| `/rustc/HASH/library/…` 或 `library/…` | std/core/alloc |
| `/rust/deps/CRATE-VER/…` | 工具链内置依赖 |
| `*/cargo/registry/src/*/CRATE-VER/…` | Cargo registry 依赖 |
**阶段 2** —— 基于 `.eh_frame` FDE 的函数范围映射 + RIP 相对地址交叉引用扫描。任何包含对用户 Location 结构体引用的函数都会被标记为 `certain`。
## 精确度验证
`--validate ` 参数会将 unhusk 的归属判定与来自配套未剥离二进制文件的 DWARF `.debug_info` 真值进行对比:
```
unhusk binary.stripped --validate binary.unstripped
```
在三个小型/合成测试套件(其中用户函数是独立的、非内联的)上测得的结果:
| 测试套件 | FDEs | DWARF 用户函数 | Certain 精确度 | Certain 召回率 |
|---|---|---|---|---|
| medium_debug (synthetic) | 541 | 1 | 100% (1/1) | 100% |
| unhusk-on-unhusk | 1490 | 8 | 100% (3/3) | 37.5% |
| scored_debug (designed) | 529 | 19 | 100% (6/6) | ~84% |
在真实的二进制文件上,精确度有所下降(按 DWARF 统计中位数为 86.2%,按符号统计为 94.5%),因为以闭包形式或库泛型实例化表达的代码会被 DWARF 归因于其在 core/std 中的定义位置。完整结果请参见 `REAL_BINARY_VALIDATION.md`。
## 局限性
**闭包垫片和泛型单态化会降低精确度。** 当用户代码是通过 `Fn*` 调用的闭包,或者将用户类型传递给了 std/dep 泛型(sort、迭代适配器、延迟初始化)时,生成的单态化函数可能会包含用户路径的 panic Location,尽管该函数体的大部分是库逻辑。unhusk 会将这些标记为 `certain`;它们构成了真实二进制文件中大部分的误报。
**没有可达 panic 位置的函数**无法被发现。纯计算、getter 以及在开启 `lto = true` 编译的情况下,优化器证明所有 panic 均不可达的代码,在二进制文件中将不会存在任何 panic Location 结构体 —— 没有可供锚定的点。
**通过标准库机制或间接调度调用的用户代码**无法被发现。仅通过 trait 对象、函数指针或穿过 std/dep 调度层的回调进行调用的函数会被归类为 `library`。
**仅支持 x86-64 ELF。** 同时支持 PIE(`ET_DYN`,`cargo build --release` 的默认设置)和非 PIE(`ET_EXEC`)。
## 构建
```
cargo build --release
```
需要 Rust 1.70+。没有 C 库依赖,运行时也不需要外部工具。
## Docker
你可以使用 Docker 轻松构建和运行 `unhusk`,它提供了一个隔离的环境,而无需安装 Rust 工具链。
### 构建镜像
```
docker build -t unhusk .
```
### 运行容器
通过 Docker 运行时,你需要将包含二进制文件的目录挂载为卷,以便容器能够访问它们。
```
# 挂载当前目录并分析 binary
docker run --rm -v "$(pwd)":/work -w /work unhusk
# 针对 unstripped binary 进行验证
docker run --rm -v "$(pwd)":/work -w /work unhusk --validate
```
## 使用方法
```
# 在 stripped binary 中识别用户编写的函数
unhusk
# 同时报告经 DWARF 验证的 precision/recall 数据
unhusk --validate
# 显示完整的 call-closure 列表(可从用户访问,主要为 dep/std)
unhusk --show-call-closure
# 将 call-graph BFS 限制为 N 跳(在 13 个真实 binary 上测得):
# depth 1:9.3% 推断 precision(+1.8x),-3.9pp recall — 高精度审计
# depth 2:6.4% 推断 precision(+1.3x),-1.1pp recall — 最佳平衡
unhusk --infer-depth 2
```
## 测试
```
cargo test
```
集成测试需要 `/tmp/unhusk-research/` 下的测试二进制文件(参见 `tests/integration.rs`)。字符串分类器的单元测试无需测试数据即可运行。
## 许可证
本项目采用双重许可:
1. **GNU Affero General Public License v3.0 (AGPLv3)**:用于开源项目和一般用途。
2. **商业许可证**:用于 AGPLv3 限制不适用的专有或商业环境。
详情请参阅 `LICENSE` 文件。
标签:Rust, 二进制分析, 云安全监控, 云安全运维, 云资产清单, 代码分析, 元数据分析, 凭证管理, 可视化界面, 网络流量审计, 请求拦截, 逆向工程, 通知系统, 静态分析