包管理器与软件清单

多数 Linux 取证只读 auth.log 和文件时间戳,很少系统性地读包管理器的账本。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:dpkg、status、info/*.list、apt history.log、apt-mark、RPM、dnf.log、snap、flatpak、软件清单反推 难度:进阶 前置知识:Linux 目录结构、发行版家族差异、文件归属概念

一、概述

多数 Linux 取证只读 auth.log 和文件时间戳,很少系统性地读包管理器的账本。

但账本能回答一批其它工件回答不了的问题,而且是结构化答案:

  • 这台机器上装没装某个软件? 账本比「目录里有没有」可靠得多,原因见下。
  • 谁在什么时候装、卸、升级了它? Debian 系的 apt/history.log 把时间、操作者、包名、版本变更四要素记全了,格式还是结构化纯文本,比 auth.log 的自由文本好解析得多。
  • 这个文件是系统自带的,还是后来放上去的? 每个包都有完整文件路径清单,逐条比对就能定性。
  • 这个包是管理员有意装的,还是依赖自动带进来的? Auto-Installed 标记能回答,这决定了该包的出现是否说明管理员有明确意图。
  • 这台机器的用途是什么? 软件组合反推。

账本比「目录里有没有」可靠,靠的是三条机制:

  • 卸载不删干净文件。 apt remove / rpm -e 只改数据库,/etc 下的配置、缓存、日志、数据目录经常留下。所以「/etc/nginx 还在」推不出「nginx 还装着」;反过来「账本里没有 nginx」能推出「官方渠道没装 nginx」。
  • 安装会覆盖系统路径。 装包时同名文件会落到 /usr/bin 等位置,可能顶替原有文件。定性方法见 3.3 的全盘归属比对。
  • 账本本身是普通文件。 /var/lib/dpkg/status 与 /var/lib/rpm/ 随镜像被复制,不需要在原机器上跑任何命令。

账本在流程里的定位是分析环节的「定性工具」——它本身通常不产出新发现,而是为其它发现定性。

上游依赖发行版家族判定。Debian 系与 RHEL 系是两套完全不同的账本结构,判错则后面全错,判定方法见取证工件地图。

下游支撑四处,都很关键:为计划任务与持久化痕迹里发现的 SUID 文件判断「是包自带的还是人为放的」;为可疑二进制的来源定性;为时间线提供包级的时间锚点(装包时间可以作为校验其它时间戳是否合理的基准);为报告里的主机画像提供软件清单。

它的方法论价值超出 Linux:「查文件归属」这个动作在任何装了包管理器的系统上都成立,容器镜像、macOS 的 Homebrew、Windows 的程序清单都属同一思路。

账本的形态与位置:

形态 位置 关键结构
Debian 状态库 /var/lib/dpkg/status RFC822 风格段落,Status: 行的状态位有七种组合,其中「已卸载但配置残留」与「完全不存在」是两个状态
Debian 文件清单 /var/lib/dpkg/info/<pkg>.list 每个包安装的全部文件路径,逐行一条 —— 归属比对的依据
Debian 安装历史 /var/log/apt/history.log(及其归档 .gz) 分块结构,含 Start-Date / Commandline / Install: / Upgrade: / Remove:
安装者标记 /var/lib/apt/extended_states Auto-Installed 标记在此,不在 status 里 —— 读错文件会得到完全相反的结论
RHEL 账本 /var/lib/rpm/Packages(Berkeley DB)、/var/lib/rpm/rpmdb.sqlite(较新版本) 二进制数据库,需专用工具离线读取
RHEL 历史 /var/log/dnf.log、/var/log/yum.log 文本,含事务 ID 与动作列表
其它生态 snap(/var/lib/snapd)、Flatpak、容器镜像层 各自独立账本,不在 dpkg/rpm 体系内,不比对就会漏
校验工具 dpkg --verify / rpm -V 校验已安装文件的完整性与属主,是发现文件被改的手段

答得出来的是:完整软件清单及其版本、每个文件的包归属、装卸升级的时间与操作者、手动装与依赖装的区分、哪些文件不属于任何包(这是发现人为放置文件的关键)、以及通过软件组合反推的主机角色。

答不出来的:

  • 装过但已完全卸载的包。账本不留卸载记录的历史细节,history.log 只在启用 APT 日志的机器上有,未配置的主机上这个答案是「查不到」而非「没装过」。
  • 非包管理安装的文件的创建时间与操作者。归属比对只能说「不属于任何包」,要拿到时间与操作者,需要靠时间线与认证日志。
  • 装软件的人的意图。Auto-Installed: 1 说明是依赖带进来的,不说明为什么需要它。
  • 未被账本覆盖的生态。snap、Flatpak、手工编译安装的二进制、自行下载的脚本,都不在 dpkg/rpm 账本里,只查这两个库会系统性地漏。
  • 文件内容是否被改过。归属比对回答「这个路径该属于谁」,「内容是否与包提供的一致」是另一件事,需要完整性校验。

走这套方法需要能区分 Debian 系与 RHEL 系的基础差异,至少知道两者包管理器不同;熟悉 Linux 目录结构,理解 /usr、/etc、/var 的分工——归属比对本质上是路径与包的对应关系;会读 RFC822 风格的段落文本,能分辨 status 文件里七种状态位的含义;了解时间戳的基本语义,用于把安装时间与文件 mtime 做对照;具备批量检索能力,3.3 的全盘归属比对需要遍历整卷路径清单。

二、核心原理

2.1 两大家族的账本结构

对比项 Debian / Ubuntu RHEL / CentOS / Rocky / Alma
数据库文件 /var/lib/dpkg/status(纯文本) /var/lib/rpm/Packages(Berkeley DB 或 sqlite)★
每包文件清单 /var/lib/dpkg/info/<pkg>.list ★★ RPM 数据库同样含文件列表(需 rpm -ql 离线读或解析 DB)
包元数据 /var/lib/dpkg/info/<pkg>.md5sums、conffiles RPM header 内含
安装历史 ** /var/log/apt/history.log** ★★ /var/log/dnf.log、/var/log/yum.log
单次事务日志 /var/log/dpkg.log 同 dnf.log
手动/自动标记 /var/lib/apt/extended_states(Auto-Installed: 1)★ dnf history user installed
查询工具 dpkg -l、dpkg -S、apt list rpm -qa、rpm -qf
发行版标识 /etc/dpkg/origins/ /var/lib/rpm/repodata/

⚠️ RHEL 系的 /var/lib/rpm/Packages 是 Berkeley DB 或 SQLite 二进制,不是纯文本。不能 grep/cat 解析。要么用 rpm 工具在分析机上指向镜像(rpm --dbpath),要么用专门的解析器。这与 Debian 系的纯文本 status 有本质差异,是两系操作方式不同的根源。

⚠️ 判断数据库格式:file /var/lib/rpm/Packages。Berkeley DB 会显示 Berkeley DB (BTree, ...), sqlite 显示 SQLite 3.x database。两者解析器不同。

2.2 dpkg 的三层数据结构

dpkg 的账本是分层的,理解这个结构就理解了它的全部取证能力:

层 位置 内容
① 已安装状态 /var/lib/dpkg/status 纯文本,分段结构,每段一个包:包名、版本、架构、状态、依赖
② 每包文件清单 /var/lib/dpkg/info/<pkg>.list 该包安装的每一个文件路径,一行一个 ★★
③ 每包校验和 /var/lib/dpkg/info/<pkg>.md5sums 文件 → md5,用于完整性验证
③ 每包配置文件 /var/lib/dpkg/info/<pkg>.conffiles 该包的配置文件路径(卸载时保留的)
附加信息 /var/lib/dpkg/info/<pkg>.postinst 等 卸载时会执行的脚本(postrm 值得看)
Package: nginx
Status: install ok installed
Priority: optional
Section: web
Installed-Size: 643
Maintainer: Jose Parrella <bureado@debian.org>
Architecture: amd64
Version: 1.18.0-6ubuntu14.4
Depends: libc6, libpcre2-8-0, zlib1g
Description: small, powerful, scalable web server

关键字段:

字段 取证意义
Package 包名
Version 含发行版修订号(1.18.0-6ubuntu14.4),比"软件版本"更精确——同一上游版本的不同修订说明"打过安全补丁"
Status install ok installed = 已装;deinstall ok config-files = 只删了程序、留了配置 ★(强信号:有人在清理但保留配置)
Architecture amd64/arm64/all(all = 架构无关,如 bash)

★ deinstall ok config-files 是Linux 取证里的一个高质量信号:它意味着有人执行了 apt remove(不是 purge),明确保留了配置文件。这在"管理员正常卸载"和"攻击者清理工具但可能还想回来"之间需要结合其它证据判断。必须逐包检查是否有这个状态。** /var/lib/dpkg/info/*.list 的取证价值(最高)**:

列出某包安装的所有文件

cat /mnt/df/var/lib/dpkg/info/nginx.list

/etc/nginx/...

/usr/sbin/nginx

/usr/share/nginx/...

  • ★ 反查:这个文件属于哪个包? dpkg -S /usr/bin/sudo 离线等价为 grep -lx '/usr/bin/sudo' /var/lib/dpkg/info/*.list。
  • ★ 全盘文件比对:把所有 *.list 合并成一个"官方文件全集",然后凡是磁盘上存在、但不在这份全集里、且又不属于已知手动部署路径的文件,都是"非包管理安装"的文件——这是发现被直接写入磁盘的文件的系统化方法(第 01 篇案例中判定 /tmp/.kworkerd 属此类,用的就是这个方法)。
  • ★ 判断文件是否被顶替:若某文件的路径在某包清单里,但文件本身的 mtime/ctime 与包安装时间不符,说明文件被改过。

2.3 ★ /var/log/apt/history.log:最有价值的单一日志

这是本篇最需要讲透的一节。

apt(不只是 dpkg)会把每一次事务(install/upgrade/remove/purge)记录成一个结构化条目:

Start-Date: 2024-03-17 22:47:33 UTC
Install: netcat [1.10-41]:amd64 (1.10-41ubuntu1)
Install: netcat-openbsd [1.219-1ubuntu1]:amd64 (1.219-1ubuntu1 -> 1.219-1ubuntu1.1)
End-Date: 2024-03-17 22:47:35 UTC
字段 含义 取证价值
Start-Date / End-Date 事务起止(带时区,通常 UTC) 精确时间锚点
Commandline 触发命令(如 apt-get install -y netcat) 可含参数
Install: 安装/升级的包 + [版本],升级用 旧 → 新 ★ 版本变更一目了然
Remove: 卸载的包 + 版本 卸载了防护软件 = 信号
Purge: 彻底清除的包 抹除痕迹的强信号
Upgrade: 升级
Requested-By: 谁触发的($ 后面是用户名) ★ 操作者身份!
Error: 失败的事务 失败的尝试也有价值

为什么说它"最有价值":

  1. 四要素齐全——时间(Start-Date)、操作者(Requested-By)、包名、版本变更(旧 → 新)。单个日志文件同时回答"谁、何时、做了什么、改了哪个版本"。
  2. 结构化——比 auth.log 的自由文本好解析得多(: 分隔、旧 → 新 固定模式)。
  3. 带时区(通常 UTC)——时间线对齐友好(见第 08 篇)。
  4. 不依赖其它工件——即使 auth.log 被清、shell 历史被删,history.log 仍独立记着"装了 netcat"这个事实(除非也被删)。

两条读表时的警告:

  • Requested-By 的用户名只在该用户自己跑 apt 时出现。若 root 跑 apt,通常是 root。但 Commandline 里的细节(如 -y、--allow-downgrades)往往比用户名更能说明意图。
  • 系统自动更新(unattended-upgrades)也会写 history.log,条目里 Requested-By: <root>(或 _apt)且 Commandline: unattended-upgrade。必须把自动更新条目和人工操作区分开——否则会把系统自动打的补丁误判为"管理员装了什么"。判据是包名 + Requested-By + Commandline 三者。

多个文件配合:

文件 粒度 说明
/var/log/apt/history.log 每次事务一条 ★★ 本篇重点
/var/log/apt/history.log.*.gz 归档 logrotate 轮转,必须一并提取
/var/log/dpkg.log 每个包动作一行 粒度更细,含 unpack/install/configure/status 等阶段
/var/log/alternatives.log 软链切换 改了系统默认工具(如 editor)

⚠️ history.log 也被 logrotate 轮转,所以历史可能分散在 history.log、history.log.1.gz 等。必须合并后再分析(方法同第 02 篇的 3.2)。

2.4 apt-mark:区分"手动装的"与"依赖带进来的"

这解决一个常见误判:很多包是安装别的包时被依赖自动带进来的,不代表管理员"想要"它。

命令 含义
apt-mark showmanual **标记为"手动安装"**的包(管理员/脚本显式安装的)
apt-mark showauto **标记为"自动安装"**的包(作为依赖被带进来的)
apt-mark showhold 被 hold(锁定不升级)的包

⚠️ 关键事实:手动/自动标记不在 /var/lib/dpkg/status 里,而在 /var/lib/apt/extended_states。

  • /var/lib/dpkg/status 只有 Status: install ok installed 这类安装状态,没有"是否自动安装"的字段。分析时不要在 status 里找 Auto-Installed,找不到是正常的。
  • /var/lib/apt/extended_states 是 APT 自己的扩展数据库,只有自动安装的包才会被写进去,字段形如:
> Package: libssl3
> Architecture: amd64
> Auto-Installed: 1
>
  • 因此判定规则是:在 extended_states 里且 Auto-Installed: 1 → 自动安装(依赖带入);不在其中 → 手动安装。 注意该文件里也可能出现 Auto-Installed: 0(曾被标记为自动、后被 apt-mark manual 改回)。
  • 用 dpkg -i 直接装的包不会写 extended_states,APT 一律视为手动安装——这一条在取证中很有用:它意味着"绕过 apt 直接 dpkg 安装"会留下"看起来像手动安装"的痕迹。
  • apt-mark showauto/showmanual 读的就是这个文件(-f 可指定其他文件)。离线分析时直接解析 extended_states 即可,无需 apt-mark 参与。

取证价值:

观察 判读
netcat 在 showmanual 里 管理员/攻击者显式装的 → 强信号(系统默认不装 netcat)
某大量 Web 库在 showauto 里 是装 web 框架时依赖带进来的,不单独说明问题
某个防护工具在 showauto 且已被 remove 可能是被卸载时顺带移除的

本篇只讲痕迹识别,不涉及规避手法的实现。

2.5 RHEL 系:RPM 数据库与 dnf 历史

RHEL 系的结构和 Debian 系不同,操作方式也不同:

项目 位置/命令 注意事项
包数据库 /var/lib/rpm/Packages(Berkeley DB 或 SQLite) 二进制,不能 grep
查已装 rpm -qa(在线);离线用 rpm --dbpath <镜像>/var/lib/rpm -qa ⚠️ dbpath 指向镜像,不写(加 --justdb --nodeps 只读)
查某文件属哪个包 rpm -qf <path>(在线);离线同上
安装历史 ** /var/log/dnf.log**(或 yum 的 /var/log/yum.log) 是自由格式文本,不是结构化的 history
事务历史 dnf history list(在线) 离线解析难度较高
repo 元数据 /var/lib/rpm/repodata/

⚠️ RHEL 系没有 Debian 系那样"单文件结构化 history"。dnf.log/yum.log 是混合自由文本,需要 grep 关键字(Installed:、Installed:、Complete!、Error:)。这是两系在取证效率上的实质差距——Debian 系的 history.log 明显更适合作为证据。

2.6 其它包管理生态

生态 查询 位置 取证意义
snap snap list /var/lib/snapd/snaps/、/snap/*/ snap 包沙箱运行、自更新;常驻后台(snapd 服务)
flatpak flatpak list /var/lib/flatpak/、~/.local/share/flatpak/ 同上
pip pip list /usr/lib/python3/dist-packages/、~/.local/lib/ 含 ~/.local 的手动装包(pip install --user)
npm 全局 npm -g ls /usr/lib/node_modules/
docker docker ps -a /var/lib/docker/(overlay2 分层) 见"容器"提示
conda conda env list /opt/conda/

