Skip to content

wtmp, btmp and lastlog Forensics, wtmpdb and lastlog2

Linux login records in an investigation: struct utmp on x86_64 and aarch64, btmp failed logins, lastlog slots, wtmpdb and lastlog2 SQLite files, tampering.

Published on 5 min read

TL;DR. wtmp is the history of interactive sessions and reboots, btmp the failed logins, utmp the sessions open right now, and lastlog the last login of each UID. They are fixed-size binary records written outside syslog, so they survive when text logs are edited and contradict them when they are not. Read them with a parser that knows the record size of the source architecture, and on newer distributions look for their SQLite replacements, wtmp.db and lastlog2.db.

The files

FilePathReaderContent
wtmp/var/log/wtmp, wtmp.1lastLogins, logouts, boots, shutdowns
btmp/var/log/btmp, btmp.1lastbFailed logins (root-only, sensitive)
utmp/run/utmpwho, wCurrent sessions, cleared at boot
lastlog/var/log/lastloglastlogOne slot per UID
wtmpdb/var/lib/wtmpdb/wtmp.db, /var/log/wtmp.db (Debian 13)wtmpdb lastSQLite replacement for wtmp
lastlog2/var/lib/lastlog/lastlog2.dblastlog2SQLite replacement for lastlog

logrotate typically rotates wtmp and btmp monthly and keeps one old generation, so expect one to two months. lastlog keeps one record per UID indefinitely.

struct utmp: 384 bytes, or 400

wtmp, btmp and utmp share struct utmp:

FieldContent
ut_type1 RUN_LVL, 2 BOOT_TIME, 5 INIT_PROCESS, 6 LOGIN_PROCESS, 7 USER_PROCESS (login), 8 DEAD_PROCESS (logout)
ut_pidPID of the login process (sshd for SSH)
ut_lineTerminal, pts/1, tty1, 32 bytes
ut_userUser name, 32 bytes; empty on logout records
ut_hostRemote host or IP; kernel version on reboot records; 256 bytes
ut_tvSeconds and microseconds, UTC
ut_addr_v6Remote address

On x86_64 glibc the record is 384 bytes: the time fields are 32-bit for compatibility with 32-bit programs. On aarch64 they are 64-bit and the record is 400 bytes. last reads records with the layout of the machine it runs on, so an aarch64 wtmp read with last on an x86_64 workstation produces garbage or nothing. Run last on a matching architecture, or use a parser that detects the layout from the file size and the plausibility of the fields.

A logout is a DEAD_PROCESS record on the same ut_line with an empty user; last pairs them. A login with no matching logout shows as gone - no logout, or crash when a boot record follows.

lastlog: one slot per UID

/var/log/lastlog is an array of struct lastlog, indexed by UID: 292 bytes per slot on x86_64 (32-bit time, 32-byte line, 256-byte host), 296 on aarch64. The offset of UID 1001 is 1001 × 292. The file is sparse: a login by UID 100000 makes it look enormous while only a few blocks are used. Collect it with tar --sparse.

The lastlog value is overwritten at each login, so it only tells you the latest one, but it is written independently of wtmp. A lastlog time for which wtmp has no session is a strong signal that wtmp lost a record.

wtmpdb and lastlog2

The 32-bit time fields overflow in 2038, so newer distributions switch to SQLite:

  • wtmpdb: table wtmp(ID, Type, User, Login, Logout, TTY, RemoteHost, Service). Login and Logout are microseconds since the epoch; Type is 1 BOOT_TIME, 2 RUNLEVEL, 3 USER_PROCESS. One row per session, with the logout filled in when the session ends, and the PAM Service name (sshd, login) that the classic format never had.
  • lastlog2: table Lastlog2(Name, Time, TTY, RemoteHost, Service), with Time in seconds and one row per user name.

openSUSE Tumbleweed uses them by default. Debian 13 removed last, lastb and lastlog and puts the wtmpdb file at /var/log/wtmp.db; the packages must be installed separately, so an upgraded trixie host may have no login database at all. Collect the -wal file next to a database if one exists: recent rows may still be in it.

sqlite3 wtmp.db "SELECT User, TTY, RemoteHost, Service, datetime(Login/1000000,'unixepoch'), datetime(Logout/1000000,'unixepoch') FROM wtmp;"
sqlite3 lastlog2.db "SELECT Name, datetime(Time,'unixepoch'), TTY, RemoteHost, Service FROM Lastlog2;"

btmp: failed logins

btmp uses the same record layout. Each failed attempt is a record with the attempted user name, the source host and the time. It is a third, independent copy of the password guessing also found in auth.log and the journal, useful when those were cleaned. Treat it as sensitive: users sometimes type their password into the user name field.

What tampering looks like

  • Blanked records. Log cleaners overwrite a login record with zeros rather than deleting it, so the file size stays a multiple of the record size. last simply skips it. In utmpdump output it appears as a record with type 0, no user and a 1970 date.
  • Partial record at the end. A file size that is not a multiple of 384 (or 400) means a truncated write, or a file copied while being written.
  • Time going backwards between consecutive records.
  • Zero-byte wtmp or btmp with an old modification time on a server that is in use.
  • An interactive SSH session in auth.log, the journal and audit.log, but not in wtmp, while wtmp covers that period.

Remember the benign case: ssh host command, scp and sftp do not open a terminal and often leave no wtmp record. Compare interactive sessions only (a pts terminal in the logs).

Commands

TZ=UTC last -F -i -x -f wtmp        # full dates, IPs, reboots
TZ=UTC lastb -F -i -f btmp
utmpdump wtmp > wtmp.txt            # every record, including blanked ones
wtmpdb last -F -f wtmp.db
lastlog2 -d lastlog2.db

last prints in the analysis host's zone unless TZ=UTC is set.

In the browser

Linux Log Parser reads wtmp, btmp and utmp in both the 384-byte and 400-byte layouts, lastlog, wtmp.db and lastlog2.db; it reports blanked, partial and backwards records, pairs logins with logouts, merges them with sshd, logind and audit sessions, and flags interactive logins with no wtmp record. See the tampering guide and the wtmp cheat sheet.

Related articles

How to prove Linux logs were edited or deleted: journal seqnum gaps, lines missing from auth.log, blanked wtmp records, stopped daemons and clearing commands.
How the four Linux log families fit together in an investigation, what each one proves, where they disagree, and how to merge them into one timeline.
Reconstruct command lines from Linux audit.log: grouping records by event, EXECVE argument reassembly, hex-encoded arguments, auid and ses across sudo.