暗色模式

Linux 文件完整性校验:从 sha256sum 到 dpkg -V 篡改排查

技术教程
2026-09-13
7
0
本文要点
  • 清单文件就是一列 哈希 + 两个空格 + 路径sha256sum -c 逐行比对;只要有一行对不上就返回 1,脚本里可以直接用它当判断条件
  • sha256sum -c 只能发现「清单里列过的文件被改了」,新增文件它一个字都不会提——要抓新增得再拿 comm 对比清单与现状
  • dpkg -V 的清单用的是不带前导斜杠的相对路径(bin/ls),所以 dpkg -S /usr/bin/ls 查不到,dpkg -S /bin/ls 才返回 coreutils
  • ??5?????? c /etc/cloud/cloud.cfg5 是 md5 与记录值不一致,c 表示这是个 conffile;本机全盘扫描只有这 1 条偏差
  • ⚠️ dpkg -V 明明报了偏差,退出码依然是 0——判断结果必须解析输出,不能看退出码

Linux 文件完整性校验:从 sha256sum 到 dpkg -V 篡改排查

「这个文件还是原来那个吗?」——下载的安装包有没有传坏、/usr/bin 下的二进制有没有被替换、上周的配置被人动过没有、备份和解压出来的东西是否一致。这些问题指向同一件事:你能不能算出一个文件的指纹,并和一份可信的记录对上

Linux 自带了两层答案:一层是你自己拉的哈希清单sha256sum -c),另一层是发行版早就替你维护好的包文件清单dpkg -V)。后者尤其被低估——它把每个包安装的文件的 md5 都记在了本地,一条命令就能扫出「系统文件里哪些和出厂状态不一样」。

本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic)上把两层都跑了一遍:先用 sha256sum -c 检出一次改动,再实际对比 dpkg -V 用的清单和磁盘上的哈希,最后自己拉基线清单,把「改动 / 删除 / 新增」三类偏差分别触发一次。文中哈希值、耗时、退出码均为真实执行结果。

第一步:清单就是一列哈希,-c 就是逐行比对

先造一个文件、给它建立清单,然后把文件改掉,看校验怎么报:

cd /tmp/intdemo && rm -f app.bin manifest.sha256 && echo "v1: 正常内容" > app.bin && sha256sum app.bin > manifest.sha256 && echo "v2: 被改过" > app.bin && sha256sum -c manifest.sha256 2>&1; echo "校验退出码=$?"

sha256sum 生成清单后文件被改,校验输出 app.bin: FAILED 与 WARNING,退出码为 1

三行输出各有分工:

  • app.bin: FAILED 指向具体哪个文件对不上(清单里几十个文件时,这行就是你的定位入口);
  • sha256sum: WARNING: 1 computed checksum did NOT match 是汇总,大清单里能一眼看出「坏了几个」;
  • 校验退出码=1 是给脚本用的——有任何一行不匹配就返回 1,全部通过才返回 0,所以 sha256sum -c manifest.sha256 || echo "数据损坏" 这种写法是成立的。

几个配套开关在实际脚本里很有用:

sha256sum -c --status manifest.sha256; echo "status-mode exit=$?"
status-mode exit=1
sha256sum -c --quiet manifest.sha256 2>&1; echo "quiet exit=$?"
app.bin: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
quiet exit=1
sha256sum -c --ignore-missing manifest.sha256 2>&1; echo "ignore-missing exit=$?"
app.bin: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
sha256sum: manifest.sha256: no file was verified
ignore-missing exit=1

三个开关的分工:--status 一个字符都不打印,只给退出码,最适合放进 cron 用退出码判断;--quiet 只去掉 OK 那些行,失败行和汇总警告照旧(所以上图里它比 --status 啰嗦);--ignore-missing 跳过「清单里列了、磁盘上没有」的文件,它的真正用途是「只校验清单里存在的那些」——

echo "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef  ghost.txt" >> manifest.sha256
sha256sum -c --ignore-missing manifest.sha256 2>&1; echo "带 ignore-missing exit=$?"
sha256sum -c manifest.sha256 2>&1 | grep ghost; echo "不带 ignore-missing: ghost.txt 被单独报出来"
app.bin: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
sha256sum: manifest.sha256: no file was verified
带 ignore-missing exit=1
sha256sum: ghost.txt: No such file or directory
ghost.txt: FAILED open or read
不带 ignore-missing: ghost.txt 被单独报出来

