☰
应急响应服务排查实战:从systemctl到进程端口的恶意服务定位
2026/10/20 16:55:09 网站建设 项目流程

1. 应急响应中服务排查的定位与核心思路

先说个场景。前段时间有个客户服务器中招,业务进程被注入,CPU飙到98%。我们上机排查后发现,攻击者通过一个“正常”的系统服务维持住了权限——这个服务伪装成内核日志模块的守护进程,名字极具欺骗性,如果不做服务层面的系统性排查,很容易忽略过去。处理完之后客户问了一句话:你们是怎么在几百个服务进程里找到这个异常的?这其实就是服务排查要回答的核心问题。

在应急响应的整个流程里,服务排查属于持久化分析的关键环节。攻击者在拿下服务器之后,为了确保权限不掉、后续能随时回来,往往会植入各类服务:自定义systemd服务、计划任务拉起脚本、守护进程二次孵化、rc.local后门等。服务排查就是把这些藏匿在系统“正常运转体系”里的异常组件挖出来。

这个排查动作解决三个问题:

  • 系统里当前运行了哪些服务,有没有不该出现的新面孔
  • 哪些服务被设置为开机自启动,启动链路上有没有可疑节点
  • 异常进程、异常端口、异常文件背后的“服务归属”是谁,方便顺藤摸瓜找到原始落地位置

适合参考这篇内容的人,是刚入门的安全运维人员、红队/蓝队新人、以及日常负责服务器维护但没系统整理过排查方法的工程师。这篇文章不会讲太深的内核原理,重点是把思路捋通、命令吃透、坑踩平。

2. 第一波实操:批量盘点系统服务状态

2.1 先看正在运行的服务清单

登录服务器后别急着翻日志,先把当前运行的服务列表拉出来。这是整个排查的地基。

systemctl list-units --type=service --state=running --no-pager

这是我最常用的第一条命令。它会列出所有处于running状态的service单元,包含服务名称、Loaded状态、Active状态、以及简要描述。

实际排查时,我建议再加一个参数:

systemctl list-units --type=service --all --no-pager

带上--all会把那些active的、inactive的、failed的全部显示出来,你才能看到一些“启动失败但依然残留在启动链路上”的服务。这很有用——攻击者写了自启动服务,但服务脚本有语法错误导致启动失败,恰好暴露了他植入的痕迹。

拿到清单之后,第一步不是一个个去看,而是做“新旧对比”。如果平时有保存服务基线,直接diff对比最省事。没有基线的话,就靠经验和直觉筛选,重点看:

  • 服务名称长得可疑的,比如随机字符串、拼写错误的系统词、带空格或特殊符号的
  • 描述信息明显与系统组件不符的
  • 同一服务存在多个实例的(正常的sshd只有一个,出现第二个sshd服务就该警惕了)
  • 运行用户不是root却拥有高权限行为特征的服务

小技巧:把输出保存到文件里,后面排查完再快照一次对比,看异常服务有没有被处理干净。

2.2 摸清楚哪些服务是开机自启的

运行状态只代表当下活着的东西,自启动链路才是决定“重启后是否还会被拉起”的关键。攻击者经常耍小聪明——当前进程不在运行,但自启动项已经写好了,重启机器后自动进入运行态。所以排查自启动是必修课。

systemctl list-unit-files --type=service --state=enabled --no-pager

这条命令列出所有enabled状态的服务单元文件,即系统重启时会被自动拉起的服务。

排查自启动时注意这几个细节:

  • /etc/systemd/system/目录下新出现的service文件:这是管理员手动放置或程序安装时生成的目录,攻击者也常在此写入服务文件。
  • /lib/systemd/system/和/usr/lib/systemd/system/目录:系统安装包自带的单元文件一般在这里,如果发现某个文件的修改时间非常新,或者文件名与已知软件包不匹配,要重点核查。
  • 服务文件的[Install]部分里,WantedBy或RequiredBy字段决定了服务挂在哪个target下。攻击者如果把服务挂在multi-user.target.wants下,开机就能拉起。

只看命令输出还不够,要对可疑服务使用status命令看完整信息:

systemctl status 可疑服务名 --no-pager

