计算机网络安全之端口扫描器

作者:shmily

实验题目

实验题目:SCANNER简单网络扫描程序实现

实验目的:熟悉并实现网络扫描的基本原理。了解网络扫描的几种常用的方法。

实验环境:linux/windows

实验内容:用C/C++语言(必须用socket函数)编写一个扫描局域网内主机的程序。要求可以显示局域网内的主机名列表,IP地址列表,并可以显示哪些主机开放了哪些端口。

实验环境

主运行机器:Linux kali 4.19.0-kali1-686-pae #1 SMP Debian 4.19.13-1kali1 (2019-01-03) i686 GNU/Linux

其他机器:Linux ubuntu 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

程序运行方法

make

程序实现说明及源码分析

程序框架

计算机网络安全之端口扫描器_第1张图片
扫描端口部分:

计算机网络安全之端口扫描器_第2张图片
如框图所示,在获取本机ip等信息之后,可以知道当前主机位于哪一个局域网,进而利用ARP广播报文获取该局域网内存活的主机。

扫描存活主机的实现运用了双进程,新开启一个进程向所有主机发送ARP询问报文,主进程用来接收存活主机发来的ARP 应答报文。

得到所有的存活主机之后,将其保存下来,再一一获取他们的主机名称,由于linux系统提供了 gethostbyaddr()函数,可以通过ip地址获取到规范名、别名等信息,因此只需要调用这个函数即可。

获取到主机名称之后,就可以对每台主机进行端口扫描,使用的方法为TCP connect扫描。对于每个 IP 来说,扫描器要扫 65536=2^16 个端口。实验中通过多线程的方式加快其扫描速度,开设 128=2^7 个线程同时扫描,直到最后一个ip的最后一组。

global.h

这个文件中包含了可能会用到的头文件,定义了一些全局变量,主要有存活的主机信息、物理地址信息、本地IP、子网掩码和本地MAC和网卡名称。备注中由更具体的说明

#ifndef _NET_GLOBAL_H_
#define _NET_GLOBAL_H_

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#if __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 1
#include 
#include  
#else
#include 
#include 
#include  
#endif
#include 
#include 

/* 以太网帧首部长度 */
#define ETHER_HEADER_LEN sizeof(struct ether_header)
/* 整个arp结构长度 */
#define ETHER_ARP_LEN sizeof(struct ether_arp)
/* 以太网 + 整个arp结构长度 */
#define ETHER_ARP_PACKET_LEN ETHER_HEADER_LEN + ETHER_ARP_LEN
/* MAC地址长度 */
#define MAC_ADDR_LEN 6
/* IP地址长度 */
#define IP_ADDR_LEN 4
#define IP_CHAR_MAX_LEN 50
/* 网关地址长度 */
// #define
/* 广播地址 */
#define BROADCAST_ADDR{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
void err_exit(const char *err_msg)
{
    perror(err_msg);
    exit(1);
}
// 活动的IP
struct hosts_alive
{
    struct in_addr ip;
    unsigned char mac[MAC_ADDR_LEN];
    char name[0x200];
} alive_hosts[0x200];
// 子网中的IP段
struct in_addr ip_list[0x300];
// 数据链路层
struct sockaddr_ll saddr_ll;
// 本地IP、子网掩码和本地MAC
struct in_addr local_ip, subnet_mask;
unsigned char local_mac_addr[MAC_ADDR_LEN];
// 网卡名称
char if_name[0x20];
#endif

获取本机信息

获取的本机信息有网卡接口名称、ip地址、mac地址、子网掩码和本机开放端口。其中本机开放端口调用了scan_port()函数,在之后会做说明。

实验中需要将获取到的信息保存在ifreq 结构体当中。ifreq结构体定义在/usr/include/net/if.h,用来配置ip地址,激活接口,配置MTU等接口信息的。其中包含了一个接口的名字和具体内容,是个共用体,有可能是IP地址,广播地址,子网掩码,MAC号,MTU或其他内容。

