基于ICMP和TCP协议的网段扫描器

1. 系统描述

1.1. 设计目标

进行网络管理时,常常需要确定当前网络中处理活动状态的主机。本设计的目标就是编制程序,利用ICMP的回送请求和回送应答消息,来发现指定网段中的活动主机,即ping消息的请求和应答。

1.2. 设计功能

编写程序,其功能是发送ICMP数据包,以获取指定网段中的活动主机,并将结果显示在标准输出上。程序具体要求如下:
1)用命令行形式运行:scanhost Start_IP End_IP
其中scanhost为程序名,Start_IP为被搜索网段的开始IP地址,End_IP为被搜索网段的结束IP地址。
2)输出格式为:
活动主机1
活动主机2
……….

1.3. 设计原理

本设计的主体思想是使用ICMPECHO数据包来探测指定网段内的活动主机。
具体方法是:通过简单的发送一个ICMPECHO(Type 8)数据包到目标主机,如果ICMPECHOReply(ICMPtype0)数据包接受到,说明主机是存活状态。如果没有就可以初步判断主机没有在线或者使用了某些过滤设备过滤了ICMP的REPLY。

2. 原理介绍

2.1. ICMP协议

为了提高IP数据报交付成功的机会,在网际层使用了ICMP协议。ICMP全称Internet Control Message Protocol,即网际控制报文协议,工作在OSI的网络层。ICMP允许主机或路由器报告差错情况和提供有关异常情况的报告。由于ICMP报文是作为IP层数据报的数据,因此需要加上IP数据报的首部,封装在IP数据报内部才能传输。其结构如(图一)所示。
基于ICMP和TCP协议的网段扫描器_第1张图片
图一: ICMP数据报与IP数据报的关系
ICMP 报文的种类有两种,即ICMP差错报告报文和ICMP询问报文。ICMP报文的前4个字节是统一的格式,共有三个字段:即0-7位的类型字段、8-15位的代码字段、16-31位的校验和字段。其中,校验和字段为2个字节,校验的范围是整个ICMP报文。接着的4个字节的内容与ICMP的类型有关。而其他字节互不相同。其结构如(图二、三)所示。本设计仅用到类型为0和8的ICMP报文,关于这两种类型报文的具体描述详见(图三)。
基于ICMP和TCP协议的网段扫描器_第2张图片

2.2. 主机存活扫描技术

主机扫描的目的是确定在目标网络上的主机是否可达。这是信息收集的初级阶段,其效果直接影响到后续的扫描。Ping就是最原始的主机存活扫描技术,利用icmp的echo字段,发出的请求如果收到回应的话代表主机存活。
常用的传统扫描手段有:

1. ICMP Echo扫描:精度相对较高。通过简单地向目标主机发送ICMP Echo Request 数据包,并等待回复的ICMP Echo Reply 包,如Ping。

2. ICMP Sweep扫描:sweep这个词的动作很像机枪扫射,icmp进行扫射式的扫描,就是并发性扫描,使用ICMP Echo Request一次探测多个目标主机。通常这种探测包会并行发送,以提高探测效率,适用于大范围的评估。

3. Broadcast ICMP扫描:广播型icmp扫描,利用了一些主机在icmp实现上的差异,设置ICMP请求包的目标地址为广播地址或网络地址,则可以探测广播域或整个网络范围内的主机,子网内所有存活主机都会给以回应。但这种情况只适合于UNIX/Linux系统。

4.Non-Echo ICMP扫描:在ICMP协议中不光光只有ICMP ECHO的ICMP查询信息类型,在ICMP扫描 技术中也用到Non-ECHO ICMP技术(不仅仅能探测主机,也可以探测网络设备如路由)。利用了ICMP的服务类型(Timestamp和Timestamp Reply 、Information Request和Information Reply 、Address Mask Request 和Address Mask Reply)。

本程序使用的是ICMP EChO扫描技术。

2.3. TCP协议

TCP是一种面向连接的,可靠的传输层协议。一次正常的TCP传输需要通过在客户端和服务器之间建立特定的虚电路连接来完成,该过程通常被称为“三次握手”。TCP通过数据分段中的序列号保障所有传输的数据可以在远端按照正常的次序进行重组,而且通过确认保证数据传输的完整性。

2.4. 端口扫描技术

在完成主机存活性判断之后,就应该去判定主机开放信道的状态,端口就是在主机上面开放的信道,0-1024为知名端口,端口总数是65535。端口实际上就是从网络层映射到进程的通道。通过这个关系就可以掌握什么样的进程使用了什么样的通信,在这个过程里面,能够通过进程取得的信息,就为查找后门、了解系统状态提供了有力的支撑。
本程序利用三次握手过程与目标主机建立完整或不完整的TCP连接。

2.5. 原始套接字

原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。

2.6. 实现方法

本设计使用原始套接字生成ICMP报文来进行活动主机的探测。设计的大体思想是把报文类型设置为回送请求,将它发送给网络上的一个IP地址,如果这个IP地址已被占用,那么使用这个IP地址的主机上的TCP/IP软件就能够接收到这个ICMP回送请求,并返回一个ICMP回送响应信息。由于接收到的回送响应ICMP包是封装在IP包内,就需要解析该IP包,从中找到ICMP数据信息。相反,如果这个IP地址没有人使用,那么发送的ICMP回送请求在设定的时延内就不可能得到响应。
在初始化原始套接字后,程序就要开始在一个IP网段内寻找活动主机。由于在某网段内需要发现的主机很多,为提高效率,采用了多线程编程。

3. 系统设计

3.1. 数据结构设计

3.1.1. 定义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头部  

3.1.2. 定义ICMP报文首部结构

代码:

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;             

3.1.3. 定义原始套接字

代码:

//创建原始套接字
    //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;             
}       

3.1.4. IP转换

将输入的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]);           
}       

3.1.5. 填充ICMP报文

为了使收到数据包的目的主机发送响应,我们需要向目的主机发送请求类型的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); 

3.1.6. 计算检验和

根据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);           
}  

3.1.7. IP数据报发送

填充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)); 

3.1.8. 解析数据包

如果所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();   

3.1.9. 利用ICMP报文进行网段主机扫描

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;           
}       

3.1.10. 端口扫描

利用一个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();               
    }  

4. 程序运行结果

基于ICMP和TCP协议的网段扫描器_第3张图片

5. 程序设计心得

本次程序设计使我对《计算机网络》课上学到的知识有了更深的认识,对C++网络编程技术有了一个基础的了解。从刚开始的不知如何下手,到后面通过上网查阅资料,对程序设计的框架有了一个大致的雏形,然后查阅相关的C++网络编程书籍,如《Visual C++精彩实例讲解》和《Visual Cpp 实例精通》,慢慢了解C++网络开发的相关知识(虽然还是没有完全弄懂),但结合相关的源码,还是能勉强完成实验设计。整个设计过程虽然非常痛苦,但通过自己动手参与一个完整的“基于ICMP和TCP的网段扫描器”的开发过程,让我对计算机网络程序开发的具体过程有了一个更为深刻的影响,而不再是仅仅停留在书本上的知识。同时也发现了自己在C++编程方面眼光的浅薄,单纯地把目光放在课堂上所学的知识上,如算法设计方面,而没有去涉及C++中的容器和套接字等更为强大功能的学习。在以后的学习中,我会主动扩大自己的知识面,多去学习以一些更为实用的技能,挣脱书本的束缚,制定自己的学习计划。

你可能感兴趣的:(算法)