全部博文(403)
分类:
2006-09-15 16:56:43
天网千帆文件搜索开源项目
TCP端口扫描模块
谢翰
一、开发目的
ftp文件搜索的第一步就是发现Internet上存在ftp服务的计算机。由于通常ftp服务都是使用TCP 21端口,所以我们发现ftp服务的第一个方法就是扫描Internet上所有计算机的TCP 21端口是否接受连接。另外,由于Internet上的机算机数目巨大,而ftp服务也存在时效性,这就要求我们必须在尽量短的时间内完成扫描。原有的天网ftp端口扫描程序在效率上已经不能满足要求,扫描结果也不够完全,因此有必要开发新的端口扫描程序。
二、实现方法
一般的TCP端口扫描都是通过系统调用connect,如果在一个较短时间内对方主机没有响应,则认为该主机的该端口不可接受连接。虽然程序可以启动多个进程,每个进程同时发出多个连接请求,但如果每个连接的超时限制设得很长,则整个扫描过程会很长;如果超时限制很短,则那些响应较慢的主机可能无法被发现。原来系统的连接超时为5秒,针对全国范围内的IP地址进行一次扫描需要1个多月。
因此,我们必须考虑一种全新的方法来实现TCP端口的扫描。我们首先必须找出原有的端口扫描方法瓶颈所在。我们发现,如果原有的端口扫描方法是调用connect,而connect所做的事情就是向远程主机发送TCP的连接请求信号(SYN),再等待对方的响应。可见其发送与接收是同步的。如果改用异步的方法:一边不断的向IP地址范围内的主机发送SYN连接信号,一边监听发往本机的IP数据包。如果某个IP地址某端口接受连接,则它会向本机发来一个SYN,并确认我们发出的SYN信号;如果某个IP地址某端口拒绝连接,则它会向本机发来一个TCP RST,同时确认我们发出的SYN信号;如果某个IP地址某端口不可达,则我们会收到一个ICMP不可达报文。这样,我们的发送与接收就成了异步进行,扫描速度会有质的发跃。
要实现这种设想,我们必须熟悉IP协议,TCP协议以及ICMP协议,因为我们必须自行构造和发送TCP SYN信号,并且自行解析收入到的TCP包和ICMP包。此外,由于网络的数据包可能会丢失,也可能被重复收到,如果我们只对每个IP地址发送一次连接请求是不可靠的,因此对那些没有响应的地址我们还要多发送几次SYN。那么,我们就必须记录下那些已经有响应的地址,在下一轮的发送中略过这些地址。在实现中我们使用了一棵红-黑树(因为是用C语言编写,如果是C++则用一个向量就可以),用于存放所有的已经有响应的地址。每接收到一个响应(接受,拒绝,不可达),也要查看该响应的地址是否已经在这棵红-黑树中,这样就可以防止一个IP地址被多次的返回给调用者。
三、模块使用说明
TCP端口扫描模块只有一个函数接口:tcp_scan。其函数原型如下:
int tcp_scan(const struct addrseg *addrscope, const unsigned short *ports,
unsigned int ifindex, const char *ifname, int resetuid,
scan_info_t info, void *arg);
下面解释一下各个参数的意义。
const struct addrseg *addrscope: 要扫描的地址范围,是一个结点类型为struct addrseg的链表,每个结点为一个IP地址段。struct addrseg的定义如下:
struct addrseg
{
int as_family;
void *as_address;
unsigned int as_bits;
struct addrseg *as_next;
};
其中,as_family表示这个地址段的类型,可以是AF_INET或AF_INET6,分别表示IPv4地址和IPv6地址;as_address指向一个struct in_addr结构(IPv4)或struct in6_addr结构(IPV6)。as_bits指明as_address的有效位数,有效位数越少,则这个地址段越大。例如,北大的地址段162.105.*.*,其地址有效位为16位;如果一个地址段只包含一个地址,如162.105.80.1,则其地址有效位为32位。as_next指向下一个地址段,若as_next == NULL则表示链表结束。
const unsigned short *ports:要扫描的端口集。以0作为结尾。
unsigned int ifindex:用于通信的网络设备号。如果不清楚什么意思该参数指定为0既可,这种情况下要指定网络设备名。
const char *ifname:用于通信的网络设备名。仅当ifindex为0时该参数有效。在单网卡的机器里,网卡的设备名通常是”eth0”。所以一般该参数指定为”eth0”既可。在此说明一下,要求指定网络设备号或网络设备名是为了方便内部的实现以及加快扫描速度,否则的话每发一个IP包都必须查看路由表以确定所用的网络设备。但带来的问题是在多网卡的计算机上,不能同时扫描内网和外网。
int resetuid:由于tcp_scan运行时需要root权限,resetuid的指明是否在无需root权限时,将进程的有效权限设置为进程所有者的权限。如果不理解则该参数置为1既可。
scan_info_t info:回调函数。当tcp_scan发现一个IP地址与端有响应是,调用一次info将该IP地址与端口,以及响应状态返回给调用者。scan_info_t的定义如下:
typedef int (*scan_info_t)(const struct sockaddr *, socklen_t, int, void *);
第一个参数类型为const struct sockaddr *,为有响应的IP地址与端口,封装在一个struct sockaddr结构里;第二个参数socklen_t表示第一个参数struct sockaddr的长度;第三个参数int为状态,有三种可能:TS_ACCEPTED,TS_REFUSED,TS_UNREACH,分别代表接受连接,拒绝连接与地址不可达;最后一个参数void *为用户自定义,回调函数的惯例。该回调函数可返回负值表示失败,在这种情况下tcp_scan将立即以失败返回。
void *arg:回调函数的用户自定义参数,该参数将原封不动的作为最后一个参数传给回调函数info。
tcp_scan若返回负值则表示调用失败,失败原因查看errno。返回非负值表示成功。
以下示例为为扫描北大清华范围内的所有http端口和ftp端口:
int info(const struct sockaddr *sockaddr, socklen_t addrlen,
int state, void *arg)
{
struct sockaddr_in *sin = (struct sockaddr_in *)sockaddr;
printf("%s:%d ", inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
switch (state)
{
case TS_ACCEPTED:
printf("Connection accepted\n");
break;
case TS_REFUSED:
printf("Connection refused\n");
break;
case TS_UNREACH:
printf("Destination unreachable\n");
break;
}
return 0;
}
int main(void)
{
struct addrseg addrseg[2];
struct in_addr inaddr[2];
unsigned short ports = { 80, 21, 0 };
addrseg[0].as_family = AF_INET;
addrseg[0].as_address = inaddr;
inet_pton(AF_INET, “162.105.0.0”, addrseg[0].as_address);
addrseg[0].as_bits = 16;
addrseg[0].as_next = addrseg + 1;
addrseg[1].as_family = AF_INET;
addrseg[1].as_address = inaddr + 1;
inet_pton(AF_INET, “166.111.0.0”, addrseg[1].as_address);
addrseg[1].as_bits = 16;
addrseg[1].as_next = NULL;
tcp_scan(addrseg, ports, 0, “eth0”, 1, info, NULL);
}
四、测试数据
测试环境:P4 1.4G,256M内存,Red Hat Linux 9.0。
测试内容:扫描全国IP地址范围内的ftp端口。
开始时间:Fri May 14 02:46:22 2004
完成时间:Fri May 14 05:03:53 2004
总用时:2小时17分31秒
测试结果:总的有响应的地址:1564712。其中接受连接地址:273074;拒绝连接地址:1291594;不可达地址:44。
占用内存:忘了统计:)不会超过100M
五、已知BUG与不足
目前本模块还是存在一些问题的。前面说过不能同时扫描内网和外网。除此之外,tcp_scan还可能会返回极少量的完全不在扫描范围之内的地址或端口。因为对每个收到的响应,我们只查看其目标端口号与确认TCP序列号,而不再判断它是否在我们的扫描范围之内,因为这是很耗时的。而有的远程主机由于操作系统的错误,可能从别的源端口向我们发来响应,并且确认序列号也是正确的。由于这样的情况并不多,并且不要影响到系统的完全性,所以我们把它留给回调函数处理。如果这少量的错误没有什么影响,回调函数也完全可以忽略它:实现上我建议这么做。
六、附加说明
首先,运行tcp_scan必须有root权限,如果要让普通用户使用则必须设置可执行文件的权限调整位,并把tcp_scan的参数resetuid置为1,以保证完全性。此外,tcp_scan的空间复杂性是随扫描的端口数线性增长的,因此,如果你要在大范围内(如全国)扫描多个端口,最好是分为几次进行,每次扫描一个端口。当然如果你内存很大则另当别论。
模块的红-黑树实现(rbtree.c,rbtree.h)取自Linux内核源代码,也是遵循GPL;另外计算校验和的函数in_cksum取自Unix Network Programming Volume 1的示例代码。除此之外不存在非自编的代码。
查看tcp_scan源代码时,请将Table Stop的距离设为4。在vi中的命令为“:set ts=4”。本模块与本项目其它所有其它模块都遵循GPL许可协议。您可将本模块用于任何商品或非商品目的,并且可对它进行任何的修改,但您不可声称是本模块的作者。如果您愿意在您的产品中给出致谢信息我们将会很高兴,但这并不是必须的。更加详细的信息请查看GPL许可协议。
Random Quote: Software is like sex. It’s better when it’s free.