结构如下:

struct ifreq
  {
	# define IFHWADDRLEN	6
	# define IFNAMSIZ	IF_NAMESIZE
    union
      {
		char ifrn_name[IFNAMSIZ];	/* Interface name, e.g. "en0".  */
      } ifr_ifrn;

    union
      {
		struct sockaddr ifru_addr;
		struct sockaddr ifru_dstaddr;
		struct sockaddr ifru_broadaddr;
		struct sockaddr ifru_netmask;
		struct sockaddr ifru_hwaddr;
		short int ifru_flags;
		int ifru_ivalue;
		int ifru_mtu;
		struct ifmap ifru_map;
		char ifru_slave[IFNAMSIZ];	/* Just fits the size */
		char ifru_newname[IFNAMSIZ];
		__caddr_t ifru_data;
      } ifr_ifru;
  };

因此实验中需要定义一个 struct ifreq ifr 存储网络接口信息。

接着,用PF_PACKET选项创建ARP类型的原始套接字。用 ioctl()函数 通过网卡接口名来获取该接口对应的mac地址,ip地址,接口索引。接口索引填充到设备无关的物理层地址结构sockaddr_ll里面。该结构体位于 /usr/include/netpacket/packet.h中:

struct sockaddr_ll
{
    unsigned short int sll_family;
    unsigned short int sll_protocol;
    int sll_ifindex;
    unsigned short int sll_hatype;
    unsigned char sll_pkttype;
    unsigned char sll_halen;
    unsigned char sll_addr[8];
};

sll_ifindex是网络(网卡)接口索引,代表从这个接口收发数据包;protocol是按照网络字节顺序(network byte order),大部分定义在头文件中,设置协议时,例如 htons(ETH_P_ALL),来接收 所有的 数据包。如果要获取从指定以太网接口卡上的数据包时,在struct sockaddr_ll中指定网络接口卡,绑定(bind)数据包到该interface上。当发送数据包时,指定sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol就足够了。其它字段设置为0;sll_hatypesll_pkttype是在接收数据包时使用的; 如果要bind, 只需要使用 sll_protocolsll_ifindex 就足够了。

程序中还用到了inet_pton()inet_ntop()函数将ip地址在点分十进制和二进制之间做转换。

GetNetInfo()函数如下:

#include "init_net.h"
void GetNetInfo()
{
    /* 网络接口 */
    struct ifreq ifr;
    /* socket接口 */
    int socket_fd;
    /* MAC */
    unsigned char src_mac_addr[MAC_ADDR_LEN];
    /* 源IP */
    char src_ip[IP_CHAR_MAX_LEN + 1];
    char netmask[IP_CHAR_MAX_LEN + 1];
    char buf[ETHER_ARP_PACKET_LEN];
    int i;
    // 打开 socket,set as promiscuous mode
    if ((socket_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
        err_exit("socket()");
    // 填充 0
    bzero(&saddr_ll, sizeof(struct sockaddr_ll));
    bzero(&ifr, sizeof(struct ifreq));
    // 获取网卡接口名称
    printf("interface: %s\n", if_name);
    memcpy(ifr.ifr_name, if_name, strlen(if_name));
    // 获取网卡接口索引
    // by ioctl func,index is sent to ifr
    if (ioctl(socket_fd, SIOCGIFINDEX, &ifr) == -1)
        err_exit("ioctl() get ifindex");
    saddr_ll.sll_ifindex = ifr.ifr_ifindex;//if_ifru.ifru_value
    saddr_ll.sll_family = PF_PACKET;
    // 获取网卡接口IP
    // by ioctl func,sddr is senr to ifr
    if (ioctl(socket_fd, SIOCGIFADDR, &ifr) == -1)
        err_exit("ioctl() get ip");
    strcpy(src_ip, inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr));
    src_ip[strlen(inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr))] = '\0';
    // change to "." format
    printf("local ip: %s\n", src_ip);
    inet_pton(AF_INET, src_ip, &local_ip);
    // change to binary format
    // 获取网卡接口MAC地址
    if (ioctl(socket_fd, SIOCGIFHWADDR, &ifr) == -1)
        err_exit("ioctl() get mac");
    memcpy(src_mac_addr, ifr.ifr_hwaddr.sa_data, MAC_ADDR_LEN);
    printf("local mac: ");
    for (i = 0; i < MAC_ADDR_LEN; ++i)
    {
        printf("%02x", src_mac_addr[i]);
        local_mac_addr[i] = src_mac_addr[i];
        if (i < MAC_ADDR_LEN - 1)
            putchar(':');
    }
    putchar('\n');
    // 获取网卡接口子网掩码
    if (ioctl(socket_fd, SIOCGIFNETMASK, &ifr) == -1)
        err_exit("ioctl() get netmask");
    strcpy(netmask, inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_netmask))->sin_addr));
    netmask[strlen(inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_netmask))->sin_addr))] = '\0';
    printf("netmask: %s\n", netmask);
    inet_pton(AF_INET, netmask, &subnet_mask);
    printf("Port: 22 Open\n");
    close(socket_fd);
}