对照很直观:带了 --ignore-missing,磁盘上不存在的 ghost.txt 被静默跳过;不带,它会明确报 No such file or directory。另外注意那行 no file was verified——它是「本次没有任何文件通过校验」的提示(本机 coreutils 8.32 的行为),不是语法错误,别被它吓到:只要有一个文件校验通过,这行就不会出现。

清单文件本身的格式简单到可以手写,就是 <哈希><两个空格><路径>

sha256sum --tag app.bin
cat manifest.sha256
SHA256 (app.bin) = 805742421edb525cb121d0525f79a9e8262e5f1f64759f4ac83b55c35de2a179
a8c067d7f9a8f0350cef35321e99e3be98fdb8148811217664c3af747189b62a  app.bin

(这里两个哈希不同是正常的:manifest.sha256 是文件被改之前生成的,记录的是原始内容 v1: 正常内容;而 --tag 算的是此刻磁盘上那份已经被改成 v2: 被改过 的内容。)

--tag 输出的是 BSD 风格(SHA256 (文件名) = 哈希),方便贴进邮件或发布页;而默认的清单格式是第二行那种。中间是两个空格——这是 GNU coreutils 的约定,-c 解析时靠它区分哈希和文件名,手写清单时少一个空格就会报格式错误。

第二步:sha256、md5、cksum 到底差在哪

同一份 100MB 文件,三个工具各跑一遍(文件是 /dev/zero 生成的,页缓存已热,1 核机器):

dd if=/dev/zero of=big.bin bs=1M count=100 && time sha256sum big.bin; time md5sum big.bin; time cksum big.bin
20492a4d0d84f8beb1767f6616229f85d44c2827b64bdbfb260ee12fa1109e0e  big.bin

real    0m0.550s
2f282b84e7e608d5852449ed940bfc51  big.bin

real    0m0.197s
2755649025 104857600 big.bin

real    0m0.320s
工具算法100MB 耗时输出长度用途
md5sumMD50.197s32 位十六进制最快,但已被证明可构造碰撞,不能用于防篡改
sha256sumSHA-2560.550s64 位十六进制默认选择:慢 2.8 倍,但抗碰撞
cksumCRC320.320s数字 + 字节数只防「传输/存储出错」,不防「有人故意改」

选择逻辑很直接:防意外用谁都行,防人必须用 SHA-256 及以上(这台机器上 b2sum 也在,需要更强的抗碰撞可以选 BLAKE2)。cksum 的输出 2755649025 104857600 里第二个数字是字节数,顺带能发现「文件被截断」这类问题,但它没有抗篡改能力——CRC32 构造一个同校验和的不同文件是秒级的事。

顺带一句:哈希只能证明「内容没变」,证明不了「来源是谁」。要确认文件确实来自官方发布者,需要签名而不是校验和,那部分见 GPG 加密与签名:从密钥生成到文件加密与验签openssl 命令行:从密钥生成到自签证书与文件加密

第三步:dpkg -V —— 发行版替你存好的清单

dpkg -V 能扫出「已安装包的文件和出厂记录不一致」的那些文件,原理和我们手工做的完全一样,只不过清单是 dpkg 早就存好的。先手工把这条链路走通,看看它到底在比什么:

cd /tmp/intdemo && grep 'bin/ls$' /var/lib/dpkg/info/coreutils.md5sums && md5sum /usr/bin/ls && { time dpkg -V; } 2>&1; echo "全盘扫描退出码=$?"

dpkg 清单里 bin/ls 的 md5 与磁盘上 /usr/bin/ls 的 md5 完全相同;全盘 dpkg -V 只报出一条 conffile 偏差,耗时 4.5 秒,退出码仍为 0

这张图里有四个值得逐个拆开的点:

1. 清单路径没有前导斜杠。 coreutils.md5sums 里写的是 bin/ls,不是 /usr/bin/ls——路径相对于根目录。这解释了两个日常疑惑:为什么 dpkg -S /usr/bin/ls 会回一句 no path found matching pattern,而 dpkg -S /bin/ls 才能返回 coreutils: /bin/ls(本机 /bin 是指向 /usr/bin 的软链,dpkg 数据库里记的是 /bin/ls)。你自己 grep 这个清单文件时也得用 bin/ls 这种相对写法。

