SSH 痕迹与密钥取证

SSH 是 Linux 取证里最值得优先分析的服务,原因有三条:互联网可达的 Linux 主机几乎必有 SSH 端口;它默认开启、默认留痕,成功、失败、会话建立与结束、连接断开服务端每一条都有日志;更关键的是,认证方式直接对应身份——日志里的 Accepted publickey for ... RSA SHA256:XXXX 那个指纹,能直接对应到某个 authorized_keys 文件里的某一行。

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

关键词:sshd_config、auth.log、authorized_keys、known_hosts、wtmp、btmp、utmp、ControlMaster、私钥格式 难度:进阶 前置知识:SSH 协议基础、文本日志分析、私钥公钥体系

一、概述

SSH 是 Linux 取证里最值得优先分析的服务,原因有三条:互联网可达的 Linux 主机几乎必有 SSH 端口;它默认开启、默认留痕,成功、失败、会话建立与结束、连接断开服务端每一条都有日志;更关键的是,认证方式直接对应身份——日志里的 Accepted publickey for <user> ... RSA SHA256:XXXX 那个指纹,能直接对应到某个 authorized_keys 文件里的某一行。

这是极少数能形成「日志 → 文件」硬闭环的证据类型。

但真正的问题出在痕迹的不对称上。~/.ssh/authorized_keys 里的陌生公钥删起来很容易,/var/log/auth.log 里的 Accepted 记录清掉代价很高。清空它需要 root,会在文件层面留下明显痕迹:文件大小骤减、.1 / .gz 归档断档、rsyslog 配置被改。

所以要解决的核心判断是:「authorized_keys 里没有可疑公钥」完全不能推出「没人用过公钥登录」。 指纹在日志里,不在文件里。

这类证据属于分析环节中的「身份类证据」,它的位置特殊:指纹这条链是双向闭环的——从文件可以查到身份,从日志指纹可以反查文件行号,其它多数工件只单向记录。

上游依赖服务配置确认。sshd_config 及其 conf.d drop-in 决定端口、认证方式、是否允许 root 登录,这些直接决定日志里可能出现哪些事件类型,配置结构见系统日志深度分析对日志落点的判定。

下游支撑三件事。确认的登录时段是时间线重建的输入时间窗;确认的公钥归属要靠计划任务与持久化痕迹核对同一账户的其他自动启动机制;私钥材料的固定与识别则是后续凭据分析的前提,这里只做离线固定与格式识别,不做口令攻击。

动手前要先清点 /etc/ssh/ 下的配置文件是否完整——配置被改过本身就是发现。

三类痕迹的分工与生命周期完全不同:

类别 落点 关键结构 生命周期
服务端配置 /etc/ssh/sshd_config、/etc/ssh/sshd_config.d/*.conf Port、PermitRootLogin、PubkeyAuthentication、AuthorizedKeysFile、AllowUsers 改配置要 root,改完需 reload 才生效,易被遗留
认证日志 /var/log/auth.log(Debian 系)/ /var/log/secure(RHEL 系) Accepted password / Accepted publickey / Failed password / Invalid user / Disconnected from authenticating user 受 logrotate 保留期限制
用户侧目录 ~/.ssh/authorized_keys、known_hosts、config、*.pub 公钥行格式为「类型 + base64 + 注释」;known_hosts 可能是含主机名的哈希形式 随时可删,删除不留日志
登录记账 /var/log/wtmp(成功)、/var/log/btmp(失败)、/run/utmp(当前) utmp 结构二进制 utmp 重启即清空,只反映当前运行期
私钥材料 ~/.ssh/id_rsa、id_ed25519 及 .pub 新旧两代格式:OpenSSH 新格式带加密头,旧格式为 PEM 的 Proc-Type 头 私钥本身即高敏感证据

配套的两个判定细节:公钥指纹要按算法分别算(SHA256: 与 MD5: 两种前缀都可能出现),以及 ControlMaster 复用连接会让「一次登录对应多条会话记录」,影响计数判读。

答得出来的是:哪个账户在什么时间从什么地址以什么方式登录成功过、失败登录的聚集特征、某个公钥指纹对应哪些授权文件、登录会话的起止时刻与来源地址、服务端的认证策略与实际开放端口、以及某个私钥文件属于哪个用户。

答不出来的,以及两个必须先纠正的误读:

  • 日志里的 port <数字> 是客户端临时源端口,不是服务端口。 Accepted password for deploy from 203.0.113.77 port 40210 里的 40210 落在 32768–60999 这个临时端口范围,服务端口在 sshd_config 的 Port 项(默认 22)。把两者当成一回事是硬伤级误读,会直接得出「服务跑在 40210」的错误结论。
  • 「authorized_keys 无可疑条目」不能推出「未发生公钥登录」。 必须回到日志查 Accepted publickey 的历史与它记录的指纹。
  • 会话期间执行了什么。SSH 日志记认证与断开,不记命令内容。
  • 私钥的口令。这里识别格式并固定文件,不做口令恢复;若委托确有授权需要,恢复须遵循最小化与脱敏要求。
  • 未落盘的会话。utmp 重启即清空,跨越重启的登录记录只能靠 wtmp。
  • 连接是否成功建立到应用层。Accepted 只到认证通过,后面的失败(磁盘满、fork 失败)要靠 Disconnected 与 session 行的组合判断。

要读懂这套东西,得先理解 SSH 协议基础:公钥认证的挑战应答流程、私钥加密私钥与公钥的关系;熟悉 Linux 用户目录结构与权限规则,知道 authorized_keys 为什么必须 600 权限、属主必须是该用户;能读懂 syslog 格式的日志行,理解其无年份、无时区标记的缺陷,见系统日志深度分析;会处理二进制结构(utmp),至少知道 last / lastb 读的是什么文件;掌握公钥指纹的计算方式(ssh-keygen -lf),否则无法建立日志与文件的闭环。

二、核心原理

2.1 三类痕迹的分工与生命周期

类别 位置 记录什么 生命周期 清除难度
服务端配置 /etc/ssh/sshd_config、/etc/ssh/sshd_config.d/*.conf 认证策略、端口、允许的用户/网段 改配置不重连也生效(多数配置项) ★ 需 root
认证日志 auth.log(Debian)/ secure(RHEL)/ journal 每次认证尝试与会话开闭 logrotate 周期(数天–数周) ★★★ 需 root 且留痕
用户侧文件 ~/.ssh/(authorized_keys、known_hosts、config) 谁能免密登录、连过哪些主机、连接配置 长期 ★★ 用户自己就能删
登录记账 utmp(当前)/ wtmp(成功)/ btmp(失败) 二进制会话记录 utmp 重启清空;wtmp/btmp 长期 ★★~★★★

重要机制:sshd_config 的大多数配置项在每次新建连接时读取(不是只在守护进程启动时读一次),所以改 PasswordAuthentication 无需重启 sshd 就能生效。这意味着攻击者可以临时改配置开一个后门窗口,然后改回去——mtime/ctime 仍会留下痕迹。

2.2 sshd_config:取证必看的配置项

现代 OpenSSH 支持 Include 指令,Ubuntu 22.04 默认有:

Include /etc/ssh/sshd_config.d/*.conf

这意味着 sshd_config 本身可能不是真正的生效配置。

排查时必须把 /etc/ssh/sshd_config.d/ 目录一起看——很多机器的端口/认证设置就在这些 drop-in 文件里。

配置项 默认 取证意义
Port <n> 22 真实服务端口。非 22 说明可能改过端口(也可能是运维主动)
PermitRootLogin prohibit-password(多数发行版) yes = 允许 root 密码登录 = 高危配置;no = 最安全
PasswordAuthentication yes no = 只允许密钥登录,此时出现的密码登录尝试全为失败
PubkeyAuthentication yes 关掉则公钥登录不可用
AllowUsers / AllowGroups / DenyUsers 空 白/黑名单;非空说明该主机做过访问限制
ListenAddress 0.0.0.0 限制监听地址/端口
PermitEmptyPasswords no yes = 允许空口令登录,极度高危
X11Forwarding yes(部分) no = 禁转发(加固)
MaxAuthTries 6 单次连接允许的尝试次数,影响爆破窗口大小
LogLevel INFO VERBOSE/DEBUG3 会记录公钥路径等更多细节

⚠️ Include 是取证陷阱。 只 cat /etc/ssh/sshd_config 会漏掉 drop-in 里的实际配置。先读 Include 行,再逐个读被包含的文件。

⚠️ 匹配规则:OpenSSH 对多数列表型指令采用**"首次出现即生效"**原则(first obtained value wins),所以 Include 放在文件开头还是结尾会改变最终生效值。这是配置矛盾时的分析要点。

2.3 认证日志:关键事件与指纹

auth.log(Debian)/ secure(RHEL)里 SSH 相关事件,详见第 02 篇的总表。

本篇聚焦能形成闭环的部分:

事件 格式要点 取证价值
公钥登录成功 Accepted publickey for <user> from <ip> port <pt> ssh2: RSA SHA256:<fingerprint> ★ 指纹可与 authorized_keys 里的公钥一一对应
密码登录成功 Accepted password for <user> from <ip> port <pt> ssh2 成功入侵的直接证据
密码失败 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) / session closed 会话窗口锚点
预认证断开 Disconnected from authenticating user <user> <ip> port <pt> [preauth] 扫描器探测后主动断开;大量出现 = 端口扫描
正常断开 Connection closed by authenticating user <user> <ip> port <pt> [preauth] 同上
连接关闭 Connection closed by <user> <ip> port <pt> 会话正常结束
密钥协商失败 error: maximum authentication attempts exceeded for <user> from <ip> port <pt> 撞了 MaxAuthTries

公钥指纹字段是本篇的重点。

ssh2: RSA SHA256:xxxx 中的 SHA256:xxxx 是公钥的 SHA256 指纹(base64),可以直接与 authorized_keys 文件里各行的指纹对应:

取某公钥的 SHA256 指纹

ssh-keygen -lf ~/.ssh/authorized_keys

256 SHA256:7Kx9mNpVqRtYwZaBcDeFgHiJkLmNoPqRsTuVwXyZ0 deploy@attacker (ED25519)

↑ 指纹 ↑ 与 auth.log 里 Accepted publickey 后的指纹完全对应

闭环价值:这是 Linux 取证里少见的"日志中的指纹 → 磁盘上具体某一行公钥"的直接映射。不要浪费它——看到 Accepted publickey 就必须把指纹抄下来,到所有 authorized_keys 里逐一比对。

2.4 ~/.ssh/ 目录:文件与权限规则

文件 作用 取证价值
authorized_keys 允许免密登录本账户的公钥 ★★★★★ 后门植入的经典位置
known_hosts 本机连过的远端主机及其主机密钥 ★★★★ 反向线索:从内网连到过哪些主机
config 本机 SSH 客户端配置(-i 私钥、ProxyJump、ControlMaster) ★★★★ 含私钥路径与跳板配置
id_rsa / id_ed25519 / id_ecdsa 私钥 ★★★★★ 高价值材料
id_rsa.pub 等 对应公钥 ★★
authorized_keys2 旧版 sshd 的备用文件 ★★ 多数现代 sshd 已不读,但存在本身值得记录
.ssh/ 目录权限 必须 700 权限不对会被 sshd 忽略

⚠️ 权限硬规则(常被误判为"配置错误"):authorized_keys 权限必须是 600(chmod 600),.ssh/ 目录必须是 700。若 authorized_keys 权限过宽(如 644),sshd 会直接忽略该文件、拒绝所有公钥登录(严格模式下会拒绝)。

取证含义:这既是"陷阱"也是"证据"。攻击者植入 authorized_keys 后如果忘了 chmod 600,会导致该公钥实际不生效——此时 authorized_keys 文件存在但从未被成功使用过。

判断方法:查 auth.log 里是否真有与该指纹对应的 Accepted publickey。文件存在 ≠ 生效过。

⚠️ authorized_keys 还可被用于维持权限:文件末尾行、支持 command="..."、from="..."、禁用 PTY 等选项前缀。这些选项不写进日志,只能从文件内容发现。

2.5 登录记账三库:utmp / wtmp / btmp

Linux 把登录记录放在三个二进制数据库文件里(不是文本,grep 无效):

文件 记录 读取工具 生命周期
/var/run/utmp 当前在线会话 who / w ★ 重启即清空(tmpfs 或重建)
/var/log/wtmp 成功登录历史 last 长期(lastlog 是其中每用户一条最近记录)
/var/log/btmp 失败登录历史 lastb / last -f 长期
/var/log/lastlog 每用户最近一次登录 lastlog 长期

三者互补的证据能力:

问题 唯一能回答的库
"这台机器现在有谁在线?" utmp(且仅在运行中有效,断电机器的 utmp 是空的)
"过去 30 天谁成功登录过?从哪台机器?" wtmp(last)
"过去 30 天有哪些登录失败尝试?" btmp(lastb)
"用户 X 最后一次登录是什么时候?" lastlog

⚠️ /var/run/utmp 不是历史记录。 它只描述"当前在线",重启/断电后内容全部消失。很多分析误把 utmp 当"登录历史"。登录历史必须看 wtmp。

⚠️ last 的输出有"仍在登录"(still logged in)的记录,这表示会话从未被正常关闭(崩溃、断电、或 wtmp 条目未更新)。这本身是一个信号,尤其与机器的断电状态吻合时。

离线读法:last -f <文件> 可指定文件,不必运行在原机器上。

last -f /mnt/df/var/log/wtmp              # 成功登录历史
lastb -f /mnt/df/var/log/btmp             # 失败登录历史(等价 last -f btmp)
lastlog -f /mnt/df/var/log/lastlog        # 每用户最近登录
who -a                                    # 需原机器运行态,离线无意义

lastlog 的格式注意:部分系统上 lastlog 是稀疏文件(/var/log/lastlog 可能占用几十 MB 但内容很少),用 icat 导出时会补零成完整声明大小。参考第 01 篇对稀疏流的处理提示。

2.6 SSH 连接复用:ControlMaster 及其取证意义

OpenSSH 的连接复用机制:ControlMaster auto + ControlPath 会让第一次连接建立的 TCP 连接被后续所有同目标连接复用,且第一次认证成功后,会在 socket 文件里缓存认证结果,后续连接无需重新认证。

~/.ssh/config 里的典型配置

Host bastion HostName bastion.example.com User deploy ControlMaster auto ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 10m

取证意义:

  1. 一次认证,多条会话。auth.log 里可能只有 1 条 Accepted,但实际发生了 10 次会话活动(复用通道)。不能用 Accepted 计数来统计活动次数。
  2. 连接复用的审计盲区。后续复用连接不再产生新的认证日志,可能也不产生新的 session opened 记录(取决于 sshd 版本与配置)。这是 SSH 取证的一个真实盲区。
  3. ControlPath 指向的文件本身是痕迹:~/.ssh/cm-<user>@<host>:<port> 这样的 socket 文件如果还在(未正常清理),说明该复用通道未正常结束。

注意 socket 文件通常在关机/退出后消失,断电机器上基本不留。

ControlPersist 的取值也有意义:ControlPersist 10m 表示认证缓存 10 分钟;ControlPersist yes 则是无限期(直到会话结束)。yes 意味着认证凭据被长期缓存在内存中,是加固审计时值得关注的一项。

2.7 私钥格式:新旧两代

识别私钥类型只需看第一行 PEM 头:

首行 格式 特征
-----BEGIN OPENSSH PRIVATE KEY----- OpenSSH 新格式(7.8+ 默认生成) 单一容器,内含私钥 + 公钥 + 注释;ssh-keygen -i -m pem 可转旧格式
-----BEGIN RSA PRIVATE KEY----- PKCS#1 / 传统 PEM 旧格式;OpenSSH 新版默认不再生成,但仍完全接受
-----BEGIN DSA PRIVATE KEY----- / -----BEGIN EC PRIVATE KEY----- 对应旧格式 同上
-----BEGIN ENCRYPTED PRIVATE KEY----- PKCS#8 加密格式 常用于其它工具,sshd 不接受这种格式做 IdentityFile

⚠️ OPENSSH PRIVATE KEY 头既不代表"加密"也不代表"未加密"。 OpenSSH 新格式是一种容器格式,是否受口令保护取决于生成时是否设置了 passphrase(ssh-keygen 会交互询问,用户可直接回车留空)。旧 PEM 格式同理。判断是否加密必须看容器内部字段 / 实测验证,绝不能仅凭 PEM 头下结论。

取证要点:

  • 是否带口令与格式新旧无关,只取决于生成时是否设置 passphrase。ssh-keygen 交互提示可直接回车留空,因此新格式私钥同样可能是无口令的。
  • 实测验证(不修改原文件):ssh-keygen -y -P "" -f <key> —— 能输出公钥即无口令保护,报 incorrect passphrase 即有口令保护。
  • 私钥文件权限应为 600;权限过宽时 ssh 客户端会拒绝使用。
  • 私钥文件的 mtime/ctime 说明它何时生成或被改动,可与 authorized_keys 的时间对照,判断是"私钥与公钥同时植入"还是"先后两次动作"。

三、操作步骤

3.1 第 1 步:确认 SSH 服务配置(含 drop-in)

1. 确认主配置里有没有 Include(现代 OpenSSH 默认有)

grep -nE '^\s*Include' /mnt/df/etc/ssh/sshd_config

2. 列出 drop-in 目录内容

ls -la /mnt/df/etc/ssh/sshd_config.d/ 2>/dev/null

若为空(不存在该目录)→ 只看主配置;若非空 → 必须逐个读

3. 提取关键项(主配置 + 所有 drop-in,注意"首次出现生效")

for f in /mnt/df/etc/ssh/sshd_config /mnt/df/etc/ssh/sshd_config.d/.conf; do [ -f "$f" ] || continue echo "== $f ==" grep -nE '^\s(Port|ListenAddress|PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AllowUsers|AllowGroups|DenyUsers|PermitEmptyPasswords|MaxAuthTries|LogLevel|AuthorizedKeysFile)\b' "$f" done

配置组合 取证判读
PermitRootLogin yes 允许 root 密码直接登录——高危;若日志有 Accepted password for root,直接定性
PasswordAuthentication no 只允许密钥登录;此时所有 Failed password 都是无效尝试(因为密码认证根本不会走)
PermitEmptyPasswords yes 极度高危;配合空口令账户 = 免密登录
AuthorizedKeysFile 非默认 授权公钥读的是别的路径(.ssh/authorized_keys 里的内容可能根本没用)
drop-in 里有 Port 2222 真实服务端口;与日志中的 port 字段无关

3.2 第 2 步:授权公钥全面清点与指纹比对(本篇核心)

1. 找出所有 authorized_keys(root + 所有用户 + 可能的自定义路径)

find /mnt/df -xdev -type f -name 'authorized_keys*'
-printf '%m %u:%g %TY-%Tm-%Td %TH:%TM %p
' 2>/dev/null | sort

2. 逐个看内容与权限(权限必须 600,.ssh 必须 700)

for f in $(find /mnt/df -xdev -type f -name