扫描局域网内的主机

扫描出局域网内的主机用 到了ARP 协议。首先构造ARP包,ARP包需要在数据链路层中传递,因此ARP包需要加一个以太网帧首部。完整的ARP帧结构如下:

计算机网络安全之端口扫描器_第3张图片

ARP报文的结构体定义在 /usr/include/netinet/if_either.h

struct    ether_arp {
    struct    arphdr ea_hdr;        /* fixed-size header */
    u_int8_t arp_sha[ETH_ALEN];    /* sender hardware address */
    u_int8_t arp_spa[4];        /* sender protocol address */
    u_int8_t arp_tha[ETH_ALEN];    /* target hardware address */
    u_int8_t arp_tpa[4];        /* target protocol address */
};
#define    arp_hrd    ea_hdr.ar_hrd
#define    arp_pro    ea_hdr.ar_pro
#define    arp_hln    ea_hdr.ar_hln
#define    arp_pln    ea_hdr.ar_pln
#define    arp_op     ea_hdr.ar_op

上面的ether_arp结构还包含一个arp首部,位于 /usr/include/net/if_arp.h 中:

struct arphdr
  {
    unsigned short int ar_hrd;		/* Format of hardware address.  */
    unsigned short int ar_pro;		/* Format of protocol address.  */
    unsigned char ar_hln;		/* Length of hardware address.  */
    unsigned char ar_pln;		/* Length of protocol address.  */
    unsigned short int ar_op;		/* ARP opcode (command).  */
  };
  • ar_hrd 是硬件类型,可供选择的数据在 net/if_arp.h 中,这里我们选择 ARPHRD_ETHER 也就是以太网。
  • ar_pro 是协议类型,在 /urs/include/net/ethernet.h 中,选择 ETHERTYPE_IP 即 IP 协议。
  • ar_hlnar_pln 分别是硬件地址长度和协议地址长度。对于以太网上的 ARP 请求或应答来说分别是 **6 **和 4,我选用了宏来定义它们。
  • ar_op 代表操作类型,位于 /usr/include/net/if_arp.h1为请求,2为应答。

综上,我们可以得到构造ARP包的函数:

