一、概述
"开机自动运行"这件事被分散在十几个互不相干的位置,每个位置有各自的格式、不同的加载时机、不同的日志记录方式。
由此产生一个取证上的特点:攻击者建立持久化时几乎不写任何日志。
结果是"日志沉默、痕迹清晰"——要判断有没有被植入持久化,不能等日志,而要直接读配置。
这一层处在分析环节的主机侧行为认定层,在 取证工件地图 之后、时间线重建 之前完成输入准备。它的实战价值在整个 Linux 板块里最高,理由很具体:
| 证据类型 |
是否依赖日志 |
依赖断电保护 |
清理成本 |
auth.log 登录记录 |
✅ 强依赖 |
✅ 是 |
低(可截断) |
bash_history 命令 |
✅ 强依赖 |
✅ 是(正常退出才落盘) |
低(可删) |
| 持久化配置 |
❌ 不依赖 |
❌ 不依赖 |
高(改了要逐个恢复) |
只要攻击者建立了持久化,痕迹就躺在 /etc、/var/spool、/home 的某个目录里等着被读。 攻击者清理它的成本远高于留下它的成本。
具体要过的检查项有这些,每一类给出「位置 → 格式 → 排查命令 → 异常判据」,照着逐项过一遍即可,通读没有意义:
- cron 五类位置 ——
/etc/crontab(系统级)、/etc/cron.d/(片段式,含用户名字段)、/var/spool/cron/crontabs/<用户>(用户级)、/etc/cron.{hourly,daily,weekly,monthly}/(目录式)、at / batch(/var/spool/cron/atjobs/)。五处格式不同,混读会漏
- cron 的环境变量行
SHELL= / PATH= —— 攻击者常在这里改 PATH 劫持相对路径命令,格式上只是一行环境变量,不看内容会漏
- systemd unit 与
.wants 软链 —— 只有存在 .wants/ 下的软链才算"已启用",unit 文件放在 /etc/systemd/system/ 但没被 enable 的不算
- systemd
drop-in 覆盖(*.d/) —— 在原 unit 基础上追加指令。只看主 unit 文件会漏掉这部分
- profile 注入系列 ——
~/.bashrc、~/.bash_profile、/etc/profile、/etc/profile.d/*.sh、/etc/environment。profile.d 下的任意 .sh 都会被每个登录 shell 执行
BASH_ENV 与 ENV —— 不写进任何配置文件的注入。由用户在环境里指定,指向任意脚本;只看配置文件永远发现不了
- motd 脚本
/etc/update-motd.d/ —— 登录显示提示前执行。命名可任意伪装成 88-sshfp 这类像系统组件的名字
/etc/rc.local、/etc/init.d/ —— 传统启动脚本
/etc/modules-load.d/*.conf 与内核模块 —— 内核级加载
/etc/ld.so.preload —— 影响面最大的隐蔽持久化:指定动态链接器预加载的 .so,对几乎所有动态链接程序生效,且不在任何进程列表里出现。文件不存在是重要阴性发现
- SUID / SGID 位 —— 权限位本身即痕迹。
chmod 4755 会更新 ctime 而不动 mtime,时间戳差值能定位赋值时刻
- 包管理器清单 ——
dpkg / rpm 的已安装列表。"文件不属于任何已安装包"是判定清单外文件的最硬判据
- 载荷文件本身 —— cron 命令、unit 的
ExecStart、pam_exec.so 参数指向的脚本。/usr/local/bin/.cache/ 这类伪装路径的共用性是自动化工具的典型特征
/home/<user>/.ssh/authorized_keys —— 公钥持久化。追加公钥时 crtime 保持原值而 mtime 更新,据此可区分"新建"与"篡改"
走完这张表能回答的是:这台机器上存在哪些持久化配置、每条配置指向什么载荷、哪些文件不属于任何已安装包、以及攻击者建立了哪几类持久化、用了什么统一路径。
不能回答的必须说清:
- "配置存在"不等于"已实际执行过" —— 这是最重要的一条边界。持久化配置证明的是"已建立",运行记录要看 journal;机器断电时只有配置证据、没有运行证据
- "排查为空"不等于"确定没有" —— 阴性发现的强度只能到"未见",第五章案例里
rc.local、自定义模块、at 任务三项都标注为"中(阴性,仅'未见'非'确定无')"
- 载荷是否可执行、是否仍在磁盘 —— 配置指向的路径可能已被删除,路径存在性要单独确认
- 是谁建立的 —— 配置文件的属主与时间戳能给方向,但没有用户身份记录。要归因靠 SSH 痕迹与密钥取证 与 系统日志深度分析
- 容器与编排层面的持久化 —— Docker restart 策略、
--privileged、K8s CronJob 属另一层机制,不在文本排查范围内
- 内核级与 UEFI 变种 —— initramfs 篡改、UEFI 启动项需要专用工具,同样不在文本排查范围内
前提有三项:知道 Linux 目录结构与文件权限语义(/etc 属 root、-rwsr-xr-x 每一位的含义)、理解 systemd 单元的基本概念,以及先完成只读接入再排查。
最后一条是硬性的:/etc、/var/spool/cron、/home 全部需要 root 才能读,很多操作又会写访问时间或生成新文件。直接对原始分区或错误的挂载方式动手,会在排查开始前就污染痕迹。
排查项很多,实际委托里时间往往不够,必须能判断哪些项可以略过。2.1 那张"按加载时机分类"的表就是排序依据——登录时执行 > 定时执行 > 开机执行,越靠前被利用的概率越高。
二、核心原理
2.1 持久化位置全景(按"何时被加载"分类)
| 加载时机 |
机制 |
位置 |
是否需要 root |
| 登录 shell 启动时 |
profile 系列 |
~/.bashrc、~/.bash_profile、/etc/profile、/etc/profile.d/*.sh、/etc/environment |
部分 |
| 非登录 shell 启动时 |
BASH_ENV / ENV |
指向任意可执行脚本 |
否(由用户自己设) |
| 登录时显示提示前 |
motd 脚本 |
/etc/update-motd.d/、/etc/motd |
是 |
| 定时(周期) |
cron |
/etc/crontab、/etc/cron.d/、/var/spool/cron/crontabs/、/etc/cron.{hourly,daily,weekly,monthly}/ |
是 |
| 定时(一次性) |
at / batch |
/var/spool/cron/atjobs/ |
是 |
| 服务启动时 |
systemd unit |
/etc/systemd/system/(含 .wants/)、/lib/systemd/system/、/usr/lib/systemd/system/ |
是 |
| 定时(systemd) |
systemd timer |
*.timer + 对应 *.service |
是 |
| 系统引导(旧机制) |
init 脚本 / rc.local |
/etc/init.d/、/etc/rc.local、/etc/rc*.d/ |
是 |
| 每次动态链接时 |
** ld.so.preload** |
/etc/ld.so.preload |
是 |
| 内核加载时 |
内核模块 |
/etc/modules-load.d/、/etc/modules |
是 |
| 执行文件时 |
SUID / SGID 位 |
任意可执行文件的权限位 |
是 |
读法:从下往上,隐蔽性递增。crontab 是明文、显眼;ld.so.preload 和 BASH_ENV 在正常运维中几乎不会被检查,但影响面最大——ld.so.preload 作用于每一个动态链接的进程。
2.2 cron:五类位置与各自格式
cron 是最常见的持久化载体,但它有五个不同的位置,格式各不相同。
混淆它们是常见错误。
| 位置 |
格式 |
触发者 |
说明 |
/etc/crontab |
有用户字段(7 字段含 user) |
系统 |
唯一的"系统主"crontab |
/etc/cron.d/ |
有用户字段 |
系统 |
文件名规则因发行版而异:Debian 系 cron 只接受 [A-Za-z0-9_-],含 . 会被忽略;部分 RHEL 系(cronie)版本允许 .。以目标系统实测为准 |
/etc/cron.hourly/ |
无用户字段、无时间字段 |
系统 |
run-parts 每天执行目录内脚本 |
/etc/cron.daily/(weekly/monthly 同理) |
同上 |
系统 |
同上 |
/var/spool/cron/crontabs/<user> |
有用户字段(冗余,字段是用户名) |
crond |
各用户自己的 crontab;crontab -l 读的就是它 |
三种带用户字段的格式(空格分隔,注意 user 位置不同):
# /etc/crontab 格式:分 时 日 月 周 user 命令
17 2 * * * root /usr/bin/find /var/spool -mtime +30 -exec rm {} \;
# /etc/cron.d/fragment 格式:同 /etc/crontab
*/5 * * * * deploy /usr/local/bin/sync-job.sh
# /var/spool/cron/crontabs/deploy 格式:分 时 日 月 周 命令(**user 字段被省略**)
*/10 * * * * /usr/local/bin/sync-job.sh
⚠️ 用户 crontab 不写用户字段,因为文件名就是用户名。把 /var/spool/cron/crontabs/ 里的文件按 /etc/crontab 解析会错位一列。
环境变量陷阱:crontab 的环境是极简的(几乎只有 HOME、LOGNAME、PATH、SHELL),没有 TERM,很多交互程序会报错。
因此攻击者常用两条规避写法:.* * * * * /dev/shm/payload 或在命令前加 PATH 赋值。
看到 crontab 里有 . 作为时间字段、或命令里带绝对路径/env,都值得多看一眼。
日志:cron 的执行记录在 /var/log/syslog(Debian 系)/ /var/log/cron(RHEL 系),关键字是 CRON[...]。
但只有 cron 服务本身在跑时才有——如果攻击者顺手停了 rsyslog,cron 日志也没了。不要把"cron 日志缺失"当"cron 没跑"。
2.3 systemd unit 与 timer
现代持久化主要走 systemd。有三个目录,优先级和归属不同:
| 目录 |
归属 |
用途 |
/etc/systemd/system/ |
管理员(本地) |
自定义 unit 放这里;也是启用自启(.wants/ 软链)的位置 ★ |
/run/systemd/system/ |
运行时 |
重启后消失,不持久(若此目录有 unit,说明是当次启动注入的) |
/lib/systemd/system/(新版 /usr/lib/systemd/system/) |
发行版包提供 |
随包安装;出现在此目录但不属于任何已安装包 = 可疑 ★ |
.wants 目录是 systemd 启用机制的核心:
/etc/systemd/system/multi-user.target.wants/
├── ssh.service -> /lib/systemd/system/ssh.service ← 正常
├── nginx.service -> /lib/systemd/system/nginx.service ← 正常
└── sysupdate.service -> /etc/systemd/system/sysupdate.service ← ★ 可疑:指向 /etc 下的自定义 unit
判定规则:软链目标在 /etc/systemd/system/ 且该 unit 不属于任何已安装软件包(用第 07 篇的包清单方法核对)→ 高度可疑的自定义自启服务。
timer 与 service 是配对的:
# /etc/systemd/system/sysupdate.timer
[Unit]
Description=System Update
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h # 每小时触发
[Install]
WantedBy=timers.target
# /etc/systemd/system/sysupdate.service(被 timer 唤起)
[Unit]
Description=System Update
[Service]
Type=oneshot
ExecStart=/usr/local/bin/.cache/sysupdate
取证要点:OnCalendar= 形式(如 OnCalendar=*-*-* 00,12:00:00)比 OnUnitActiveSec= 更隐蔽——不写就看不出触发周期。查 timer 时必须同时看对应的 .service 的 ExecStart,那才是真正执行的命令。
日志:systemd 单元的启动/停止在 journal 里,关键字是单元名(journalctl -u <unit>)。但同样,journal 在哪、是否被清,取决于第 02 篇说的存储模式。
2.4 profile 注入:最容易被忽略的一类
每次用户登录 bash 都会读一批文件。
在这些文件里加一行,就是"登录即执行"。这类持久化不留任何 cron/systemd 痕迹,且在海量文件中极易漏检。
| 文件 |
何时被读 |
适用 |
/etc/profile |
登录 shell 启动 |
系统级,所有用户 |
/etc/profile.d/*.sh |
同上(被 /etc/profile 逐个 source) |
系统级,推荐按服务分文件,排查时逐个过 |
/etc/bash.bashrc |
交互式非登录 shell |
Debian/Ubuntu 系统级 |
~/.bashrc |
交互式非登录 shell |
用户级 |
~/.bash_profile / ~/.profile |
登录 shell |
用户级(Ubuntu 默认 ~/.profile) |
~/.bash_login / ~/.bash_logout |
登录 shell / 登出 |
用户级(少见) |
/etc/environment |
PAM 环境(不是 shell 读) |
系统级;改环境变量 |
⚠️ 登录 shell 与非登录 shell 读的文件不同:SSH 登录进来是登录 shell,读 ~/.bash_profile / ~/.profile(不是 ~/.bashrc,除非被 source);而 ssh host "command" 形式是非登录 shell,只读 ~/.bashrc。排查时两类都要看。** /etc/environment 值得单独说**:它由 PAM(不是 shell)读取,只接受简单的 KEY=VALUE 格式。它的存在会让"环境变量持久化"生效,包括 LD_PRELOAD(见 2.7)。
2.5 BASH_ENV 与 ENV:不写进任何配置文件的注入
这是技术上最隐蔽的一类,因为痕迹不在文件里,在环境变量里。
BASH_ENV:当 bash 作为非交互式 shell(执行脚本 bash script.sh)启动时,会 source 这个变量指向的文件。
ENV:对 POSIX sh(及某些 shell)的等价机制。
取证含义:如果 /etc/environment、某个 ~/.bashrc、或某个进程的 /proc/<pid>/environ 里出现 BASH_ENV=/path/to/script,那么执行任何非交互式 bash 脚本都会先跑那个脚本。判定方法:搜配置里的 BASH_ENV / ENV= 赋值,并在 /proc/*/environ 里找(在线取证时,见第 06 篇)。
2.6 SUID / SGID 与内核模块
判定 SUID 恶意性的三条规则(缺一不可):
- 不在包管理器清单里(用
*.list / RPM 数据库核对,见第 07 篇)——这是最强的判据。
- 位置在
/tmp、/var/tmp、/dev/shm、/home、/var/www 等可写目录——系统 SUID 文件从不出现在这里。
ctime 远晚于 mtime——只改了权限位(chmod +s)而未改内容,是"赋权"而非"安装"的特征。
内核模块:/etc/modules-load.d/*.conf(Debian/Ubuntu)、/etc/modules(RHEL)、/etc/modprobe.d/。检查是否有非常规 .ko 文件(同样用包清单核对——内核模块几乎都来自 linux-modules-* 包)。加载的模块在 /proc/modules(仅在线)和 lsmod 可见。
2.7 ld.so.preload:影响面最大的隐蔽持久化
机制:动态链接器(ld.so)在启动每个动态链接的可执行文件时,都会读取 /etc/ld.so.preload,把里面列出的每个 .so 文件预先加载到这个新进程里。
为什么它是最强的持久化:
- 作用于每一个动态链接进程(包括
ls、ps 等几乎所有命令行工具本身)。
- 不需要任何 root 之外的配合——写一次
/etc/ld.so.preload 就够了。
- 在正常运维中几乎不被检查——它是一个"编译期优化"配置文件,管理员很少看。
- 隐蔽性极高——
.so 文件伪装成正常库名,.so 不在包清单里很容易被忽略。
取证判定:
/etc/ld.so.preload 存在且非空 → 高度可疑(干净系统上这个文件通常不存在,或为空)。
- 文件里列的
.so 路径是否在包清单里——列出系统 lib 目录之外的路径 = 恶意。
- 该文件自身的
mtime/ctime/属主(root:root,权限 644)也是独立证据。
⚠️ 本篇只讲检测。 不复现劫持手法,不给出绕过方法。
2.8 motd / init 脚本 / rc.local
| 位置 |
触发时机 |
要点 |
/etc/motd |
登录时显示提示前 |
应是纯文本;若含 $(...) 或可疑命令需警惕 |
/etc/update-motd.d/ |
登录时(Debian/Ubuntu) |
目录下是可执行脚本(00-、50- 编号);多出的、不属任何包的脚本 = 可疑 |
/etc/update-motd.conf |
同上 |
配置里 ENABLED= 引用了哪些脚本 |
/etc/init.d/ |
引导时(SysV 遗留) |
RHEL 系在 /etc/rc.d/init.d/(软链至此)。清单外脚本 = 可疑 |
/etc/rc.local |
引导末期 |
#!/bin/sh + 命令;若发行版不用它,非空即可疑 |
/etc/rc*.d/ |
引导时 |
SysV 的 runlevel 软链目录(Debian 系) |
/etc/update-motd.d/ 值得强调:这个目录里的脚本在每次 SSH 登录时执行,且它的存在不写在任何"自启"配置里——systemctl list-unit-files 看不到。排查时必须单独 ls 这个目录并与包清单核对。
三、操作步骤:逐项排查清单
用法:把下面每一项当作一个检查点逐条跑。每项给出「位置 / 排查命令 / 异常判据」。建议把输出统一重定向到 /evidence/persistence-*.txt 留档。
3.1 cron 全面排查
# (1) 系统主 crontab
cat /mnt/df/etc/crontab
# (2) 系统级片段 /etc/cron.d/(注意文件名合法性)
ls -la /mnt/df/etc/cron.d/
for f in /mnt/df/etc/cron.d/*; do echo "== $f =="; cat "$f"; done
# (3) 所有用户 crontab
ls -la /mnt/df/var/spool/cron/crontabs/
for f in /mnt/df/var/spool/cron/crontabs/*; do echo "== $f =="; cat "$f"; done
# (4) 周期目录脚本(hourly/daily/weekly/monthly)
ls -la /mnt/df/etc/cron.{hourly,daily,weekly,monthly}/
for d in hourly daily weekly monthly; do
for f in /mnt/df/etc/cron.$d/*; do [ -f "$f" ] && echo "$f"; done
done
# (5) 一次性别漏:cron 的其它 spool
ls -la /mnt/df/var/spool/cron/
异常判据:
| 判据 |
说明 |
crontab 里的命令在 /tmp、/dev/shm、/var/tmp |
载荷暂存地,强烈可疑 |
. 作为分/时/日/月/周字段(.* * * * * cmd) |
借"每分钟"语义但写成通配,常见于刻意伪装 |
cron.d 里有含 . 的文件名 |
Debian 系 cron 会静默忽略(不执行、不报错)——可能是被停用的旧任务,也可能是"写了但根本没生效"的伪装 |
| 用户 crontab 里的命令属 root 但用户是普通账户 |
提权残留 |
| cron.d 片段里的命令不在任何包清单 |
载荷 |
| 周期目录脚本的 mtime 与其他脚本明显不同 |
后期加入 |
3.2 at / batch 一次性任务排查
# Debian 系 / RHEL 系位置相同
ls -la /mnt/df/var/spool/cron/atjobs/
# 文件名即 job id,内容是待执行脚本
for f in /mnt/df/var/spool/cron/atjobs/*; do
[ -f "$f" ] || continue
echo "== job $f =="; cat "$f"
stat -c 'mtime=%y owner=%U' "$f"
done
# spools 目录(at/batch 各一个)
ls -la /mnt/df/var/spool/cron/ | grep -E 'atjobs|atspool'
异常判据:atjobs 目录非空 = 有排定的未来任务。攻击者常用 at 一次性执行(不留 crontab 常驻痕迹)。注意区分 atjobs(待执行脚本)与 atspool(已执行完但结果未取走的输出)。
3.3 systemd 自启与 timer 排查
# (1) 所有 unit 文件(系统 + 自定义 + 运行时)
find /mnt/df/etc/systemd/system /mnt/df/lib/systemd/system /mnt/df/usr/lib/systemd/system \
-name '*.service' -o -name '*.timer' 2>/dev/null | sort
# (2) ★ 关键:所有自启软链,标注目标
for d in /mnt/df/etc/systemd/system/*.wants /mnt/df/etc/systemd/system/*.requires; do
[ -d "$d" ] || continue
ls -l "$d"
done 2>/dev/null
# (3) 关注指向 /etc 的软链(自定义服务)
find /mnt/df/etc/systemd/system -name '*.wants' -type d -exec ls -l {} \; \
| grep -E '\-> /etc/'
# (4) 所有 timer 及其配对 service
find /mnt/df/etc/systemd/system -name '*.timer' 2>/dev/null | while read t; do
echo "== $t =="; cat "$t"
s="${t%.timer}.service"; [ -f "$s" ] && { echo "-- ExecStart --"; grep -A5 '\[Service\]' "$s"; }
done
# (5) 运行时注入(重启即失)
ls /mnt/df/run/systemd/system/ 2>/dev/null
# (6) 全盘 ExecStart 汇总,快速看有没有指向临时目录的
grep -rhE '^Exec(Start|Pre|StartPost)=' \
/mnt/df/etc/systemd/system /mnt/df/lib/systemd/system /mnt/df/usr/lib/systemd/system 2>/dev/null \
| sort -u | grep -E '/tmp/|/dev/shm/|/var/tmp/|/home/'
异常判据:
| 判据 |
说明 |
.wants 软链目标在 /etc/systemd/system/ |
自定义服务启用 ★ |
| 该 unit 不属于任何已安装包 |
非发行版自带 = 自定义(用第 07 篇核对) |
ExecStart 指向 /tmp、/dev/shm、/home |
载荷路径 |
timer 用 OnCalendar= 隐藏触发周期 |
需看 |