我的整体思路如下:首先实现扫描一个ip的所有端口的功能,然后再获取本机局域网ip,检测局域网内的其他存活ip,通过端口扫描逐个扫描存活ip即可

实现扫描一个ip的scanner

connect扫描原理

TCP connect方式使用系统网络API connect向目标主机的端口发起连接,如果无法连接,说明该端口关闭。但是缺点是connect扫描速度较慢,所以接下来我采用connect扫描与多线程相结合的方式,提高扫描速度

connect扫描的多线程设置

我的大致思路是将要扫描的端口均分给每一个线程,通过并行来提高端口扫描的效率

首先通过宏定义初始化一些基本参数

    char *addr=ip;                   //IP地址
    int num_thread=NUM_THREAD;      //线程个数
    int port_front=PORT_START;      //起始端口号
    int port_end=PORT_END;         //终止端口号
    int num_port=port_end-port_front+1; //扫描的端口个数

后期如需更改扫描的范围或者测试效率,更改宏定义的数据即可

    #define PACKET_SIZE 4096
    #define NUM_THREAD 512
    #define PORT_START 1
    #define PORT_END 65535
    #define NUM_IP 20

NUM_IP是扫描的局域网的ip个数,如果需要扫描整个局域网,一般局域网都是设置的24网段,那么只需要扫描255个端口即可,而且通常最后一个是广播地址,无需扫描,那么只用扫描254个即可

PORT_START 和 PORT_END 是要扫描的起始端口和结束端口,例如我这里扫描的所有端口,所以设置为1和65535

NUM_THREAD 是用的线程个数,其他宏定义后续再做解释

多线程传递多个参数要用结构体型数据传参,设置结构体类型如下:

struct THREAD_TEMP
{
    char *addr;
    int num_port;
    int num_thread;
    int head;
    int end;
};

addr是全局变量ip地址的指针,num_port是扫描的端口的个数,num_thread是线程个数,head是开始扫描的端口,end是结束扫描的端口

通过for循环创建NUM_THREAD个线程,同时将传参的数据存在结构体数组ranges中,通过调用scan_a_port线程子函数来达到端口来实现端口扫描,最后通过pthread_join()阻塞主函数,当所有子线程的函数全部返回后才结束

    pthread_t id[num_thread];
    struct THREAD_TEMP ranges[num_thread];

    for(i=0;i<num_thread;i++){
        ranges[i].addr=addr;
        ranges[i].num_port=num_port;
        ranges[i].num_thread=num_thread;
        ranges[i].head=port_front+(num_port/num_thread)*i;
        ranges[i].end=port_front+(num_port/num_thread)*(i+1)-1;      
        pthread_create(&id[i],NULL,scan_a_port,(void *)&ranges[i]);
    }

    for(i=0;i<num_thread;i++){
        pthread_join(id[i],NULL);
    }

子线程scan_a_port()函数

将主函数传递过来的结构体变量解析出来ip,端口号,然后直接调用connect函数即可实现功能

以端口号的解析为例:

    port=((struct THREAD_TEMP*)arg)->head;

将数据填入地址中

    memset(&serv_addr,0,sizeof(serv_addr));//初始化
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(((struct THREAD_TEMP*)arg)->addr);   //IP
    serv_addr.sin_port=htons(port);              //port

上述的inet_addr函数是将str型数据转化为二进制形式存储在s_addr中

htons函数是将本地字节序转化为网络字节序,因为网络通信可能从不同种机器之间进行,有的大端序,有的小端序,为了实现统一,所以统一改为网络字节序(大端)存储

调用connect函数,如果返回为0,则该端口为开启端口

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==0){   
        flag=1;
        printf("%d\t",ntohs(serv_addr.sin_port));
        close(sock);  
    }

至此,已经完整实现了扫描一个ip的所有端口的功能

获取本机局域网ip

这里使用pcap库来实现本机ip、掩码、网桥等信息的获取