struct ether_arp *fill_arp_packet(const unsigned *local_mac_addr, struct in_addr src_ip, struct in_addr dst_ip)
{
    struct ether_arp *arp_packet;
    struct in_addr src_in_addr, dst_in_addr;
    unsigned char dst_mac_addr[MAC_ADDR_LEN] = BROADCAST_ADDR;
    /* ARP package */
    arp_packet = (struct ether_arp *)malloc(ETHER_ARP_LEN);
    arp_packet->arp_hrd = htons(ARPHRD_ETHER);
    arp_packet->arp_pro = htons(ETHERTYPE_IP);
    arp_packet->arp_hln = MAC_ADDR_LEN;
    arp_packet->arp_pln = IP_ADDR_LEN;
    arp_packet->arp_op = htons(ARPOP_REQUEST);
    memcpy(arp_packet->arp_sha, local_mac_addr, MAC_ADDR_LEN);
    memcpy(arp_packet->arp_tha, dst_mac_addr, MAC_ADDR_LEN);
    memcpy(arp_packet->arp_spa, &src_ip, IP_ADDR_LEN);
    memcpy(arp_packet->arp_tpa, &dst_ip, IP_ADDR_LEN);
    return arp_packet;
}

其中发送者的硬件地址和 IP 地址可以确定,目标 IP 地址可以确定,硬件地址不能确定,系统发送的 ARP 报文会将其自动填充为 0。接下来就利用 pid = fork() 函数开启两个进程,子进程负责发送ARP 报文,父进程负责接收ARP报文。

void GetAliveHosts()
{
    pid_t pid; // short type,is process number
    int i;
    for (i = 0; i < 0x200; ++i)  //0x200=256
    {
        alive_hosts[i].ip.s_addr = 0;
    }
    if (signal(SIGCHLD, SIG_IGN) == SIG_ERR)
    {
        perror("signal error");
        exit(EXIT_FAILURE);
    }
    pid = fork();
    if (pid == -1)
        err_exit("fork error");
    if (pid == 0)
        /* subprocess send ARP package */
        SendARPPacket();
    if (pid > 0)
        /* parent process get ARP package  */
        GetARPPacket(pid);
}

发送ARP报文时,需要向其添加以太网首部形成以太网帧,首部的结构体在 /usr/include/net/ethernet.h 中定义:

/* 10Mb/s ethernet header */
struct ether_header
{
  uint8_t  ether_dhost[ETH_ALEN];	/* destination eth addr	*/
  uint8_t  ether_shost[ETH_ALEN];	/* source ether addr	*/
  uint16_t ether_type;		        /* packet type ID field	*/
} __attribute__ ((__packed__));
  • ether_dhost 为以太网目的地址,填充为广播地址。
  • ether_shost 为以太网源地址
  • ether_type 为帧类型,和 ar_pro 类似,实验中选择 ETHERTYPE_ARP代表ARP 类型。

这样我们就可以对子网中的每一个ip段(暴利扫描,从0到255依次询问)构造出整个 ARP 报文了。填好参数后,调用sendto(socket_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll)),发送到刚刚的物理地址sockaddr_ll中去,等待父进程的接收。以下是发送报文函数:

/* 扫描所有256个ip段 */
void init_iplist()
{
    // get ip addr
    int i;
    char tmp[19];
    int ip4_1 = local_ip.s_addr&0xff, ip4_2 = (local_ip.s_addr&0xff00)>>8, ip4_3 = (local_ip.s_addr&0xff0000)>>16;
    for (i = 0; i < 255; ++i)
    {
        sprintf(tmp, "%u.%u.%u.%u",ip4_1,ip4_2,ip4_3,  i + 1);
        inet_pton(AF_INET, tmp, &ip_list[i]);
    }
    
    ip_list[256].s_addr = 0;
}
void SendARPPacket()
{
    int socket_fd;
    int i;
    struct ether_arp *arp_packet;
    struct ether_header *eth_header;
    char buf[ETHER_ARP_PACKET_LEN];
    unsigned char dst_mac_addr[MAC_ADDR_LEN] = BROADCAST_ADDR;
    memset(alive_hosts, 0, sizeof(uint32_t) * 0x200);
    if ((socket_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1){
        err_exit("socket()");
    }
    init_iplist();
    /*对子网中的每一个ip段*/
    for (i = 0; ip_list[i].s_addr != 0; ++i)
    {
        bzero(buf, ETHER_ARP_PACKET_LEN);
        /* 填充以太首部 */
        eth_header = (struct ether_header *)buf;
        memcpy(eth_header->ether_shost, local_mac_addr, MAC_ADDR_LEN);
        memcpy(eth_header->ether_dhost, dst_mac_addr, MAC_ADDR_LEN);
        eth_header->ether_type = htons(ETHERTYPE_ARP);
        /* 填充ARP包 */
        arp_packet = fill_arp_packet((unsigned *)local_mac_addr, local_ip, ip_list[i]);
        memcpy(buf + ETHER_HEADER_LEN, arp_packet, ETHER_ARP_LEN);
        /* 发送请求 */
        usleep(10000);
        ssize_t ret_len = sendto(socket_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll));
    }
    close(socket_fd);
    exit(0);
}

