news 2026/9/16 21:36:42

深入 syft 的 ELF 二进制包目录器:从 elf-testdata 测试夹具看 .note.package 解析与 SBOM 包级依赖关系构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 syft 的 ELF 二进制包目录器:从 elf-testdata 测试夹具看 .note.package 解析与 SBOM 包级依赖关系构建

深入 syft 的 ELF 二进制包目录器:从 elf-testdata 测试夹具看 .note.package 解析与 SBOM 包级依赖关系构建

【免费下载链接】syftCLI tool and library for generating a Software Bill of Materials from container images and filesystems项目地址: https://gitcode.com/GitHub_Trending/sy/syft

导读

本文以 syft 仓库中的 elf-testdata 测试夹具文档 为主线,讲解 syft 如何从 ELF 可执行文件与共享库的.note.package段中提取包元数据(名称、版本、许可证、PURL、CPE 等),并据此在 SBOM 中建立"包到包"(package-to-package)而非"包到文件"(package-to-file)的依赖关系。读完本文,你将掌握 syft ELF 二进制包目录器的底层原理、该测试夹具的构建与验证方法,以及一个真实仓库中"逻辑包聚合 + 依赖关系表达"的完整设计思路。

一、背景:syft 的二进制包目录器与.note.package

syft 是一个从容器镜像与文件系统生成软件物料清单(SBOM)的 CLI 工具与库。对于 ELF 格式的可执行文件与共享库,syft 提供了一套专门的目录器(cataloger),其核心实现位于 elf_package_cataloger.go,注册名为elf-binary-package-cataloger(见 Name() 方法)。

该目录器的工作思路是:读取 ELF 文件中的.note.package。该段通常由打包工具(如 RPM、dpkg、systemd 生态下的构建工具链)注入,携带一段 JSON 格式的包元数据,其规范背景可追溯到 systemd 的 ELF_PACKAGE_METADATA 设计。源码注释中明确指出(见 elf_package_cataloger.go):当前实现只针对.note.package段中单一的 JSON 数据形态,未来需要泛化以支持非 JSON 数据(例如 Fedora 会在 JSON 载荷前附加一个 ELF section header 前缀——这也正是源码中getELFNotes对"段头前缀 + JSON"双格式兼容处理的由来,见 elf_package_cataloger.go)。

为了持续验证这套解析与关系建模逻辑,仓库在 testdata/elf-testdata 目录下维护了一组专门构造的 ELF 测试夹具,配套的 README 文档正是本文的主体素材。

二、elf-testdata 测试夹具的目录结构与构建方式

2.1 三个测试场景

elf-testdata目录下包含三个独立的构造场景:

syft/pkg/cataloger/binary/testdata/elf-testdata/ ├── elfbinwithnestedlib/ # 嵌套共享库场景:库位于 bin/lib 子目录 ├── elfbinwithsisterlib/ # "姊妹库"场景:elfsrc1 与 elfsrc2 两个源码目录 ├── elfbinwithcorrupt/ # 损坏/异常场景 ├── Dockerfile # 一键构建镜像的 Dockerfile └── README.md

README 明确说明该夹具的用途:"This image illustrates a few examples of how ELF executables can be assembled and illustrated in an SBOM.",即演示 ELF 可执行文件如何在 SBOM 中被装配与表达。

2.2 用 objcopy 注入包元数据:夹具的核心构造技巧

每个场景的源码目录中都包含makefile*.cpp*.h文件。以 elfbinwithnestedlib/elfsrc/makefile 为例,其核心构造逻辑分两步:

  1. 编译共享库:使用g++ -std=c++17 -Wall -Wextra -pedantic编译hello_world.cpp为位置无关目标文件,再链接为libhello_world.so
  2. 注入元数据:通过objcopy将一段 JSON 字符串写入.note.package段:
echo '{"type": "testfixture","license":"MIT","commit":"5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb","sourceRepo":"https://github.com/someone/somewhere.git","vendor": "syft","system": "syftsys","name": "libhello_world.so","version": "0.01","purl": "pkg:generic/syftsys/syfttestfixture@0.01","cpe": "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01"}' | objcopy --add-section .note.package=/dev/stdin --set-section-flags .note.package=noload,readonly $@

关键点:--add-section .note.package=/dev/stdin从标准输入读取 JSON 并作为新段写入;--set-section-flags .note.package=noload,readonly将该段标记为不加载(noload)、只读(readonly),这保证注入的元数据段不会在程序加载时占用内存,与真实发行版的做法一致。可执行文件本身也会注入一段内容几乎相同、但namesyfttestfixture的 JSON(见同一 makefile 中$(BIN_DIR)/$(EXECUTABLE)目标)。

