暗色模式

Linux 登录审计:从 w 与 last 到 lastb 爆破溯源

技术教程
2026-09-25
2
0
本文要点
  • 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 只有开机的 tty 记录,而 loginctl 明确列出 1 个 root 会话

注意这三段输出的矛盾: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.so

sshd 自己的 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 6
runlevel (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 -3
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)
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

lastb 列出同一 IP 对 root 的密集失败登录,管道聚合出 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

utmpdump 把 btmp 与 wtmp 的二进制记录摊成 8 个方括号字段,含类型、PID、用户、来源与微秒时间戳

每一行是一个定长结构体,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 默认显示的分钟精度高得多

两个反直觉的点:

  1. btmp 里失败记录的 ut_type 是 6(USER_PROCESS),不是什么「失败」类型。utmp 结构体里根本没有表示「密码错误」的类型值——一个文件叫 btmp,里面的记录就等于失败登录,语义由文件承载。
  2. 末两行 Utmp dump of ... 的顺序。那是 utmpdump 打到标准错误上的标题,和记录混在一起输出时就会跑到最后(本文为了看清结构,把它一起留在截图里)。

想要完整的高精度时间,lastb -F 可以直接显示到秒:

lastb -F -n 2
root     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 -2
Sep 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 ssh2

sshd[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 yes

permitrootlogin 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 <用户名>

加固方向也很明确,按性价比排序:

  1. 关掉密码认证。PasswordAuthentication no 之后,btmp 上的爆破会直接失去意义——没有密码可猜。这是收益最大的一步,具体配置见 SSH 服务器安全加固:从密钥认证到 PerSourcePenalties。
  2. 限制 root 直登。PermitRootLogin prohibit-password,日常用普通用户加 sudo。
  3. 自动封禁。如果确实需要保留密码认证,用 Fail2ban 按 btmp/auth.log 的失败记录自动封 IP,见 Fail2ban:从 SSH 暴力破解封禁到通用服务防护。
  4. 定期看一眼 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 值得加进你的例行巡检清单。

发表评论

暂无评论,快来抢沙发吧!