父进程直接调用接收函数 recv(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0),之后会收到网卡接收的ARP数据包,判断收到的ARP包操作是否为ARP应答包,也就是检查报文操作码op是否为2。如果是,说明这个数据报的mac地址已被填充,剥去以太首部,取出源mac地址和ip地址即可。将ip用sprint函数以点分十进制写进临时字符串tmp_ip 中,以便直观输出。

**注意:**alive_hosts结构体中存放的ip是网络字节序,而tmp_ip是点分十进制,因此需要 inet_pton() 函数做转换。

完成主机扫描后关闭socket即可。接收函数源码如下:

unsigned char tmp_ip[0x20];
void GetARPPacket(pid_t pid)
{
    struct ether_arp *arp_packet;
    char buf[ETHER_ARP_PACKET_LEN];
    int sock_raw_fd, ret_len, i;
    if ((sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
        err_exit("socket()");
    signal(SIGALRM, handler);
    alarm(10);
    int cnt = 0;
    unsigned char tmp_mac[0x20];
    if (!setjmp(mark))
    {
        while (1)
        {
            bzero(buf, ETHER_ARP_PACKET_LEN);
            ret_len = recv(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0);
            if (ret_len > 0)
            {
                /* 剥去以太头部 */
                arp_packet = (struct ether_arp *)(buf + ETHER_HEADER_LEN);
                /* arp操作码为2代表arp应答 */
                if (ntohs(arp_packet->arp_op) == 2)
                {
                    sprintf(tmp_ip, "%u.%u.%u.%u", arp_packet->arp_spa[0], arp_packet->arp_spa[1], arp_packet->arp_spa[2], arp_packet->arp_spa[3]);
                    inet_pton(AF_INET, tmp_ip, &(alive_hosts[cnt].ip));
                    printf(" - ARP Reply Package:\n");
                    printf("from ip:");
                    for (i = 0; i < IP_ADDR_LEN; i++)
                    {
                        printf(".%u", arp_packet->arp_spa[i]);
                    }
                    printf("\nfrom mac");
                    for (i = 0; i < ETH_ALEN; i++)
                    {
                        printf(":%02x", arp_packet->arp_sha[i]);
                        alive_hosts[cnt].mac[i] = arp_packet->arp_sha[i];
                    }
                    printf("\n\n");
                    cnt++;
                }
            }
        }
    }
    else
    {
		// close socket
        printf("Done, close socket\n");
        close(sock_raw_fd);
    }
}

将扫描到的每个活跃的主机都存在一个结构体数组 alive_hosts 中。

获取每个主机的主机名

由于linux系统提供了gethostbyaddr()函数,可以通过ip地址获取到规范名、别名等信息,因此只需要调用这个函数即可。同样的问题,需要将 alive_hosts 中的网络字节序ip形式转换成点分十进制的字符串,用到了inet_ntoa()函数。

gethostbyaddr()函数的原型为

struct hostent *gethostbyaddr(const char * addr, int len, int type)

返回值是 hostent结构体,结构体原型如下:

struct hostent  
{  
    char *h_name;         //正式主机名  
    char **h_aliases;     //主机别名  
    int h_addrtype;       //主机IP地址类型:IPV4-AF_INET  
    int h_length;         //主机IP地址字节长度,对于IPv4是四字节,即32位  
    char **h_addr_list;   //主机的IP地址列表  
}; 

gethostbyaddr()函数的第一个参数addr是指向网络字节顺序地址的指针。 (**PS: **按理说本可以将alive_hosts[cnt].ip的值直接传进去,但不知道什么原因总是报错Segment fault,因此只能先转化成字符串,再转化成网络字节顺序地址,这样就不会报错。)

将函数解析的结果放在这个结构体的h_name中,再将这个值复制到alive_hosts[cnt].name中,便完成了主机名的获取。这部分源码如下:

struct hostent *phost;
void assist(char temp[])
{
	char *ptr, **pptr;
	struct in_addr addr;
	char str[32] = {0};
	ptr = temp;
	if (inet_pton(AF_INET, ptr, &addr) <= 0) {
		printf("inet_pton error:%s\n", strerror(errno));
		exit(0);
	}
    /* 调用gethostbyaddr获取主机名 */
	phost = gethostbyaddr((const char*)&addr, sizeof(addr), AF_INET);
	if (phost == NULL) {
		printf("gethostbyaddr error:%s\n", strerror(h_errno));
		exit(0);
	}	
}
void GetHostsName()
{
    int cnt;
    int i;
    struct hostent *ht = NULL;
    for (cnt = 0;; cnt++)
    {
	char temp[25];
	strcpy(temp,inet_ntoa(alive_hosts[cnt].ip));
        if (alive_hosts[cnt].ip.s_addr == 0)
            break;
	assist(temp);
	strcpy(alive_hosts[cnt].name, phost->h_name);
        printf("device %d\n", cnt + 1);
        printf("ip: %s\n", inet_ntoa(alive_hosts[cnt].ip));
        printf("mac: ");
        for (i = 0; i < 6; ++i)
        {
            printf("%02x", alive_hosts[cnt].mac[i]);
            if (i < 6 - 1)
                putchar(':');
        }
        putchar('\n');
        printf("hostname: %s\n", alive_hosts[cnt].name);
	printf("\n");
    }
}

扫描每个主机开放的端口

遍历 alive_hosts 中的每个存活主机,进行端口扫描,端口扫描的原理很简单,就是建立socket通信,切换不同端口,通过connect()函数,如果成功则代表端口开放,否则端口关闭。首先先定义一个ScanArgs结构体来存放ip+端口信息,每个端口的result的值为正(成功)或为负(失败)。

typedef struct
{
    struct in_addr ip;
    int port;
    int result;
} ScanArgs;

对于每个 IP 来说,扫描器要扫 65536=2^16 个端口。实验中通过多线程的方式加快其扫描速度,开设 128=2^7 个线程同时扫描,这样对于每个ip只要512次循环即可,直到最后一个ip的最后一组。用函数 pthread_join() 来实现等待一个线程的结束,线程间同步的操作,根据 args_list[p_j].result 的值来判断端口是否开放。

void ScanIP()
{
    int i;
    int p_i, p_j;
    for (i =1 ;; ++i)
    {
        if (alive_hosts[i].ip.s_addr == 0)
            break;
	printf("device %d:\n",i+1);
        printf("IP: %s\n", inet_ntoa(alive_hosts[i].ip));
        printf("hostname: %s\n", alive_hosts[i].name);
        ScanArgs args_list[MAX_PTH_NUM];
        pthread_t pth_list[MAX_PTH_NUM];
        for (p_i = 0; p_i < 512; ++p_i)
        {
            for (p_j = 0; p_j < 128; ++p_j)
            {
                args_list[p_j].ip.s_addr = alive_hosts[i].ip.s_addr;
                args_list[p_j].port = p_i * 128 + p_j + 1;
                /*创建线程*/
                pthread_create(pth_list + p_j, NULL, ScannerThread, args_list + p_j);
            }
            for (p_j = 0; p_j < 128; ++p_j)
            {
            	 /*线程同步操作*/
                pthread_join(pth_list[p_j], NULL);
                if (args_list[p_j].result)
                {
                    printf("Port: %5d  Open\n", args_list[p_j].port);
                }
            }
	}
		printf("\n");
    }
}

用pthread_create()函数创建线程时,线程运行函数 ScannerThread() 的内容如下:

void *ScannerThread(void *args)
{
    ScanArgs *p = (ScanArgs *)args;
    p->result = SYNScan(p->ip, p->port);
}

result的值通过SYNScan()函数获取,首先创建一个socket标识符,将参数传进去。

接收端接收到TCP报文之后存入scoket接收缓冲区,用户进程通过receive读取缓冲区的数据。如果接收缓冲区满了会通知TCP发送端关闭窗口,保证接收端的缓冲区不会被溢出。如果接收缓冲区的数据为空的时候,那么receive调用scoket的read方法就会处于阻塞状态,直到有数据过来。同样,对于写来说,如果发送缓冲区满了,那么调用scoket的write方法就会处于阻塞状态,直到报文送到网络上。

阻塞IO会一直等待,所以非阻塞IO是用来解决 IO线程与scoket之间的解耦问题 (引入事件机制),如果scoket发送缓存区可写的话会通知IO线程进行write,同样如果scoket的接受缓冲区可读的话会通知IO线程进行read。

计算机网络安全之端口扫描器_第4张图片

在一个TCP套接口被设置为非阻塞之后调用connect,connect会立即返回 EINPROGRESS 错误,表示连接操作正在进行中,但是仍未完成;同时TCP的三路握手操作继续进行;在这之后,我们可以调用 select()函数 来检查这个链接是否建立成功:

当连接建立成功时,套接口描述符变成可写;

当连接出错时,套接口描述符变成既可读又可写。

注意:当一个套接口出错时,它会被select调用标记为既可读又可写。当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将上述第二种情况和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。

因此,仅从socket可读或可写无法判断socket连接的状态。并且,有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来.在这种情况下,连接成功时套接口将既可读又可写.这和连接失败时是一样的.这个时候我们还得通过 getsockopt() 函数来读取错误值。

非阻塞connect的实现:

(一)建立socket套接字;

(二)将socket建立为非阻塞,此时socket被设置为非阻塞模式;

(三)建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立,会立即返回-1。同时将 errno(包含errno.h就可以直接使用)设置为EINPROGRESS,表示此时tcp三次握手仍旧进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。

(四)设置等待时间。使用select函数等待正在后台连接的connect函数,使用select监听socket描述符是否可读或者可写,如果只可写,说明连接成功,可以进行之后的操作。如果描述符既可读又可写,分为两种情况,第一种情况是socket连接出现错误,第二种情况是connect连接成功,socket读缓冲区得到了远程主机发送的数据。通过调用 getsockopt() 函数返回值来判断是否发生错误。

struct timeval tout;
int SYNScan(struct in_addr ip, int port)
{
    tout.tv_sec = 1;
    tout.tv_usec = 0;
    int fd, res, valopt, retflag = 1;
    long arg;
    unsigned int temp;
    fd_set set;
    struct sockaddr_in tgt_addr;
    fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    tgt_addr.sin_addr.s_addr = ip.s_addr;
    tgt_addr.sin_family = AF_INET;
    tgt_addr.sin_port = htons(port);
    arg = fcntl(fd, F_GETFL, NULL);
    arg |= O_NONBLOCK;
    fcntl(fd, F_SETFL, arg);
    // Trying to connect with timeout
    res = connect(fd, (struct sockaddr *)&tgt_addr, sizeof(tgt_addr));
    if (res < 0) {
        /*连接操作正在进行中,但是仍未完成*/
        if (errno == EINPROGRESS) {
            while (1)
            {
                FD_ZERO(&set);
                FD_SET(fd, &set);
                res = select(fd + 1, NULL, &set, NULL, &tout);
                if (res < 0 && errno != EINTR)
                {
                    retflag = 0;
                    goto ret;
                }
                else if (res > 0)
                {
                    // Socket selected for write
                    temp = sizeof(int);
                    if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)(&valopt), &temp) < 0)
                    {
                        retflag = 0;
                        goto ret;
                    }
                    // Check the value returned
                    if (valopt)
                    {
                        retflag = 0;
                        goto ret;
                    }
                    break;
                }
                else
                {
                    retflag = 0;
                    goto ret;
                }
            }
        }
        else
        {
            retflag = 0;
            goto ret;
        }
    }
