< Back to site

Forensic Learning Lab

Understand Linux authentication logs at the binary level

What does a lastlog record look like?

Each UID has exactly one 292-byte record. Hover over the bytes to see what they mean.

UID 0 (root) - Last login from 185.220.101.34 at 2026-03-28 03:47:00
ll_time (4 bytes)
ll_line (32 bytes)
ll_host (256 bytes)
Hover over a byte to see its meaning

The same compromise, seen from 3 perspectives

An attacker brute-forces SSH, gets root, creates a backdoor. Each log source captures different aspects.

$ python3 LastLogAudit.py -f samples/compromised.lastlog Terminal From Last Login ------------------------------------------------------ pts/0 185.220.101.34 2026-03-28 03:47:00 <-- root from Tor exit node pts/1 10.0.1.101 2026-03-27 14:22:00 normal admin pts/2 10.0.1.102 2026-03-26 08:45:00 normal admin pts/0 10.0.1.103 2026-03-25 17:10:00 normal admin pts/3 10.0.1.50 2026-01-05 11:00:00 <-- stale (83 days) pts/4 45.153.160.140 2026-03-28 03:52:00 <-- dormant account reactivated pts/5 185.220.101.34 2026-03-28 04:01:00 <-- same IP as root login What you see: last login per account. No history, no failures. What you miss: the 9 failed attempts before root succeeded.
$ python3 LastLogAudit.py --wtmp samples/compromised.wtmp Username Terminal From Timestamp Type PID --------------------------------------------------------------------------- admin pts/1 10.0.1.101 2026-03-27 14:22:00 LOGIN 12001 pts/1 2026-03-27 17:45:00 LOGOUT 12001 dev pts/2 10.0.1.102 2026-03-27 08:45:00 LOGIN 12050 pts/2 2026-03-27 12:30:00 LOGOUT 12050 root pts/0 185.220.101.34 2026-03-28 03:47:00 LOGIN 31337 <-- NO LOGOUT svc_backup pts/4 45.153.160.140 2026-03-28 03:52:00 LOGIN 31338 pts/4 2026-03-28 03:58:00 LOGOUT 31338 implant pts/5 185.220.101.34 2026-03-28 04:01:00 LOGIN 31339 pts/5 2026-03-28 04:14:00 LOGOUT 31339 What you see: full session history with login AND logout. Key finding: root session (PID 31337) has no logout - still open. What you miss: failed attempts (wtmp only records successes).
$ python3 LastLogAudit.py --auth-log samples/compromised.auth.log Timestamp Event Username IP Method -------------------------------------------------------------------------- Mar 27 03:41:07 LOGIN_FAILED root 185.220.101.34 password Mar 27 03:41:09 LOGIN_FAILED root 185.220.101.34 password Mar 27 03:41:12 LOGIN_FAILED root 185.220.101.34 password Mar 27 03:41:15 LOGIN_FAILED root 185.220.101.34 password Mar 27 03:41:18 LOGIN_FAILED root 185.220.101.34 password Mar 27 03:42:33 LOGIN_FAILED admin 185.220.101.34 password Mar 27 03:42:36 LOGIN_FAILED admin 185.220.101.34 password Mar 27 03:43:01 LOGIN_FAILED deploy 185.220.101.34 password Mar 27 03:44:19 LOGIN_FAILED www-data 185.220.101.34 password Mar 27 03:47:12 LOGIN_SUCCESS root 185.220.101.34 password <-- COMPROMISED Mar 27 03:47:34 SUDO root - /usr/bin/cat /etc/shadow Mar 27 03:48:02 SUDO root - /usr/bin/useradd svc-backup Mar 27 04:13:08 SUDO svc-backup - /usr/bin/curl -o /tmp/.x ... Mar 27 04:15:18 SUDO svc-backup - crontab persistence What you see: EVERYTHING - failed attempts, successes, sudo commands. Key finding: 9 failed attempts before success = brute force confirmed. Key finding: post-exploitation chain visible (shadow, useradd, curl, crontab).

How the compromise unfolded