调用合适的网卡

    devStr=pcap_lookupdev(errBuf);  //获取合适的网卡
    inet_sock = socket(AF_INET, SOCK_DGRAM, 0);  
    strcpy(ifr.ifr_name, devStr); 

本机ip的获取:

    ioctl(inet_sock, SIOCGIFADDR, &ifr);  
    strcpy(local_ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

掩码获取:

    ioctl(inet_sock,SIOCGIFNETMASK,&ifr);
    strcpy(mask,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));

网桥获取:

    ioctl(inet_sock,SIOCGIFBRDADDR,&ifr);
    strcpy(broad_ip,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr));

检测局域网其他存活ip并扫描

首先这里就由提到之前说的网络字节序的问题了,在结构体in_addr中,ip地址以二进制形式存储,而且是大端序,例如,假设一个ip地址是0.128.1.255,那么存储为:

str:    0.128.1.255
二进制: 11111111 00000001 10000000 00000000
16进制:  0xff018000

如果是在24网段,我只需要将前8位修改为我想要扫描的ip即可

首先创建一个指向8位的指针,指向最前面一位,因为是大端序,所以实际上是从最后倒排,此时最前一位实际上是最后一位,所以+3

    unsigned char *temp=(unsigned char*)&addr+3;

通常局域网ip的.1是路由器地址所以从.2开始扫描才能扫描其他主机,用初始化为0的int型数flag[255]来标记存活ip,如果存活才开始扫描

    for(int i=2;i<=NUM_IP;i++){
        *temp=i;
        ip_after=inet_ntoa(addr);
        if(livetest(ip_after)==1)
            ip_flag[i]=1;
        sleep(1);
    }

    for(int i=2;i<=NUM_IP;i++){
        if(ip_flag[i]==1){
            *temp=i;
            ip_after=inet_ntoa(addr);
            scan_ip(ip_after);
        }
    }

结果展示

单独测试检测本地存活ip:

测试扫描局域网内所有主机:

因为我家是开的店铺,所以有很多同种设备的网络摄像头,扫描出来很多ip开放端口一样不是程序错误,而是扫描到了同种设备,仅此而已

个人总结

首先整个实验能带给我的收益还挺多的,既是新知识的学习过程,也是对以前学习的一个复习与巩固。新的知识包括多线程的学习,socket的数据结构和相关函数等,旧知识就是计算机组成原理学的大端序小端序和位操作。当然我的实验也有不完善的地方,因为testlive函数没有实现完整的ping功能,所以效率比较低,但是完整的ping功能实现代码量其实挺大的,可能比我这个地方实验的代码还多,所以我也没实现完整功能。再一个就是多线程的优化,很早之前就听说线程池的高效率和简便性,但是自己也还没能弄懂,所以这里也就只能写了一个“伪线程池”结构。希望自己早日提高个人能力后,回过头来能重新优化一下自己的功能和效率。

然后很多比较基础的知识可能本篇中没有着重强调,但是我已经提前学习并写过文档了,可以点击下列文字参考:
Linux C 多线程学习笔记Socket编程基础for循环实现的“伪线程池“demo

完整源码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<pthread.h>
#include<time.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/ioctl.h>   
#include <net/if.h>
#include <pcap.h>

#define PACKET_SIZE 4096
#define NUM_THREAD 512
#define PORT_START 1
#define PORT_END 65535
#define NUM_IP 20

void * scan_a_port(void* arg);
int scan_ip(char *ip);
unsigned short cal_chksum(unsigned short *addr,int len);
int livetest(char* ip);
void get_localhost_infor(void);
void show_localhost_infor(void);

char local_ip[16];  
char mask[32];
char broad_ip[16];
int ip_flag[256]; 
int flag=0;

struct THREAD_TEMP
{
    char *addr;
    int num_port;
    int num_thread;
    int head;
    int end;
};