这个输出里有Loaded的单元文件路径、Active的运行状态、Process的主进程ID和命令行、以及最近几条日志。一条命令就能把服务的静态定义、动态状态、进程信息、日志摘要全带上,是排查的“主力工具”。

  1. 结合定时任务的隐性自启

服务排查另一个容易漏的地方是systemd timer。很多攻击者不用传统的service自启动,而是通过timer单元周期性触发恶意脚本。这本质上是“定时任务形式”的服务自启动。

systemctl list-timers --all --no-pager

把timer单元列出来,比对触发时间和对应unit文件。如果看到一个timer关联的service指向了/tmp目录下的脚本,或者指向某个不常见的路径,基本可以确定是异常持久化。

这一段不展开太多,后面3.3会结合具体的深挖技巧再讲。

3. 结合端口与进程,反向锁定可疑服务

3.1 查端口对应的服务是什么

很多情况是:业务反馈“服务连不上了”,或者安全设备报警“某IP在扫描内网”,到了服务器上一看,发现某个端口在监听,但netstat输出里显示的进程名很陌生。这时候就开始端口反向排查。

ss -tlnp

ss命令比netstat更高效,在连接数大的服务器上也稳定,推荐直接用。-tlnp表示列出tcp监听端口并显示进程信息。

看到可疑端口后,用进程PID进一步确认:

ps -fp PID

或者直接看该进程的完整启动命令行和工作目录:

ls -l /proc/PID/exe cat /proc/PID/cmdline | tr '\0' ' ' ls -l /proc/PID/cwd

这三个命令分别能看到:进程的可执行文件真实路径、完整的命令行参数(包括被截断的部分)、以及进程的工作目录。很多恶意服务为了让脚本能读取配置文件,会把工作目录故意设置成某个隐蔽目录,这一步能直接看到。

有一个非常容易踩的坑:某些攻击者通过进程名称替换、argv伪装等方式,让ps里看到的进程名是一个正常名字(比如sshd),但/proc/PID/exe指向了真正可执行文件。所以永远不要只信ps的输出,一定要看exe软链接指向哪。

3.2 通过端口找到服务后,要看它的“关联生态”

锁定了PID之后,千万别急着kill。一个服务往往不是孤立的——它可能有守护父进程、有同伙子进程、还定时拉起别的脚本。先把这个服务周围的“生态”摸清楚:

pstree -ap PID

pstree能直观展示进程树:谁是这个进程的父进程,它又孵化出了哪些子进程。如果发现异常进程的父进程是PID为1的systemd,说明它是独立启动的service;如果父进程是某个常见业务进程(比如nginx、apache),则存在“借壳启动”的可能。

还要看一下端口的用户归属:

ss -tlnp | awk 'NR>1 {print $1, $2, $4, $NF}'

统计所有监听端口的进程用户,正常系统里的监听服务通常集中在root、www-data、mysql等少数几个账户。如果发现某个不该有监听权限的用户(比如nobody、games)持有了某个端口,这是很大的异常信号。

3.3 定位启动源头:这在隐秘服务排查里很关键

单个进程看的差不多了,就要回答“这个服务是谁拉起来的”。最常见的拉起点:systemd service、系统启动脚本、计划任务、其他进程二次孵化。

快速溯源的办法:

systemctl status PID # 如果PID对应的是systemd管理的服务,会直接显示单元名称

这个命令的输出会明确告诉你:这个进程是不是由某个systemd服务单元拉起的,单元文件在哪,enable状态如何。

如果输出是“Unit xxx.service could not be found”或者直接提示不是systemd管理的进程,那就说明它可能是被某个脚本或进程手动拉起的。这时候查两个位置:

  • /var/spool/cron/和/etc/crontab:计划任务里有没有对应的启动语句
  • /etc/rc.local或/etc/rc.d/rc.local:传统自启动脚本里有么有可疑行

我还习惯配合排查inetd/xinetd服务。虽然现代系统很少用,但一旦被攻击者利用,会非常隐蔽:

grep -r "可疑路径" /etc/xinetd.d/ 2>/dev/null

