通過局域網中間人攻擊學網絡 第三篇 netfilter之內核篇

通過局域網中間人攻擊學網絡

第三篇 netfilter框架之內核篇

在第二篇中,我們講到可以用ARP欺騙的形式將局域網內某個主機的流量轉發到我們的機器上,那我們如何對該流量進行攔截修改呢?在Linux下,我們可以 使用netfilter框架來實現對ip數據攔截修改;

什麼是netfilter?

Netfilter是Linux 2.4.x引入的一個子系統,它作爲一個通用的、抽象的框架,提供一整套的hook函數的管理機制,使得諸如數據包過濾、網絡地址轉 換(NAT)和基於協議類型的連接跟蹤成爲了可能。

在netfilter框架中有五個hook點,如下圖所示:

 

五個hook點說明如下:

  • PRE_ROUTING:經過混雜丟棄後所有到達本機的數據包;
  • LOCAL_IN:經過PRE_ROUTING後,如果數據包的ip地址是本機,那麼進入該hook;
  • FORWARD:經過PRE_ROUTING後,如果發現數據包的地址不是本機,那麼將會進入到該hook;
  • LOCAL_OUT:本機的上層協議棧發出的數據包都會進入到該hook;
  • POST_ROUTING:所有經過本機出去的數據包最終都會進入到該hook;

示例

netfilter框架可以允許我們註冊一個回調函數,用來在指定hook點對數據包進行一些處理,一個簡單的hook如下(nf_kernel_custom_hook.c):

#define __KERNEL__
#define MODULE

#include <linux/netfilter_ipv4.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/netfilter.h>


static struct nf_hook_ops nfho;

// 實際的hook函數,函數定義是固定的,
unsigned int hook_func(unsigned int hooknum,struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff*)){
    // 返回NF_QUEUE,表示要將該數據包交給用戶空間處理
    return NF_QUEUE;
}

// 模塊初始化函數
int init(void){
    printk(KERN_INFO "init_module nf_kernel_custom_hook\n");
    // hook函數指向我們定義的hook函數
    nfho.hook = hook_func;
    /* hook點,表示我們要將我們的函數註冊到NF_IP_PRE_ROUTING這個hook點 */
    nfho.hooknum = NF_IP_PRE_ROUTING;
    // 該hook點在IPv4數據包下生效
    nfho.pf = PF_INET;
    // 定義優先級
    nfho.priority = NF_IP_PRI_FIRST;
    nf_register_hook(&nfho);
    return 0;
}

// 模塊卸載函數
void cleanup(void){
    printk(KERN_INFO "cleanup_module nf_kernel_custom_hook\n");
    nf_unregister_hook(&nfho);
}

// 聲明模塊的初始化/卸載函數分別是哪個
module_init(init);
module_exit(cleanup);

對於hook_func函數來說,可以返回五種操作,分別如下:

  • NF_DROP:0:直接刪除該包
  • NF_ACCEPT:1:接受該包,繼續往後處理,會繼續調用後續的hook函數;
  • NF_STOLEN:2:忘記該包,與NF_DROP的區別是NF_DROP會釋放sk_buff資源,而NF_STOLEN不會釋放sk_buff資源,需要函數自己釋放;
  • NF_QUEUE:3:將包加入隊列,然後等待用戶空間決策;怎麼處理;
  • NF_REPEAT:4:將數據包返回上個節點處理;
  • NF_STOP:5:與NF_ACCEPT類似,不同的是後邊的HOOK攔截器不會被執行了,注意,實際上這個操作已經廢除了;

從五種操作來說,NF_QUEUE是我們需要的,可以將數據包轉到用戶空間決策,所以上述代碼直接拿來用即可;

爲什麼要轉到用戶空間操作呢?一是因爲開篇我們就說了,作者主要是做Java的,所以還是用Java寫起來比較順手,而且我們本身是要做一個工具的,具 體的性能什麼的我們其實並不是太關心,所以沒必須要用c在內核搞,如果你是想要做一個簡單的功能或者是對性能有要求可以直接用c寫,放內核空間執行; 二是因爲我們這個後續可能還需要跟用戶(我們自己)交互,放內核空間不太可行,交互起來不方便,所以最終決定用NF_QUEUE轉發到用戶空間,由用戶 空間的程序處理,這樣我們就可以用Java來搞了;