这正对应了目录器的数据模型:elfBinaryPackageNotes(见 elf_package_cataloger.go)中的字段与上述 JSON 一一对应:

JSON 字段Go 结构体字段说明
nameName包名
versionVersion版本号
licenseLicense许可证(如MIT
purlPURL包 URL 标识
cpeCPECPE 标识
type/vendor/system/sourceRepo/commitpkg.ELFBinaryPackageNoteJSONPayload(内嵌)类型、厂商、系统、源码仓库、提交哈希
osCpeCorrectOSCPE与 systemd ELF 包元数据规范对齐的 OS CPE(大小写修正版)

2.3 多阶段 Docker 构建:模拟真实 RPM 系统环境

Dockerfile 采用两阶段构建:

  • 第一阶段(base):基于rockylinux:8,安装make automake gcc gcc-c++ kernel-devel等编译工具链,随后将三个场景源码 COPY 到/usr/local/bin/elftests/下并逐个make,并通过ENV LD_LIBRARY_PATH=/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib保证运行时能找到嵌套库;
  • 第二阶段:基于busybox:1.36.1-musl精简镜像,仅保留编译产物与运行依赖——/usr/local/bin/elftests、RPM 数据库/var/lib/rpmlibstdc++.so.6libc.so.6,并执行ln -s /usr/lib64 /lib64创建符号链接,专门用于验证 syft 能正确处理符号链接路径(注释写明 "prove we can operate over symlinks")。

RPM 数据库与 glibc/libstdc++ 的保留并非偶然:README 的"期望状态"部分明确提出,除二进制与共享库之间的依赖外,还希望表达二进制与由共享库带来的 RPM 包传递依赖(如glibclibstdc++)之间的关系,这一点在下方第 4 节的关系图中会再次体现。

三、案例详解:elfbinwithsisterlib 与"同名库"的聚合

3.1 场景构成

README 的 Example 1(elf-testdata/elfbinwithsisterlib)构建了两个二进制:

  • elfsrc1/编译出elfwithparallellibbin1与共享库libhello_world.so
  • elfsrc2/编译出elfwithparallellibbin2与共享库libhello_world2.so

三个共享库中,两个同名libhello_world.so)、一个不同名(libhello_world2.so),且位于不同目录(nested/bin/lib/sister/lib/)。README 特别指出:尽管位置不同,它们的.note.package段内容输出一致,因此它们应当被聚合成同一个逻辑包

3.2 用 objdump 验证注入结果

README 提供了用objdump验证元数据段内容的标准手段:

objdump -s -j .note.package /usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so

输出显示该段为elf64-littleaarch64格式(此处以 aarch64 文件为例),十六进制内容可还原为 JSON 明文:{"type": "testfixture","license":"MIT","commit":"5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb","sourceRepo":"https://github.com/someone/somewhere.git","vendor": "syft","system": "syftsys","name": "libhello_world.so","version": "0.01","purl": "pkg:generic/syftsys/syfttestfixture@0.01","cpe": "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01"}

这条验证命令是测试夹具设计的关键一环:它确保"段内容可被外部工具读取、且字段与目录器解析逻辑对齐",使夹具不仅能驱动 syft 测试,也能被人工审计。

3.3 产物清单与期望关系

README 给出的最终产物清单如下:

Binaries(可执行文件)

/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib /usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2 /usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1

Libraries(共享库)

/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so /usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so /usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so

Binaries related to Libraries(期望的二进制→库关系)

elfbinwithnestedlib -> libhello_world.so elfwithparallellibbin2 -> libhello_world.so elfwithparallellibbin1 -> libhello_world2.so

注意最后一行是 README 原文的期望关系(elfwithparallellibbin1 -> libhello_world2.so),结合后文源码测试可以发现,实际目录器是按"动态库导入 + 同名聚合"来统一建模的,具体见第 5 节。

四、实际状态与期望状态:两张关系图的背后含义

README 的核心诉求在"Desired State"一节中表达得非常明确:

We want to drop the package to file relationships and instead do package to package.

即:放弃"包到文件"关系,改为"包到包"关系。期望的单条关系是:

ElfPackage libhello_world.so -> ElfPackage syfttestfixture(共享库)

同时还要表达二进制与 RPM 包传递依赖之间的关系。

4.1 实际状态(Actual State)

README 用 mermaid 图描述了未经聚合时的实际状态——每个二进制/库节点指向其直接导入的共享库,最终收敛到系统库libc.so.6libstdc++.so.6

这张图展示了实际扫描时发现的完整导入网络:3 个二进制、3 个共享库(其中libhello_world.so出现于两个不同位置)、2 个系统库,共 11 个节点与 12 条 import 边。

4.2 期望状态(Desired Relationships)

期望的关系图将分散的文件聚合为两个逻辑包,再表达包间依赖:

