Package Manager Forensics on Linux: dpkg, APT, RPM and DNF
Use dpkg, APT, RPM and DNF logs and databases to date installs, verify file integrity and find binaries no package owns on a compromised Linux host.
Package managers keep a detailed, timestamped record of what changed on a system and a hash inventory of every file they installed. For an investigator that is two things at once: a change log that can date the arrival of tooling such as nmap, socat or a compiler, and an integrity baseline that can reveal a trojanised sshd or ps. This guide covers the Debian family (dpkg and APT) and the RHEL family (RPM, YUM and DNF), with notes on snap, Flatpak and pip.
Work on a mounted read-only copy of the evidence where possible (see acquiring Linux evidence). The paths below are shown under /mnt/evidence.
Debian and Ubuntu: dpkg and APT
/var/log/dpkg.log
dpkg.log is written by dpkg itself, so it records every package operation regardless of which frontend triggered it: apt, apt-get, unattended-upgrades, a GUI or a manual dpkg -i. Each line starts with a local-time timestamp without a time zone.
2026-09-14 10:22:31 startup archives unpack
2026-09-14 10:22:31 install socat:amd64 <none> 1.7.4.4-2
2026-09-14 10:22:31 status half-installed socat:amd64 1.7.4.4-2
2026-09-14 10:22:32 status unpacked socat:amd64 1.7.4.4-2
2026-09-14 10:22:32 configure socat:amd64 1.7.4.4-2 <none>
2026-09-14 10:22:32 status installed socat:amd64 1.7.4.4-2
The actions you care about are install, upgrade, remove and purge. The file is rotated by logrotate (typically monthly, keeping about a year), so collect dpkg.log.1 and the compressed dpkg.log.*.gz copies too.
zcat -f /mnt/evidence/var/log/dpkg.log* | grep -E ' (install|upgrade|remove|purge) ' | sort
/var/log/apt/history.log and term.log
APT adds context that dpkg lacks: the full command line and, when the command ran through sudo, the requesting user.
Start-Date: 2026-09-14 10:22:29
Commandline: apt-get install -y socat
Requested-By: deploy (1001)
Install: socat:amd64 (1.7.4.4-2)
End-Date: 2026-09-14 10:22:32
term.log in the same directory holds the terminal output of each transaction, including maintainer script messages and errors. Correlate Requested-By with sudo entries in auth.log or the journal, as described in Linux log forensics. A package that appears in dpkg.log but in no APT transaction was most likely installed with dpkg -i from a local .deb, which is worth asking about: where did that file come from?
/var/lib/dpkg: the package database
| Path | What it holds | Forensic use |
|---|---|---|
/var/lib/dpkg/status | Every known package, its state, version and conffile hashes | Current inventory, packages left in deinstall or half-configured states |
/var/lib/dpkg/info/<pkg>.list | Files installed by the package | File ownership without running dpkg |
/var/lib/dpkg/info/<pkg>.md5sums | MD5 of each non-conffile | Integrity baseline |
/var/lib/dpkg/info/<pkg>.postinst and friends | Maintainer scripts | Scripts run as root at install time; malicious .deb files abuse them |
/var/backups/dpkg.status.* | Daily backups of the status file | Earlier inventory states to diff |
The inode timestamps of .list files are a useful fallback when logs have been rotated away or deleted: a .list file is created when the package is unpacked. Multi-arch packages use names such as libc6:amd64.list.
Verifying files with dpkg --verify and debsums
dpkg --verify (short form -V) checks installed files against the stored MD5 sums and prints only failures. Its output imitates the RPM format, but only the digest test is implemented, so other positions show ?.
dpkg --root=/mnt/evidence --verify 2>/dev/null
# ??5?????? /usr/sbin/sshd
# ??5?????? c /etc/ssh/sshd_config
A c marks a conffile, which administrators are expected to edit. A 5 on a binary in /usr/sbin is a serious lead. debsums (a separate package) does the same job with friendlier output: debsums -c lists changed files and -a includes conffiles, and it also accepts --root for offline use.
Know the blind spots. Packages without an .md5sums file are not checked, files dpkg never installed are ignored entirely, and on merged-/usr systems dpkg -S /bin/ls may fail to match because the package records /usr/bin/ls or vice versa.
RHEL, Rocky, Alma and Fedora: RPM and DNF
The RPM database
RPM stores its database under /var/lib/rpm. RHEL 8 uses Berkeley DB files; RHEL 9 and current Fedora use SQLite (rpmdb.sqlite). Recent Fedora releases moved the database to /usr/lib/sysimage/rpm and leave /var/lib/rpm as a symlink, so check that the link resolves inside your mount rather than on the analysis host. Query an offline image with --root, ideally from an analysis system whose rpm version supports the same database backend.
rpm --root=/mnt/evidence -qa --last | head -30
rpm --root=/mnt/evidence -qi openssh-server | grep -E 'Install Date|Signature'
--last sorts by install time, which quickly surfaces packages added around the incident window.
rpm -Va output
rpm -Va verifies every installed file against size, digest, permissions, ownership and more. Each character position is one test:
| Flag | Test | Typical meaning when set |
|---|---|---|
S | File size differs | Content replaced |
M | Mode (permissions or file type) differs | Added SUID bit, changed permissions |
5 | Digest differs | Content changed |
D | Device major/minor mismatch | Rare; device nodes |
L | Symlink target differs | Redirected link |
U | Owner differs | chown |
G | Group differs | chgrp |
T | Modification time differs | Edited or timestomped file |
P | Capabilities differ | Added file capabilities |
A . means the test passed and ? means it could not be run (often a permissions issue). An attribute letter before the path (c config, d doc, g ghost, l license, r readme) gives context, and missing flags a deleted file.
S.5....T. /usr/bin/ps
.M....... /usr/bin/find
S.5....T. c /etc/ssh/sshd_config
S.5....T. on /usr/bin/ps is exactly what a replaced binary looks like; an M on find warrants a check for a newly set SUID bit, a classic privilege escalation backdoor covered in hunting Linux persistence. Filter out expected config noise with grep -v ' c /', but review it separately afterwards.
DNF and YUM logs and history
| Source | Distros | Notes |
|---|---|---|
/var/log/yum.log | RHEL/CentOS 7 | Lines like Sep 14 10:22:31 Installed: ... carry no year |
/var/log/dnf.log, dnf.rpm.log | RHEL 8/9, Fedora with DNF 4 | dnf.rpm.log lists each package action |
/var/lib/dnf/history.sqlite | DNF 4 | Transaction history database |
/var/log/dnf5.log | Fedora with DNF 5 | Newer log location and format |
On a live DNF 4 system, dnf history list and dnf history info <id> show each transaction with the user and command line. Offline, query the SQLite history directly:
sqlite3 -readonly /mnt/evidence/var/lib/dnf/history.sqlite \
"SELECT id, datetime(dt_begin,'unixepoch'), user_id, cmdline FROM trans ORDER BY id;"
Timestamps here are epoch seconds and therefore UTC, while dnf.log and yum.log use the host's local time. Normalise before you merge them into a super timeline.
Finding files no package owns
Attacker tools dropped into system paths rarely come with a package. Query ownership for each executable:
# Debian family
find /mnt/evidence/usr/bin /mnt/evidence/usr/sbin -type f | while read -r f; do
p="${f#/mnt/evidence}"
dpkg --root=/mnt/evidence -S "$p" >/dev/null 2>&1 || echo "UNOWNED $p"
done
# RHEL family
rpm --root=/mnt/evidence -qf /usr/sbin/sshd-helper
# file /usr/sbin/sshd-helper is not owned by any package
Expect legitimate hits: /usr/local, vendor agents, alternatives symlinks and files generated at install time. Extend the sweep to /usr/lib, /lib/modules, /etc/cron.*, /etc/systemd/system and library directories, where unowned shared objects may indicate an ld.so.preload implant.
Other package sources
- snap: packages live as squashfs images in
/var/lib/snapd/snaps/;snap changeson a live host and the snapd journal show install history. - Flatpak: system installs under
/var/lib/flatpak, per-user under~/.local/share/flatpak. - pip: each package has a
*.dist-infodirectory with aRECORDfile listing SHA-256 hashes of installed files and anINSTALLERfile. Packages under/usr/local/lib/python3*/on Debian are not dpkg-managed and will always show as unowned.
Limitations
Package databases are evidence, not ground truth. An attacker with root can rewrite .md5sums files, alter the RPM database, or install a trojanised package through the normal package manager so it verifies cleanly. Logs can be edited or truncated. For critical binaries, download the exact package version from a trusted mirror on a clean system, extract it (dpkg-deb -x or rpm2cpio | cpio -idm) and compare hashes directly. Also remember that verification only covers the file on disk: a binary that was swapped, executed and restored leaves no trace here, which is why memory and process evidence still matter.
Key takeaways
dpkg.logrecords every dpkg operation;apt/history.logadds the command line and sudo user. A gap between them points to a manualdpkg -i.dpkg --verify,debsums -candrpm -Vaquickly surface modified binaries; learn theS M 5 D L U G T Pflags.- Unowned files in system paths (
dpkg -S,rpm -qf) are strong leads for dropped tooling. - Mind time zones: dpkg, dnf and yum logs are local time; DNF history timestamps are epoch UTC;
yum.loghas no year. - A clean verification is weak evidence, because root can rewrite the database. Compare against trusted repository packages for anything critical.