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 <[email protected]>");
- 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);
在上述實現中,我們自定義了一個簡單的協議aa:
- struct aa_proto {
- int type; //類型,如果是12則說明緊接著的端口,地址信息有效,需要初始化一個expect連接
- int port; //若有效,表示expect連接的目的端口
- union nf_inet_addr addr; //若有效,表示expect連接的目標地址
- };