该图传达三条核心设计意图:

  1. 文件→逻辑包:3 个可执行文件聚合为syfttestfixture包(因为它们的.note.packagename均为syfttestfixture);3 个共享库聚合为libhello_world.so包;
  2. 包间依赖libhello_world.so包是syfttestfixture应用包的依赖(dependency-of);
  3. RPM 传递依赖glibc同时是应用包与库包的依赖,libstdc++是应用包的依赖——这正是 Dockerfile 中保留/var/lib/rpm与系统库的原因,用于让测试夹具能同时驱动 RPM 目录器产生这些传递依赖节点。

五、源码印证:目录器如何实现"同名聚合"与"逻辑包"

5.1 两遍扫描与按键聚合

elf_package_cataloger.go 的Catalog方法采用两遍扫描策略:

  • 第一遍:通过resolver.FilesByMIMEType按 MIME 类型(mimetype.ExecutableMIMETypeSet)筛选出所有可执行文件,逐个解析.note.package段;解析成功的文件按elfPackageKey(Name/Version/PURL/CPE 四元组,见 elf_package_cataloger.go)聚合到notesByLocation映射中;
  • 第二遍:对每个 key 下的所有 note,将它们的 Location 合并进同一个file.NewLocationSet(),并为每个 note 标注pkg.PrimaryEvidenceAnnotation证据注解,最后基于第一个 note 生成一个逻辑包。

源码注释(elf_package_cataloger.go)明确指出这样设计的原因:可能存在多个 ELF 二进制具有相同的 name 与 version,它们集合起来才共同代表一个逻辑包——这与 README 中"三个位置的同名库应聚合成一个包"的诉求完全对应。

5.2 包对象的组装:newELFPackage 与 PURL 生成

elf_package.go 中的newELFPackage将解析出的元数据组装为pkg.Package

  • NameVersion取自 note;
  • Licensesmetadata.License构建(测试中可看到MIT被解析为 SPDX 表达式MIT、类型为declared);
  • PURLelfPackageURL动态生成;
  • Type由元数据中的type字段映射:rpmpkg.RpmPkgdebpkg.DebPkgapkpkg.ApkPkgalpmpkg.AlpmPkg,其余回落为pkg.BinaryPkg(见 elf_package.go);
  • 元数据主体存入Metadatapkg.ELFBinaryPackageNoteJSONPayload)。

elfPackageURL(见 elf_package.go)还会根据type与 OS 信息构造带distro限定符的 PURL:OS 信息优先取元数据中的os/osVersion字段,缺失时回退解析osCpe(见osNameAndVersionFromMetadata,elf_package.go)。以本夹具为例,purl字段被直接注入为pkg:generic/syftsys/syfttestfixture@0.01

5.3 测试用例中的期望输出

elf_package_cataloger_test.go 的Test_ELFPackageCataloger中,名为 "go case" 的用例直接使用elf-testdata夹具,其期望输出精确验证了聚合逻辑

  • 包 1:libhello_world.so@0.01,PURL 为pkg:generic/syftsys/libhello_world.so@0.01LocationSet 包含 3 个路径(nested 的bin/lib/libhello_world.so、sister 的lib/libhello_world.so、sister 的lib/libhello_world2.so)——即三个不同路径的同名/同元数据库被聚合为一个包;
  • 包 2:syfttestfixture@0.01,LocationSet 包含 3 个可执行文件路径,且每个 Location 都带PrimaryEvidenceAnnotation——三个二进制聚合为一个应用逻辑包。

同一测试文件中的 fedora / debian / wolfi 用例则展示了生产环境的真实形态:coreutils的 note 元数据(type: rpmArchitectureOSCPE)被映射为pkg:RpmPkg与带?distro=限定符的 PURL。

5.4 依赖关系的落点:交给最终关系任务

值得注意的设计细节:elfPackageCataloger.Catalog的返回值中relationships 恒为 nil。源码注释(elf_package_cataloger.go)解释了原因:二进制目录器会记录每个二进制导入的动态库,但包到包、包到文件的关系不在目录器内直接生成,而是由扫描管线末端的专门任务(见 syft/internal/task/relationship_tasks.go,以及 elf_package_cataloger.go 的注释)基于"每个二进制导入的动态库集合"统一创建。这保证了关系建模的全局一致性——README 中"Actual State"导入网络图所呈现的多对多关系,正是这一最终任务依据ldd式动态库导入信息(含libc.so.6libstdc++.so.6这类系统库)绘制出来的。

5.5 健壮性保障:elfutil 的节大小上限