2. 两行哈希一模一样95f5b7f398fe64a91da476b136356981。前者来自 dpkg 的清单、后者来自磁盘上的真实文件,相等就意味着这个文件是干净的。dpkg -V 干的就是把这条比对重复几十万次(本机 /var/lib/dpkg/info/ 下有 597 份 .md5sums 清单,光 /etc 下就有 700 个文件)。

3. ??5?????? c /etc/cloud/cloud.cfg 怎么读。 前面 9 个字符是各属性位(. 表示该项没问题),后面跟一个类型标记和文件名:

片段含义
5该文件的 md5 与记录值不一致——内容被改过
?该项没有可比对的数据(dpkg 库里没存这个属性)
c这是个 conffile(配置文件),dpkg 输出的是 conffile 名而不是完整路径

这条偏差是真实存在的,而且能一路追到底:dpkg 对这个文件的期望值是 6839ffc91cc363eb1b3ac45a78a8fdb5,来自 dpkg 状态库里的 Conffiles 字段(dpkg-query -W -f='${Conffiles}\n' cloud-init);而磁盘上现在是 cf59085953fed471029f8590e3f7fee6conffile 被改是正常现象(这台机器的 cloud.cfg 是镜像构建时改写的),dpkg -V 报出来不是「出事了」,而是「这里和出厂值不一样,你自己确认下」。注意 cloud-init.md5sums 里压根没有 cloud.cfg——conffile 的期望值存在状态库里,不在 md5sums 清单里,两条记录渠道是分开的。

4. ⚠️ 退出码是 0 明明报了一条偏差,dpkg -V 依然返回 0。这一点非常容易踩:dpkg -V || echo "有问题" 这种写法永远不会触发。要判断结果只能解析输出,比如 dpkg -V | grep -q .,或者用 --verify-format 指定输出格式后交给脚本处理(dpkg 手册明确说了默认是 rpm 格式、且「将来可能变」,解析前最好显式指定)。

耗时方面:这台 1 核机器全盘扫描,页缓存冷的时候 10.3s ~ 12.0s,热缓存 4.5s(就是截图里那次)。单个包快得多——dpkg -V coreutils 无输出、立刻返回 0。所以日常巡检可以全盘跑,放到 cron 里也扛得住。

dpkg 手册里对这件事有一句关键的自述:

Currently the only functional check performed is an md5sum verification of the file contents against the stored value in the files database.

也就是说 dpkg -V 只比对内容(md5),不比对权限、属主、时间戳。文件被 chmod 777 它不会吭声,要发现权限变化得另外查(dpkg --audit 是查「数据库里元数据缺失/不一致」的另一条路,和内容校验互补)。

第四步:自己拉一份基线清单

dpkg -V 只管包里的文件,你自己的代码、配置、数据不在它的覆盖范围里,得自己拉基线。流程是「先记一份,之后比」:

cd /tmp/intdemo && rm -rf base && mkdir base && cd base && for f in a b c; do echo "content-$f" > $f.txt; done && find . -type f -exec sha256sum {} + | sort -k2 > /tmp/baseline.sha256 && cat /tmp/baseline.sha256
1cda6c6688e7f317a0990cf0fa94f3ef9fe40e5a21da7516a56df8da075d9f0d  ./a.txt
cc90fe98e8f6c86e99f645575d46762723b0a12b5e5653903078b26149a67e04  ./b.txt
d115ed9b6d4d22c529cf890335ae9a562b4bd0c4cf00e44eb3a2460ac9f4dc0b  ./c.txt

清单落到 /tmp/baseline.sha256——故意放在被扫描目录之外。然后制造三类变化:改一个(a.txt 追加一行)、删一个(b.txt)、加一个(d.txt),再校验:

cd /tmp/intdemo/base && sha256sum -c /tmp/baseline.sha256 2>&1; echo "校验退出码=$?"; echo "清单外新增的文件:"; comm -13 <(awk '{print $2}' /tmp/baseline.sha256 | sort) <(find . -type f | sort)

基线校验:a.txt 因内容变化 FAILED,b.txt 因文件不存在报 FAILED open or read,c.txt 通过;退出码 1;再用 comm 找出清单外新增的 d.txt

三类偏差,sha256sum -c 只报得出前两类:

变化sha256sum -c 的反应
内容被改./a.txt: FAILED
文件被删sha256sum: ./b.txt: No such file or directory + ./b.txt: FAILED open or read
新增文件完全不提

