进行网络管理时,常常需要确定当前网络中处理活动状态的主机。本设计的目标就是编制程序,利用ICMP的回送请求和回送应答消息,来发现指定网段中的活动主机,即ping消息的请求和应答。
编写程序,其功能是发送ICMP数据包,以获取指定网段中的活动主机,并将结果显示在标准输出上。程序具体要求如下:
1)用命令行形式运行:scanhost Start_IP End_IP
其中scanhost为程序名,Start_IP为被搜索网段的开始IP地址,End_IP为被搜索网段的结束IP地址。
2)输出格式为:
活动主机1
活动主机2
……….
本设计的主体思想是使用ICMPECHO数据包来探测指定网段内的活动主机。
具体方法是:通过简单的发送一个ICMPECHO(Type 8)数据包到目标主机,如果ICMPECHOReply(ICMPtype0)数据包接受到,说明主机是存活状态。如果没有就可以初步判断主机没有在线或者使用了某些过滤设备过滤了ICMP的REPLY。
为了提高IP数据报交付成功的机会,在网际层使用了ICMP协议。ICMP全称Internet Control Message Protocol,即网际控制报文协议,工作在OSI的网络层。ICMP允许主机或路由器报告差错情况和提供有关异常情况的报告。由于ICMP报文是作为IP层数据报的数据,因此需要加上IP数据报的首部,封装在IP数据报内部才能传输。其结构如(图一)所示。
图一: ICMP数据报与IP数据报的关系
ICMP 报文的种类有两种,即ICMP差错报告报文和ICMP询问报文。ICMP报文的前4个字节是统一的格式,共有三个字段:即0-7位的类型字段、8-15位的代码字段、16-31位的校验和字段。其中,校验和字段为2个字节,校验的范围是整个ICMP报文。接着的4个字节的内容与ICMP的类型有关。而其他字节互不相同。其结构如(图二、三)所示。本设计仅用到类型为0和8的ICMP报文,关于这两种类型报文的具体描述详见(图三)。
主机扫描的目的是确定在目标网络上的主机是否可达。这是信息收集的初级阶段,其效果直接影响到后续的扫描。Ping就是最原始的主机存活扫描技术,利用icmp的echo字段,发出的请求如果收到回应的话代表主机存活。
常用的传统扫描手段有:
本程序使用的是ICMP EChO扫描技术。
TCP是一种面向连接的,可靠的传输层协议。一次正常的TCP传输需要通过在客户端和服务器之间建立特定的虚电路连接来完成,该过程通常被称为“三次握手”。TCP通过数据分段中的序列号保障所有传输的数据可以在远端按照正常的次序进行重组,而且通过确认保证数据传输的完整性。
在完成主机存活性判断之后,就应该去判定主机开放信道的状态,端口就是在主机上面开放的信道,0-1024为知名端口,端口总数是65535。端口实际上就是从网络层映射到进程的通道。通过这个关系就可以掌握什么样的进程使用了什么样的通信,在这个过程里面,能够通过进程取得的信息,就为查找后门、了解系统状态提供了有力的支撑。
本程序利用三次握手过程与目标主机建立完整或不完整的TCP连接。
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。
本设计使用原始套接字生成ICMP报文来进行活动主机的探测。设计的大体思想是把报文类型设置为回送请求,将它发送给网络上的一个IP地址,如果这个IP地址已被占用,那么使用这个IP地址的主机上的TCP/IP软件就能够接收到这个ICMP回送请求,并返回一个ICMP回送响应信息。由于接收到的回送响应ICMP包是封装在IP包内,就需要解析该IP包,从中找到ICMP数据信息。相反,如果这个IP地址没有人使用,那么发送的ICMP回送请求在设定的时延内就不可能得到响应。
在初始化原始套接字后,程序就要开始在一个IP网段内寻找活动主机。由于在某网段内需要发现的主机很多,为提高效率,采用了多线程编程。
由于socket发送和捕获的是IP包,因此要分别定义IP首部和ICMP首部
typedef struct iphdr{ //IP头
unsigned int headlen:4; //IP头长度
unsigned int version:4; //IP版本号
unsigned char tos; //服务类型
unsigned short id; //ID号
unsigned short flag; //标记
unsigned char ttl; //生存时间
unsigned char prot; //协议
unsigned short checksum; //效验和
unsigned int sourceIP; //源IP
unsigned int destIP; //目的IP
}IpHeader;
//IP头部
代码:
typedef struct icmp_hdr
{
unsigned char icmp_type; // 消息类型
unsigned char icmp_code; // 代码
unsigned short icmp_checksum; // 校验和
// 下面是回显头
unsigned short icmp_id; // 用来惟一标识此请求的ID号,通常设置为进程ID
unsigned short icmp_sequence; // 序列号
unsigned long icmp_timestamp; // 时间戳
} ICMP_HDR, *PICMP_HDR;
代码:
//创建原始套接字
//AF_INET表示地址族为IPV4
//SOCK_RAW表示创建的为原始套接字,若在UNIX/LINUX环境下,应该获得root权限,在Windows环境下使用管理员权限运行程序
SOCKET sRaw=::socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
SetTimeout(sRaw,1000,TRUE);
//设置超时时间
int SetTimeout(SOCKET s, int nTime, BOOL bRecv)
{
int ret = ::setsockopt(s, SOL_SOCKET,bRecv ? SO_RCVTIMEO : SO_SNDTIMEO,(char*)&nTime, sizeof(nTime));
return ret != SOCKET_ERROR;
}
将输入的IP地址转换为点分十进制数
void change(int a,int b,int c,int d,char IP[20]) //IP转换
{
char IPPort[4][4]={'\0'};
char temp[2]={'.','\0'};
itoa(a,IPPort[0],10);
itoa(b,IPPort[1],10);
itoa(c,IPPort[2],10);
itoa(d,IPPort[3],10);
strcat(IP,IPPort[0]);
strcat(IP,temp);
strcat(IP,IPPort[1]);
strcat(IP,temp);
strcat(IP,IPPort[2]);
strcat(IP,temp);
strcat(IP,IPPort[3]);
}
为了使收到数据包的目的主机发送响应,我们需要向目的主机发送请求类型的ICMP报文。请求类型的ICMP报文的填充如下
代码:
//初始化ICMP请求包
pIcmp->icmp_type=8; //设置类型
pIcmp->icmp_code=0;
pIcmp->icmp_id=(USHORT)::GetCurrentProcessId(); //设置ID为当前线程号
pIcmp->icmp_checksum=0; //先将校验和置0
pIcmp->icmp_sequence=0; //序列号为0
//填充ICMP包
memset(&buff[sizeof(ICMP_HDR)],'E',32); //填入数据
pIcmp->icmp_checksum=0;
pIcmp->icmp_timestamp=::GetTickCount();
pIcmp->icmp_sequence=nSeq++;
pIcmp->icmp_checksum=checksum((USHORT *)buff,sizeof(ICMP_HDR)+32);
根据TCP/IP协议,IP数据包在传输过程前必须计算检验和,对收到的数据也要计算检验和。Checksum()函数实现了首部的检验和计算,首先设校验和初值为0,然后对每16位求异或,结果取反。
代码:
USHORT checksum(USHORT* buff, int size)
{
unsigned long cksum = 0;
while(size>1)
{
cksum += *buff++;
size -= sizeof(USHORT);
}
// 是奇数
if(size)
{
cksum += *(UCHAR*)buff;
}
// 将32位的chsum高16位和低16位相加,然后取反
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16); // ???
return (USHORT)(~cksum);
}
填充ICMP报文后,应在ICMP报文之前加上IP报头发送出去。
代码:
//填写目的主机相关信息,不需要填写端口号,因为ICMP是网络层协议
SOCKADDR_IN dest;
dest.sin_family=AF_INET;
dest.sin_port=htons(0);
dest.sin_addr.S_un.S_addr=inet_addr(szDestIP); //填入搜索的IP地址
nRet=::sendto(sRaw,buff,sizeof(ICMP_HDR)+32,0,(SOCKADDR *)&dest,sizeof(dest));
如果所Ping的目的主机所在,那么它会发送一个回送应答包。这是一个IP包,收到后解析此数据包并获取其中的ICMP信息。根据IP报头信息中的IP报头长度字段,就可以得到ICMP报文的真实地址。ICMP数据包中的IP地址就是活动主机的IP。
代码:
nRet=::recvfrom(sRaw,revBuf,1024,0,(sockaddr *)&from,&nLen);
if (nRet==SOCKET_ERROR)
{
/* if(WSAGetLastError()==WSAETIMEDOUT)
{
printf("Timed out.\n");
}
*/
printf("%s 主机没有存活!\n",szDestIP);
return -1;
}
printf("%s 主机存活!\n",szDestIP);
closesocket(nRet);
WSACleanup();
int Computer(char szDestIP[30]) //扫描主机是否存活
{
WSADATA wsaData;
WORD wVersionRequested=MAKEWORD(1,1);
if (WSAStartup(wVersionRequested , &wsaData))
{
printf("Winsock Initialization failed \n");
exit(1);
}
//创建原始套接字
//AF_INET表示地址族为IPV4
//SOCK_RAW表示创建的为原始套接字,若在UNIX/LINUX环境下,应该获得root权限,在Windows环境下使用管理员权限运行程序
SOCKET sRaw=::socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
SetTimeout(sRaw,1000,TRUE);
SOCKADDR_IN dest;
//填写目的主机相关信息,不需要填写端口号,因为ICMP是网络层协议
dest.sin_family=AF_INET;
dest.sin_port=htons(0);
dest.sin_addr.S_un.S_addr=inet_addr(szDestIP);
//创建ICMP数据包
char buff[sizeof(ICMP_HDR)+32];
ICMP_HDR * pIcmp=(ICMP_HDR *)buff;
//初始化ICMP包
pIcmp->icmp_type=8;
pIcmp->icmp_code=0;
pIcmp->icmp_id=(USHORT)::GetCurrentProcessId();
pIcmp->icmp_checksum=0;
pIcmp->icmp_sequence=0;
memset(&buff[sizeof(ICMP_HDR)],'E',32);
USHORT nSeq=0;
char revBuf[1024];
SOCKADDR_IN from;
int nLen=sizeof(from);
static int nCount=0;
int nRet;
//填充ICMP包
pIcmp->icmp_checksum=0;
pIcmp->icmp_timestamp=::GetTickCount();
pIcmp->icmp_sequence=nSeq++;
pIcmp->icmp_checksum=checksum((USHORT *)buff,sizeof(ICMP_HDR)+32);
//开始发送和接受ICMP封包
nRet=::sendto(sRaw,buff,sizeof(ICMP_HDR)+32,0,(SOCKADDR *)&dest,sizeof(dest));
if (nRet==SOCKET_ERROR)
{
printf("sendto() failed:%d\n",::WSAGetLastError());
return -1;
}
//接受回显回答
nRet=::recvfrom(sRaw,revBuf,1024,0,(sockaddr *)&from,&nLen);
if (nRet==SOCKET_ERROR)
{
/* if(WSAGetLastError()==WSAETIMEDOUT)
{
printf("Timed out.\n");
}
*/
printf("%s 主机没有存活!\n",szDestIP);
return -1;
}
printf("%s 主机存活!\n",szDestIP);
closesocket(nRet);
WSACleanup();
return 0;
}
利用一个for(;;)循环,对确定的起始和终止IP地址内的所有可能存在的主机发送,请求回显的ICMP,并对返回的ICMP报文进行分析,提取出type和Code字段根据ICMP报文格式所定义的数值,对各个主机判断,并打印信息。
代码:
for(int i=startport; i<=endport; i++)
{
//创建一个Socket
if((mysocket = socket(AF_INET, SOCK_STREAM,0)) == INVALID_SOCKET)
exit(1);
my_addr.sin_family = AF_INET;
//主机字节序转换为网络字节序
my_addr.sin_port = htons(i);
my_addr.sin_addr.s_addr = inet_addr(adr);
//用此Socket连接目的主机
if(connect(mysocket, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == SOCKET_ERROR)
{
//连接失败
/* switch(WSAGetLastError())
{
case 10060:
printf("%s\t%d\tError\t\tConnection timed out\n",i);
break;
case 10061:
printf("%s\t%d\tERROR%d\tConnection refused\n",i);
break;
default:
printf("%s\t%d\tERROR\tCode:\n",WSAGetLastError());
break;
}*/
printf("Port %d - 关闭\n", i);
closesocket(mysocket); //关闭Socket,回收资源
}
else
{
//连接成功
pcount++;
printf("Port %d - 打开\n", i);
}
printf("%d ports open on host - %s\n", pcount, adr);
closesocket(mysocket);
WSACleanup();
}
本次程序设计使我对《计算机网络》课上学到的知识有了更深的认识,对C++网络编程技术有了一个基础的了解。从刚开始的不知如何下手,到后面通过上网查阅资料,对程序设计的框架有了一个大致的雏形,然后查阅相关的C++网络编程书籍,如《Visual C++精彩实例讲解》和《Visual Cpp 实例精通》,慢慢了解C++网络开发的相关知识(虽然还是没有完全弄懂),但结合相关的源码,还是能勉强完成实验设计。整个设计过程虽然非常痛苦,但通过自己动手参与一个完整的“基于ICMP和TCP的网段扫描器”的开发过程,让我对计算机网络程序开发的具体过程有了一个更为深刻的影响,而不再是仅仅停留在书本上的知识。同时也发现了自己在C++编程方面眼光的浅薄,单纯地把目光放在课堂上所学的知识上,如算法设计方面,而没有去涉及C++中的容器和套接字等更为强大功能的学习。在以后的学习中,我会主动扩大自己的知识面,多去学习以一些更为实用的技能,挣脱书本的束缚,制定自己的学习计划。