前两篇我们处理的是"静态"的文件权限——谁可以读、谁可以写、哪些特殊位会改变执行身份。但 Linux 系统真正"活"起来的那一刻,是从内核启动第一个进程开始的。文件是程序的静态存储形式,进程是程序的动态执行实例。一个编译好的 /usr/bin/nmap 放在磁盘上只是 5MB 的二进制数据,当你在终端输入 nmap 并回车,内核把它加载进内存、分配 CPU 时间片、打开网络套接字——这才诞生了一个进程。
每个进程有一个唯一的进程 ID(PID),从 1 开始递增。PID 1 是所有进程的祖先——在 systemd 系统中就是 systemd 本身。每个进程记录自己的父进程 ID(PPID),形成一棵从 PID 1 向下展开的进程树。这个树状结构在安全审计中极其重要:攻击者植入的后门进程一定挂在这棵树的某个节点上,溯源 PPID 链就能找到入口点。
上面的输出说明:当前 Shell 的 PID 是 31245,它的父进程 PID 是 31198(通常是 SSH 守护进程派生的会话)。$$ 是 Bash 内置变量,保存当前 Shell 的 PID。
Linux 进程并非只有"运行"和"停止"两种状态。内核为每个进程维护一个状态机,理解这些状态是排查"服务卡死""进程僵尸"等问题的基础:
| 状态码 | 名称 | 含义 |
|---|---|---|
R |
Running | 正在 CPU 上执行或在运行队列中等待调度 |
S |
Sleeping | 可中断睡眠,等待事件(I/O、信号、定时器) |
D |
Disk Sleep | 不可中断睡眠,通常在等待磁盘 I/O(kill -9 也杀不掉) |
Z |
Zombie | 僵尸进程:已终止但父进程尚未回收其退出状态 |
T |
Stopped | 被信号暂停(如 Ctrl+Z),可由 SIGCONT 恢复 |
I |
Idle | 空闲内核线程,不消耗 CPU |
kill -9 杀死——因为它在内核态等待 I/O 完成,信号要等它回到用户态才能被处理。如果磁盘损坏导致 I/O 永远不返回,D 状态进程只能通过重启系统清除。排查时用 cat /proc/<pid>/stack 查看它卡在哪个内核函数。知道了进程的基本概念,接下来掌握在终端里"看到"进程的方法。安全工作中最常用的四个工具是 ps、top、pgrep 和 pstree——它们各有侧重,组合使用覆盖从快速查询到持续监控的全部场景。
ps 提供某一时刻的进程列表快照。最常用的组合是 ps aux(BSD 语法)和 ps -ef(System V 语法),两者输出信息略有差异但核心一致。
每列含义:USER(进程所有者)、PID(进程 ID)、%CPU(CPU 占用率)、%MEM(内存占用率)、VSZ(虚拟内存大小 KB)、RSS(物理内存大小 KB)、TTY(终端)、STAT(状态码)、START(启动时间)、TIME(累计 CPU 时间)、COMMAND(启动命令)。
安全审计常用技巧——查找以 root 权限运行的可疑进程:
ps 是拍快照,top 是放录像。top 每隔 3 秒刷新一次进程列表,实时显示 CPU、内存、负载情况。在 SSH 会话中保持一个 top 窗口是排查性能问题的标配操作。
load average 三个数字分别是过去 1/5/15 分钟的平均负载。单核 CPU 上 load 超过 1.0 表示有进程在排队等待 CPU;4 核的树莓派 4B 上 load 到 4.0 才算满载。
当你知道进程名但不知道 PID 时,pgrep 比 ps + grep 更高效。pstree 则以树形结构展示进程间的父子关系,直观显示谁启动了谁。
上面的 pstree 输出清晰地展示了:systemd(1) 是根节点,它启动了 NetworkManager、sshd、cron 等服务。sshd(689) 是守护进程,当有 SSH 连接进来时它 fork 出 sshd(31198) 处理这个连接,后者再启动 bash(31245) 给用户使用。这条链路完整还原了一次 SSH 登录的进程派生过程。
查看进程只是第一步。安全工具经常需要长时间运行——比如 nmap 全端口扫描可能跑 30 分钟,aircrack-ng 抓包需要持续监听。你需要知道怎么把进程丢到后台、怎么在退出 SSH 后保持进程运行、怎么优雅地终止失控的进程。
Linux 中控制进程的核心机制是信号(signal)。kill 命令本质上不是"杀死"进程,而是向进程发送一个信号。进程收到信号后可以选择默认处理(终止、忽略、核心转储等)或执行自定义处理函数。以下是安全工作必须熟记的信号:
| 信号 | 编号 | 含义与用途 |
|---|---|---|
SIGTERM |
15 | 优雅终止(默认),进程可清理资源后退出 |
SIGKILL |
9 | 强制杀死,内核直接清理,不可被捕获或忽略 |
SIGINT |
2 | 中断,Ctrl+C 产生,通常终止前台进程 |
SIGSTOP |
19 | 暂停进程,Ctrl+Z 产生,不可被捕获 |
SIGHUP |
1 | 挂起信号,常用于通知守护进程重新加载配置 |
kill $(pgrep nmap) 发 SIGTERM,等 5 秒后如果进程还在,再用 kill -9 强杀
在命令末尾加 & 可以把命令放到后台执行,终端不阻塞。用 Ctrl+Z 暂停前台进程后,bg 放入后台继续运行,fg 调回前台。
但 & 有个致命问题:当 SSH 会话断开时,内核会向前台进程组发送 SIGHUP,后台进程收到 SIGHUP 后默认退出。这意味着你跑了一半的 nmap 扫描会随 SSH 断线而中断。
两种方案让进程在 SSH 断开后继续运行:nohup 在启动时就忽略 SIGHUP 信号;disown 把已在后台的作业从 Shell 的作业表移除,使其不再受 Shell 退出影响。
tmux attach 即可恢复。这比 nohup 更灵活——你能随时回到终端查看实时输出。前面三节处理的是"进程"——由用户手动启动、随终端关闭而消亡。但 Linux 系统上有一类特殊进程叫"服务"(service),它们在系统启动时自动运行、在后台持续监听、即使没有用户登录也保持活跃。SSH 守护进程、Web 服务器、数据库引擎都属于服务。管理这些服务的工具就是 systemd。
传统的 SysV init 采用串行启动:系统开机后 init 按 /etc/rc*.d/ 目录下的脚本顺序逐个启动服务,一台服务器启动 3-5 分钟是常态。systemd 于 2010 年发布,采用并行启动、依赖关系图、Socket 激活三大核心机制,将启动时间从分钟级压缩到秒级。Kali Linux(基于 Debian)默认使用 systemd 作为 init 系统。
如果把整个系统比作一支交响乐团,systemd 就是指挥——它决定谁先演奏(依赖关系)、谁同时演奏(并行启动)、谁在候场等待(Socket 激活)。每个乐手(服务)的乐谱就是 Unit 文件,规定了演奏的节拍和规则。
systemd 用"Unit"(单元)来管理系统资源。每种 Unit 类型有不同的后缀名,理解这些后缀是阅读 systemctl 输出的前提:
| 类型 | 后缀 | 用途 |
|---|---|---|
| Service | .service |
系统服务(最常用),如 sshd.service、nginx.service |
| Socket | .socket |
IPC/网络套接字,实现按需启动(Socket 激活) |
| Target | .target |
启动目标,一组 Unit 的集合(类似传统运行级别) |
| Timer | .timer |
定时任务,cron 的 systemd 替代方案 |
| Mount | .mount |
文件系统挂载点 |
安全工作中最常打交道的是 .service 和 .timer。攻击者持久化后门时,往往会创建一个恶意的 .service 文件设为开机自启,或用 .timer 替代 crontab 定时执行恶意脚本。审计时检查异常的 Unit 文件是发现持久化后门的关键手段。
systemctl 是与 systemd 交互的核心命令。从启动服务到查看状态、从设置开机自启到修改配置文件,所有服务管理操作都通过这一个命令完成。下面以 SSH 服务(sshd.service)为例演示全流程操作。
输出中三个关键字段:Loaded 显示 Unit 文件路径和是否设为开机自启(enabled/disabled);Active 显示当前运行状态(active/inactive/failed)和启动时间;Main PID 是主进程 PID。如果服务启动失败,status 输出末尾会显示最近几条日志,这是排查启动失败的第一手信息。
reload 和 restart 的区别是安全运维的关键:restart 会先停止再启动,期间服务不可用,已有连接全部断开;reload 只是通知进程重新读取配置文件,不中断现有连接。对 SSH 服务执行 restart 会把你自己的 SSH 连接踢掉——这在远程管理时是灾难性的。修改 sshd_config 后应该用 reload 而非 restart。
enable --now 是最常用的组合:enable 创建开机自启的符号链接,--now 立即启动服务。取消开机自启用 disable,停止并取消用 disable --now。
安全工具经常需要长期运行——比如一个 Python 编写的蜜罐服务、一个持续监听 C2 流量的分析脚本。把脚本写成 systemd 服务的好处是:开机自动启动、崩溃自动重启、日志统一收集到 journald、用 systemctl 统一管理。
Unit 文件采用 INI 格式,分为 [Unit]、[Service]、[Install] 三个主要段落。下面是一个真实的蜜罐服务配置示例——假设我们写了一个 Python 蜜罐脚本放在 /opt/honeyport/honeypot.py:
逐段解析:[Unit] 段的 After=network.target 声明此服务在网络就绪后启动(但不强制等待网络完全可用,Wants 才是硬依赖)。[Service] 段的 Type=simple 表示主进程就是 ExecStart 启动的进程本身;User=honey 以非 root 用户运行(最小权限原则);Restart=on-failure 配合 RestartSec=5s 实现崩溃后 5 秒自动重启;WorkingDirectory 设置工作目录,脚本中的相对路径以此为基准。[Install] 段的 WantedBy=multi-user.target 表示此服务在多用户模式(即正常启动)下自动运行。
systemctl daemon-reload 让 systemd 重新加载配置,然后再 restart
systemd 不仅管理服务,还统一收集所有服务的日志——这个组件叫 journald,查询工具叫 journalctl。传统的 syslog 把日志分散写到 /var/log/ 下的不同文件中,journald 把所有日志集中存储为二进制格式,支持按时间、服务、优先级、PID 等多维度过滤,查询效率远超 grep。
日志优先级从高到低:emerg(0) > alert(1) > crit(2) > err(3) > warning(4) > notice(5) > info(6) > debug(7)。-p err 会显示 err 及以上所有级别的日志,是快速排查系统异常的首选过滤器。
Storage=persistent,日志会写入 /var/log/journal/,重启后保留。取证时这个配置决定了你能否看到攻击者重启系统前的操作记录。前面七节讲的是"正常使用"。从安全视角看,进程和服务既是防御的重点也是攻击的入口。攻击者拿到初始 shell 后的第一件事就是查看进程列表(了解目标环境)、第二件事是查看服务状态(寻找提权路径)、第三件事是植入持久化后门(创建恶意 service)。防御者需要反向思考:这些操作每一步都会留下什么痕迹。
入侵检测的第一步是建立"正常基线"——在系统干净时记录正常运行的服务列表,入侵后与基线对比。以下命令在干净系统上执行并保存输出作为基线:
攻击者持久化后门的典型手法是在 /etc/systemd/system/ 或 ~/.config/systemd/user/ 下创建一个伪装成正常服务名的 .service 文件。审计时重点检查:Service 名是否伪装成系统服务(如 sshd2.service、update-daemon.service)、ExecStart 指向的路径是否在非标准目录、User 字段是否为 root。
高级攻击者会使用 rootkit 隐藏恶意进程——ps 和 top 看不到它,但 /proc 文件系统会暴露痕迹。对比 ps 输出的 PID 列表与 /proc 下的目录编号,差异即为被隐藏的进程:
这个检测方法基于一个事实:rootkit 通常 hook 了 ps 和 /proc 读取的系统调用,但两者使用的接口不完全相同。某些用户态 rootkit 只 hook 了 readdir 系统调用(影响 ps),却漏掉了 /proc 的直接目录读取,导致 /proc 下能看到隐藏进程的目录但 ps 中看不到。
ps aux 找到占用内存最大的 3 个进程;(2) 用 systemctl status 查看 ssh.service 的运行状态和 Main PID;(3) 用 journalctl -u ssh.service --since today 查看今天的 SSH 登录记录。ps aux --sort -%mem | head -4 即可按内存降序排列取前 3。pgrep -x sshd 精确匹配进程名,返回值非 0 表示进程不存在。timer 文件中设置 OnCalendar=*:0/5 实现每 5 分钟触发。