某次实际项目中,我们发现恶意服务进程的父进程是PID 1,但systemctl里查不到任何unit信息。最后通过审计/etc/systemd/system/下所有service文件的修改时间,锁定了一个晚上2点被写入的.service文件——里面的ExecStart指向了/opt目录下的一个加密脚本。这就是服务排查的典型链路:进程→端口→文件→启动定义,一层层剥开。

4. 服务单元文件与开机自启动的深挖技巧

4.1 服务文件里最容易藏东西的字段

systemd服务单元文件的排查,不能只看文件名是否可疑。很多攻击者会直接修改已有的服务定义,而不是新建文件,这样文件名看起来完全正常。

拿到可疑的服务文件后,用cat或systemctl cat把完整内容读出来:

systemctl cat 服务名 --no-pager

重点看这几个字段:

  • ExecStart:这行是服务启动时执行的命令。常见伪装手法是正常的服务后追加一段命令,比如/usr/sbin/sshd -D; curl xxx | bash。分号、&&、|这些符号出现在ExecStart里,就是强烈的异常信号。
  • ExecStartPre/ExecStartPost:部分攻击者会把恶意动作放在这两个钩子里,表面ExecStart是正常的,但pre/post阶段已经执行了恶意命令。
  • EnvironmentFile/Environment:有些服务会从配置文件里读取环境变量,攻击者可以在环境变量里指定恶意库文件路径,配合LD_PRELOAD实现动态库注入。
  • User/Group:如果服务运行在root下但路径指向了/tmp或/var/tmp,那基本可以断定是异常项。正常系统服务不会从/tmp目录加载可执行文件。

我自己排查的时候,还习惯用systemctl show看服务的所有属性:

systemctl show 服务名 --no-pager | grep -E "(ExecStart|User|Group|FragmentPath|Environment)"

show输出的信息比cat更结构化,还能看到一些默认值和继承属性。

4.2 服务文件的属性、修改时间和所有权

服务单元文件本身是文本文件,攻击者编辑它不需要编译,所以文件属性和时间戳就是重要线索。

stat /etc/systemd/system/xxx.service

重点关注:

  • Modify时间:服务文件什么时候被修改过。如果系统运行了很久,服务文件的修改时间却是最近几天,即使内容看起来正常也要仔细审。
  • Uid/Gid:文件owner不是root(而是某普通用户)的服务文件,极可能是攻击者留下的。
  • Size大小异常:几十KB的service文件本身就不正常,正常服务定义一般只有几行到几十行配置。

我还会做一次全盘扫描,找最近7天内新增或修改过的service文件:

find /etc/systemd/system/ -name "*.service" -mtime -7 -ls find /usr/lib/systemd/system/ -name "*.service" -mtime -7 -ls

这台机器上如果没有任何正常的软件更新操作,却出现新service文件,优先级直接提到最高。

再搭配一个技巧:把系统中所有service文件的哈希值记录下来,与同版本操作系统的一份干净源码hash列表比对。虽然大部分场景不需要这么重的操作,但对于那些管理严格的高安全环境,这是最可靠的做法。

4.3 溯源与清理时必须保存原始证据

发现异常service文件后,先别急着删除。正确做法是三步走:

  1. 先停服并禁止自启:
systemctl stop 服务名 systemctl disable 服务名
  1. 复制原始文件留证:
cp /etc/systemd/system/xxx.service /取证目录/xxx.service.bak
  1. 再做完整取证(hash、时间戳、内容),然后才决定是否删除或修复。

提示:如果只是简单删掉service文件,但服务还在运行,systemd检测到unit文件消失后行为可能会异常。所以顺序一定是先stop再disable,最后才移除文件。

5. 日志联动与异常服务时间线还原

5.1 journalctl查指定服务的运行痕迹

服务排查不能只看“当前状态”,还要看“历史轨迹”。攻击者的服务启动后,总会在系统日志里留下痕迹。systemd的journal是排查的第一手资料。

journalctl -u 服务名 --no-pager -n 200

这条命令查看指定服务最近的200条日志。-n控制条数,可自行调整。排查时我喜欢把时间范围扩大到最近7天:

journalctl -u 服务名 --since "7 days ago" --until "now" --no-pager

通过日志能看到:

  • 服务第一次启动的具体时间点,如果是攻击者植入的,这个时间往往与入侵事件的时间窗口吻合
  • 服务启动后的报错信息,有些恶意脚本会因为环境不兼容或路径错误在日志里留下堆栈或命令回显
  • 服务重启的次数和触发原因,异常频繁重启说明有watchdog机制或守护进程在拉起它

一个容易忽视的操作:查看某个时间点的全系统日志,把攻击者可能执行过的操作串成时间线。

journalctl --since "2024-xx-xx 01:00" --until "2024-xx-xx 01:30" --no-pager

结合入侵检测设备给出的攻击时间,精确查看这个时间窗口内系统发生了哪些服务启动、进程创建、用户登录行为。这是把“服务排查”和“入侵溯源”结合起来的关键动作。

5.2 auth日志与last日志里的挂账痕迹

服务排查经常要和登录日志交叉验证。攻击者植入了服务,但服务本身是给谁用的?留了什么后门账号?这些信息藏在认证日志里。

last -f /var/log/wtmp | head -50 grep "Accepted" /var/log/auth.log | tail -50 # Debian/Ubuntu grep "Accepted" /var/log/secure | tail -50 # CentOS/RHEL

排查时我会重点看两点:

  • 异常时间点的登录来源IP:和恶意服务的启动时间做相关性比对。如果那个IP往这台机器投递过payload,同时这个时间点出现了一个新服务,基本可以锁定攻击链路。
  • 登录账号是否有新增:查看/etc/passwd的最后几个账户,以及/etc/sudoers有没有新加条目。很多攻击者通过服务自启动创建新用户来维持权限,这个动作比单纯的服务植入更持久。

有一次排查,我们发现一个恶意服务每次重启后都会创建一个useradd命令,把用户名写入/etc/passwd。服务本身是“壳”,真正的持久化目标是账号。所以排查服务时,同步查看账号变更、sudo权限变更,才能把“服务排查”的深度落到实处。

5.3 时间线还原:服务文件时间、进程启动时间与入侵时间的碰撞

把服务排查获得的时间信息集中起来,构建攻击事件时间线。主要采集三类时间:

时间类型采集命令排查价值
服务文件创建/修改时间stat服务文件判断服务植入时间点
服务进程启动时间ps -eo pid,lstart,cmd | grep 服务名精确到秒的启动时间,和入侵告警时间对比
监听端口建立时间ss -tlnp结合/proc/PID下的fd目录判断是否需要关注某个端口开放的时间顺序

ps输出中的lstart参数非常实用,它会显示进程启动的确切日期和时间(不同于默认只显示启动后经过的时间)。用法:

ps -eo pid,ppid,lstart,user,cmd | grep -E "(可疑进程名|PID)"

当这三个时间点都指向同一个时间窗口时,事件归因就基本清晰了。比如:

  • 攻击告警发生在5月12日02:15
  • /tmp/某脚本文件的mtime是5月12日02:17
  • 异常服务进程lstart是5月12日02:18
  • 服务文件的stat修改时间是5月12日02:16

这四个时间点连在一起,还原的完整链条就是:攻击者入侵→写入脚本→建立service文件→启动服务。排查报告这样写,既清晰又有说服力。

6. 确认异常后的处置流程与常踩的坑

6.1 从发现问题到隔离处置的标准动作

确认某个服务是恶意服务后,别急着删文件。建议严格按下面的顺序走:

# 第一步:立即停止服务 systemctl stop 服务名 # 第二步:取消开机自启 systemctl disable 服务名 # 第三步:记录pid和进程参数,保留现场 ps -fp $(pgrep -f 服务名) > 取证文件.txt 2>&1 ls -l /proc/PID/exe >> 取证文件.txt # 第四步:复制恶意服务文件和关联文件 cp -r /etc/systemd/system/服务名.service /取证目录/ cp -r 恶意文件所在目录 /取证目录/ # 第五步:杀掉残留进程 kill -9 PID