目录器解析 ELF 时并非直接使用 Go 标准库debug/elf,而是通过 syft/internal/elfutil/elfutil.go 提供的受控 ELF 打开器。该包专门防御"压缩节声明了不合理的解压后大小"导致的 Go 内存爆炸问题(maxDeclaredSectionSize为 128MB/节,见 elfutil.go):对.note.package这类 syft 实际会读取的节做解压大小上限约束,超限时拒绝展开并返回ErrDeclaredSizeExceeded,宁可跳过该文件也不让整个扫描 OOM。这为.note.package解析链路提供了关键的安全边界。

六、如何复现与验证

6.1 构建测试镜像

在 elf-testdata 目录 下执行:

docker build -t syft/elf-testdata .

构建完成后,镜像内/usr/local/bin/elftests/即包含三个场景的完整产物(可执行文件、共享库、RPM 数据库与系统库)。

6.2 用 syft 扫描并核对 SBOM

# 扫描镜像 syft scan syft/elf-testdata -o syft-json # 扫描本地目录(等价) syft scan /usr/local/bin/elftests -o syft-json

对照测试期望(elf_package_cataloger_test.go)检查输出中的libhello_world.so@0.01syfttestfixture@0.01两个包,以及它们与glibclibstdc++之间的依赖关系。

6.3 用 objdump 独立校验注入段

objdump -s -j .note.package /usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so

比对十六进制输出与 makefile 中注入的 JSON,即可确认夹具元数据与目录器解析假设的一致性。

6.4 运行目录器测试

# 仅运行 elf-testdata 相关用例(默认使用已入库的 fixture) go test -v ./syft/pkg/cataloger/binary -run Test_ELFPackageCataloger

关于该目录器测试夹具的通用管理方式(make list/make download/make add-snippet,以及 snippets 与完整二进制的取舍),可进一步参考 binary 目录器 README。

七、总结

elf-testdata测试夹具是理解 syft ELF 二进制包建模的最佳入口。它以极小的构造成本(g++ + objcopy 注入 JSON)复现了真实发行版中.note.package段的形态,并围绕三个精心设计的场景——同名共享库、多位置聚合、嵌套库、符号链接、RPM 传递依赖——把 syft 目录器最复杂的行为边界(同名逻辑包聚合、包到包关系替换包到文件关系)具象化。配合 elf_package_cataloger.go 的两遍扫描聚合逻辑、elf_package.go 的 PURL/类型映射,以及 elf_package_cataloger_test.go 的期望输出,读者可以完整还原 syft 从"ELF 文件的元数据段"到"SBOM 中逻辑包与依赖关系"的整条数据链路。

【免费下载链接】syftCLI tool and library for generating a Software Bill of Materials from container images and filesystems项目地址: https://gitcode.com/GitHub_Trending/sy/syft

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 21:36:13

比特币与以太坊深度解析:从底层原理到链上实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:34:33

PHP竞拍商城源码解析:多用户挂售转卖与闪拍系统实现

简介:这是一套面向多用户挂售转卖、竞拍闪拍场景的商城系统完整源码,基于PHP后端与UNIAPP前端开发,覆盖后台商品挂单、竞拍场次设置、用户实时出价、提货与转售等核心流程。包内共有2002个文件,压缩包约142.12MB,其中p…

作者头像 李华
网站建设 2026/9/16 21:34:00

AI系统提示词泄露:三大高危场景与工程化防御

1. 这个标题不是Bug报告,而是一份隐性安全告警单“system_prompts_leaks”——乍看像一段代码片段、一个日志报错,或是某次调试时随手打下的临时变量名。但过去三个月里,我在三类不同场景中反复撞见它:一次是帮某教育SaaS客户做AI…

作者头像 李华
网站建设 2026/9/16 21:32:51

Docker镜像仓库选型与部署:从Docker Hub到Harbor完整指南

要说 Docker 用得久了,一定会遇到一个问题:镜像从哪来、推到哪去。如果只是本地开发,docker pull一下官方镜像还算舒服;可一旦你开始建设环境、对接测试、部署上线,没有自己的 docker 仓库,整套流程就会像没…

作者头像 李华
网站建设 2026/9/16 21:29:58

Excel SUM求和不生效?文本型数字与隐藏行排查指南

做数据的人,十有八九都经历过这种崩溃瞬间:表格里肉眼可见全是数字,SUM(A1:A10) 一回车,结果要么是 0,要么比实际少了一大截。更气人的是,你点进单元格里看,数字明明是数字,格式也改…

作者头像 李华
网站建设 2026/9/16 21:29:53

LIBERO-Plus:面向VLA模型的鲁棒性评测框架

1. 项目概述:为什么VLA模型需要LIBERO-Plus这样的鲁棒性评测框架最近在机器人感知与决策交叉领域,VLA(Vision-Language-Action)模型正从实验室走向真实场景——不是那种调好光照、固定背景、只跑预设轨迹的Demo环境,而…

作者头像 李华