snap / flatpak 的特殊性:它们在系统包之外另起一套,所以** dpkg -l 看不到 snap 装的软件**。而且它们会自动更新(除非企业版策略禁用),意味着"某个 snap 的版本是某天的"这类推论不成立。这是一个容易导致时间线误判的点。

2.7 从软件清单反推主机功能

软件组合是主机用途的强证据,比任何单一文件都有说服力:

软件组合 推断的主机功能 佐证方向
nginx + php-fpm 网站服务器 /var/www、nginx 访问日志
docker/containerd + docker-compose 容器化应用主机 /var/lib/docker/overlay2
mysql-server/postgresql + php 数据库驱动的应用 数据库数据目录、连接日志
openssh-server + fail2ban + ufw 有防护的远程运维主机 见第 05 篇
auditd + rsyslog + 集中转发配置 合规审计环境(日志留存要求高) /etc/rsyslog.d/、logrotate 策略
多个 *-server + 无桌面环境 纯服务器(无 X11/图形栈) 包清单里无 xorg
tcpdump/wireshark/nmap 有抓包/扫描工具(安全人员,或攻击者) ★ 结合其他证据判断意图
telnet 存在明文远程登录(高危配置) 见第 05 篇
无 *-server 且只装了基础工具 容器基础镜像 / 最小化系统 /etc/os-release 交叉

