计划任务与持久化痕迹

"开机自动运行"这件事被分散在十几个互不相干的位置,每个位置有各自的格式、不同的加载时机、不同的日志记录方式。

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

关键词:持久化、cron、systemd timer、at、init 脚本、rc.local、profile 注入、motd、ld.so.preload、SUID 难度:进阶 前置知识:Linux 目录结构、systemd 单元概念、文件权限与 inode 时间戳

一、概述

"开机自动运行"这件事被分散在十几个互不相干的位置,每个位置有各自的格式、不同的加载时机、不同的日志记录方式。

由此产生一个取证上的特点:攻击者建立持久化时几乎不写任何日志。

结果是"日志沉默、痕迹清晰"——要判断有没有被植入持久化,不能等日志,而要直接读配置。

这一层处在分析环节的主机侧行为认定层,在 取证工件地图 之后、时间线重建 之前完成输入准备。它的实战价值在整个 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/) 发行版包提供 随包安装;出现在此目录但不属于任何已安装包 = 可疑 ★
/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 恶意性的三条规则(缺一不可):

  1. 不在包管理器清单里(用 *.list / RPM 数据库核对,见第 07 篇)——这是最强的判据。
  2. 位置在 /tmp、/var/tmp、/dev/shm、/home、/var/www 等可写目录——系统 SUID 文件从不出现在这里。
  3. 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 不在包清单里很容易被忽略。

取证判定:

  1. /etc/ld.so.preload 存在且非空 → 高度可疑(干净系统上这个文件通常不存在,或为空)。
  2. 文件里列的 .so 路径是否在包清单里——列出系统 lib 目录之外的路径 = 恶意。
  3. 该文件自身的 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 篇核对)
**`Exe