int main(){

    get_localhost_infor();
    show_localhost_infor();

    char *ip_before=local_ip;
    char *ip_after;
    struct in_addr addr;

    inet_aton(ip_before,&addr);
    unsigned char *temp=(unsigned char*)&addr+3;

    for(int i=2;i<=NUM_IP;i++){
        *temp=i;
        ip_after=inet_ntoa(addr);
        if(livetest(ip_after)==1)
            ip_flag[i]=1;
        sleep(1);
    }

    for(int i=2;i<=NUM_IP;i++){
        if(ip_flag[i]==1){
            *temp=i;
            ip_after=inet_ntoa(addr);
            scan_ip(ip_after);
        }
    }

    return 0;
}

//获取本机局域网IP/掩码/网关等information
void get_localhost_infor(){
    int inet_sock;  
    struct ifreq ifr;  
    char errBuf[PCAP_ERRBUF_SIZE],*devStr;

    devStr=pcap_lookupdev(errBuf);  //获取合适的网卡
    inet_sock = socket(AF_INET, SOCK_DGRAM, 0);  
    strcpy(ifr.ifr_name, devStr);  

    //本机ip
    ioctl(inet_sock, SIOCGIFADDR, &ifr);  
    strcpy(local_ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));
    //掩码
    ioctl(inet_sock,SIOCGIFNETMASK,&ifr);
    strcpy(mask,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));
    //网桥ip
    ioctl(inet_sock,SIOCGIFBRDADDR,&ifr);
    strcpy(broad_ip,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr));
    //

}

void show_localhost_infor(){

    printf("localhost_ip:\t%s\n",local_ip);
    printf("ip_mask:\t%s\n",mask);
    printf("broad_ip:\t%s\n",broad_ip);

}

//扫描一个制定ip的所有端口
int scan_ip(char *ip){
    time_t time_start,time_end;
    time_start=time(NULL);              //记录起始时间

    char *addr=ip;                   //IP地址
    int num_thread=NUM_THREAD;      //线程个数
    int port_front=PORT_START;      //起始端口号
    int port_end=PORT_END;         //终止端口号
    int num_port=port_end-port_front+1; //扫描的端口个数

    printf("\n");
    printf("IP:%s\n",addr);
    printf("Begin scan this ip --->\n");
    printf("Up_Port:\t");

    //将端口个数转换为线程个数的整数倍
    while(1){
        if(num_port%num_thread==0)
            break;
        else{
            num_port++;
            port_end++;
        }  
    }

    pthread_t id[num_thread];
    struct THREAD_TEMP ranges[num_thread];

    int i;
    flag=0;
    for(i=0;i<num_thread;i++){
        ranges[i].addr=addr;
        ranges[i].num_port=num_port;
        ranges[i].num_thread=num_thread;
        ranges[i].head=port_front+(num_port/num_thread)*i;
        ranges[i].end=port_front+(num_port/num_thread)*(i+1)-1;      
        pthread_create(&id[i],NULL,scan_a_port,(void *)&ranges[i]);
    }

    for(i=0;i<num_thread;i++){
        pthread_join(id[i],NULL);
    }

    if(flag==0){
        printf("NULL");
    }

    time_end=time(NULL);
    printf("\nFinish scan this ip --->\tUse_time:%d s\n",time_end-time_start);
    return 0;
}

//多线程调用函数--扫描指定ip的指定端口
void * scan_a_port(void* arg){
    int sock;
    struct sockaddr_in serv_addr;
    int port;
    port=((struct THREAD_TEMP*)arg)->head;
    int i;

    for(i=0;i<((struct THREAD_TEMP*)arg)->num_port/((struct THREAD_TEMP*)arg)->num_thread;i++){
        sock=socket(PF_INET,SOCK_STREAM,0);
        if(sock==-1){
            printf("%s\n","socket() error");
            exit(0);
        }

        memset(&serv_addr,0,sizeof(serv_addr));//初始化
        serv_addr.sin_family=AF_INET;
        serv_addr.sin_addr.s_addr=inet_addr(((struct THREAD_TEMP*)arg)->addr);   //IP
        serv_addr.sin_port=htons(port);              //port
        port++;

        if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==0){   
            flag=1;
            printf("%d\t",ntohs(serv_addr.sin_port));
            close(sock);  
        }
        else {
            close(sock);
            continue;
        }
    }   
}


