ip_conntrack有一个特性,那就是可以跟踪expect连接,所谓的expect连接,理解起来很简单,那就是“在一个连接中生成的另一个连接”,那么如何来识别一个连接要生成另一个连接呢?以FTP为例,FTP服务器会将文件传输所用的地址和端口信息作为数据载荷传输到对端的,Linux网关捕获这个数据包,将其解开然后根据FTP的协议规范获取地址和端口信息,随后就生成了一个expect连接。也就说,expect连接的参数是从数据载荷中得到的。
既然可以从数据载荷中得到一个“期望的连接”,那么随后的该期望的连接真正到来的时候一般是被允许通过的,这在防火墙上就是所谓的动态规则,在这里,一个约定就是防火墙本身对应用层协议是完全信任的,比方说FTP载荷中附带了生成expect连接的地址和端口信息,防火墙认为此信息是可信的,真的就是服务器或者客户端自己设置上去的。然而现实并不完美,这些信息可能是被攻击者硬添加进去的,如此一来,就有了绕过防火墙的可能,实现方式多种多样,最常见的就是包重放,攻击者截获一个包,然后在其载荷中按照一定的协议规范添加地址和端口信息,然后将此包重放在网络,当其经过防火墙的时候,防火墙就会生成一条动态的针对expect连接的允许规则,这样攻击者便可以绕过防火墙去访问本不该被访问的地址和端口了。
原理很简单,作为一个例子,我编写了一个内核模块,注册了一个捕获expect连接的helper(具体ip_conntrack的helper机制本文不再赘述,本质上就是一堆和既有显式ip_conntrack相关联的链表),模块代码如下:
- #include <linux/module.h>
- #include <linux/netfilter.h>
- #include <linux/ip.h>
- #include <net/tcp.h>
-
- #include <net/netfilter/nf_conntrack.h>
- #include <net/netfilter/nf_conntrack_expect.h>
- #include <net/netfilter/nf_conntrack_helper.h>
-
- MODULE_LICENSE(“GPL”);
- MODULE_AUTHOR(“Marywangran <marywangran@126.com>”);
- MODULE_DESCRIPTION(“expect helper test”);
-
- struct aa_proto {
- int type;
- int port;
- union nf_inet_addr addr;
- };
-
- static int aa_help(struct sk_buff *skb,
- unsigned int protoff,
- struct nf_conn *ct,
- enum ip_conntrack_info ctinfo)
- {
- unsigned int dataoff, datalen;
- const struct tcphdr *th;
- struct tcphdr _tcph;
- int ret;
- char *dt_ptr;
- struct nf_conntrack_expect *exp;
- int dir = CTINFO2DIR(ctinfo);
- struct aa_proto prot = {0};
- uint16_t port = ntohs((uint16_t)prot.port);
- char aa_buffer[512];
-
- if (ctinfo != IP_CT_ESTABLISHED
- && ctinfo != IP_CT_ESTABLISHED+IP_CT_IS_REPLY) {
- return NF_ACCEPT;
- }
- //开始解析数据包的内容
- th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph);
- dataoff = protoff + th->doff * 4;
- datalen = skb->len – dataoff;
- dt_ptr = skb_header_pointer(skb, dataoff, datalen, aa_buffer);
- //将协议头复制下来
- memcpy(&prot, dt_ptr, sizeof(struct aa_proto));
- if (prot.type != 12) { //如果不是预定义的12类型,www.linuxidc.com说明不需要expect连接
- ret = NF_ACCEPT;
- goto out;
- }
-
- exp = nf_ct_expect_alloc(ct);
- port = ntohs((uint16_t)prot.port);
- nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, AF_INET,
- &ct->tuplehash[dir].tuple.src.u3, &prot.addr,
- IPPROTO_TCP, NULL, &port);
- if (nf_ct_expect_related(exp) != 0)
- ret = NF_DROP;
- else
- ret = NF_ACCEPT;
- out:
- return ret;
- }
- static const struct nf_conntrack_expect_policy aa_policy = {
- .max_expected = 10,
- .timeout = 50 * 60,
- };
-
- static struct nf_conntrack_helper aa = {
- .name = “aa”,
- .me = THIS_MODULE,
- .tuple.src.l3num = AF_INET,
- //作用于TCP的12345端口
- .tuple.src.u.tcp.port = cpu_to_be16(12345),
- .tuple.dst.protonum = IPPROTO_TCP,
- .help = aa_help,
- .expect_policy = &aa_policy,
- };
-
- static void nf_conntrack_aa_fini(void)
- {
- nf_conntrack_helper_unregister(&aa);
- }
-
- static int __init nf_conntrack_aa_init(void)
- {
- int ret = nf_conntrack_helper_register(&aa);
- if (ret) {
- nf_conntrack_aa_fini();
- }
- return ret;
- }
-
- module_init(nf_conntrack_aa_init);
- module_exit(nf_conntrack_aa_fini);
- struct aa_proto {
- int type; //类型,如果是12则说明紧接着的端口,地址信息有效,需要初始化一个expect连接
- int port; //若有效,表示expect连接的目的端口
- union nf_inet_addr addr; //若有效,表示expect连接的目标地址
- };
1.首先定义aa协议:
- struct aa_proto {
- int type;
- int port;
- in_addr_t addr;
- };
- while ((c = getchar())!= ‘q’) {
- if (c == ‘g’) {
- struct aa_proto ap = {0};
- ap.addr = inet_addr(“192.168.188.82”);
- ap.type = 12;
- ap.port = 80;
- send(cClient, &ap, sizeof(struct aa_proto), 0);
- } else {
- send(cClient, “11111111”, 8, 0);
- }
- }
如此一来,一旦敲入g字符,就会初始化一条expect连接,初始化一条这样的连接到第有什么用呢?这正是本文剩下的目的。我们先定义一个规则序列:
1.允许所有expect连接通过;
2.允许和一台单独的主机192.168.40.247通信
3.拒绝所有其它的通信
接下来我们把一个侦听12345端口的TCP服务器运行在192.168.40.247上,然后在本机上运行TCP客户端,随便输入几个不是q和g的字符,然后我们访问192.168.188.82的80端口,发现不通,这是合理的,毕竟我们拒绝了与该机器的通信,然后我们敲入g,再访问192.168.188.82的80端口,通了,这就是上述第一条规则“允许所有expect连接通过”起作用了。
这能说明什么呢?这说明,Netfilter可以建立一种所谓的“动态规则”,这种规则不是人工设置上去的,而是根据数据包的载荷内容自动生成的,正是所谓基于expect连接的规则。是的,这是一个概念,又能怎样?关键问题是,“我们对非人工干预的规则总是不信任的”,如果我们坐上了一架无人驾驶的飞机,心里面总是会扑通扑通的…具体来讲,我们知道数据包是可以被sniffer的,是可以被重放的,如果仔细研读一下关于FTP,SIP等协议的RFC的话,总是能发现一些可以产生expect连接的地方,那么如果防火墙上的第一条规则是“允许所有expect连接通过”,那么就有办法绕过防火墙访问不该访问的目标了,方法就是sniffer一个FTP/SIP/…包,然后根据RFC的标准按照自己的预设目标重新填充某些expect相关的字段,重新计算一下校验码之类的,然后将包重放到网络上,至此就完成了攻击。
至于如何利用expect连接绕过防火墙,我们的机制是有了,然而具体的操作需要自己的聪明才智了,除了阅读RFC理解协议细节之外,还需要一种采用怪诞方式处理问题的风格,很多时候这不是一种工作意义上的风格,更属于一种hack的风格…