系统日志深度分析

Linux 日志分析真正的困难只有一个:你要找的那份日志,可能不叫那个名字,也可能不在那个目录,甚至可能已经不是文本了。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词: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 相关的行有四类,分开看:

  1. 认证:user : TTY=pts/0 ; PWD=/home/user ; USER=root ; COMMAND=/bin/bash
  2. 执行:sudo: user : COMMAND=/usr/bin/xxd /etc/shadow(执行了什么)
  3. 策略:sudo: pam_unix(sudo:session): session opened for user root
  4. 失败: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 时文件