PersistenceAnti-forensicsExecution
Linux Kernel Modules: lsmod, Taint Flags and Boot Config
Loaded and configured Linux kernel modules (/proc/modules, /sys/module, modules-load.d, modprobe.d, taint flags) for spotting rootkits and module persistence.
- Location
- /proc/modules
- Proves
- Which kernel modules are loaded or set to load at boot, and whether unsigned or out-of-tree code entered the kernel
- Timestamps
- Kernel log in seconds since boot (dmesg) or journal usec since Unix epoch (UTC); config file times
- Access
- root; kernel addresses in /proc/modules are zeroed for unprivileged readers
- Retention
- Loaded state and taint until reboot; config files until deleted; log lines per journal/syslog limits
- Collection
- UAC, Velociraptor, AVML, LiME, journalctl
Tools
Compare all tools- RAM ParserIn browser
- Volatility 3CLI · open source
- modinfoCLI · built into Linux
- VelociraptorPlatform · open source
- UACCLI · open source
What it is
Loadable kernel modules (LKMs, .ko files) extend the running kernel with drivers, file systems and security hooks. They run with full kernel privileges, so a malicious module can hide processes, files, network connections and itself from every userland tool. Modules are also a persistence mechanism: a module name in a boot configuration file loads on every start (MITRE ATT&CK T1547.006).
Two kinds of evidence matter: the live state (what is loaded now, and the kernel's taint flags) and the configuration on disk (what loads at boot and how modprobe behaves).
Where it lives
| Artifact | Path | Notes |
|---|---|---|
| Loaded module list | /proc/modules | lsmod only formats this file |
| Per-module sysfs | /sys/module/<name>/ | Also contains entries for built-in code with parameters, so it is not a 1:1 match with /proc/modules |
| Kernel taint | /proc/sys/kernel/tainted | Bitmask, sticky until reboot |
| Module lock | /proc/sys/kernel/modules_disabled | 1 means no loading or unloading until reboot |
| Module tree | /lib/modules/<release>/ (kernel/, updates/, extra/, modules.dep) | /usr/lib/modules on merged-/usr systems |
| Boot-time load lists | /etc/modules-load.d/*.conf, /run/modules-load.d/, /usr/local/lib/modules-load.d/, /usr/lib/modules-load.d/ | Read by systemd-modules-load.service |
| Legacy load list (Debian/Ubuntu) | /etc/modules | Linked in as /etc/modules-load.d/modules.conf |
| modprobe configuration | /etc/modprobe.d/, /run/modprobe.d/, /usr/local/lib/modprobe.d/, /lib/modprobe.d/ | install, options, blacklist, alias, softdep |
| depmod configuration | /etc/depmod.d/, /lib/depmod.d/ | override can prefer a module from another directory |
| Kernel messages | dmesg, journal (-k), /var/log/kern.log (Debian/Ubuntu), /var/log/messages (RHEL family) | Taint and signature warnings |
What it proves
- Which modules the kernel reports as loaded, their size, dependencies and state.
- Whether any proprietary, out-of-tree, unsigned or force-loaded module has been loaded since boot (taint flags), even if later unloaded.
- Which modules are configured to load at boot, and whether a
modprobeinstalldirective runs a command instead of (or besides) loading a module. - With auditd rules on
init_module/finit_module, who loaded which module and when. - Not proven: a clean
/proc/modulesdoes not prove a clean kernel. A rootkit can unlink itself from the module list; only memory analysis gives an independent view.
Key fields
/proc/modules columns (from the kernel source):
| Column | Meaning |
|---|---|
| name | Module name |
| size | Memory size in bytes |
| refcount / used by | Reference count and dependent modules (- if none) |
| state | Live, Loading or Unloading |
| address | Base address; zeros depending on kptr_restrict and privileges |
| taint flags | Trailing (POE)-style letters when the module tainted the kernel |
Taint bits worth knowing (Documentation/admin-guide/tainted-kernels): bit 0 P proprietary module, bit 1 F force-loaded, bit 3 R force-unloaded, bit 10 C staging driver, bit 12 O out-of-tree, bit 13 E unsigned, bit 15 K live patched. /sys/module/<name>/taint shows the letters per module.
modinfo on a .ko file shows filename, vermagic, srcversion, depends, license and, when signed, signer, sig_key and sig_hashalgo.
Kernel log messages when a module taints the kernel:
<mod>: loading out-of-tree module taints kernel.
<mod>: module verification failed: signature and/or required key missing - tainting kernel
<mod>: module license taints kernel.
Timestamps
dmesg prints seconds since boot; convert with the boot time, or read the same messages from the journal, which stores wall-clock time in microseconds since 1970-01-01 UTC. Configuration and .ko files have only file system times: package modules carry build-time mtimes, so a module with a recent ctime that no package owns stands out. auditd records use msg=audit(<epoch seconds>.<ms>:<serial>).
# offline: filter on the kernel transport (-k would imply the analysis host's current boot)
journalctl -D /mnt/evidence/var/log/journal _TRANSPORT=kernel -o short-iso | grep -iE 'taint|module verification'
Retention
The loaded list and taint flags live only in kernel memory and reset at reboot. Configuration files persist until deleted. Kernel messages survive in the journal or kern.log/messages within rotation limits; the ring buffer shown by dmesg is small and overwritten on busy systems.
Collection
Follow the order of volatility: capture memory first (AVML or LiME; LiME is itself an out-of-tree module and taints the kernel), then live state, then disk.
cat /proc/modules > modules.txt; ls -la /sys/module > sys_module.txt
cat /proc/sys/kernel/tainted; grep '(' /proc/modules # modules that set taint
dmesg | grep -i taint
tar -C /mnt/evidence -czf modconf.tgz etc/modules etc/modules-load.d etc/modprobe.d \
etc/depmod.d usr/lib/modules-load.d usr/lib/modprobe.d 2>/dev/null
UAC's live response collects lsmod, /sys/module listings with module parameters, the taint value, taint-related dmesg lines and tainting modules from /proc/modules. Velociraptor's Linux.Proc.Modules parses /proc/modules across a fleet.
Parsing
# decode the taint bitmask
t=$(cat tainted.txt); for b in $(seq 0 19); do [ $(( (t>>b)&1 )) -eq 1 ] && echo "bit $b set"; done
# module files changed after install (then check ownership with dpkg -S / rpm -qf)
find /mnt/evidence/lib/modules -name '*.ko*' -newer /mnt/evidence/etc/hostname -printf '%C+ %p\n'
# commands hidden in modprobe config, and every module set to load at boot
grep -rnE '^\s*(install|remove)\s' /mnt/evidence/etc/modprobe.d
grep -rhvE '^\s*([#;]|$)' /mnt/evidence/etc/modules-load.d /mnt/evidence/etc/modules 2>/dev/null
In memory, Volatility 3 offers linux.lsmod, linux.malware.check_modules (compares the module list with sysfs), linux.malware.hidden_modules (carves module structures missing from the list), linux.malware.modxview, linux.module_extract and linux.malware.check_syscall. The older names without malware. were deprecated shims with a 2026-06-07 removal date.
Investigator tips
- The out-of-tree and unsigned warnings are printed only for the first such module per boot, so later modules can load without a new message. Use
/proc/modulestaint letters and/sys/module/*/taintto find every culprit. - A module can be loaded with
insmodfrom any path, such as/tmpor/dev/shm; there may be no copy under/lib/modules. install <name> <command>in/etc/modprobe.druns a shell command whenever that module is requested, a quiet persistence and execution path.- A server with no third-party drivers that shows
OorEtaint needs an explanation; compare with DKMS packages and vendor agents. - Record
modules_disabledduring live response: when it is1, nothing can be loaded or unloaded until reboot, which also blocks LiME. - Add auditd rules for
init_module,finit_moduleanddelete_module; theKERN_MODULErecord names the module even when a custom loader is used. See auditd. - Userland tools cannot see a hiding LKM; when ld.so.preload checks and
/proclook clean but behaviour does not, rely on memory acquisition.