編譯/安裝

現在有了上面的程序,我們就能將發往本機或者由本機轉發的IP數據包攔截到用戶空間了,這樣我們就能進行一些操作了,比如偷看下該IP數據包的內容、攔 截該數據包等;但是我們需要將他編譯安裝到內核中才行,那麼如何編譯呢?首先我們在該文件的同一個目錄中創建一個Makefile文件,文件名字就叫 Makefile,內容如下:

CONFIG_MODULE_SIG=n
obj-m+=nf_kernel_custom_hook.o
all:
	make  -C /lib/modules/$(shell uname -r)/build/ M=${PWD} modules
clean:
	make  -C /lib/modules/$(shell uname -r)/build/ M=${PWD} clean

然後我們在該目錄下執行make命令即可,編譯結果中會有一個.ko文件,然後我們使用insmod nf_kernel_custom_hook.ko命令來將該程序安裝到 內核中,安裝成功後可以使用lsmod查看是否安裝成功,如果裏邊有一個叫nf_kernel_custom_hook的module,說明我們的程序安裝成功;

到這裏,我們內核空間的程序就完成了,現在我們已經藉助netfilter框架將本機路由的數據包發往用戶空間去決策了,我們只需要在用戶空間接受該數據 包然後作相應的處理(比如刪除、查看)即可;

過程中出現任何問題,都可以加作者微信qiao1213812243尋求幫助,作者將盡最大努力幫你解答疑惑;

附錄

如果你只想快速的完成一個內網中間人攻擊工具,那麼上邊的內容已經足夠了,但是如果你想要了解更多,比如當我們搜索NF_QUEUE的時候,很多答案會 告訴我們可以選擇將數據放入指定隊列號的隊列中,默認是0號隊列,那麼如果想要指定放入1號隊列該怎麼做呢?要知道該問題,我們就要知道netfilter的 執行鏈路是怎麼樣的,這樣才能知道netfilter是怎麼根據我們的返回值決策出來走哪個隊列的;

通過翻閱代碼可以找到函數NF_HOOK(定義在netfilter.h中),可以看到該函數就是netfilter的入口函數了,在ip_forward、 ip_local_deliver等函數處都回調了該函數,該函數又回調了nf_hook函數,該函數定義如下:

static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,
                          struct sock *sk, struct sk_buff *skb,
                          struct net_device *indev, struct net_device *outdev,
                          int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
        // 定義hook鏈表
        struct nf_hook_entries *hook_head = NULL;
        int ret = 1;

#ifdef CONFIG_JUMP_LABEL
        if (__builtin_constant_p(pf) &&
            __builtin_constant_p(hook) &&
            !static_key_false(&nf_hooks_needed[pf][hook]))
                return 1;
#endif
        rcu_read_lock();
        // 開始路由該數據包的hook鏈表(我們註冊的hook就在這裏邊)
        // 可以看出,netfilter框架支持IPv4、IPv6、ARP(竟然還支持ARP,有點兒強大)、Bridge、DECnet;我們現在只需要IPv4,所以其他
        // 幾個雖然有不認識的協議,但是我們並不關心,有興趣的可以自行百度瞭解下其他幾個協議,其實主要是DECnet這個沒怎麼見過;
        switch (pf) {
        case NFPROTO_IPV4:
                // 如果數據是IPv4的數據,那麼會走這裏,我們的hook函數也註冊在hooks_ipv4裏邊,如果該hook點是NF_IP_PRE_ROUTING
                // 那麼返回的hook鏈表裏邊將會包含我們上邊的示例程序註冊的函數;
                hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
                break;
        case NFPROTO_IPV6:
                hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
                break;
        case NFPROTO_ARP:
#ifdef CONFIG_NETFILTER_FAMILY_ARP
                if (WARN_ON_ONCE(hook >= ARRAY_SIZE(net->nf.hooks_arp)))
                        break;
                hook_head = rcu_dereference(net->nf.hooks_arp[hook]);
#endif
                break;
        case NFPROTO_BRIDGE:
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
                hook_head = rcu_dereference(net->nf.hooks_bridge[hook]);
#endif
                break;
#if IS_ENABLED(CONFIG_DECNET)
        case NFPROTO_DECNET:
                hook_head = rcu_dereference(net->nf.hooks_decnet[hook]);
                break;
#endif
        default:
                WARN_ON_ONCE(1);
                break;
        }

        // 如果對應的協議,對應的hook點有對應的hook函數,那麼將調用nf_hook_slow
        if (hook_head) {
                struct nf_hook_state state;

                nf_hook_state_init(&state, hook, pf, indev, outdev,
                                   sk, net, okfn);

                ret = nf_hook_slow(skb, &state, hook_head, 0);
        }
        rcu_read_unlock();

        return ret;
}

