2024 年 3 月 29 日,PostgreSQL 开发者 Andres Freund 在邮件列表里报告了一件怪事:Debian sid 上 sshd 登录变慢、CPU 异常。他顺着 perf 的调用栈查下去,发现问题不在 OpenSSH,而在 liblzma——XZ Utils 的压缩库。
一个压缩库出现在 sshd 的进程里,本身就不正常。继续查下去,是一段被藏了两年的后门。
这段后门后来被编号为 CVE-2024-3094。它没有利用任何内存漏洞,靠的是构建脚本和人与人之间的信任。
背景:后门是怎么进到发行版里的#
XZ Utils 长期由一位维护者业余维护。2021 年起,一个自称 Jia Tan 的贡献者开始高频提交,逐步获得提交权限,随后又拉来“帮忙”的账号施压,逼走了原维护者。这套操作持续了两年多,是典型的长期社工。
后门没有进 git 仓库,而是藏在发布用的 tarball 里。构建脚本 build-to-host.m4 在打包时被注入,只在特定的打包环境下生效——检测到 dpkg 或 rpmbuild 才展开。从 git clone 编译不会中招,这也是它躲过代码审查的原因。
用一句话概括手法:代码审查看的是 git,发行版打包用的是 tarball,两者在事件里第一次被证明可以不一致。
发现过程也带着偶然。如果这点延迟没被注意到,后门会随下一个发行版进入更多机器。性能问题有时就是安全问题的第一现场。
这类攻击有个共同点:它不需要攻破哪道技术防线,只需要一个人有耐心。代码审查、签名校验、双人复核,防的都是“不信任”,而不是“技术漏洞”。
漏洞原理:构建期注入,运行时挂钩#
点到为止。注入的代码在编译 liblzma 时改写了若干函数,利用 IFUNC 机制在动态链接阶段替换实现。当 sshd 因为 systemd 的通知机制间接加载 liblzma 后,这段代码就能在认证流程里检查客户端证书,让持有特定密钥的人绕过认证直接拿到 shell。
几个设计细节值得记住:
- 只对 deb/rpm 构建生效,源码构建不受影响;
- 通过环境变量和构建参数识别目标发行版;
- 后门代码经过混淆,静态扫描很难发现;
- 依赖
libsystemd → liblzma的传递链,属于“合法依赖被滥用”。
这条链的隐蔽性在于:sshd 并没有“链接 xz”,是 systemd 的通知库把它带进来的。只看 sshd 的直接依赖,什么都发现不了。
为什么选 IFUNC?因为它允许在加载时替换函数实现,不需要改调用方代码,也不留明显的符号引用。这类手法在恶意软件里不新鲜,出现在一个被广泛信任的压缩库里,才是真正的问题。
影响范围#
| 发行版 | 情况 | 备注 |
|---|---|---|
| Debian sid / testing | 曾进入 2024 年 3 月的窗口期 | 已回滚 |
| Fedora 40 / 41 预发布 | 曾进入 | 已回滚 |
| openSUSE Tumbleweed | 曾进入 | 已回滚 |
| Debian stable / Ubuntu LTS / RHEL | 使用 5.4.x | 未受影响 |
关键判断不是“我用不用 xz”,而是“我用的发行版在窗口期有没有装过 5.6.0 或 5.6.1”。具体的版本与时间窗请以各发行版公告为准。
真正被波及的是滚动发行版和预发布版本。用稳定版的企业环境基本安全,但这也提醒了一件事:CI 里跑 debian:sid 或 fedora:rawhide 的基础镜像,风险等级和生产环境不是一回事。
时间窗也要算清楚。后门在 5.6.0 发布后进入滚动发行版,到 3 月 29 日被发现,中间大约几周。这期间装过系统、更新过包、拉过基础镜像的机器,都在范围内。
检测#
# 1. 版本
xz --version
# 2. 包管理器视角
dpkg -l | grep -E 'liblzma|xz-utils' || rpm -qa | grep -E '^xz|liblzma'
# 3. sshd 是否间接链接了 liblzma
ldd /usr/sbin/sshd 2>/dev/null | grep -i lzma
# 4. 构建脚本残留(打过补丁的包通常没有)
grep -R "build-to-host" /usr/share/doc 2>/dev/null | head
第三条要谨慎解读。很多发行版上 ldd /usr/sbin/sshd 本来就会显示 liblzma(通过 libsystemd),它只能说明“具备被挂钩的条件”,不能证明中招。
真正的判断依据是包版本与发行版公告。第四条只是辅助:如果 /usr/share/doc 下残留了被篡改的构建脚本,值得进一步查。
补充两个容易漏的地方。基础镜像:2024 年 2 到 3 月构建的镜像如果基于 sid 或 testing,要单独核对。vendor 目录:有些项目把 liblzma 源码拷进仓库自己编,这条路径完全绕过了包管理器。
再加一条文件完整性检查:rpm -Va 或 debsums -c 能发现被替换的库文件。输出通常很长,先 grep liblzma 与 xz,再看其余部分。
修复与缓解#
- 降级到发行版提供的安全版本,通常是 5.4.x 或已回滚的包。不要自己从源码编 5.6.x。
- 如果机器在窗口期运行过受影响版本,按“可能失陷”处理:轮换 SSH 主机密钥与用户密钥、检查
authorized_keys、审计登录日志。 - 重建镜像。基于受影响基础镜像构建的镜像要重新构建,而不是在容器里打补丁。
- 检查构建链:CI 缓存的 tarball、vendor 目录、私有镜像仓库里的旧包。
轮换密钥要分轻重。主机密钥泄露的影响面最大,必须换;用户密钥是否换,取决于机器在窗口期有没有被外部登录过。别一刀切,也别因为怕麻烦就不换。
# Debian/Ubuntu:装回发行版指定版本并确认来源
sudo apt-get update
sudo apt-get install --allow-downgrades liblzma5=5.4.1-0.2 # 版本号以发行版为准
apt-cache policy liblzma5
# 确认无影响后再轮换主机密钥(会让客户端 known_hosts 失效)
sudo rm -f /etc/ssh/ssh_host_*
sudo ssh-keygen -A
sudo systemctl restart ssh
换主机密钥是有感的操作:所有客户端都会报 “REMOTE HOST IDENTIFICATION HAS CHANGED”。先想清楚是否真的需要换——只有确认机器在窗口期运行过受影响版本,才值得付这个代价。
验证#
xz --version与apt-cache policy一致,且等于发行版的安全版本。dpkg -V liblzma5或rpm -V xz-libs无输出,说明已安装文件未被改动。- 用一台外部机器 SSH 登录,确认新主机指纹符合预期,并同步给使用方。
- 复查窗口期内的登录日志,看有没有来自陌生来源的成功登录;有条件的做一次流量回溯。
- 确认 CI 流水线里不再出现从 tarball 直接安装的 xz。
结论#
- 这次攻击的关键不在技术,而在“人”:花两年时间换一个提交权限。
- tarball 与 git 不一致是盲区,构建产物要能追溯到源码。
- 自查只需三步:版本、来源、窗口期。
- 窗口期内运行过受影响版本的机器,按失陷处理,别只降级了事。
- 长期动作:可复现构建、依赖锁定、对关键依赖做人工复核。
完