前一段时间看到这篇帖子,确实很经典,于是翻出了英文原版再读,顺便再翻译出来供大家学习,这篇文章的中文版也早都有了,不过出于完全理解的目的,我还是将它翻译了出来,加进了自己的代码,虽然在上一周的翻译过程中,我尽量保留文章的原汁原味,但错误肯定在所难免,在末尾附上原文和我自己调试通过的代码,已经够构运行,大家可以参考一下!(有错误之处请指出)
深入Linux内核网络堆栈
作者:bioforge alkerr@yifan.net
原名: <<Hacking the Linux Kernel Network Stack>>
翻译,修改: duanjigang <duanjigang1983@126.com>
翻译参考:raodan (raod_at_30san.com) 2003-08-22
第一章 简介
本文将描述如何利用Linux网络堆栈的窍门(不一定都是漏洞)来达到一些目的,或者是恶意的,或者是出于其它意图的。文中会就后门通讯对Netfilter钩子进行讨论,并在本地机器上实现将这个传输从基于Libpcap的嗅探器(sniffer)中隐藏。
Netfilter是2.4内核的一个子系统。Netfilter可以通过在内核的网络代码中使用各种钩子来实现数据包过滤,网络地址转换(NAT)和连接跟踪等网络欺骗。这些钩子被放置在内核代码段,或者静态编译进内核,或者作为一个可动态加载/卸载的可卸载模块,然后就可以注册称之为网络事件的函数(比如数据包的接收)。
1.1 本文论述的内容
本文将讲述内核模块的编写者如何利用Netfilter的钩子来达到任何目的,以及怎样将网络传输从一个Libpcap的应用中隐藏掉。尽管Linux2.4支持对IPV4,IPV6以及DECnet的钩子,本文只提及IPV4的钩子。但是,对IPV4的大多数应用内容同样也可以应用于其他协议。出于教学目的,我们在附录A给出了一个可以工作的内核模块,实现基本的数据包过滤功能。针对本文中所列技术的所有开发和试验都在Intel机子上的Linux2.4.5系统上进行过。对Netfilte 钩子行为的测试使用的是回环设备(Loopback device),以太网设备和一个点对点接口的调制解调器。
对Netfilter进行完全理解是我撰写本文的另一个初衷。我不能保证这篇文章所附的代码100%的没有差错,但是所列举的所有代码我都事先测试过了。我已经饱尝了内核错误带来的磨砺,而你却不必再经受这些。同样,我不会为按照这篇文档所说的任何东西进行的作所所为带来的损失而负责。阅读本篇文章的读者最好熟悉C程序设计语言,并且对内核可卸载模块有一定的经验。
如果我在文中犯了任何错误的话,请告知我。我对于你们的建议和针对此文的改进或者其它的Netfilter应用会倾心接受。
1.2 本文不会涉及到的方面
本文并不是Netfilter的完全贯穿(或者进进出出的讲解)。也不是iptables命令的介绍。如果你想更好的学习iptables的命令,可以去咨询man手册。
让我们从介绍Nerfilter的使用开始吧……….
第二章 各种NetFilter 钩子及其用法
2.1 Linux内核对数据包的处理
我将尽最大努力去分析内核处理数据包的详细内幕,然而对于事件触发处理以及之后的Netfilter 钩子不做介绍。原因很简单,因为Harald Welte 关于这个已经写了一篇再好不过的文章<<Journey of a Packet Through the Linux 2.4 Network Stack>>,如果你想获取更多关于Linux对数据包的相关处理知识的话,我强烈建议你也阅读一下这篇文章。目前,就认为数据包只是经过了Linux内核的网络堆栈,它穿过几层钩子,在经过这些钩子时,数据包被解析,保留或者丢弃。这就是所谓的Netfilter 钩子。
2.2 Ipv4中的Netfilter钩子
Netfilter为IPV4定义了5个钩子。可以在 linux/netfilter-ipv4.h里面找到这些符号的定义,表2.1列出了这些钩子。
表 2.1. ipv4中定义的钩子
NF_IP_PRE_ROUTING 完整性校验之后,路由决策之前
NF_IP_LOCAL_IN 目的地为本机,路由决策之后
NF_IP_FORWARD 数据包要到达另外一个接口去
NF_IP_LOCAL_OUT 本地进程的数据,发送出去的过程中
NF_IP_POST_ROUTING 向外流出的数据上线之前
不管钩子函数对数据包做了哪些处理,它都必须返回表2.2中的一个预定义好的Netfilter返回码。
表2.2 Netfilter 返回码
NF_DROP 丢弃这个数据包
NF_ACCEPT 保留这个数据包
NF_STOLEN 忘掉这个数据包
NF_QUEUE 让这个数据包在用户空间排队
NF_REPEAT 再次调用这个钩子函数
第三章 注册和注销NetFilter 钩子
注册一个钩子函数是一个围绕nf_hook_ops结构体的很简单的过程,在linux/netfilter.h中有这个结构体的定义,定义如下:
{
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
注册一个Netfilter钩子要用到nf_hook_ops这个结构体和nf_register_hook()函数。nf_register_hook()函数以一个nf_hook_ops结构体的地址作为参数,返回一个整型值。如果你阅读了net/core/netfilter.c中nf_register_钩子()的源代码的话,你就会发现这个函数只返回了一个0。下面这个例子注册了一个丢弃所有进入的数据包的函数。这段代码同时会向你演示Netfilter的返回值是如何被解析的。
代码列表1. Netfilter钩子的注册
* drop all incoming packets. */
#define __KERNEL__
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
/* This is the structure we shall use to register our function */
static struct nf_hook_ops nfho;
/* This is the hook function itself */
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; /* Drop ALL packets */
}
/* Initialisation routine */
int init_module()
{
/* Fill in our hook structure */
nfho.hook = hook_func; /* Handler function */
nfho.hooknum = NF_IP_PRE_ROUTING; /* First hook for IPv4 */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* Make our function first */
nf_register_hook(&nfho);
return 0;
}
/* Cleanup routine */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}
第四章 基本的NetFilter数据包过滤技术
4.1 钩子函数近距离接触
现在是我们来查看获得的数据如何传入钩子函数并被用来进行过滤决策的时候了。所以,我们需要更多的关注于nf_hookfn函数的模型。Linux/netfilter.h给出了如下的接口定义:
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));
或许sk_buff结构体中最有用的域就是其中的三个联合了,这三个联合描述了传输层的头信息(例如 UDP,TCP,ICMP,SPX),网络层的头信息(例如ipv4/6, IPX, RAW)和链路层的头信息(Ethernet 或者RAW)。三个联合相应的名字分别为:h,nh和mac。根据特定数据包使用的不同协议,这些联合包含了不同的结构体。应当注意,传输层的头和网络层的头极有可能在内存中指向相同的内存单元。在TCP数据包中也是这样的情况,h和nh都是指向IP头结构体的指针。这就意味着,如果认为h->th指向TCP头,从而想通过h->th来获取一个值的话,将会导致错误发生。因为h->th实际指向IP头,等同于nh->iph。
其他比较有趣的域就是len域和data域了。len表示包中从data开始的数据总长度。因此,现在我们就知道如何通过一个skbuff结构体去访问单个的协议头或者数据包本身的数据。还有什么有趣的数据位对于Netfilter的钩子函数而言是有用的呢?
跟在sk_buff之后的两个参数都是指向net_device结构体的指针。net_devices结构体是Linux内核用来描述各种网络接口的。第一个结构体,in,代表了数据包将要到达的接口,当然 out就代表了数据包将要离开的接口。有很重要的一点必须认识到,那就是通常情况下这两个参数最多只提供一个。 例如,in通常情况下只会被提供给NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN钩子。out通常只被提供给NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING钩子。在这个阶段,我没有测试他们中的那个对于NF_IP_FORWARD是可用的。如果你能在废弃之前确认它们(in和out)不空的话,那么你很优秀。
最后,传给钩子函数的最后一个参数是一个名为okfn的指向函数的指针,这个函数有一个sk_buff的结构体作为参数,返回一个整型值。我也不能确定这个函数做什么,在net/core/netfilter.c中有两处对此函数的调用。这两处调用就是在函数nf_hook_slow()和函数nf_reinject()里,在这两个调用处当Netfilter钩子的返回值为NF_ACCEPT时,此函数被调用。如果有谁知道关于okfn更详细的信息,请告诉我。
现在我们已经对Netfilter接收到的数据中最有趣和最有用的部分进行了分析,下面就要开始介绍如何利用这些信息对数据包进行各种各样的过滤。
4.2 基于接口的过滤
这将是我们能做的最简单的过滤技术。是否还记得我们的钩子函数接收到的net_device结构体?利用net_device结构体中的name键值,我们可以根据数据包的目的接口名或者源接口名来丢弃这些数据包。为了抛弃所有发向”eth0”的数据,我们只需要比较一下“in->name”和“eth0”,如果匹配的话,钩子函数返回NF_DROP,然后这个数据包就被销毁了。它就是这样的简单。列表2给出了示例代码。请注意轻量级防火墙(LWFW)会使用到这里提到的所有过滤方法。LWFW同时还包含了一个IOCTL方法来动态改变自身的行为。
列表2. 基于源接口(网卡名)的数据过滤技术
* drop all incoming packets from an IP address we specify */
#define __KERNEL__
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/ip.h> /* For IP header */
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
/* This is the structure we shall use to register our function */
static struct nf_hook_ops nfho;
/* IP address we want to drop packets from, in NB order */
static unsigned char *drop_ip = "/x7f/x00/x00/x01";
/* This is the hook function itself */
unsigned int hook_func(unsigned int hook_num,
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 == drop_ip) {
printk("Dropped packet from... %d.%d.%d.%d/n",
*drop_ip, *(drop_ip + 1),
*(drop_ip + 2), *(drop_ip + 3));
return NF_DROP;
} else {
return NF_ACCEPT;
}
}
/* Initialisation routine */
int init_module()
{
/* Fill in our hook structure */
nfho.hook = hook_func;
/* Handler function */
nfho.hook_num = NF_IP_PRE_ROUTING; /* First for IPv4 */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* Make our func first */
nf_register_hook(&nfho);
return 0;
}
/* Cleanup routine */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}
4.3 基于IP地址的过滤
类似基于接口的数据包过滤技术,基于源/目的IP地址的数据包过滤技术也很简单。这次我们对sk_buff结构体比较感兴趣。现在应该记起来,Skb参数是一个指向sk_buff结构体的指针的指针。为了避免运行时出现错误,通常有一个好的习惯就是另外声明一个指针指向sk_buff结构体的指针,把它赋值为双重指针所指向的内容,像这样:
列表3.检测接收到数据包的源IP地址
...
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;
}
列表4. 基于数据包源IP地址的过滤技术
* drop all incoming packets from an IP address we specify */
#define __KERNEL__
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/ip.h> /* For IP header */
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
/* This is the structure we shall use to register our function */
static struct nf_hook_ops nfho;
/* IP address we want to drop packets from, in NB order */
static unsigned char *drop_ip = "/x7f/x00/x00/x01";
/* This is the hook function itself */
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 == drop_ip) {
printk("Dropped packet from... %d.%d.%d.%d/n",
*drop_ip, *(drop_ip + 1),
*(drop_ip + 2), *(drop_ip + 3));
return NF_DROP;
} else {
return NF_ACCEPT;
}
}
/* Initialisation routine */
int init_module()
{
/* Fill in our hook structure */
nfho.hook = hook_func;
/* Handler function */
nfho.hooknum = NF_IP_PRE_ROUTING; /* First for IPv4 */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* Make our func first */
nf_register_hook(&nfho);
return 0;
}
/* Cleanup routine */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}
另外一个要执行的简单的规则就是基于TCP目的端口的数据包过滤。这比检验IP地址稍微复杂一点,因为我们要自己创建一个指向TCP头的指针。还记得前面关于传输层头和网络层头所做的讨论吗?获得一个TCP头指针很简单,只需要申请一个指向tcphdr(定义在linux/tcp.h中)结构体的指针,并将它指向包数据中的IP头后面。或许一个例子就可以了。列表5展示了怎样检测一个数据包的TCP目的端口与我们想丢弃数据的指定端口是否一致。与列表3一样,这段代码也是从LWFW中拿出来的
列表5. 检测接收到数据包的TCP目的端口
...
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;
}
第五章 NetFilter钩子其他可能的用法
在这里我将会就Netfilter在其它方面的更有趣的应用给你作一些建议。在5.1我会给你提供一些思想源泉。5.2节将会讨论并提供能运行的代码,这个代码使一个基于内核的FTP密码嗅探器,能够远程获取密码。事实上,它运行的很好以至于我有些惊恐,所以将它写了出来。
5.1 隐藏后门守护进程
内核模块编程实际上是Linux开发最有意思的领域之一。在内核中写代码意味着你在一个只被你的想象力限制的地方写代码。从恶意一点的观点来思考,你可以隐藏一个文件,一个进程,或者说你能做任何rootkit能实现的很酷的事情。或者说从不太恶意(有这种观点的人)的观点来说,你可以隐藏文件,进程,和各种各样很酷的动作,内核真正是一个很迷人的地方。
拥有一个内核级的程序员所具有的所有能力,许多事情都是可能的。或许最有趣(对于系统管理员来说这可是很恐怖的事情)的一件事情就是在内核植入一个后门程序。毕竟,当一个后门没有作为进程而运行的时候,你怎么会知道它在运行?当然肯定存在一些可以使你的内核能够嗅到这些后门的方法,但是这些方法却绝不会象运行PS命令那样的简单。将后门代码植入内核中并不是一个很新的话题。我这里要讲的,却是利用(你能够猜到的)Netfilter钩子植入简单的网络服务,将之作为内核后门。
如果你有必要的技能并且愿意承担在做实验时将你的内核导致崩溃的风险的话,你可以构造一个简单而有用的网络服务,将能够完全的装入内核并能进行远程访问。基本上说,Netfilter可以从所有接收到的数据包中查找指定的“神秘”数据包,当这个神秘的数据包被接收到的时候,可以进行一些特殊的处理。结果可以通过Netfilter钩子函数发送出去,Netfilter钩子函数然后返回一个NF_STOLEN结果以便这个神秘的数据包不会被继续传递下去。但是必须注意一点,以这样的方式来发送输出数据的时候,向外发送的数据包对于输出Netfilter钩子函数仍然是可见的。因此对于用户空间来说,完全看不到这个“神秘”数据包曾经来过,但是他们却能够看到你发送出来的数据。你必须留意,泄密主机上的Sniffer程序不能发现这个数据包并不意味着中间的宿主机上的嗅探器(sniffer)也不能发现这个数据包。
Kossak和lifeline曾为Phrack杂志写过一篇精彩的文章,文中描述了如何通过注册数据包类型处理器的方法来坐这些事情。虽然这片文章是关于Netfilter钩子的,我还是强烈建议你阅读一下那片文章(Issue 55, file 12),这片文章非常有趣,向你展示了很多有趣的思想。
那么,后门的Netfilter钩子到底能做哪种工作呢?好的,下面给出一些建议:
-------远程访问的击键记录器。模块会记录键盘的点击并在远程客户机发送一个Ping包的时候,将结果发送给客户机。因此,一连串的击键记录信息流会被伪装成稳定的Ping包返回流发送回来。你也可以进行简单的加密以便按键的ASC 值不会马上暴露出来,一些警觉的系统管理员回想:“坚持,我以前都是通过SSH会话来键入这些的,Oh $%@T%&!”
--------简单的管理任务,例如获取机器当前的登录用户列表,或者获取打开的网络连接信息。
--------一个并非真正的后门,而是位于网络边界的模块,并且阻挡任何被疑为来自特洛伊木马、ICMP隐蔽通道或者像KaZaa这样的文件共享工具的通信。
--------文件传输服务器。我最近已经实现了这个想法。最终得到的Linux内核模块会给你带来数小时的愉悦。
--------数据包跳跃。将发送到装有后门程序主机的特定端口的数据重新定向到另外一个IP主机的不同端口。并且将这个客户端发送的数据包返回给发起者。没有创建进程,最妙的是,没有打开网络套接字。
--------利用上面说到的数据包跳跃技术已以一种半传输的方式实现与网络上关键系统的交互。例如配置路由等。
--------FTP/POP3/Telnet的密码嗅探器。嗅探向外发送的密码并保存起来,直到神秘数据包到来所要这些信息的时候,就将它发送出去。
好了,上面是一些简单的思想列表。最后一个想法将会在下一节中进行详细的介绍,因为这一节为读者提供了一个很好的机会,使得我们能够接触更多的内核内部的网段络代码。
5.2 基于内核的FTP密码获取Sniffer
针对前面谈到的概念,这里给出了一个例证—一个后门Netfilter程序。这个模块嗅探流向服务器的外出的FTP数据包,寻找USER和PASSWD命令对,当获取到一对用户名和密码时,模块就会等待一个神秘的并且有足够大空间能存储用户名和密码的ICMP包(Ping包)的到来,收到这个包后,模块会将用户名和密码返回。很快的发送一个神秘的数据包,获取回复并且打印信息。一旦一对用户名和密码从模块中读走都,模块便会开始下一对数据的嗅探。注意模块平时最多能存储一对信息。已经大致介绍过了,我们现在对模块具体怎样工作进行详尽的讲解。当模块被加载的时候,init_module()函数简单的注册两个Netfilter钩子。第一个钩子负责从进入的数据包(在NF_IP_PRE_ROUTING时机调用)中寻找神秘的ICMP数据包。另外一个负责监视离开(在NF_IP_POST_ROUTING时调用)安装本模块的机器的数据包。在这里寻找和俘获FTP的登录用户名和密码,cleanup_module()负责注销这两个钩子。
watch_out()函数是在NF_IP_POST_ROUTING时调用的钩子函数。看一下这个函数你就会发现它的动作很简单。当一个数据包进入的时候,它会被经过多重的检测以便确认这个数据包是否是一个FTP数据包。如果不是一个FTP数据包,将会立即返回一个NF_ACCEPT。如果是一个FTP数据包,模块会确认是否已经获取并存储了一对用户名和密码。如果已经存储了的话(这时 have_pari变量的值非零),那么就会返回一个NF_ACCPET值,并且数据包最终可以离开这个系统。否则的话,check_ftp()方法将会被调用。通常在这里密码被提取出来,如果以前没有接收到数据包的话,target_ip和target_port这两个变量将会被清空。
Check_ftp()一开始在数据段的开头寻找“USER”,“PASS”或者“QUIT”字段。注意,在没有“USER”字段被处理之前通常不处理“PASS”字段。这是为了防止在收到密码后连接断开,而这时没有获取到用户名,就会陷入锁中。同样,当收到一个“QUIT”字段时,如果这时只有一个“USER”字段的话,就将所有变量复位,以便于Sniffer能继续对新的连接进行嗅探。当“PASS”或者“USER”命令被收到时,在必要的完整性校验之后,命令的参数会被拷贝下来。通常操作中都是在check_ftp()函数结束之前,检验有无用户名和密码者两个命令字段。如果有的话,have_pair会被设置,并且在这对数据被取走之前不会再次获取新的用户名和密码。
到目前为止你已经知道了这个模块怎样安装自己并且查找用户名和密码并记录下来。下面你将会看到“神秘”数据包到来时会发生什么。在这块儿要特别留意,因为开发中的大多数问题会在此处出现。如果没有记错的话,我在这里遇到了16个内核错误。当数据到达安装此模块的机器时,watch_in()将会检查每一个数据包看他是否是一个神秘的数据包。如果数据包没有满足被判定为神秘数据包的条件的话,watch_in()会简单的返回一个NF_ACCEPT来忽略这个数据包。注意,神秘数据包的判定标准就是这个数据包有足够的空间能够容纳IP地址,用户名和密码这些字符串。这样做是为了使得数据的回复更容易些。可能需要申请一个新的sk_buff结构体。但是要保证所有的数据域都正确却是件不容易的事情,所以你必须想办法确保这些域的键值正确无误。因此,我们在此并不创建一个新的结构体,而是直接修改请求数据包的结构,将其作为一个返回数据包。为了能正确返回,需要做几个修改。首先,IP地址进行交换,结构体(sk_buff)中的数据包类型这个域的值要改为“PACKET_OUTGOING”,这个在linux/if_packet.h中定义了。第二步要确保每个链路层信息已经被包含在其中。我们接收到数据包的数据域就是链路层头信息后面的指向sk_buff结构体的指针,并且指向数据包中数据开头的指针传递了数据域。所以,对于需要链路层头信息的接口(以太网卡,回环设备和点对点设备的原始套结字)而言,我们的数据域指向mac.ethernet或者mac.raw结构。你可以通过检测sb->dev->type的值(sb是指向sk_buff结构体的指针)的值来判断这个数据包进入了什么类型的接口。你可以在linux/ip_arp.h中找到这些有效的值。最有用的都在表三列了出来。
表三.常见接口(网卡)类型
ARPHRD_ETHER 以太网卡
ARPHRD_LOOPBACK 回环设备
ARPHRD_PPP 点对点设备
5.2.1 nsniffer 的代码
代码超过发贴上限,见附件
5.2.2 getpass.c 代码
代码超过发贴上限,见附件
第六章 在Libpcap中隐藏网络通讯
6.1 SOCK_PACKET, SOCK_RAW 和Libpcap
系统管理员经常用到的一些软件可“数据包嗅探器”这个标题进行分类。最普通的用于一般目的的数据包嗅探器有
Tcpdump(1)和Ethreal(1)。这两个应用都是利用了libpcap这个库来获取原始套结字的数据包。网络入侵检测系统(NetWork Intrusion Detection System NIDS)也利用了libpcap这个库。SNORT也需要libpcap, Libnids----一个提供IP重组和TCP流跟踪的NIDS开发库(参见参考文献[2]),也是如此。
在一台Linux系统上,libpcap利用SOCK_PACKET接口。Packet套结字是一种能够在链路层接收和发送数据包的特殊套结字。关于packet套结字和它的用途可以说一大堆东西,但是本文是从它们当中隐藏而不是讲述如何利用它们的。感兴趣的读者可以从packet(7)的man手册中了解到更详细的信息。在此处。我们只需要知道packet套结字能够被libpcap用来从机器上的原始套结字中获取进入的和发送的数据。
当内核的网络堆栈收到一个数据包时,要对其进行一定的校验以便确定是否有packet套结字对它感兴趣。如果有的话,这个数据包就被分发给对它感兴趣的套结字。如果没有的话,这个数据包继续流向TCP层,UDP层,或者其它的真正目的地。对于SOCKET_RAW型的套结字也是这样的情形。SOCKET_RAW非常类似于SOCKET_PACKET型的套结字,区别就在于SOCKET_RAW不提供链路层的头信息。我在附录[3]中的SYNalert就是SOCKET_RAW利用的一个例子。
现在你应该知道Linux系统上的数据包嗅探软件都是利用libpcap库了吧。Libpcap在Linux上利用PACKET_SOCKET接口从链路层获取原始套结字数据包。原始套结字可以在用户空间被用来从IP头中获取所有的数据包。下一段将会讲述一个Linux内核模块(LKM)怎样从数据包中或者SOCKET_RAW套结字接口中隐藏一个网络传输。
6.2 给狼披上羊皮
(这个译法借鉴于参考译文)
当一个数据包被接收到并发送给一个packet套结字时,packet_rcv()函数会被调用。可以在net/packet/af_packet.c中找到这个函数的源代码。packet_rcv()负责使数据通过所有可能应用于数据目的地的Netfilter,最终将数据投递到用户空间。为了从PACKET中隐藏数据包,我们需要设法让packet_rcv()对于一些特定的数据包一点也不调用。我们怎样实现这个?当然是优秀的ol式的函数劫持了。
函数劫持的基本操作是:如果我们知道一个内核函数,甚至是那些没有被导出的函数的入口地址,我们可以在实际的代码运行前将这个函数重定位到其他的位置。为了达到这样的目的,我们首先要从这个函数的开始,保存其原来的指令字节,然后将它们换成跳转到我们的代码处执行的绝对跳转指令。例如以i386汇编语言实现该操作如下:
jmp *eax
0xff 0xe0
要从packet套接字隐藏数据包,我们首先要写一个钩子函数,用来检查这个数据包是否满足被隐藏的标准。如果满足,钩子函数简单的向它的调用者返回一个0,这样packet_rcv()函数也就不会被调用。如果packet_rcv()函数不被调用,那么这个数据包就不会递交给用户空间的packet套接字。注意,只是对于"packet"套接字来说,该数据包被丢弃了。如果我们要过滤送到packet套接字的FTP数据包,那么FTP服务器的TCP套接字仍然能收到这些数据包。我们所做的一切只是使运行在本机上的嗅探软件无法看到这些数据包。FTP服务器仍然能够处理和记录连接。
理论上大致就这么多了,关于原始套接字的用法同理可得。不同的是我们需要钩子的是raw_rcv()函数(在net/ipv4/raw.c中可以找到)。下一节将给出并讨论一个Linux核心模块的示例代码,该代码劫持packet_rcv()函数和raw_rcv()函数,隐藏任何来自或去往指定的IP地址的数据包。
第七章 结束语
希望到现在为止,你对于什么是Netfilter,怎样使用Netfilter,可以对Netfilter做些什么已经有了一个基本的了解。你应该也具有了在本地机器上将一些特定的网络传输从运行在这些机器上的嗅探型软件中隐藏的知识了。如果你想要关于这方面的压缩包的话,可以直接给我发送E-mail邮件。我会为你做的任何修改,注释和建议而感激。现在,我就把这些有趣的东西留给你,你可以自由发挥自己的想象力。
附录A 轻量级防火墙
A.1 纵览
轻量级防火墙(Light weight fire wall ,LWFW)是一个简单的内核模块,它演示了第四章介绍的基本的数据包过滤技术。LWFW并通过系统调用ioctl提供了一个控制接口。
由于LWFW已经有了足够多的文档,所以我在此只就它怎么工作进行简单的概述。当LWFW模块被安装时,第一个任务就是尝试去注册一个控制设备。注意,在针对于LWFW的ioctl接口能够使用之前,需要在/dev目录下建立一个字符设备文件,如果这个控制设备注册成功的话,“in use”标识符将被清空,为NF_IP_PRE_ROUTE注册的钩子函数也就注册上了。clean_up函数做一些与此过程相反的事情。
LWFW提供了三个丢弃数据包的判定条件,它们按照处理的顺序依次是:
-----源接口(网卡名,如“eth0”,“eth0:1”等)
------源IP地址(如“10.0.1.4”,“192.168.1.1”等)
------目的TCP端口号(如ssh常用的22,FTP常用的19)
这些规则的具体设定是通过ioctl接口来实现的。当一个数据包到来时,LWFW会根据设定好的规则对这些数据包进行检测。如果某个数据包符合其中的任何一个规则,那么钩子函数将返回一个NF_DROP结果,从而Netfilter就会默默地丢弃这个数据包。负责的话,钩子函数会返回一个NF_ACCEPT结果,这个数据包就会继续它的旅途。
最后一个需要提到的就是LWFW的统计记录。任何一个数据包到达钩子函数时,只要LWFW是活跃的,那么看到的数据包总数目将会增加。单个的规则校验函数负责增加由于符合此项规则而丢弃的数据包数目。需要注意的就是,当某个规则的内容变化时,这个规则对应的丢弃数据包总数也会被清零。Lwfwstats函数利用IOCTL的LWFW_GET_STATS命令获取statistics结构体的一份拷贝值,并显示它的内容。
A.2 源代码 lwfw.c
见附件
A.3 lwfw.h,Makefile
见附件
A.4 译者添加的测试程序
下面是译者自己在学习时写的一个对LWFW的过滤规则进行设置和改动的例子,你也可以对此段代码进行修改,当模块成功加载之后,建立一个字符设备文件,然后这个程序就能运行了。
Name: test.c
Author: duanjigang<duanjigang1983@gmail.com>
Date: 2006-5-15
*/
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<linux/rtc.h>
#include<linux/ioctl.h>
#include "lwfw.h"
main()
{
int fd;
int i;
struct lwfw_stats data;
int retval;
char msg[128];
/*来自这个IP地址的数据将被丢弃*/
char * deny_ip = "192.168.1.105";
/*这个接口发出的数据将被丢弃,无法外流*/
char *ifcfg = "eth0";
/*要禁止的TCP目的端口22, ssh的默认端口*/
unsigned char * port = "/x00/x16";
/*打开设备文件*/
fd = open(LWFW_NAME, O_RDONLY);
if(fd == -1)
{
perror("open fail!");
exit(-1);
}
/*激活LWFW,设置标志位*/
if( ioctl(fd,LWFW_ACTIVATE,0) == -1 )
{
perror("ioctl LWFW_ACTIVATE fail!/n");
exit(-1);
}
/*设置禁止IP*/
if( ioctl(fd, LWFW_DENY_IP, inet_addr(deny_ip)) == -1)
{
printf("ioctl LWFW_DENY_IP fail/n");
exit(-1);
}
/*设置禁止端口*/
if(ioctl(fd, LWFW_DENY_PORT, *(unsigned short *)port) == -1)
{
printf("ioctl LWFW_DENY_PORT fail!/n");
exit(-1);
}
/*获取数据,这应该是一段时间之后的事,此处直接获取,不妥*/
if( ioctl(fd, LWFW_GET_STATS,*(unsigned long*)&data) == -1)
{
printf("iotcl LWFW_GET_STATS fail!/n");
exit(-1);
}
/*
禁用这个接口
if(ioctl(fd, LWFW_DENY_IF, (unsigned*)ifcfg) == -1)
{
printf("ioctl LWFW_DENY_IF fail!/n");
exit(-1);
}
*/
printf("ip dropped : %d/n", data.ip_dropped);
printf("if dropped : %d/n", data.if_dropped);
printf("tcp dropped : %d/n", data.tcp_dropped);
printf("total dropped : %d/n", data.total_dropped);
printf("total seen: %d/n", data.total_seen);
close(fd);
}
附录B 第六部分的代码
这里是一个简单的模块,在这个模块中将对packet_rcv()函数和raw_rcv()函数进行替换,从而隐藏到达或者离开我们指定所IP地址的数据包。默认的IP是“127.0.0.1”,但是,可以通过修改#define IP 来改动这个值。同样提供了一个bash的脚本,负责从Sytem.map文件中获取所需函数的地址,并且负责模块的插入,在插入模块时,以所需的格式将这些函数的地址传递给内核。这个加载脚本是grem写的。原来是为我的mod-off项目而写,经过简单的修改就能用于这里的模块,再次感谢grem。
这里给出的模块只是原理性的代码,没有任何模块隐藏的方法。有很重要的一点需要记住,尽管这个模块能够从运行于同一台机子上的嗅探器中隐藏指定的传输,但是,位于同一个网段上的其他机子上的嗅探器仍然能够看到这些数据包。看了这个模块,精干的读者很快就能设计一些Netfilter钩子函数来阻断任何一种想要阻断的数据包。我就利用本文中提到的技术成功地在其它内核模块项目中实现了对控制和信息获取数据包的隐藏。
(此处代码见附件)
[参考文献]:
[1] The tcpdump group
http://www.tcpdump.org
[2] The Packet Factory
http://www.packetfactory.net
[3] My network tools page -
http://uqconnect.net/~zzoklan/software/#net_tools
[4] Silvio Cesare's Kernel Function Hijacking article
http://vx.netlux.org/lib/vsc08.html
[5] Man pages for:
- raw (7)
- packet (7)
- tcpdump (1)
[6] Linux kernel source files. In particular:
- net/packet/af_packet.c (for packet_rcv())
- net/ipv4/raw.c (for raw_rcv())
- net/core/dev.c
- net/ipv4/netfilter/*
[7] Harald Welte's Journey of a packet through the Linux 2.4 network
stack
http://gnumonks.org/ftp/pub/doc/packet-journey-2.4.html
[8] The Netfilter documentation page
http://www.netfilter.org/documentation
[9] Phrack 55 - File 12 -
http://www.phrack.org/show.php?p=55&a=12
[A] Linux Device Drivers 2nd Ed. by Alessandro Rubini et al.
[B] Inside the Linux Packet Filter. A Linux Journal article
http://www.linuxjournal.com/article.php?sid=4852