//计算校验和
unsigned short cal_chksum(unsigned short *addr,int len){
    int sum=0;
    int nleft = len;
    unsigned short *w = addr;
    unsigned short answer = 0;
    /* 把ICMP报头二进制数据以2字节为单位累加起来 */
    while(nleft > 1){
        sum += *w++;
        nleft -= 2;
    }
    /*
     * 若ICMP报头为奇数个字节,会剩下最后一字节。
     * 把最后一个字节视为一个2字节数据的高字节,
     * 这2字节数据的低字节为0,继续累加
     */
    if(nleft == 1){
        *(unsigned char *)(&answer) = *(unsigned char *)w;
        sum += answer;    /* 这里将 answer 转换成 int 整数 */
    }
    sum = (sum >> 16) + (sum & 0xffff);        /* 高位低位相加 */
    sum += (sum >> 16);        /* 上一步溢出时,将溢出位也加到sum中 */
    answer = ~sum;             /* 注意类型转换,现在的校验和为16位 */
    return answer;
}

//探测是否存在指定ip的活动主机
int livetest(char* ip){

    char    sendpacket[PACKET_SIZE];    /* 发送的数据包 */
    char    recvpacket[PACKET_SIZE];    /* 接收的数据包 */
    pid_t    pid;
    int    datalen = 56;    /* icmp数据包中数据的长度 */
    struct protoent *protocol;
    protocol = getprotobyname("icmp");
    int sockfd;
    int size = 50*1024;
    if((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0) {
        perror("socket error");
    }
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size) );

    struct sockaddr_in dest_addr;
    bzero(&dest_addr, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_addr.s_addr = inet_addr(ip);
    //send packet;
    int packsize;
    struct icmp *icmp;
    struct timeval *tval;
    icmp = (struct icmp*)sendpacket;
    icmp->icmp_type = ICMP_ECHO;    /* icmp的类型 */
    icmp->icmp_code = 0;            /* icmp的编码 */
    icmp->icmp_cksum = 0;           /* icmp的校验和 */
    icmp->icmp_seq = 1;       /* icmp的顺序号 */
    icmp->icmp_id = pid;            /* icmp的标志符 */
    packsize = 8 + datalen;   /* icmp8字节的头 加上数据的长度(datalen=56), packsize = 64 */
    tval = (struct timeval *)icmp->icmp_data;    /* 获得icmp结构中最后的数据部分的指针 */
    gettimeofday(tval, NULL); /* 将发送的时间填入icmp结构中最后的数据部分 */
    icmp->icmp_cksum = cal_chksum((unsigned short *)icmp, packsize);/*填充发送方的校验和*/

    if(sendto(sockfd, sendpacket, packsize, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0){
        perror("sendto error");
    }
    //printf("send %d, send done\n",1 );
    int n;
    struct sockaddr_in from;
    int fromlen = sizeof(from);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    struct timeval timeo = {1,0};
    fd_set set;
    FD_ZERO(&set);
    FD_SET(sockfd, &set);

    //read , write;
    int retval = select(sockfd+1, &set, NULL, NULL, &timeo);
    if(retval == -1) {
        printf("select error\n");
        return 0;
    }else if(retval == 0 ) {
        // printf("timeout\n");
        return 0;
    }else{
        if( FD_ISSET(sockfd, &set) ){
            // printf("host is live\n");
            return 1;
        }
        return 0;
    }
}
Last modification:November 17th, 2020 at 02:52 pm