作者简介:智一方,西安电子科技大学硕士研究生,主要研究方向为SDN与网络安全
一、前言
SDN的集中控制以及网络可编程的特性可以实现对网络的集中式保护,本文通过将入侵检测工具snort和floodlight控制器上的防火墙模块联动实现了一种入侵防御系统,主要的思路是将snort部署到数据平面的OVS交换机上检测OVS交换机转发的流量,根据检测规则发现危险流量,并将警告消息通过socket通信发送给floodlight控制器,或者通过创建RESTful API将警告消息以JSON数据格式传递给floodlight控制器,floodlight控制器接收到警告事件后在防火墙模块上添加相应的过滤规则,从而阻止危险流量,实现入侵防御的功能。
二、架构图
该入侵防御系统的总体架构图如下图所示:
具体而言,将snort部署到OVS交换机所在的主机上,监测交换机某端口转发和接收的流量,并与snort规则库进行对比,当检测出危险流量后会发出警告,而警告事件会存储到本地,根据SDN架构的特点,控制与转发是分离的,因此SDN控制器是无法直接读取警告事件的。这里采用添加一个守护进程的方法来解决这一问题,守护进程在OVS交换机所在的主机上运行,与snort通过unix socket进行通信,读取snort的警告消息,同时使用network socket与远端的控制器进行通信,将警告事件传递给控制器,或者通过发送HTTP请求将警告消息以JSON数据的格式发送给控制器的RESTful API上,本文对这两种通信方式都有提到,并着重对通过RESTful API进行通信的方式进行介绍,在后边提供的源码中这两种通信方式都实现了,控制器收到警告消息后,对消息进行解析向防火墙中添加过滤规则,实现了snort和防火墙的联动,从而实现了入侵防御的功能。
三、入侵检测工具snort安装
本文的入侵防御系统的相关组件的版本如下表所示:
操作系统 | Ubuntu-16.04LTS |
控制器 | Floodlight-1.2 |
交换机 | Openvswitch-2.3.0 |
模拟环境 | Mininet-2.2.0 |
snort | Snort-2.9.9.0 |
在环境部署时,各个组件都安装到同一个主机上,其中mininet采用的是源码安装的方式,笔者在安装mininet的时候,没有使用mininet自己提供的OVS交换机,而是自己安装了Openvswitch-2.3.0,具体的安装步骤均参照了官网提供的安装步骤,本文重点介绍snort的安装步骤,主要参考的也是snort官网给出的安装说明。
3.1 安装snort的相关依赖
Snort主要有4个依赖:pcap、PCRE、Libdnet、DAQ,通过以下命令进行安装。
首先应该安装build-essential包(一般情况下系统已经安装好):
1 |
sudo apt-get install -y build-essential |
然后通过下述命令安装pcap、PCRE和Libdnet:
1 |
sudo apt-get install -y libpcap-dev libpcre3-dev libdumbnet-dev |
DAQ同样有一些依赖要安装,输入如下命令:
1 |
sudo apt-get install -y bison flex |
由于snort在Ubuntu上安装需要下载一些软件包,因此创建snort_src文件夹,将相关的软件包都下载到这个文件夹下:
1 2 |
mkdir ~/snort_src cd ~/snort_src |
然后在这个文件夹下下载DAQ,执行如下步骤,下载2.0.6版本的DAQ:
1 2 3 4 5 6 7 |
cd ~/snort_src wget https://snort.org/downloads/snort/daq-2.0.6.tar.gz tar -xvzf daq-2.0.6.tar.gz cd daq-2.0.6 ./configure make sudo make install |
3.2安装snort
在Ubuntu上安装snort之前,还需要安装zlibg库,执行如下命令:
1 |
sudo apt-get install -y zlib1g-dev liblzma-dev openssl libssl-dev |
然后安装与Nghttp2相关的库文件:
1 |
sudo apt-get install -y libnghttp2-dev |
现在,我们可以下载snort的压缩包并进行安装了从https://snort.org/downloads/#上下载snort-2.9.9.0.tar.gz到snort_src文件夹中,然后解压缩,并在安装时注意要开启其unix socket通信功能,即在./configure后边加上后缀--enable-control-socket,否则unix socket通信功能不可用:
1 2 3 |
tar -xvzf snort-2.9.9.0.tar.gz ./configure --enable-control-socket make&&make install |
然后更新一个依赖库,并创建软连接:
1 2 |
sudo ldconfig sudo ln -s /usr/local/bin/snort /usr/sbin/snort |
在终端下输入snort -V命令看到如下显示,说明安装成功:
3.3 对snort进行配置
本节介绍对snort的配置步骤让其能够运行在NIDS模式,具体命令如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# 创建snort用户和用户组,这样snort不用在root权限下运行: sudo groupadd snort sudo useradd snort -r -s /sbin/nologin -c SNORT_IDS -g snort # 创建snort相关的文件夹: sudo mkdir /etc/snort sudo mkdir /etc/snort/rules sudo mkdir /etc/snort/rules/iplists sudo mkdir /etc/snort/preproc_rules sudo mkdir /usr/local/lib/snort_dynamicrules sudo mkdir /etc/snort/so_rules # 创建用来存储snort匹配规则和ip地址的文件 sudo touch /etc/snort/rules/iplists/black_list.rules sudo touch /etc/snort/rules/iplists/white_list.rules sudo touch /etc/snort/rules/local.rules sudo touch /etc/snort/sid-msg.map # 创建日志目录 sudo mkdir /var/log/snort sudo mkdir /var/log/snort/archived_logs # 调整权限 sudo chmod -R 5775 /etc/snort sudo chmod -R 5775 /var/log/snort sudo chmod -R 5775 /var/log/snort/archived_logs sudo chmod -R 5775 /etc/snort/so_rules sudo chmod -R 5775 /usr/local/lib/snort_dynamicrules # 改变这些文件的所有权 sudo chown -R snort:snort /etc/snort sudo chown -R snort:snort /var/log/snort sudo chown -R snort:snort /usr/local/lib/snort_dynamicrules |
运行如下命令讲相关的配置文件复制到我们刚才创建的文件夹中:
1 2 3 4 5 6 7 |
cd ~/snort_src/snort-2.9.9.0/etc/ sudo cp *.conf* /etc/snort sudo cp *.map /etc/snort sudo cp *.dtd /etc/snort cd ~/snort_src/snort-2.9.9.0/src/dynamic-preprocessors/build/usr/local/lib/snort_dynamicpreprocessor/ sudo cp * /usr/local/lib/snort_dynamicpreprocessor/ |
完成之后,我们需要对snort的配置文件snort.conf进行配置,根据实际需要修改ipvar HOME_NET的值,将其修改为需要保护的网络,并对如下值进行修改,让其snort在启动时读取我们刚刚创建的相关文件:
1 2 3 4 5 |
var RULE_PATH /etc/snort/rules var SO_RULE_PATH /etc/snort/so_rules var PREPROC_RULE_PATH /etc/snort/preproc_rules var WHITE_LIST_PATH /etc/snort/rules/iplists var BLACK_LIST_PATH /etc/snort/rules/iplists |
为了测试我们首先将snort.conf的所有的include $RULE_PATH全部注释掉,除了include $RULE_PATH/local.rules,这样在测试时我们可以手动在/etc/snort/rules/local.rules文件中添加自定义的规则。
四、守护进程的编写
守护进程的作用主要有两点:和snort通信以及和远端的控制器通信,和snort通信主要通过unix socket进行通信,和远端的控制器通信有两种方式:通过network socket或者通过发送HTTP请求,前者需要在控制器上编写相应的socket服务端,后者需要在控制器上编写相应的RESTful API,守护进程的编写使用python语言,用到了ryu的相关库,因为需要对snort传递的数据包的消息进行解析,用pip install ryu命令即可安装ryu相关库,篇幅有限,这里仅给出核心代码。
1 2 3 |
if __name__ == '__main__': server = SnortListener() server.start_recv() |
主程序就创建一个SnortListener类的实例然后调用start_recv()方法,start_recv()方法中创建了unix socket服务端,而此时snort是作为unix socket的客户端存在的,一下是start_recv()方法的代码:
1 2 3 4 5 6 7 8 |
def start_recv(self): if os.path.exists(SOCKFILE): os.unlink(SOCKFILE) self.unsock=socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self.unsock.bind(SOCKFILE) logger.info("Unix Domain Socket listening...") self.recv_loop() |
这里调用了recv_loop()方法,其主要作用是接收snort发送的alert消息并将其发送给远端的控制器,代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
def recv_loop(self): self.start_send() while True: data = self.unsock.recv(BUFSIZE) time.sleep(0.5) if data: logger.debug("Send {0} bytes of data.".format (sys.getsizeof(data))) self.send(data) else: pass |
这里的start_send()方法主要是创建了一个network socket的客户端用于和控制器上的服务器进行连接通信,最后调用self.send()方法将警告消息发送,send方法内容如下:
1 2 3 4 5 6 7 8 |
def send(self, data): data2 = data[:BUFSIZE] msg = alert.AlertPkt.parser(data2) s1= '%s' % ''.join(msg.alertmsg)# s2=self.packet_print(msg.pkt,s1) #self.nwsock.sendall(s2) self.send_json(s2) logger.info("Send the alert messages to floodlight.") |
这里可以选择使用nwsock.sendall()方法通过socket通信来发送警告消息,或者用send_json()方法,这样可以直接通过http请求将消息经过RESTful API传递给控制器。
五、Snort联动模块编写
在控制器上需要创建相应的模块来接收守护进程传递的警告消息,接收警告消息的方式有两种:创建socket服务端与守护进程进行socket通信以及创建RESTful API接收守护进程发送的JSON数据。
首先在控制器上新建一个类并实现IFloodlightModule接口,由于本模块不对外提供服务因此getModuleServices()和getServiceImpls()方法不用填写,本模块需要依赖IFirewallService和IRestApiService因此需要在getModuleDependencies()方法中添加相关依赖:
1 2 3 4 5 6 7 8 |
// 填写依赖的模块 @Override public Collection<Class<? extends IFloodlightService>> getModuleDependencies() { Collection<Class<? extends IFloodlightService>> l = new ArrayList<>(); l.add(IFirewallService.class); l.add(IRestApiService.class); return l; } |
并需要在init()方法中进行初始化:
1 2 3 4 5 6 |
// 通过context将依赖的模块初始化 @Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { firewall = context.getServiceImpl(IFirewallService.class); restApi = context.getServiceImpl(IRestApiService.class); } |
如果采用的是调用RESTful API的方式,则需要在startUp()方法中写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public void startUp(FloodlightModuleContext context) throws FloodlightModuleException { // 注册本模块的RESTful API restApi.addRestletRoutable(new SnortWebRoutable()); // 打开防火墙模块 if(!firewall.isEnabled()){ firewall.enableFirewall(true); // 添加一条允许通过的规则 FirewallRule rule = new FirewallRule(); rule.priority=2; rule.action = FirewallAction.ALLOW; rule.ruleid=rule.genID(); firewall.addRule(rule); } } |
即创建创建RESTful API需要通过IRestApiService注册一个SnortWebRoutable类,在这个类中定义具体的RESTful API接口,并且需要打开Firewall功能并注意添加一条允许任何流量通过的规则,否则默认过滤所有网络流量,SnortWebRoutable类具体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class SnortWebRoutable implements RestletRoutable{ // 将相应的资源类与URL关联 @Override public Restlet getRestlet(Context context) { Router router = new Router(context); router.attach("/alerts/json",AlertResource.class); return router; } @Override public String basePath() { return "/wm/snort"; } } |
即通过上述的方式定义一个资源类,将其通过Router类去一个特定的URL绑定在一起,这样外界就能通过这个URL来访问这个资源类,这个资源类中主要有这样一个方法:
1 2 3 4 5 6 7 |
@Post public String addRule(String json){ IFirewallService firewall = (IFirewallService)getContext().getAttributes().get(IFirewallService.class.getCanonicalName()); FirewallRule rule = jsonToFirewallRule(json, id++); firewall.addRule(rule); return "{\"status\":\"success\"}"; } |
这个方法就是用来处理接收守护进程发来的JSON数据格式通过解析,将其转化为FirewallRule,然后将这个过滤规则添加,即实现了入侵防御的功能。
如果采用socket通信的方式,则在startUp()方法中做写入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public void startUp(FloodlightModuleContext context) throws FloodlightModuleException { // 打开防火墙模块 if(!firewall.isEnabled()){ firewall.enableFirewall(true); // 添加一条允许通过的规则 FirewallRule rule = new FirewallRule(); rule.priority=2; rule.action = FirewallAction.ALLOW; rule.ruleid=rule.genID(); firewall.addRule(rule); } SnortThread snortThread = new SnortThread(firewall); snortThread.start(); } |
同样需要开启防火墙功能,并添加一个允许全部流量通过的规则,不同的是这里新建了一个SnortThread线程,并注意将在init()方法中初始化的firewall对象传递进去,在这个新建的线程中建立socket服务端与守护进程进行连接,并对警告消息进行解析,然后通过firewall来添加相应的规则,socket通信具体的实现的例子很多,本文不在赘述。
六、测试
这一部分进行测试,主要实现的是对nmap扫描的防护,比如nmap由Null扫描的方式,为了防护这种扫描可以在local.rules文件中添加如下的过滤规则:
1 |
alert tcp any any -> $HOME_NET any (msg:"NULL Scan"; flags: 0;GID:1; sid:10000006; rev:001;) |
通过设置这样的规则,当恶意主机通过nmap进行Null扫描时,snort就会产生报警。
在终端输入如下命令:
1 |
sudo mn --controller=remote,ip=127.0.0.1,port=6654 --switch ovsk,protocols=OpenFlow13 |
这样可以创建一个简单的拓扑图:
假设ip地址为10.0.0.2的主机为攻击者,ip地址为10.0.0.1的主机为受保护主机,此时查看网卡信息会得到如下信息:
s1-eth1则指的是交换机的1号端口,s1-eth2指的是2号端口,我们通过如下命令将让snort监听交换机的2号端口:
1 |
sudo snort -c /etc/snort/snort1.conf -A unsock -l /tmp/temp1 -i s1-eth2 |
其中-c是指要读取的具体的配置文件,这样可以让snort运行在IDS模式下,-A unsock是打开snort的unix socket通信功能,-l则指定了unix socket通信时需要绑定的文件,注意要和守护进程中的绑定文件保持一致,-i则指需要监听的具体网卡,我们指定到交换机的2号端口上。然后我们通过python命令运行守护进程,在mininet中输入xterm h2命令打开h2的虚拟终端。
然后在终端上输入nmap -sN 10.0.0.1对受保护主机进行扫描(注意xterm与本机公用一套文件系统,因此需要在本机上安装nmap),此时发现守护进程将报警消息发送给控制器。
在控制器的日志中我们也看到了相应的日志消息(这里演示的是使用RESTful API的方式)。
并且此时转发模块已经开始过滤这些威胁流量,我们再从Web UI界面查看,发现已经添加了相应的防火墙过滤规则。
这样就实现了简单的入侵防御功能。
七、总结
本实验实现了snort和floodlight控制器的联动,之前也看到过ryu控制器与snort的联动,本文也参考了相关的内容并在floodlight控制器上实现了类似的功能,最后进行了测试,当然也可以基于这种方法编写其他的snort过滤规则或者下载snort官方的规则库来实现更复杂的入侵防御功能,篇幅有限许多实现细节并没有全部展开,如果有兴趣欢迎大家和我交流。
最后附上完整代码链接:
链接:https://github.com/ZhiYiFang/Intrusion-prevention-system-based-on-floodlight