一个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命令看不到则无效
  • 恶意程序以正常进程子线程的方式运行则无效

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

6 + 11 =