第三行是最容易漏的:-c 是「拿着清单逐条查」,清单里没有的文件它根本不知道要管。「系统里多了个不该有的文件」这类问题,必须用清单和现状做差集——就是命令最后那句 comm -13(只输出「现状有、清单没有」的行),它抓出了 ./d.txt

两个实操细节:

  • 清单别放进被扫描的目录。我第一版把 baseline.sha256 生成在被扫目录里,结果 find 把清单自己也算了进去;而生成清单的那一刻它还是空文件,于是它记录的是空文件的哈希(e3b0c442...),之后每次校验都会稳定地报一条 ./baseline.sha256: FAILED——一个自己制造的假警报。要么放目录外,要么用 ! -name '*.sha256' 排除。
  • 规模不是问题/etc 下也就 700 个文件;前面测过 SHA-256 处理 100MB 只要 0.55s,几千个配置文件的目录是毫秒级到秒级的事,放进每天一次的 cron 完全没有压力。想让基线本身也被保护,可以给清单再加一层签名(GPG 签一份清单,比给每个文件签名现实得多)。

第五步:这套方法的边界

用之前先认清三件事,能避免把「校验通过」误当成「安全」:

1. 防不住已经拿到 root 的人。 哈希清单是普通文件,dpkg 的清单也在 /var/lib/dpkg/info/ 下——能改文件的人同样能改清单和 md5sums。校验和只能发现「意外损坏」和「没清理干净的改动」,对主动攻击者只是提高了成本。要往这个方向走,清单必须离线保管或做签名,这就是 AIDE、Tripwire 这类工具在做的事,也是 Linux 安全审计:从 auditd 规则监控到 ausearch 操作溯源 那套「记录谁动了什么」的互补面:一个查「变了没有」,一个查「谁变的」。

2. 覆盖范围要心里有数。 dpkg -V 只覆盖包安装的文件;/etc 下 conffile 的改动会正常报出来(这台机器就是 cloud.cfg);而 /root/var/www、自己编译安装到 /usr/local 的东西、Docker 容器里的文件系统,统统不在覆盖范围。这些恰恰是业务真正跑的地方,必须自己拉基线。

3. 校验和解决不了「来源是否可信」。 从官网页面复制来的哈希,和文件走的是同一条网络路径——如果那条路径被劫持,两个一起被换掉。涉及安装包时,优先用发行版的签名机制(apt 自己会验 GPG 签名,见 Linux 软件包管理:从 apt 软件源到 dpkg 包管理),下载类工具也尽量走 HTTPS 并核对官方公布的摘要,这部分细节见 curl 命令行:从文件下载到 HTTP 接口调试wget 命令行:从文件下载到递归镜像与断点续传

至于「文件是什么时候变的」,哈希不记录时间,得靠文件系统自己的时间戳——mtime 可以被随意伪造,但 ctime 改不了,这套判断逻辑见 Linux 文件时间戳:atime、mtime、ctime 与 find 时间筛选。归档和备份场景里还有一层:rsync 靠大小和时间判断要不要重传,tar 解包可能保留原 mtime,所以跨机器搬运之后再校验一遍哈希是稳妥的收尾动作,可参考 rsync 数据同步与备份:从增量原理到定时快照Linux tar 打包与压缩:从归档原理到增量备份

小结

把三层工具按「谁维护清单」排一下,选型就清楚了:

  • sha256sum -c:你自己维护清单,灵活、能覆盖任何文件,代价是清单要自己生成和保管(记住放目录外、记得用 comm 补上「新增文件」这一课);
  • dpkg -V:清单是发行版替你存的,零成本上手,一条命令扫全系统,但只覆盖包内文件、只比内容、而且退出码不可信(报错也返回 0);
  • dpkg -S / dpkg -L:定位「这个文件属于哪个包」「这个包装了什么」,是排查偏差时的配套动作(注意路径写法和 dpkg 库里一致,别带 /usr 就直接查)。

最省事的日常组合是这样:每天 cron 里跑一次 dpkg -V | grep .(有输出就说明有偏差,比看退出码靠谱),再对自己的业务目录跑一次 sha256sum -c 基线校验,两份结果都非空时才值得人工介入。这样一台 1 核小机器几秒钟就能跑完,比出事之后再回头猜要划算得多。

发表评论

暂无评论,快来抢沙发吧!