該函數比較簡單,只是路由到了對應的hook鏈表,然後就交給nf_hook_slow函數處理了;nf_hook_slow函數定義如下:

int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
                 const struct nf_hook_entries *e, unsigned int s)
{
        unsigned int verdict;
        int ret;
        
        // 循環調用hook鏈表中的hook函數,可以看到NF_STOP操作確實已經廢除了,所以這裏只剩下四個操作了
        for (; s < e->num_hook_entries; s++) {
                verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
                switch (verdict & NF_VERDICT_MASK) {
                case NF_ACCEPT:
                        // 什麼都不做,繼續下個hook函數
                        break;
                case NF_DROP:
                        // 直接釋放該網絡數據包的內存(即刪除)
                        kfree_skb(skb);
                        ret = NF_DROP_GETERR(verdict);
                        if (ret == 0)
                                ret = -EPERM;
                        return ret;
                case NF_QUEUE:
                        // 這裏是我們需要的,可以看到對於返回NF_QUEUE的,最終都會調用nf_queue函數去處理,並且會將hook函數的決策結果
                        // verdict傳入進去
                        ret = nf_queue(skb, state, s, verdict);
                        if (ret == 1)
                                continue;
                        return ret;
                default:
                        // 只要不是上邊一個操作,都默認是NF_STOLEN
                        return 0;
                }
        }

        return 1;
}

這個函數也很簡潔,注意這裏的verdict & NF_VERDICT_MASK操作,爲什麼這樣操作呢?NF_VERDICT_MASK的定義是0x000000ff,可以看出該 操作只取了低8位,如果只需要低8位,那直接返回一個8位的數字就行,爲什麼還要一個32位的返回呢?別急,後邊我們就知道爲什麼了;下面我們進入 nf_queue函數,nf_queue函數定義如下:

int nf_queue(struct sk_buff *skb, struct nf_hook_state *state,
             unsigned int index, unsigned int verdict)
{
        int ret;
        
        ret = __nf_queue(skb, state, index, verdict >> NF_VERDICT_QBITS);
        if (ret < 0) {
                if (ret == -ESRCH &&
                    (verdict & NF_VERDICT_FLAG_QUEUE_BYPASS))
                        return 1;
                kfree_skb(skb);
        }

        return 0;
}

注意看下邊這一行

ret = __nf_queue(skb, state, index, verdict >> NF_VERDICT_QBITS);

其中NF_VERDICT_QBITS定義是16,將verdict右移16位,verdict是32位的,也就是取verdict的高16位,取這個幹嗎呢?做隊列號用!! __nf_queue函數的最後一個參數就是隊列號,到這裏,我們前邊的問題就得到解答了,原來隊列號是在返回的決策結果的高16位記錄的,這也是爲什麼 返回結果定義爲32位的數字而不是8位的數字,因爲這個隊列號是16位數字,所以也決定了隊列最多有65536個;

到這裏,我們已經知道netfilter的大概執行流程,並且知道了返回NF_QUEUE時如何指定隊列號了,如果我們不想用默認的0號隊列,比如想要用1號隊 列,那麼可以這樣返回:

return 1 << 16 | NF_QUEUE;

學習過程中也可以看出,內核源碼還是很簡潔的,感覺還是挺有意思的^_^

相關資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章