通過局域網中間人攻擊學網絡
第三篇 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;
學習過程中也可以看出,內核源碼還是很簡潔的,感覺還是挺有意思的^_^
相關資料
- netfilter源碼下載:git clone https://git.kernel.org/pub/scm/linux/kernel/git/pablo/nf.git
- netfilter源碼(Linux內核源碼)在線閱讀:https://lxr.missinglinkelectronics.com/linux