ret:
    close(fd);
    return retflag;
}

使用非阻塞connect的优势:

可以在三路握手的同时做一些其它的处理:connect操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。在这段时间内我们可能有一些其他的处理想要执行。

并且,由于我们使用select来等待连接的完成,因此我们可以给select设置一个时间限制,从而缩短connect的超时时间.在大多数实现中,connect的超时时间在75秒到几分钟之间。有时候应用程序想要一个更短的超时时间,使用非阻塞connect就可以实现。

主函数

有了各个子函数实现所有功能后,主函数只需调用它们即可。默认(缺省)情况下,网卡名称为eth0,超时时间**timeout= 2s **,初始网络为本机网络。

#include "global.h"
#include "init_net.h"
#include "get_host.h"
#include "scan_port.h"

int main(int *argc, const char *argv[])
{
    // input from shell
    int timeout = 2;
    // set my interface name
    const char default_ifname[] = "eth0";
    strncpy(if_name, default_ifname, strlen(default_ifname));
    // Get IP and Subnet mask from adapter and get all IP
    printf("\n");
    printf("\33[;32m============================= Scanner Start ==============================\n\33[0m");
    printf("\n");
    printf("\33[;33m>> Local Network Infomation\n\33[0m");
    GetNetInfo();
    printf("\n");
    // get all alive hosts.
    printf("\33[;33m>> Scan Alive Hosts\n\33[0m");
    GetAliveHosts();
    printf("\n");
    // get all hosts' name.
    printf("\33[;33m>> Show Alive Hosts Name\n\33[0m");
    GetHostsName();
    // TCP SYN scanning, which will get all open ports.
    printf("\33[;33m>> Show Hosts and Ports which has open ports\n\33[0m");
    ScanIP();
    printf("\33[;32m======================= Program End =============================\n\33[0m");
    printf("\n");
}

运行结果与验证

在192.168.163.0/24局域网内共扫描到5台存活主机,其中包含网关、DNS服务器以及自己搭建的主机。

计算机网络安全之端口扫描器_第5张图片

计算机网络安全之端口扫描器_第6张图片

计算机网络安全之端口扫描器_第7张图片

计算机网络安全之端口扫描器_第8张图片
使用nmap工具验证:

计算机网络安全之端口扫描器_第9张图片

扫描到的5个主机正是程序的运行结果。再对每个主机开放的端口做验证:

计算机网络安全之端口扫描器_第10张图片

计算机网络安全之端口扫描器_第11张图片

与实验结果相同,验证完毕。

参考资料

《linux原始套接字(1)-arp请求与接收》:https://blog.csdn.net/weijinqian0/article/details/51861499

《linux网络编程——域名转换 》:https://blog.csdn.net/weixin_34279579/article/details/94002991

《UNIX网络编程 非阻塞connect的实现》:http://www.it165.net/os/html/201404/7763.html

《socket实验指导手册》

你可能感兴趣的:(计算机网络安全之端口扫描器)