网络安全课程实验报告
《Linux Firewall Exploration Lab》
Overview
了解防火墙是如何工作的,并实现一个简化的数据包过滤防火墙。
防火墙有多种类型,在这个实验中,我们专注于数据包过滤器。数据包过滤器检查数据包,并根据防火墙规则决定是丢弃还是转发数据包。通常是无状态的,只根据该包中包含的信息来过滤每个包,而不注意一个包是否是现有流量的一部分。包过滤器通常组合使用包的源地址和目标地址、协议,对于TCP和UDP流量,还使用端口号。此外,学生们还将学习如何使用SSH隧道来绕过防火墙。
**防火墙(英语:Firewall)**是目前最重要的一种网络防护设备,是位于两个或多个设备间,实行网络间访问控制的硬件或软件。可以隔离网络,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
Lab Tasks
Task 1: Using Firewall
任务说明:
使用Linux的iptables软件,在两台虚拟机之间进行防火墙实验。
iptables介绍:
iptabels是与Linux内核集成的包过滤防火墙系统,软件的结构是
i
p
t
a
b
l
e
s
→
T
a
b
l
e
s
→
C
h
a
i
n
s
→
R
u
l
e
s
.
iptables \to Tables \to Chains \to Rules.
iptables→Tables→Chains→Rules.
iptables有四种内建表table,分别是Filiter,NAT,Mangle,Raw。每种表内有多种链chain,每个链上可以定义多个规则rule。
规则(rules)其实就是网络管理员预定义的条件,规则一般的定义为“如果数据包头符合这样的条件,就这样处理这个数据包”。规则存储在内核空间的信息包过滤表中,这些规则分别指定了源地址、目的地址、传输协议(如TCP、UDP、ICMP)和服务类型(如HTTP、FTP和SMTP)等。当数据包与规则匹配时,iptables就根据规则所定义的方法来处理这些数据包,如放行(accept)、拒绝(reject)和丢弃(drop)等。
配置防火墙的 主要工作就是添加、修改和删除这些规则。
实验过程:
-
Prevent A from doing telnet to Machine B
命令:
sudo iptables -A OUTPUT -d 10.0.2.5 -p tcp --dport 23 -j DROP
先在主机A上telnet 主机B 10.0.2.5,可以正常通信,然后添加一条rule,在OUTPUT链上,阻止A发送到B的端口号为23的tcp包,然后再次尝试telnet,连接失败。
-
Prevent B from doing telnet to Machine A
命令:
sudo iptables -A INPUT -s 10.0.2.5 -p tcp --dport 23 -j DROP
先用B主机telnet A,可以正常和A通信,然后在A主机添加rule,在INPUT链上,阻止B主机访问A主机的23端口,再次尝试,telnet连接失败。
-
Prevent A from visiting an external web site. You can choose any web site that you like to block, but keep in mind, some web servers have multiple IP addresses
命令:
sudo iptables -A OUTPUT -d baidu.com -p tcp --dport 80/443 -j DROP
我们尝试在A主机上添加rule,禁止主机A访问百度的网站,注意在iptables的命令中,目的地地址要填写baidu.com,而不是www.baidu.com:
使用sudo iptables -t filter --list
命令可以查看当前的表和链和rule的信息。
打开浏览器尝试访问 baidu.com,可以看到连接超时:
Task 2: Implementing a Simple Firewall
任务说明:
学习使用LKM和Netfilter编写包过滤内核模块实现至少5条过滤规则。
Netfilter:
Netfilter 是一个由Linux 内核提供的框架,可以进行多种网络相关的自定义操作。Netfilter 在 Linux 内核中表现为一系列的hook, 并允许Linux 内核模块注册为回调函数,Linux内核模块通过回调函数操作网络报文。
Netfilter 在内核协议栈的包处理路径上提供了 5 个 hook 点,分别是:
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
};
这些hook点在内核处理包的路径上的位置如图所示:
netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理。当有数据包经过 hook 点时, 就会调用相应的 handlers 进行处理。这就像有5个钓鱼台,在每个钓鱼台放了一个鱼钩(钩子函数),把经过的数据包钓上来处理。
当一个报文通过hook的时候,hook将会依据优先级调用钩子函数。钩子函数对数据包处理后,必须向netfilter框架返回以下返回值中的一个:
-
NF_ACCEPT
: 继续正常处理此报文,即允许报文通过。 -
NF_DROP
: 丢弃此报文,不再进行继续处理,即拒绝此报文。 -
NF_STOLEN
: 取走这个报文,不再继续处理。 -
NF_QUEUE
: 报文进行重新排队,可以将报文发到用户空间的程序,进行修改或者决定是拒绝或者允许。 -
NF_REPEAT
: 报文重新调用hook。
如果我们想在对应的hook加入自己的钩子函数,需要使用如下函数:
//旧版内核的钩子函数注册方法
int nf_register_hook(struct nf_hook_ops *reg)
//新版内核的钩子函数注册方法,增加了网络命名空间,默认可以使用&init_net
void nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
注册函数需要我们提供一个结构体nf_hook_ops
,结构体的定义如下:
struct nf_hook_ops {
nf_hookfn *hook; //需要注册的钩子函数
struct net_device *dev; //网络设备
void *priv;
u_int8_t pf; //协议簇,对于ipv4而言,是PF_INET
unsigned int hooknum;//指明HOOK位置
int priority; //优先级
};
我们注意到,钩子函数是一个nf_hookfn
类型的函数指针,该函数有固定的形式的,钩子函数原型如下:
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb, // 网络数据的缓存栈
const struct nf_hook_state *state);
总结来说,就是自己写一个nf_hookfn
类型的函数,然后填到一个nf_hook_ops
结构体中,然后使用注册函数将该结构体注册到对应的hook点。
LKM
-
LKM(Loadable Kernel Modules)“可加载内核模块程序”,这是一种区别于一般应用程序的系统级程序,它主要用于扩展linux的内核功能。LKM可以动态地加载到内存中,无须重新编译内核。由于LKM具有这样的特点,所以它经常被用于一些设备的驱动程序,例如声卡,网卡等。
-
LKM没有main()函数,但是必须有以下两个函数:
/*初始化函数*/ int module_init(int) {... } /*关闭函数*/ void module_exit(int) {... }
LKM编译之后可以得到一个二进制的可执行文件,然后我们就可以使用
insmod <filename>
命令将可执行文件加载到内存中(需要root权限)。还可以使用lsmod
查看当前内存中的模块,卸载内存中的模块命令是rmmod <file>
。
- 结合netfilter框架,我们就可以在
module_init(int)
函数中,完成钩子函数的注册,在module_exit(int)
函数中完成钩子函数的注销。
实验过程:
参考如下博客编写代码:
-
规则一:
阻止B主机(10.0.2.5)通过telnet访问本机,需要在
NF_INET_PRE_ROUTING
或NF_INET_LOCAL_IN
这两个hook点,添加钩子函数,如果数据包的是tcp,端口是23,源地址是10.0.2.5,那么就返回NF_DROP丢弃数据包。代码:
############################################## ## task2_1.c ## LKM+netfilter 组织10.0.2.5向本机的telnet数据包 ############################################## #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/ip.h> #include <linux/in.h> #include <linux/tcp.h> #include <linux/if_ether.h> MODULE_LICENSE("GPL"); /*模块许可证明,描述内核模块的许可权限,必须*/ MODULE_AUTHOR("zzp"); /*模块作者,可选*/ MODULE_DESCRIPTION("task2 netfilter");/*模块说明,可选*/ MODULE_VERSION("0.0.1");/*verison,可选*/ static struct nf_hook_ops *hook_1 = NULL; static struct nf_hook_ops *hook_2 = NULL; static struct nf_hook_ops *hook_3 = NULL; static struct nf_hook_ops *hook_4 = NULL; static struct nf_hook_ops *hook_5 = NULL; //了5个结构体,对应5个hook函数 static unsigned int hookfunc_1(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *iph; //取出ip头 struct tcphdr *tcph; //取出tcp头 if (!skb) return NF_ACCEPT; iph = ip_hdr(skb); tcph = tcp_hdr(skb); // get tcph from iph unsigned char* saddr = (unsigned char*)&iph->saddr; //源IP地址 if (iph->protocol == IPPROTO_TCP && tcph->dest == htons(23) && (int)saddr[0] == 10 && saddr[1] == 0 && saddr[2] == 2 && saddr[3] == 5) // filter的过滤条件 组织B telnet A { printk(KERN_INFO "Drop telnet packet from 10.0.2.5\n"); return NF_DROP; } return NF_ACCEPT; } void hook_1_reg(void) { hook_1 = (struct nf_hook_ops*)kcalloc(1, sizeof(struct nf_hook_ops), GFP_KERNEL); hook_1->hook = (nf_hookfn*)hookfunc_1; // hook函数指针 hook_1->hooknum = NF_INET_PRE_ROUTING; // hook点 hook_1->pf = PF_INET; // IPv4 hook_1->priority = NF_IP_PRI_FIRST; // hook函数的优先级,越小优先级越高 nf_register_net_hook(&init_net, hook_1); //注册函数 } void hook_1_unreg(void) { printk(KERN_INFO "Delete telnet filter from 10.0.2.5\n"); nf_unregister_net_hook(&init_net, hook_1); kfree(hook_1); } ######################################################## ## LKM函数(已经包括了5条规则的注册和注销,后续的代码中不再展示) ######################################################## static int __init LKM_init(void) { hook_1_reg(); hook_2_reg(); hook_3_reg(); hook_4_reg(); hook_5_reg(); } static void __exit LKM_exit(void) { hook_1_unreg(); hook_2_unreg(); hook_3_unreg(); hook_4_unreg(); hook_5_unreg(); } module_init(LKM_init); // LKM初始化函数 module_exit(LKM_exit); // LKM关闭函数
-
使用
Makefile
进行编译,注意这里和手册提供的makefile略有不同,手册中的M=\$(PWD)
在编译时会出错,参考stackoverflow:No rule to make target,将M=\$(PWD)
修改为M=$(shell pwd)
:obj-m=task2_all.o all: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
然后再shell上使用
sudo make
命令,可以得到好几个文件,其中有一个task2_1.ko
,再使用sudo insmod task2_1.ko
命令将程序加载到内存中:
测试:
在主机B上进行测试,注意在这之前需要删除任务一中通过iptables添加的过滤器,如图,删除任务一的过滤器之后,主机B上telnet可以连接A主机,然后当A主机将task2_1.ko加载到内存中后,B主机再使用telnet就不能通过telnet访问A了:
在主机A上使用dmesg
命令可以查看log信息,就是我们printk的信息:
-
规则二:
阻止主机通过telnet访问B(10.0.2.5),需要在
NF_INET_POST_ROUTING
或NF_INET_LOCAL_OUT
这两个hook点,添加钩子函数,如果数据包的是tcp,端口是23,目的地址是10.0.2.5,那么就返回NF_DROP丢弃数据包。代码:
static unsigned int hookfunc_2(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *iph; struct tcphdr *tcph; if (!skb) return NF_ACCEPT; iph = ip_hdr(skb); tcph = tcp_hdr(skb); // get tcph from iph unsigned char* daddr = (unsigned char*)&iph->daddr; if (iph->protocol == IPPROTO_TCP && tcph->dest == htons(23) && (int)daddr[0] == 10 && daddr[1] == 0 && daddr[2] == 2 && daddr[3] == 5) // prevent A telnet B { printk(KERN_INFO "Drop telnet packet to 10.0.2.5\n"); return NF_DROP; } return NF_ACCEPT; } void hook_2_reg(void) { hook_2 = (struct nf_hook_ops*)kcalloc(1, sizeof(struct nf_hook_ops), GFP_KERNEL); /* Initialize netfilter hook */ hook_2->hook = (nf_hookfn*)hookfunc_2; // hook function hook_2->hooknum = NF_INET_LOCAL_OUT; // received packets hook_2->pf = PF_INET; // IPv4 */ hook_2->priority = NF_IP_PRI_FIRST; // max hook priority nf_register_net_hook(&init_net, hook_2); } void hook_2_unreg(void) { printk(KERN_INFO "Delete telnet filter to 10.0.2.5\n"); nf_unregister_net_hook(&init_net, hook_2); kfree(hook_2); }
运行结果:
在主机A行telnet访问B,被阻止了:
查看主机A的log信息,成功组织了A向B的telnet包:
其他规则就是简单重复,不再赘述,可以自定义规则,和包处理方法,实现更灵活的防火墙
Task 3: Evading Egress Filtering 使用SSH隧道绕过防火墙
在主机A上部署防火墙,①:drop所有通向主机B的telnet数据包,②:drop所有通向lol.qq.com(140.249.243.249)的数据包。使用ssh隧道,绕过防火墙。
iptables设置如下:
Task 3.a: Telnet to Machine B through the fifirewall
使用ssh隧道,命令如下:
#先在一个shell上开启ssh并连接到主机B
ssh -L 8000:10.0.2.5:23 seed@10.0.2.5
#然后再另一个shell上,telnet 本机的8000端口
telnet localhost 8000
这样所有发往本机8000端口的数据都会通过ssh发向10.0.2.5的23端口。
运行结果:
本机网卡抓包,可以看到有很多和8000端口交互的数据包:
外部网卡抓包,可以看到SSH在担当一个隧道作用:
Task 3.b: Connect to Facebook using SSH Tunnel.
建立动态ssh隧道:
ssh -D 8888 -C seed@10.0.2.5
所有发往本机8888端口的数据包都会通过ssh隧道发往10.0.2.5
运行结果:
建立隧道:
打开火狐浏览器,设置代理:
然后就可以登录被防火墙屏蔽的网页了:
如果断开了ssh连接,那么隧道就断开了:
Task 4: SSH建立反向隧道,绕过防火墙
通过ssh建立反向隧道,绕过防火墙。
先在主机B上,修改/etc/hosts
文件,让主机B可以访问主机A上的网站(前面实验使用过的XSS或CSRF的网站)
然后在主机A上建立防火墙,阻止主机B访问本机的22和80端口,如图,可以看到建立防火墙后,主机B不能登录主机A的网站了,也不能访问ssh端口了:
命令:
sudo iptables -A INPUT -s 10.0.2.5 -p tcp --dport 80 -j DROP
sudo iptables -A INPUT -s 10.0.2.5 -p tcp --dport 22 -j DROP
为了绕过防火墙,我们可以先在主机A上运行如下命令,建立一个反向隧道:
#所有发往主机B 10.0.2.5:7777端口的数据包都会转发到主机A的80端口
ssh -R 7777:localhost:80 seed@10.0.2.5
然后在主机B上建立一个动态正向隧道:
ssh -D 8888:localhost:7777 -C locahost
然后设置一下主机B的浏览器的代理,让浏览器访问8888端口,如图,这样就可以通过反向隧道,访问到主机A上的网站了:
注意:
代理在设置的时候,一定要选上http协议,为了方便,可以把所有协议都勾选上,全设置成localhost:8888
端口。但是对于某些公网的网站,只设置SOCK协议就可以。
❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀完结撒花❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