关键词:auth.log、rsyslog、journald、journalctl、logrotate、时间戳格式、日志轮转
难度:进阶
前置知识:Linux 目录结构、发行版家族差异、文本日志分析基础
一、概述
Linux 日志分析真正的困难只有一个:你要找的那份日志,可能不叫那个名字,也可能不在那个目录,甚至可能已经不是文本了。
落到磁盘上是这么几件事。
/var/log/auth.log 在 CentOS 上不存在。/var/log/secure 在 Ubuntu 上不存在。路径是发行版的家庭作业,不是操作系统的常量。
systemd 的 journal 是自研二进制容器。grep 和 strings 上去只能得到不完整碎片,字段边界全丢。
logrotate 会把昨天的日志改名并压缩过去。你要找的那个时间窗口,大概率躺在 .gz 里,不在当前文件里。
三者叠在一起,后果很具体:你可能完整地报告了一份「没有记录」的日志,而记录就在归档里躺着。
这层证据的位置是分析环节的主机侧证据入口,属于本 wiki 里「计算机取证」大板块的起点之一,往下同时喂两个环节。
时间线合并。日志给出的是「什么时间、什么身份、什么动作」,但传统 syslog 格式没有年份,跨源对齐得单独做一步(见 时间线重建)。
持久化与后继行为判读。登录成功之后发生了什么、日志没覆盖的时段是否还有痕迹,靠 计划任务与持久化痕迹。
上游是采集与接入,这里有个容易忽略的依赖:所有命令都在只读挂载或离线目录上执行。直接对着镜像挂载点跑 journalctl 或 last 会产生副作用——写 journal、生成 .bash_history。接入方式不对,后面所有分析的可信度都要打折。
先看落点。/etc/os-release 是判断发行版家族的唯一入口,先读它再定路径,顺序反了就是在找错文件。Debian 系与 RHEL 系的路径对照(auth.log / secure、syslog / messages)见 2.1,这是全文最要紧的一张表。
journald 有两个存储位置。
/var/log/journal/<machine-id>/ 落盘持久,/run/log/journal/ 纯内存,断电即失。
所以**「journal 不存在」和「journal 在 /run」是两个完全不同的结论**。
里面的 .journal 是二进制,必须用 journalctl --directory 读,不能 grep。
strings 出来的碎片既不完整也不可靠,字段会错位。
导出成文本之后用哪种格式,同样决定精度:short、short-precise、iso8601 的精度和时区标注各不相同,只有 short-precise 带亚秒和时区偏移。
格式选错等于丢掉字段。
文本日志这一侧,路径由配置决定。/etc/rsyslog.conf 与 rsyslog.d/ 里可以写任意输出路径,「没有 auth.log」本身就可能是被改过的证据。
/etc/logrotate.conf 与 logrotate.d/ 决定归档命名(.1、.2、日期后缀)、保留期和压缩策略。
保留期就是「日志最多能回溯多久」的上限——配置里写 7 就只能看 7 天。
行格式上有几类事件必须一眼认出:Failed password、Accepted password、session opened、session closed、sudo: ... COMMAND=。
其中 COMMAND= 行带具体命令,是提权行为最硬的证据。
传统 syslog 的时间格式是 Mar 17 22:41:15。没有年份,没有时区,跨源对齐时这两项都缺。
日志之外还有三个独立源。
wtmp / btmp / lastlog 记登录成功与失败,last -f 可以离线读。
/home/<user>/.sudo_as_admin_successful 是提权发生过的痕迹。
零字节文件的 mtime 就是事件时间,可以和日志交叉。
/var/log/audit/audit.log 由 auditd 写出,同样是二进制,要用 ausearch。文件访问和权限变更里有些东西,系统日志根本不覆盖。
最后看断电痕迹:journal 的 session closed 是否早于断电。这决定了历史日志可不可信。
答得出来的有四件:某台 Linux 的日志实际落在哪些路径、某个时间窗口的事件在当前文件还是归档里、有没有成功的登录与提权、以及某个「消失的日志」是轮转、清理还是配置改动造成的。
回答不了的部分也得说清。
sudo 行能看到命令。普通 SSH 会话里敲的命令不在系统日志里,得去 ~/.bash_history 或审计日志找。而且 bash_history 只在会话正常退出时落盘,强杀会话会全丢。
登录之后做了什么——那是 计划任务与持久化痕迹 的范围。
「没有记录」不等于「没有发生」。logrotate 保留期、审计未启用、应用自身日志覆盖、外部转发接收端都可能造成缺失,阴性发现必须限定范围。
攻击者是谁(自然人)回答不了。IP 可被代理、可被跳板,日志里的来源地址是网络层事实,不等于行为人。
传统日志的年份也回答不了。除非有 journal 或其他带年份的源来补,否则不能声称具体年份。第五章案例里的年份是 journal 补的,不是推断的。
断电前最后那段时间的日志,若 journal 只在 /run,该时段不可得,必须写成证据局限。
读到这里你应该已经能读写 Linux 命令行、知道 Debian 与 RHEL 的路径划分不是随便定的,并且已经把只读接入做完再动手。难的不是工具,是顺序:先定发行版、再定路径、再定轮转状态、最后才读内容。跳一步的代价,是你分析了一个根本不存在的文件,而且自己不知道。
二、核心原理
2.1 日志落盘路径:Debian 系 vs RHEL 系
这是本篇最重要的一张表。 判断顺序:先读 /etc/os-release,再按下表取路径。
| 记录内容 |
Debian / Ubuntu |
RHEL / CentOS / Rocky / Alma |
| 认证/授权/sudo |
/var/log/auth.log |
/var/log/secure |
| 系统总日志(服务启动、内核通用) |
/var/log/syslog |
/var/log/messages |
| 内核启动/模块加载 |
/var/log/kern.log |
/var/log/messages |
| 守护进程运行日志 |
/var/log/daemon.log |
各服务自己的文件 |
| 邮件、cron、启动 |
mail.log、cron.log |
/var/log/maillog、/var/log/cron |
| 崩溃转储 |
/var/log/apport.log(Ubuntu) |
/var/log/messages(crash 关键字) |
| 登录成功 / 失败历史 |
/var/log/wtmp / /var/log/btmp |
同 |
| 当前在线会话 |
/run/utmp |
同 |
⚠️ syslog(Debian)与 messages(RHEL)不是简单改名。 两者的 facility 映射与默认覆盖的子系统范围都可能不同,跨发行版比较哪些服务写了日志时不要假设等价。
⚠️ 最小化安装(systemd 但无 rsyslog)根本没有纯文本日志,只有 journal。以 ls 的实际结果为准,不要以习惯为准。
2.2 journald:二进制格式与「双位置」陷阱
这是本篇最容易被忽略、也最容易翻车的部分。
systemd-journald 统一收集日志。和 rsyslog 相比有两处关键区别:格式是自研二进制,以及存储位置有两个。
| 位置 |
存储形式 |
重启后 |
何时使用 |
/run/log/journal/<machine-id>/ |
内存易失(tmpfs) |
全部丢失 |
Storage=volatile 或 auto 但目录不存在 |
/var/log/journal/<machine-id>/ |
磁盘持久 |
保留 |
Storage=persistent,或 auto 且目录已存在 |
<machine-id> 来自 /etc/machine-id。
Storage=auto 是所有发行版的默认值。auto 不会自动创建 /var/log/journal——它只是目录存在就写盘,不存在就退回内存。
所以能否拿到持久化 journal,完全取决于该目录在镜像里是否实际存在,跟发行版叫什么名字没有必然关系:
| 发行版 |
出厂状态 |
说明 |
| Debian / Ubuntu |
多数为易失,部分版本/镜像预建目录 |
目录存在则持久,否则易失 |
| RHEL 7 及更早 |
易失(/var/log/journal 不存在) |
需管理员显式开启 |
| RHEL 8+ / Rocky / Alma |
多数预建 /var/log/journal |
出厂即持久 |
| 最小化 / 容器镜像 |
journald 常被 mask 或不运行 |
只有 JSON 日志(/var/log/pods/,非 journal 格式) |
取证含义(本篇最重要的一条):若机器强制断电 / 被 kill -9,而 journal 只在 /run/log/journal(内存)——那部分日志已随内存一起消失,磁盘上无法恢复。必须以 /var/log/journal/<machine-id>/ 是否存在为准,不能凭发行版推断。
journal 文件家族(同一 machine-id 目录下):
| 文件 |
内容 |
system.journal |
系统级日志(PID 1 及各服务的 stdout/stderr) |
user-<UID>.journal |
某用户级日志(如 user-0.journal = root) |
remote-<host>.journal |
转发自远端的(集中日志场景) |
*.journal~ |
损坏或不完整文件的备份 |
不能直接 grep。 journal 是「entry 列表」的二进制序列化,每条 entry 含单调时钟、实时时钟、PID、UID、进程名、正文等字段。
只能通过 journalctl(在线)或专用解析库(离线)读。strings 只能捞到正文片段,时间戳、优先级、结构化字段都会丢失或错位,不能作为定性依据。
2.3 journalctl:必须用工具读,输出格式决定取证精度
离线读镜像中的 journal 有两种方式:--file <单个 .journal> 或 --directory <目录,自动合并该机全部 .journal>。
| 参数 |
作用 |
-u <unit> / --directory / --file |
按 systemd 单元过滤 / 读目录 / 读单文件 |
_COMM=sshd / _UID=0 / _PID= |
按进程名 / 运行用户 / PID 过滤(比 grep 精确,过滤发生在结构化字段上) |
-k |
内核日志 |
--since / --until |
时间窗口过滤(收敛时间线的第一道工具) |
-f |
实时跟随(仅在线取证) |
--no-pager |
必须加,否则输出进 pager 会截断 |
-o short-precise |
取证首选:微秒 + 明确时区偏移 |
-o json / -o export |
结构化全字段,便于机器处理 |
为什么 -o short-precise 是首选:亚秒精度加上明确的时区偏移,两样都给了。
前者解决「同一秒内多条日志谁先谁后」——爆破和批量操作场景里这是刚需。后者避免把本地时间误当 UTC。默认的 -o short 只到秒。
2.4 rsyslog 配置:日志路径是配置项,不是常量
/etc/rsyslog.conf 与 /etc/rsyslog.d/*.conf 用 facility.severity → 文件 定义去向。文件名和路径由管理员决定,不能假定固定值。
# Debian 系 50-default.conf 的典型规则
auth.* /var/log/auth.log
authpriv.* /var/log/auth.log
kern.* /var/log/kern.log
cron.* /var/log/cron.log
local7.* /var/log/boot.log
取证第一步就是读配置,搞清楚哪个文件收什么。
除主配置外重点看 /etc/rsyslog.d/50-default.conf,以及任何指向非常规路径、非标准文件名或 /dev/null 的规则——这些就是发现。
三种日志消失方式(都是篡改信号,不等于无活动):
| 消失方式 |
特征 |
判定依据 |
| 截断当前文件 |
auth.log 大小为 0 或骤减;.1/.gz 归档不连续 |
文件大小/归档连续性 |
| 改 rsyslog 配置 |
规则写 auth.* /dev/null,或删掉 auth.* 规则 |
配置文件自身的 mtime/ctime 是独立证据 |
| 停掉日志服务 |
rsyslog / journald 未运行 |
journal 里 systemctl 的服务状态记录可印证 |
2.5 logrotate:轮转决定你要找的日志在哪个文件里
/var/log/auth.log ← 当前活跃文件(正在写入)
/var/log/auth.log.1 ← 上一周期(.1 最近,未压缩)
/var/log/auth.log.2.gz ← 再上周期(已 gzip)
/var/log/auth.log.3.gz ← 更早
(若配了 dateext,形如 auth.log-20240310.gz)
/etc/logrotate.conf 是全局默认,/etc/logrotate.d/<service> 逐服务配置。取证必看指令:
| 指令 |
取证含义 |
rotate N |
保留 N 个旧档。rotate 0 = 覆盖即消失,历史被刻意切断 |
daily/weekly/monthly |
时间分辨率(daily = 每天一个分界) |
compress |
归档被 gzip,必须 zcat 才能读 |
delaycompress |
上一轮才压 → 存在未压缩的 .1,最易漏的一档 |
missingok / notifempty |
缺失/为空则跳过 → 没有日志 ≠ 被清 |
dateext |
归档名带日期,合并时需确认字典序=时间序 |
取证含义:rotate 0 或配置被删 → 检查该 logrotate.d 文件的 mtime/ctime;归档编号不连续(只有 .1 没有 .2)→ 有人删过中间档。** .gz 里常藏着入侵的初始阶段**,别只看当前文件。
⚠️ .gz 的文件属性时间不能替代内部时间戳。 文件的 mtime 是被压缩或改名的时间,行内时间才是事件发生时间。判定事件时间一律用日志行内时间戳。
2.6 auth.log / secure 关键事件与三种时间戳格式
| 事件关键字 |
含义 |
取证价值 |
Accepted password for <user> from <ip> port <pt> ssh2 |
密码登录成功 |
成功入侵最直接证据;<pt> 是客户端临时端口 |
Accepted publickey for <user> from <ip> port <pt> ssh2: RSA SHA256:... |
公钥登录成功 |
查对应 authorized_keys(见第 05 篇) |
Failed password for (invalid user )?<user> from <ip> port <pt> ssh2 |
密码失败 |
爆破;invalid user = 在猜用户名 |
Invalid user <user> from <ip> port <pt> |
无效用户 |
扫描器行为;用户名常来自字典库 |
session opened for user <user> by (uid=0) |
会话建立 |
会话起点(Accepted 才是认证成功,两者都要) |
session closed for user <user> |
会话结束 |
算会话时长;也是「机器何时还活着」的锚 |
sudo: ... COMMAND=/path/cmd |
sudo 执行 |
提权操作的具体命令,本类最高价值 |
usermod/useradd 的 PAM 行 |
账户变更 |
账号被创建/改组的证据 |
「port 22」的常见误读:Accepted password for deploy from 198.51.100.7 port 40582 ssh2 里的 40582 是客户端源端口,也就是 ephemeral 端口,不是服务端口。
sshd 的服务端口固定 22;port <n> ssh2 里的 n 恒为客户端临时端口。
别拿源端口当服务端口去分析。
三种时间戳格式(务必分清):
| 来源 |
格式示例 |
有无年份 |
精度 |
备注 |
| 传统 syslog(auth.log/messages) |
Mar 18 09:41:07 |
无年 |
秒 |
跨年归档要靠文件名/轮转顺序推断,有歧义 |
| rsyslog RFC3339(需自定义模板) |
2024-03-18T09:41:07.123456+08:00 |
有 |
微秒+时区 |
默认模板不带年份与时区 |
journald(journalctl -o short-precise) |
2024-03-17T23:58:04.123456+08:00 |
有 |
微秒 |
默认 short 为秒+时区偏移 |
无年份是真实痛点:Mar 18 09:41:07 是哪一年?单看一行无法判定。结合 (1) 文件系统时间戳;(2) 归档文件名与轮转顺序;(3) 与 journal(有年)交叉——journal 是给传统日志补年的最好锚点。
时区:认证日志时间戳通常是本地时间,且无时区标记;journal 的 short-precise 带时区偏移。统一到 UTC 的方法见第 08 篇。
2.7 sudo / 提权痕迹
认证日志里跟 sudo 相关的行有四类,分开看:
- 认证:
user : TTY=pts/0 ; PWD=/home/user ; USER=root ; COMMAND=/bin/bash
- 执行:
sudo: user : COMMAND=/usr/bin/xxd /etc/shadow(执行了什么)
- 策略:
sudo: pam_unix(sudo:session): session opened for user root
- 失败:
sudo: user : incorrect password attempt(累计失败会触发锁定)
Ubuntu 特有:~/.sudo_as_admin_successful 存在 = 该用户曾成功执行过 sudo。
它是有用的辅助证据(说明该用户是 sudoer 且用过),但不能代替日志——里面既没有命令,也没有可信时间戳。
三、操作步骤
3.1 第 1 步:确认日志在哪,而不是假设
# 1. 日志目录全貌 + 轮转归档
ls -l --time-style=long-iso /mnt/df/var/log/
# 特别关注:auth.log / secure / syslog / messages / *.journal / *.gz / *.1
# 2. 读 rsyslog 配置(判断是否有改路径/丢弃)
grep -vE '^\s*#|^\s*$' /mnt/df/etc/rsyslog.conf | head -40
grep -RniE '\*\s+/var/log|/dev/null|\*\.\*' /mnt/df/etc/rsyslog.d/ 2>/dev/null
# 3. journal 存在性与存储模式(以实际目录为准,不凭发行版推断)
cat /mnt/df/etc/machine-id
ls -la /mnt/df/var/log/journal/$(cat /mnt/df/etc/machine-id)/ 2>/dev/null
grep -iE '^\s*Storage' /mnt/df/etc/systemd/journald.conf 2>/dev/null
# 目录存在 → 持久化 ✓ ;目录不存在且 Storage 未改 → 易失,断电前日志不可得
3.2 第 2 步:合并归档 + 当前文件,按时间排序
单一日志可能有多个档位。 按最旧到最新合并(.1 比当前文件更早,直接 cat 会错序):
cd /mnt/df/var/log
# 顺序:*.gz(由旧到新)→ .1 → 当前
{
ls -1tr auth.log.*.gz 2>/dev/null | while read f; do zcat "$f"; done
cat auth.log.1 2>/dev/null
cat auth.log
} > /evidence/auth-merged.txt
wc -l /evidence/auth-merged.txt
# RHEL 系把 auth.log 换成 secure
用了 dateext 时文件