反推的使用纪律:软件组合只能作为"这台机器具备某功能"的证据,不能作为"某行为已发生"的证据。装了 tcpdump ≠ 抓过包;装了 nmap ≠ 扫过别人。必须与日志/时间戳交叉才能定性行为。


三、操作步骤

3.1 第 1 步:确认家族与账本格式

家族判定

cat /mnt/df/etc/os-release | grep -E '^(NAME|VERSION_ID)=' ls -d /mnt/df/var/lib/dpkg /mnt/df/var/lib/rpm 2>/dev/null

RHEL 系:确认 RPM 后端格式(Berkeley DB vs sqlite)

file /mnt/df/var/lib/rpm/Packages

Berkeley DB (BTree, native byte-order, version 8, format 62) → Berkeley DB 后端

SQLite 3.x database ... → sqlite 后端(解析器不同!)

3.2 第 2 步:提取已安装软件清单

Debian 系(纯文本,可直接解析):

提取包名 + 版本 + 状态(从 status 文件)

awk '/^Package: /{p=$2} /^Status: /{s=$2" "$3" "$4} /^Version: /{v=$2} /^$/{if(p)print p"\ "v"\ "s; p=s=v=""} END{if(p)print p"\ "v"\ "s}'
/mnt/df/var/lib/dpkg/status | sort > /evidence/dpkg-installed.tsv

