本文要点
- Linux 有三本「登录账本」:
utmp(当前登录)、wtmp(历史登录与重启)、btmp(失败登录),再加一本lastlog(每个用户最后一次登录)——但现代 systemd 发行版里它们的分工已经变了 - ⚠️ Ubuntu 22.04 上
w会告诉你0 users,哪怕你此刻正 SSH 登录着:sshd 只把失败登录写进 btmp,成功会话交给 systemd-logind——查当前登录要用loginctl list-sessions - 实测证据:
strings /usr/sbin/sshd里只有/var/log/btmp而没有 wtmp,PAM 栈里也没有pam_lastlog,所以w/who/lastlog全是空的 - 真正有料的是 btmp:这台机器 1.9MB 的 btmp 里躺着 4987 条失败登录,且全部来自同一个 IP 对 root 的密码爆破
lastb | awk 'NF>=10 {print $3}' | sort | uniq -c | sort -rn一条管道定位爆破源;NF>=10是为了滤掉末尾那行btmp begins ...(不过滤会把Mon当成 IP 统计)utmpdump把二进制记录摊成 8 个方括号字段;btmp 里失败记录的ut_type是[6](USER_PROCESS)——「失败」是靠文件本身表达的,不是靠类型字段- btmp 记录里的 PID 能和
/var/log/auth.log里的sshd[PID]一一对上,两边交叉验证才站得住脚
Linux 登录审计:从 w 与 last 到 lastb 爆破溯源
服务器被人扫了、被人试密码,第一手证据在哪?ps 只看得到此刻的进程,journalctl 里翻到的多半是自己服务的日志。Linux 其实专门有一套「登录账本」——utmp、wtmp、btmp、lastlog 四个文件加一组命令,记录谁在什么时候从哪里登录过、失败过。
麻烦的是:这套东西在 systemd 时代行为已经变了。很多人第一次查服务器登录记录时会撞上同一个困惑——w 明明是空的,可就有人正在连着。本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic)上把这套账本逐个翻了一遍,包括它「为什么会是空的」,以及从一堆失败记录里把爆破源揪出来的完整过程。
第一步:w 和 who 为什么是空的
先看最直观的两条命令,外加 systemd 时代真正的替代品:
w; echo "--- who -a:"; who -a; echo "--- loginctl list-sessions:"; loginctl list-sessions 21:43:05 up 5 days, 7:46, 0 users, load average: 0.11, 0.06, 0.01
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
--- who -a:
system boot 2026-09-09 13:56
LOGIN ttyS0 2026-09-09 13:57 719 id=tyS0
LOGIN tty1 2026-09-09 13:57 750 id=tty1
run-level 5 2026-09-09 13:57
--- loginctl list-sessions:
SESSION UID USER SEAT TTY
393 0 root
1 sessions listed.
注意这三段输出的矛盾:w 说 0 users,who -a 里只有开机时留下的 ttyS0/tty1 和 run-level 5 记录,可我此刻正是通过 SSH 登录着这台机器的——而 loginctl 老老实实列出了 1 个 root 会话(PID 393)。
矛盾的原因不在命令,而在写入方。utmp/wtmp 这些文件没人写了:Ubuntu 从 20.10 起把 SSH 会话的记录交给了 systemd-logind,who/w/lastlog 读的 utmp 体系就不再更新。PAM 栈里能直接看出这一点:
grep -n "pam_systemd\|pam_lastlog" /etc/pam.d/sshd /etc/pam.d/common-session/etc/pam.d/common-session:29:session optional pam_systemd.sosshd 自己的 PAM 配置里只有 pam_loginuid.so,pam_lastlog.so 完全不在——没有它,登录就不会往 utmp/lastlog 里写记录。更硬的证据在 sshd 二进制里:
strings /usr/sbin/sshd | grep -x "utmp\|wtmp\|btmp\|/var/log/btmp\|/var/log/wtmp"/var/log/btmp答案很清楚了:这个 sshd 里只编进了 btmp 的路径,没有 wtmp。所以「成功的登录不记录、失败的登录照记」不是配置问题,而是发行版编译时的选择。这也解释了后面为什么 btmp 里数据满满、wtmp 却几乎是空的。
结论:在 systemd 发行版上查「当前谁登录着」,who/w 已经不可靠,用 loginctl list-sessions;要看某个会话的细节(是否来自远程、会话类型)用 loginctl show-session <ID>。
第二步:last 与 wtmp——重启和运行级别历史
成功登录虽然不写了,wtmp 里还留着开机记录:
last -x -n 6runlevel (to lvl 5) 5.15.0-30-generi Wed Sep 9 13:57 still running
reboot system boot 5.15.0-30-generi Wed Sep 9 13:56 still running
wtmp begins Wed Sep 9 13:56:49 2026两行记录对应这台机器最近一次开机:reboot 是内核启动,runlevel (to lvl 5) 是进入图形/多用户运行级别。最后一行的 wtmp begins ... 是 last 自己加的时间范围说明——它总是指出文件里最早的一条记录。
两个细节值得留意:
- 日期列会被截断。
5.15.0-30-generi是内核版本被砍掉了最后一个字符,不是机器出了问题;last按固定列宽排版,内核版本串太长就会被截。 -x会带出非登录记录。不加-x时last只显示用户登录,加上它才会显示reboot、runlevel、shutdown这些系统级事件。想知道服务器什么时候重启过、是不是异常重启,last -x比翻 journal 快得多。
wtmp 里只有 2304 字节,时间戳停在 Sep 9——因为这台机器已经连续运行 5 天,期间没有任何成功登录被写进去。文件大小本身就是个信号:
ls -l /var/log/wtmp /var/log/btmp-rw-rw---- 1 root utmp 1915008 Sep 14 16:42 /var/log/btmp
-rw-rw-r-- 1 root utmp 2304 Sep 9 13:57 /var/log/wtmp同一个目录下,wtmp 2.3KB、btmp 1.9MB,差了 800 倍。哪本账被人写满了,一眼就能看出来。
第三步:lastb 与 btmp——失败登录才是真数据
lastb 读的就是 btmp,用法和 last 完全一样:
lastb -n 4; echo "--- 按来源 IP 聚合:"; lastb | awk 'NF>=10 {print $3}' | sort | uniq -c | sort -rn | head -3root ssh:notty 31.57.219.55 Mon Sep 14 16:42 - 16:42 (00:00)
root ssh:notty 31.57.219.55 Mon Sep 14 16:42 - 16:42 (00:00)
root ssh:notty 31.57.219.55 Mon Sep 14 16:42 - 16:42 (00:00)
root ssh:notty 31.57.219.55 Mon Sep 14 16:42 - 16:42 (00:00)
btmp begins Mon Sep 14 15:05:02 2026
--- 按来源 IP 聚合:
4987 31.57.219.55
这段输出信息量很大:
- 用户名全是
root:攻击者不做用户枚举,直接赌 root 的密码。 - 终端列是
ssh:notty:这是 SSH 密码认证失败的标准记法,表示「没分配到 tty 就失败了」。 - 时间密集到秒级:连续多条时间戳挤在同一分钟,是脚本化爆破而非人工尝试。
btmp begins Mon Sep 14 15:05:02 2026:这是文件里最早一条记录,说明爆破从 15:05 开始。
聚合命令里的 NF>=10 是关键,它把两行「杂质」滤掉了。不加过滤时 awk '{print $3}' 会把末尾的 btmp begins Mon Sep 14 15:05:02 2026(第三个字段是 Mon)和一行空记录也算进去,结果里就多出 1 Mon 和 1(空)两行噪音:
lastb | awk '{print $3}' | sort | uniq -c | sort -rn | head -4 4987 31.57.219.55
1 Mon
1正常记录行有 10 个字段(用户名、终端、来源、星期、月、日、起、止时间、时长),而 btmp begins ... 只有 7 个字段——用 NF>=10 卡一道就够了。
顺带一提:lastb | wc -l 会输出 4989,比真实的 4987 条多 2 行(一行空行 + 一行 btmp begins)。要计数就别用 wc -l,用聚合结果或者 lastb | awk 'NF>=10' | wc -l。
第四步:utmpdump 看记录的真实结构
上面那些命令都在替我们翻译一个二进制文件。想直接看原始记录,用 utmpdump:
utmpdump /var/log/btmp | tail -3; echo "--- wtmp 里的开机记录:"; utmpdump /var/log/wtmp | head -2[6] [115357] [ ] [root ] [ssh:notty ] [31.57.219.55 ] [31.57.219.55 ] [2026-09-14T16:42:50,000000+00:00]
[6] [115359] [ ] [root ] [ssh:notty ] [31.57.219.55 ] [31.57.219.55 ] [2026-09-14T16:42:51,000000+00:00]
[6] [115361] [ ] [root ] [ssh:notty ] [31.57.219.55 ] [31.57.219.55 ] [2026-09-14T16:42:53,000000+00:00]
--- wtmp 里的开机记录:
[2] [00000] [~~ ] [reboot ] [~ ] [5.15.0-30-generic ] [0.0.0.0 ] [2026-09-09T13:56:49,644592+00:00]
[5] [00719] [tyS0] [ ] [ttyS0 ] [ ] [0.0.0.0 ] [2026-09-09T13:57:14,316443+00:00]
Utmp dump of /var/log/btmp
Utmp dump of /var/log/wtmp
每一行是一个定长结构体,8 个方括号字段从左到右是:
| 字段 | 例子 | 含义 |
|---|---|---|
ut_type | [6] | 记录类型:1=RUN_LVL、2=BOOT_TIME、5=INIT_PROCESS、6=USER_PROCESS |
| PID | [115357] | 写这条记录的进程 PID(SSH 会话就是 sshd 的 PID) |
ut_id | [ ] | 终端标识,SSH 失败记录里是空的 |
| 用户名 | [root ] | 尝试登录的账号 |
| 设备名 | [ssh:notty ] | tty 名;SSH 未分配终端时固定为这个值 |
| 主机名 | [31.57.219.55 ] | 来源主机 |
| IP | [31.57.219.55 ] | 来源 IP,与上一列常常相同 |
| 时间戳 | [2026-09-14T16:42:50,000000+00:00] | 带微秒的 ISO 时间,比 lastb 默认显示的分钟精度高得多 |
两个反直觉的点:
- btmp 里失败记录的
ut_type是6(USER_PROCESS),不是什么「失败」类型。utmp 结构体里根本没有表示「密码错误」的类型值——一个文件叫 btmp,里面的记录就等于失败登录,语义由文件承载。 - 末两行
Utmp dump of ...的顺序。那是utmpdump打到标准错误上的标题,和记录混在一起输出时就会跑到最后(本文为了看清结构,把它一起留在截图里)。
想要完整的高精度时间,lastb -F 可以直接显示到秒:
lastb -F -n 2root ssh:notty 31.57.219.55 Mon Sep 14 16:42:53 2026 - Mon Sep 14 16:42:53 2026 (00:00)
root ssh:notty 31.57.219.55 Mon Sep 14 16:42:51 2026 - Mon Sep 14 16:42:51 2026 (00:00)第五步:和 auth.log 交叉验证
btmp 是「结论」,/var/log/auth.log 是「过程」。两边对得上,结论才算坐实:
grep "Failed password" /var/log/auth.log | tail -2Sep 14 16:42:51 MFY001832765701 sshd[115359]: Failed password for root from 31.57.219.55 port 59694 ssh2
Sep 14 16:42:53 MFY001832765701 sshd[115361]: Failed password for root from 31.57.219.55 port 59698 ssh2sshd[115359]、sshd[115361] 这两个 PID 和上面 btmp 记录里的 [115359]、[115361] 完全对应——同一个进程干的事,被记在了两个地方。auth.log 比 btmp 多了关键信息:源端口(59694、59698)和认证方式(ssh2),做溯源取证时比 btmp 更细。
那为什么这台机器会被盯着爆破?看看 sshd 的实际生效配置:
sshd -T | grep -iE "^(permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|logingracetime)"logingracetime 120
maxauthtries 6
permitrootlogin yes
passwordauthentication yes
pubkeyauthentication yespermitrootlogin yes + passwordauthentication yes 的组合意味着:root 可以直接用密码登录,且允许 6 次尝试、每次连接给 120 秒。这正是爆破脚本最喜欢的配置——目标明确(root)、无需枚举用户名、还能在一条连接里试 6 个密码。公网机器只要开着密码认证,btmp 迟早会被写满。
第六步:日常排查清单与加固
把这套账本用起来,实际就几条命令:
| 想知道什么 | 命令 | ||||
|---|---|---|---|---|---|
| 当前谁登录着 | loginctl list-sessions(w/who 在 systemd 发行版上可能为空) | ||||
| 会话细节(是否远程) | loginctl show-session <ID> | ||||
| 服务器重启历史 | last -x | ||||
| 谁在试密码 | lastb | ||||
| 爆破来源排行 | `lastb \ | awk 'NF>=10 {print $3}' \ | sort \ | uniq -c \ | sort -rn` |
| 原始记录结构 | utmpdump /var/log/btmp | ||||
| 某个用户最后登录 | lastlog -u <用户名> |
加固方向也很明确,按性价比排序:
- 关掉密码认证。
PasswordAuthentication no之后,btmp 上的爆破会直接失去意义——没有密码可猜。这是收益最大的一步,具体配置见 SSH 服务器安全加固:从密钥认证到 PerSourcePenalties。 - 限制 root 直登。
PermitRootLogin prohibit-password,日常用普通用户加sudo。 - 自动封禁。如果确实需要保留密码认证,用 Fail2ban 按 btmp/auth.log 的失败记录自动封 IP,见 Fail2ban:从 SSH 暴力破解封禁到通用服务防护。
- 定期看一眼 btmp 的体积。这篇文章里的机器就是典型的反面教材——
/etc/logrotate.conf里没有任何 btmp/wtmp 条目,也就是说这个文件不会被轮转,只会一直涨。1.9MB 只是 15:05 到 16:42 一个半小时的战果。
清理 btmp 时要保留文件本身,用 truncate -s 0 /var/log/btmp(或 : > /var/log/btmp),不要 rm —— utmp 系文件靠固定的属主和权限(这里是 root:utmp 0660)协作,截断能保住这些属性。如果想让系统持续记录并自动轮转,把 btmp/wtmp 补进 logrotate 配置,思路和 logrotate 日志轮转 里普通日志一样。
小结
Linux 的登录账本有四本,在现代 systemd 发行版上的实际可用性差别很大:
- utmp(
/var/run/utmp)→who/w:SSH 会话已不写入,基本作废,改用loginctl。 - wtmp(
/var/log/wtmp)→last:只剩开机/运行级别记录,last -x看重启历史仍然好用。 - btmp(
/var/log/btmp)→lastb:唯一还在稳定写入的账本,也是安全排查的主战场。 - lastlog:在这台机器上
lastlog -u root返回**Never logged in**——同样是 systemd 迁移的牺牲品。
所以「查登录记录」这件事的正确姿势是:当前会话问 logind(loginctl),历史重启问 wtmp(last -x),失败登录问 btmp(lastb),需要精确定位时再用 utmpdump 和 auth.log 交叉验证。对于一台暴露在公网的服务器,lastb 值得加进你的例行巡检清单。
评论 (0)
暂无评论,快来抢沙发吧!