停服之后还要检查这个服务有没有衍生出来的“附加产物”,常见的有:

  • 服务脚本里指定的数据目录或日志目录
  • /var/spool/cron/下新出现的任务文件
  • 服务启动时新建的动态链接库文件
  • 被服务拉起的子进程或兄弟进程

这些衍生物如果不清理,可能下次系统重启时通过其他路径再次复活。所以杀完服务后,我会做一个快照对比,确认监听端口清干净了、相关进程消失了、开机启动项里没有残留。

6.2 最隐蔽的三个坑

第一个坑:systemd服务被mask。mask和disable不一样,mask会把服务单元链接到/dev/null,让任何试图启动它的行为全部失败。如果你发现某服务被mask了,先别误认为异常——有些加固脚本或安全软件会mask不需要的服务。但如果排查中发现某个常见服务莫名其妙被mask,且系统原来没做这个操作,就要怀疑是不是攻击者故意mask掉安全服务(比如禁用防火墙、禁用日志收集)。

systemctl list-unit-files --state=masked --no-pager

第二个坑:修改现有服务的ExecStart。攻击者不新建服务文件,而是直接改/usr/lib/systemd/system/下的一个已有服务,把ExecStart改成“正常命令+恶意命令”的拼接形式。这时候你如果只看服务名,完全发现不了问题;只有把内容拉出来逐行读,才能看到拼接的痕迹。所以我排查服务的时候,不会只看文件列表,一定会抽查几个看似正常的服务文件内容。

第三个坑:残留的SysV init脚本。很多教程只在systemd层面排查,但有些版本的系统、某些第三方软件包还会使用/etc/init.d/下的脚本做自启动。攻击者手动安装了一个init脚本,并且在/etc/rc3.d/或/etc/rc5.d/下建立了S开头的链接,那么即使systemd里查得干干净净,系统重启后服务照样会被拉起来。

ls -l /etc/rc*.d/ | grep "^S" | grep -v "K" | awk '{print $NF}'

还有更古老的/etc/inittab虽然现代系统基本弃用,但排查老旧系统时仍要看一眼。

6.3 复盘清单:一次标准服务排查该覆盖哪些面

把服务排查的经验整理成一张可复用的清单,每次上机排查时对照着做,不容易漏项:

排查维度关键命令结果要求
当前运行服务systemctl list-units --type=service --state=running能识别每个服务归属
自启动服务systemctl list-unit-files --state=enabled无新增/无异常项
异常服务单元systemctl status 可疑服务明确单元文件路径及启动命令
端口监听ss -tlnp所有端口有明确进程归属
进程真实性ls -l /proc/PID/exe可执行文件路径无伪装
定时任务关联systemctl list-timers无指向异常路径的timer
服务文件时间find /etc/systemd/system/ -name "*.service" -mtime -7最近改动均有解释
SysV脚本ls -l /etc/rc*.d/无新增S开头脚本
日志时间线journalctl -u 服务名、last时间窗口完整闭合

这张表每次排查都用得上。不过实际现场情况千变万化,这张表是底册,面对具体环境还需要灵活延伸。

最后的实战体会

我刚开始做应急响应那会儿,看到一堆服务列表就懵,不知道从哪下手。后来一个前辈跟我讲了一句点醒的话:别急着查毒,先把系统的“正常样子”摸清楚。这句话到现在都是我排查服务的第一原则。你连正常的服务清单、正常的开机启动项、正常的端口映射都没有概念,就谈不上找异常。

现在的恶意服务植入手段越来越“精细”,有的甚至连服务描述、依赖关系都伪造得很像正常组件。但无论如何,它必然在某个维度留下破绽——可能是文件修改时间不对,可能是启动路径指向了/tmp,可能是父进程链不合理,也可能是日志时间线对不上。服务排查本质上是“找不同”的过程,把系统当前状态和应有状态做对比,异常自然浮现。

最后分享一个自己一直在用的习惯:每排查一台服务器,就把关键命令输出存成一份带时间戳的备份文件,放到项目目录里。哪怕当时没发现异常,这些资料后面排查新问题时也经常能派上用场。另外就是养成看/proc目录的习惯,很多伪装都能在那里被识破。这些琐碎的积累,关键时刻能救命。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询