看总数与关键包

wc -l /evidence/dpkg-installed.tsv grep -iE 'nginx|openssh|netcat|nmap|tcpdump|docker|audit' /evidence/dpkg-installed.tsv

★ 只删了程序、保留了配置的状态(强信号)

awk '/^Package: /{p=$2} /^Status: /{s=$0} /^$/{if(s ~ /config-files|deinstall/)print p" "s; s=""}'
/mnt/df/var/lib/dpkg/status

RHEL 系(二进制,需 rpm 工具):

离线读镜像里的 RPM 数据库(--justdb --nodeps = 只读,不触发脚本)

rpm --dbpath /mnt/df/var/lib/rpm --justdb --nodeps -qa 2>/dev/null | head -40 rpm --dbpath /mnt/df/var/lib/rpm --justdb --nodeps -qf /usr/bin/bash 2>/dev/null

⚠️ 一定加 --justdb --nodeps,避免触发脚本 / 写入

3.3 第 3 步:★ 全盘文件归属比对(发现"非包管理安装"的文件)

这是包清单最高价值的用法。

1) 汇总"官方文件全集"(所有包安装的文件路径)

cat /mnt/df/var/lib/dpkg/info/*.list | sed 's#^./##' | sort -u > /evidence/official-files.txt wc -l /evidence/official-files.txt

2) 拿一个可疑文件,查它是否属于任何包

check_file() { grep -qx "$1" /evidence/official-files.txt && echo "属于某个包" || echo "★ 不属于任何包"; } check_file /usr/bin/sudo check_file /tmp/.kworkerd

3) 批量:找出 /usr/bin /usr/sbin 下不属于任何包的可执行文件

find /mnt/df/usr/bin /mnt/df/usr/sbin /mnt/df/bin /mnt/df/sbin -type f 2>/dev/null
| sed 's#^/mnt/df##' | sort > /tmp/on-disk.txt comm -23 /tmp/on-disk.txt