一、概述
多数 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: |
失败的事务 |
失败的尝试也有价值 |
为什么说它"最有价值":
- 四要素齐全——时间(Start-Date)、操作者(
Requested-By)、包名、版本变更(旧 → 新)。单个日志文件同时回答"谁、何时、做了什么、改了哪个版本"。
- 结构化——比
auth.log 的自由文本好解析得多(: 分隔、旧 → 新 固定模式)。
- 带时区(通常 UTC)——时间线对齐友好(见第 08 篇)。
- 不依赖其它工件——即使
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
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 /evidence/official-files.txt | head -40
↑ 这些是"磁盘上有、包清单里没有"的可执行文件 → 值得逐个看