一个linux下限制进程的小脚本
缘起
最近在做一门课程设计,需要实现这样的功能:在linux操作系统中部署一个网站,当网站部署完毕后,监控操作系统中的进程,杀死所有不在白名单中的进程,以防止恶意程序运行。大概搜索了下,没有找到现成可用的解决方案,决定自己实现。
设计
在linux中有许多系统必须的进程,若将这些进程都纳入白名单则白名单的配置会很繁琐。
简单起见,我们选择在操作系统启动完成、稳定运行后再运行限制进程的程序,检测新生进程,若不在白名单中则杀死该进程。
已经运行了较长时间的进程则被默认为系统进程而不做处理。
如何检测新生进程?用“ps -aux”命令查看进程,可以看到有名为START的列为进程启动时间,通过启动时间可以得知进程是否新生。该命令输出如下所示:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 34124 4660 ? Ss 18:09 0:01 /sbin/init
root 2 0.0 0.0 0 0 ? S 18:09 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 18:09 0:00 [ksoftirqd/0]
root 5 0.0 0.0 0 0 ? S 18:09 0:00 [kworker/0:0H]
root 7 0.0 0.0 0 0 ? S 18:09 0:01 [rcu_sched]
但“ps -aux”命令输出的内容太多了,我们实际上只需要PID、START和COMMAND这三列信息就足够了。
用“man ps”查看“ps”命令的帮助,发现竟然有一千多行,还真是个强大的命令。从man手册中,我找到了想要的参数“ps -eo “%p%t%c””,该命令输出如下所示:
PID ELAPSED COMMAND
1 50:59 init
2 50:59 kthreadd
3 50:59 ksoftirqd/0
5 50:59 kworker/0:0H
7 50:59 rcu_sched
8 50:59 rcu_bh
9 50:59 migration/0
10 50:59 watchdog/0
PID和COMMAND都在,START没有了,取而代之的是ELAPSED——运行时长,这样处理起来就更方便了。
读取各个进程的信息,若某个进程的ELAPSED小于一个阈值,则判断该进程是否位于白名单中,若不在白名单中,则杀死它。
如何杀死一个进程呢?我们通过“ps”命令已经知道要杀死的进程的PID了,所以用kill命令就可以杀死进程。
kill命令实际上是在向目标进程发送信号,不加任何参数默认发送的信号是SIGTERM,这一信号是可以被目标进程忽略的,所以这里要加上参数“-KILL”,发送无法忽略的信号SIGKILL,这样能更为有效地杀死进程。
当然,为了避免没有权限而无法杀死进程,杀死进程的操作要有root权限。
进程是在不断产生的,进程检测也需要不断进行。如何周期性地检测进程呢?写一个死循环并sleep当然是一种方法,但在linux中有更方便地crontab(定时任务)。假设我们检测、限制进程的脚本名叫“pctrl.py”(是的,我是用Python实现的),放在目录“/opt/”中,则可以这样做:先用命令“sudo su”切换到root身份,然后执行命令“crontab -e”来编辑定时任务的配置文件,在其中加入一行,内容为:
*/1 * * * * /usr/bin/python /opt/pctrl.py
保存并退出后脚本“pctrl.py”便会以root权限每分钟被执行一次。分钟级是crontab所能达到的最小的时间粒度,想要更小的时间粒度,只能在脚本中实现了。例如,我想要每0.5秒检测一次,则脚本中应该有一个120次的循环,每次循环sleep半秒。
最后,日志也是很关键的。所有尝试杀死的进程都可能是恶意程序,记录日志是必要且有价值的。我们设计日志中输出时间、进程号、程序名(COMMAND)和是否成功杀死它,若失败,则也输出失败原因(失败原因即kill命令的输出)。
实现
用Python来实现上述设计。Python执行系统命令使用了库commands,用该库执行系统命令可以返回命令执行结果状态和命令输出,这些都是我们需要的。脚本“pctrl.py”内容如下:
#!/usr/bin/python
# ^_^ coding:utf8 ^_^
###############################################
# sudo su #
# crontab -e #
# */1 * * * * /usr/bin/python /opt/pctrl.py #
###############################################
import os
import sys
import time
import logging
import commands
#配置日志,根据实际情况修改日志路径
logging.basicConfig(level=logging.DEBUG,
format='[%(asctime)s] [%(levelname)s] %(message)s',
datefmt='%Y.%m.%d %H:%M:%S',
filename='/var/log/pctrl.log',
filemode='a+')
#配置白名单,根据实际需要修改白名单内容
white_list = ['/usr/sbin/apache2 -k start']
#杀死进程
def killprocess(pid, comm):
(status, output) = commands.getstatusoutput('kill -KILL ' + pid)
if status == 0:
logging.info('Successfully killed the process ' + pid + ': ' + comm)
elif 'kill: No such process' not in output:
reason = output.replace('\n', '')
logging.warning('Failed to kill the process ' /
+ pid + ': ' + comm + ': '+ reason)
#检测进程
def monitorprocess(timelimit):
(status, output) = commands.getstatusoutput('ps -eo "%p_%t_%c"')
if status == 0:
processes = output.split('\n')[1:]
for process in processes:
process = process.replace(' ', '').split('_')
pid = process[0]
time = process[1].split(':')
time = int(time[0])*60 + int(time[1])
comm = process[2]
if str(os.getpid()) == pid:
#不杀死自己
continue
if comm in white_list:
#不杀死在白名单中的程序
continue
if time < timelimit:
killprocess(pid, comm)
else:
logging.warning('The process of obtaining information failed')
if __name__ == '__main__':
for i in range(0, 120):
monitorprocess(60)
time.sleep(0.5)
测试
将上述脚本部署到测试用的安装了Apache服务器的虚拟机中进程测试。
部署完成后确实无法启动各种耗时较长的程序,而类似ifconfig这样一瞬间就能执行完毕的程序则不受影响。
从另一台机器访问测试虚拟机中的网站,发现Web服务是不受影响的。
查看“pctrl.py”的日志如下所示:
test@test-VirtualBox:~$ cat /var/log/pctrl.log
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6270: crontab
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6271: sh
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6272: sensible-editor
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6280: select-editor
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6294: cron
[2017.09.20 19:41:01] [INFO] Successfully killed the process 6295: sh
[2017.09.20 19:41:04] [INFO] Successfully killed the process 6327: crontab
[2017.09.20 19:41:04] [INFO] Successfully killed the process 6328: sh
[2017.09.20 19:41:04] [INFO] Successfully killed the process 6329: sensible-editor
[2017.09.20 19:41:04] [INFO] Successfully killed the process 6337: editor
[2017.09.20 19:41:37] [INFO] Successfully killed the process 6614: firefox
[2017.09.20 19:41:37] [INFO] Successfully killed the process 6620: firefox<defunct>
[2017.09.20 19:41:42] [INFO] Successfully killed the process 6680: gnome-screensho
若不关闭crontab而直接重启操作系统,很可能会导致系统无法重启。所以在关机前,一定要先关闭crontab。
总结
整个脚本写完后自己很不满意,怎么看都很幼稚。我虽然用了两年Ubuntu,但并不了解其细节,所以只能想出这种水平的实现方法。
这种方式有诸多缺点:
- 部署、关闭都很不方便
- 要频繁检测,不是触发式的,消耗资源较多
- 恶意程序开机时就运行则无效
- 恶意进程在0.5秒内执行完毕则无效
- 恶意进程被隐藏,用ps命令看不到则无效
- 恶意程序以正常进程子线程的方式运行则无效