03:41:07 - 03:44:19
Brute force: 9 failed SSH attempts across 4 usernamesauth.log
03:47:12
Root login successful from 185.220.101.34 (Tor exit node)auth.loglastlogwtmp
03:47:34
cat /etc/shadow - credential harvestingauth.log
03:48:02
useradd svc-backup - backdoor account createdauth.log
03:52:44
svc-backup SSH login from 45.153.160.140 (second Tor node)auth.loglastlogwtmp
04:13:08
curl C2 beacon downloaded to /tmp/.xauth.log
04:15:18
Crontab persistence: */5 * * * * /tmp/.xauth.log
14:00:31
Data exfiltration: tar + base64 of /etc/shadow and /homeauth.log
14:00:22
Legitimate deploy login from 10.0.5.22 (unaware of compromise)auth.log

Notice how the colored tags show which log source captured each event. Only auth.log sees the full picture. lastlog and wtmp only capture the successful logins.

Test your analysis skills

A lastlog record for root shows timestamp=0 on an active production server. What is the FIRST thing you should do?
You see 6 accounts logged in from the same IP (194.26.29.113) within 13 minutes, with sequential terminal allocation (pts/1 through pts/5). What is the most likely scenario?
wtmp shows a root login (PID 31337) from an external IP but no corresponding DEAD_PROCESS (logout) record. What should you investigate?
An attacker with root access runs hidemylogs to wipe their IP from utmp, wtmp, and lastlog. What is the ONLY reliable defense?

Blue vs Red: What each side sees

Blue team sees

lastlog: last login per UID
wtmp: full session history
auth.log: all auth events including failures
correlate: cross-reference discrepancies

Red team can

hidemylogs wipe -a IP wipe IP from all log files
hidemylogs forge --uid 0 -t TIME fake lastlog
hidemylogs wipe -t 03:00-04:00 --and time-range wipe
All with preserved atime/mtime

Defense in depth

Remote syslog (rsyslog, syslog-ng)
File integrity monitoring (AIDE)
Immutable audit logs (auditd)
Network telemetry (NetFlow, DNS)

Cannot hide from

Logs already shipped to remote server
Network-level captures (PCAP)
EDR memory forensics
Cross-source correlation (--correlate)

How LastLog-Audit works internally

The tool parses 3 fundamentally different file formats. Here's how each one is read.

INPUT
Binary or text file on disk
DETECT
Format detection: --file | --wtmp | --auth-log
PARSE
struct.unpack (binary) or regex (text)
OUTPUT
table | line | csv | txt export
lastlog (292 bytes)
uint32 ll_time 4B
char[32] ll_line 32B
char[256] ll_host 256B
struct format: "I32s256s"
Indexed by UID: offset = UID * 292
timestamp=0 means never logged in
wtmp / utmp (384 bytes)
short ut_type 2B
pad 2B
int ut_pid 4B
char[32] ut_line 32B
char[4] ut_id 4B
char[32] ut_user 32B
char[256] ut_host 256B
... exit, session, tv, addr 52B
struct format: "hhi32s4s32s256shhiii4I20s"
type 7 = LOGIN, type 8 = LOGOUT
Sequential records (not indexed by UID)
auth.log (text/syslog)
regex sshd Accepted
regex sshd Failed
regex sudo COMMAND=
Parsed line-by-line with compiled regex
Handles "invalid user" variant
Extracts: timestamp, user, IP, method

Paste hex bytes from a lastlog record and see them decoded in real time.

When --correlate is used, the tool loads all 3 sources and cross-references them:

1. Collect external IPs from each source
Filter out RFC1918 (10.x, 192.168.x, 172.16-31.x)
Flag IPs appearing in 2+ sources (persistent attacker)
2. Compute failed/success ratio per IP from auth.log
3+ failures = brute force indicator
Failures + success from same IP = confirmed compromise
3. Detect open sessions from wtmp
Match LOGIN (type 7) against LOGOUT (type 8) by PID
Unmatched logins from external IPs = active attacker session
4. Flag suspicious sudo commands
Keywords: shadow, useradd, usermod, crontab, wget, curl
5. Generate risk assessment
CRITICAL: brute force succeeded OR open external sessions
HIGH: suspicious sudo commands detected
User SSH login | +---> PAM ---> syslog/rsyslog ---> /var/log/auth.log (text, all events) | +---> login(1) ---> /var/log/wtmp (binary, append LOGIN record) | (LOGOUT appended on session end) | +---> login(1) ---> /var/log/lastlog (binary, OVERWRITE record at UID offset) Key difference: auth.log = append-only text log (includes failures) wtmp = append-only binary log (successes only, with logout) lastlog = overwrite per UID (only most recent login survives)