第8单元 端口扫描

8.1 知识讲解

8.1.1 ICMP扫描

ping程序只是简单探测某个IP地址所对应的主机是否存在,其工作原理十分简单:

8.1.2 TCP扫描

1. TCP连接的建立过程

建立TCP连接的三路握手过程如下:

2. 三种TCP扫描

1) CONNECT扫描

扫描发起主机通过套接字应用编程接口(Socket API)的connect函数尝试连接目标主机的被扫描端口,如果connect函数返回成功,即表明扫描发起主机和目标主机之间至少经历了一轮包含三路握手在内的TCP连接建立过程,据此可以断定目标主机的被扫描端口是开放的,否则该端口就是关闭的。

由于TCP协议是可靠传输协议,connect函数不会仅在一次尝试建立连接失败后即返回失败,而是要经历多次失败的连接尝试后才会彻底放弃,这中间需要漫长的等待时间。

失败的connect函数会在系统中产生大量连续的失败日志,容易被系统管理员发现。

2) SYN扫描

扫描发起主机向目标主机的被扫描端口发送一个SYN数据包:

SYN扫描不需要完成TCP连接的三路握手过程,因此它又被称为半开放扫描。

SYN扫描的最大优点就是速度快。在互联网环境下,如果不考虑防火墙等安全设备的影响,每秒钟可以扫描数千个端口。但由于其扫描行为明显,很容易被入侵检测系统发现,也极易被防火墙等安全设备屏蔽,同时构造原始TCP数据包通常需要较高的系统权限,如果是在Linux系统上,就必须拥有root权限。

3) FIN扫描

扫描发起主机向目标主机的被扫描端口发送一个FIN数据包:

FIN扫描具有很好的隐蔽性,不会留下日志,但它的局限性很大,仅对UNIX/Linux系统的网络协议栈有效。运行Windows系统的目标主机,无论被扫描端口开放与否,都会返回RST数据包,因此无法对该端口当前所处状态做出准确的判断。

8.1.3 UDP扫描

向一个处于关闭状态的UDP端口发送数据,目标主机会返回端口不可达(Port Unreachable)错误。

扫描发起主机向目标主机的被扫描端口发送0字节的UDP数据包:

大部分系统都限制了ICMP差错报文的产生速度,因此大范围的UDP端口扫描可能会相当耗时。

UDP和ICMP都是不可靠协议,数据包丢失也可能导致收不到响应,因此扫描程序有必要对同一端口进行多次尝试,每次都收不到响应才有理由相信该端口是开放的。

8.1.4 通过原始套接字发送数据包

在以上所列举的各种端口扫描方法中,只有TCP CONNECT扫描可以直接通过connect函数完成数据包的构造、发送和接收,其它端口扫描方法都必须通过原始套接字手动构造数据包,并借助专门的系统调用函数实现对所构造数据包的发送和接收。

1. 创建原始套接字

协议族和套接字类型与网络协议栈的关系如下图所示:

graph TB application_layer(L5: Application Layer) transport_layer(L4: Transport Layer) network_layer(L3: Network Layer) data_link_layer(L2: Data Link Layer) physical_layer(L1: Physical Layer) application_layer--PF_INET
SOCK_STREAM/SOCK_DGRAM-->transport_layer application_layer==PF_INET
SOCK_RAW==>network_layer application_layer--PF_PACKET
SOCK_DGRAM-->data_link_layer application_layer--PF_PACKET
SOCK_RAW-->physical_layer transport_layer-->network_layer network_layer-->data_link_layer data_link_layer-->physical_layer
1) 创建基于IP协议的原始套接字
int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_IP);
2) 创建基于ICMP协议的原始套接字
int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
3) 创建基于TCP协议的原始套接字
int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
4) 创建基于UDP协议的原始套接字
int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_UDP);

2. 设置套接字选项

int setsockopt(
    int          sockfd,  // 套接字文件描述符
    int          level,   // 选项层次
                          // SOL_SOCKET  : 通用选项
                          // IPPROTO_IP  : IP选项
                          // IPPROTO_TCP : TCP选项
    int          optname, // 选项名称
    const void * optval,  // 选项值地址
    socklen_t  * optlen); // 选项值长度

3. 创建并填充包头

Linux系统已经预定义了常用网络协议的包头结构体:

1) IP包头
#include <netinet/ip.h>

struct iphdr {
    unsigned int version:4; // 版本(4位)
    unsigned int ihl:4;     // 包头长度(4位)
    uint8_t      tos;       // 服务类型
    uint16_t     tot_len;   // 包长度
    uint16_t     id;        // 标识符
    uint16_t     frag_off;  // 标志(3位)和偏移(13位)
    uint8_t      ttl;       // 生存时间
    uint8_t      protocol;  // 上层协议
    uint16_t     check;     // 校验和
    uint32_t     saddr;     // 源IP地址
    uint32_t     daddr;     // 目的IP地址
};
2) ICMP包头
#include <netinet/ip_icmp.h>

struct icmphdr {
    uint8_t  type;                     // 类型
    uint8_t  code;                     // 代码
    uint16_t checksum;                 // 校验和
    union {
        struct {
            uint16_t id;               // 标识符
            uint16_t sequence;         // 序号
        }   echo;                      // 回射数据包
        uint32_t gateway;              // 网关地址
        struct {
            uint16_t __glibc_reserved; // 保留未用
            uint16_t mtu;              // 最大传输单元
        }   frag;
    }   un;
};
3) TCP包头
#include <netinet/tcp.h>

struct tcphdr {
    uint16_t source;  // 源端口
    uint16_t dest;    // 目的端口
    uint32_t seq;     // 序号
    uint32_t ack_seq; // 确认序号
    uint16_t doff:4;  // 包头长度(4位)
    uint16_t res1:4;  // 保留未用(4位)
    uint16_t res2:2;  // 保留未用(2位)
    uint16_t urg:1;   // URG位 \
    uint16_t ack:1;   // ACK位 |
    uint16_t psh:1;   // PSH位 | 标志(6位)
    uint16_t rst:1;   // RST位 |
    uint16_t syn:1;   // SYN位 |
    uint16_t fin:1;   // FIN位 /
    uint16_t window;  // 窗口大小
    uint16_t check;   // 校验和
    uint16_t urg_ptr; // 紧急指针
};
4) UDP包头
#include <netinet/udp.h>

struct udphdr {
    uint16_t source; // 源端口
    uint16_t dest;   // 目的端口
    uint16_t len;    // 包长度
    uint16_t check;  // 校验和
};

4. 发送数据包

在填充完数据包之后,即可通过sendto函数发送数据包。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(
    int                     sockfd,    // 套接字文件描述符
    const void            * buf,       // 发送数据包缓冲区
    size_t                  len,       // 发送数据包字节数
    int                     flags,     // 发送标志
    const struct sockaddr * dest_addr, // 目的地址结构
    socklen_t               addrlen);  // 目的地址结构字节数

对于面向连接的SOCK_STREAM类型套接字,最后两个参数将被忽略。

sendto函数成功返回实际发送的字节数,失败返回-1,并设置errno变量。

一般情况下,buf所指向的发送数据包缓冲区中并不需要包含IP包头,网络层会负责IP报文的封装。如果希望自己组织IP包头的内容,可以通过setsockopt函数设置相应的套接字选项:

int on = 1;
setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));

通过带有IP_HDRINCL选项的套接字发送的数据包,由IP包头、TCP或UDP包头以及数据载荷三部分组成。

5. 接收数据包

在完成数据包的发送以后,可以通过recvfrom函数接收远程主机的响应数据包。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(
    int               sockfd,   // 套接字文件描述符
    void            * buf,      // 接收数据包缓冲区
    size_t            len,      // 期望接收的字节数
    int               flags,    // 接收标志
    struct sockaddr * src_addr, // 源地址结构
    socklen_t       * addrlen); // 源地址结构字节数

recvfrom函数成功返回实际接收的字节数,失败返回-1,并设置errno变量。通过基于TCP协议的流式套接字(SOCK_STREAM)接收数据包,该函数可能返回0,这通常意味着远程主机关闭了连接。

一般在接收数据包的过程中,recvfrom函数会一直处于阻塞状态,直到收到一个数据包之后,该函数才会返回。如果希望recvfrom函数以非阻塞方式接收数据包,可以通过fcntl函数将套接字设置为非阻塞模式:

fcntl(sockfd, F_SETFL, O_NONBLOCK);

通过阻塞或非阻塞套接字调用sendto函数的返回值如下表所示:

sendto slen = sendto(sockfd, buf, len, 0, (struct sockaddr*)&addr, addrlen)
slen == len 0 < slen && slen < len slen == 0 slen == -1
阻塞套接字 发送缓冲区充足 —— 出错
非阻塞套接字 发送缓冲区不足 发送缓冲区充满

通过阻塞或非阻塞套接字调用recvfrom函数的返回值如下表所示:

recvfrom rlen = recvfrom(sockfd, buf, len, 0, (struct sockaddr*)&addr, &addrlen)
rlen == len 0 < rlen && rlen < len rlen == 0 rlen == -1
阻塞套接字 可接收数据足够 可接收数据不足 TCP连接断开 出错
非阻塞套接字 没有可接收数据

8.2 实训案例

8.2.1 网络端口扫描器

在Linux环境中编写一个网络端口扫描器。

基于套接字实现ping程序,探测用户输入的主机IP地址是否可达。

基于套接字实现TCP CONNECT扫描、TCP SYN扫描、TCP FIN扫描和UDP扫描,针对用户输入的主机IP地址和端口号范围进行扫描,打印每个端口号的扫描结果。

8.2.2 程序清单

1. 声明Scanner类

// scanner.h
// 声明Scanner类

#pragma once

#include <netinet/in.h>
#include <string>
using namespace std;

// 扫描器
class Scanner {
public:
    // 构造函数
    Scanner(const char* ip, const char* begin, const char* end);

    // 端口扫描
    virtual int scan(void);

protected:
    // 校验和结构
    typedef struct tag_Checksum {  
        uint32_t saddr;
        uint32_t daddr;
        uint8_t  useless;
        uint8_t  protocol;
        uint16_t length;
    }   CHECKSUM;

    // 计算校验和
    uint16_t checksum(const void* buf, size_t len) const;

    // 初始化
    int init(void);
    // 目标主机是否存在
    int exist(void) const;

    const string ip;    // 目标主机IP
    const string begin; // 起始端口号
    const string end;   // 终止端口号

    in_addr_t saddr; // 源IP地址
    in_addr_t daddr; // 目的IP地址
    in_port_t bport; // 起始端口号
    in_port_t eport; // 终止端口号
};

2. 实现Scanner类

// scanner.cpp
// 实现Scanner类

#include <unistd.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/ip_icmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;

#include "scanner.h"

// 构造函数
Scanner::Scanner(const char* ip, const char* begin, const char* end) :
    ip(ip), begin(begin), end(end),
    saddr(INADDR_NONE), daddr(INADDR_NONE), bport(0), eport(0) {}

// 端口扫描
int Scanner::scan(void) {
    // 初始化
    if (init() == -1)
        return -1;

    // 目标主机是否存在
    if (exist() == -1)
        return -1;

    return 0;
}

// 计算校验和
uint16_t Scanner::checksum(const void* buf, size_t len) const {
    const uint16_t* ptr = (const uint16_t*)buf;
    uint32_t sum = 0;

    for (; len > 1; len -= 2)
        sum += *ptr++;

    if (len)
        sum += *(const uint8_t*)ptr;

    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return ~sum;
}

// 初始化
int Scanner::init(void) {
    cout << "Initializing ..." << endl;
    //
    // 源IP地址
    //
    FILE* fp = popen("/sbin/ifconfig | grep inet | grep -v 127 | "
        "awk '{print $2}' | cut -d \":\" -f 2", "r");
    if (! fp) {
        perror("popen");
        return -1;
    }

    char s[16];
    fscanf(fp, "%s", s);
    pclose(fp);

    if ((saddr = inet_addr(s)) == INADDR_NONE) {
        perror("inet_addr");
        return -1;
    }
    //
    // 目的IP地址
    //
    if ((daddr = inet_addr(ip.c_str())) == INADDR_NONE) {
        perror("inet_addr");
        return -1;
    }
    //
    // 端口范围
    //
    int b = atoi(begin.c_str()), e = atoi(end.c_str());
    if (b < 1 || 65535 < b || e < 1 || 65535 < e || e < b) {
        cerr << "Invalid port range" << endl;
        return -1;
    }

    bport = b;
    eport = e;

    cout << string(inet_ntoa(*((struct in_addr*)&saddr))) << '-' <<
        string(inet_ntoa(*((struct in_addr*)&daddr))) << ':' <<
        bport << '-' << eport << endl;

    cout << "Initializing OK!" << endl;
    return 0;
}

// 目标主机是否存在
int Scanner::exist(void) const {
    cout << "Ping destination host ..." << endl;

    // 创建基于ICMP协议的原始套接字
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd == -1) {
        perror("socket");
        return -1;
    }

    // 自己组织IP包头
    int on = 1;
    if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on,
        sizeof(on)) == -1) {
        perror("setsockopt");
        close(sockfd);
        return -1;
    }

    // 组织IP包头
    uint8_t buf[1024];
    struct iphdr* iph = (struct iphdr*)buf;
    iph->version  = 4;            // 版本(4位)
    iph->ihl      = 5;            // 包头长度(4位)
    iph->tos      = 0;            // 服务类型
    iph->tot_len  = htons(
        sizeof(struct iphdr) +
        sizeof(struct icmphdr) +
        sizeof(struct timeval));  // 包长度
    iph->id       = rand();       // 标识符
    iph->frag_off = 0x40;         // 标志(3位)和偏移(13位)
    iph->ttl      = 64;           // 生存时间
    iph->protocol = IPPROTO_ICMP; // 上层协议
    iph->check    = 0;            // 校验和
    iph->saddr    = saddr;        // 源IP地址
    iph->daddr    = daddr;        // 目的IP地址

    // 组织ICMP包头
    struct icmphdr* icmph = (struct icmphdr*)(iph + 1);
    icmph->type             = ICMP_ECHO;    // 类型
    icmph->code             = 0;            // 代码
    icmph->checksum         = 0;            // 校验和
    icmph->un.echo.id       = htons(10086); // 标识符
    icmph->un.echo.sequence = 0;            // 序号

    // 组织数据载荷
    struct timeval* data = (struct timeval*)(icmph + 1);
    gettimeofday(data, NULL);

    // 计算校验和
    icmph->checksum = checksum(icmph, sizeof(*icmph) + sizeof(*data));

    // 发送ICMP请求
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);
    bzero(&addr, addrlen);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = daddr;
    if (sendto(sockfd, buf, sizeof(*iph) + sizeof(*icmph) +
        sizeof(*data), 0, (struct sockaddr*)&addr, addrlen) == -1) {
        perror("sendto");
        close(sockfd);
        return -1;
    }

    // 将套接字设置为非阻塞模式
    if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
        perror("fcntl");
        close(sockfd);
        return -1;
    }

    // 接收循环
    struct timeval start;
    gettimeofday(&start, NULL);
    for (;;) {
        // 接收ICMP响应
        if (recvfrom(sockfd, buf, sizeof(buf), 0,
            (struct sockaddr*)&addr, &addrlen) > 0) {
            iph = (struct iphdr*)buf;
            icmph = (struct icmphdr*)(buf + iph->ihl * 4);
            if (iph->saddr == daddr && iph->daddr == saddr &&
                icmph->type == ICMP_ECHOREPLY) {
                struct timeval now;
                gettimeofday(&now, NULL);
                cout << noshowpoint << ((now.tv_sec - start.tv_sec) * 1e6 +
                    now.tv_usec - start.tv_usec) / 1e3 << " ms" << endl;
                break;
            }
        }

        // 超时目标主机不可达
        struct timeval now;
        gettimeofday(&now, NULL);
        if (((now.tv_sec - start.tv_sec) * 1e6 +
            now.tv_usec - start.tv_usec) / 1e6 > 5) {
            cerr << "Destination host Unreachable" << endl;
            close(sockfd);
            return -1;
        }
    }

    close(sockfd);

    cout << "Ping destination host OK!" << endl;
    return 0;
}

3. 声明TCPScanner类

// tcpscanner.h
// 声明TCPScanner类

#pragma once

#include "scanner.h"

// TCP CONNECT扫描器
class TCPScanner : public Scanner {
public:
    // 构造函数
    TCPScanner(const char* ip, const char* begin, const char* end);

    // 端口扫描
    int scan(void);
};

4. 实现TCPScanner类

// tcpscanner.cpp
// 实现TCPScanner类

#include <unistd.h>
#include <strings.h>
#include <stdio.h>
#include <iostream>
using namespace std;

#include "tcpscanner.h"

// 构造函数
TCPScanner::TCPScanner(const char* ip, const char* begin,
    const char* end) : Scanner(ip, begin, end) {}

// 端口扫描
int TCPScanner::scan(void) {
    cout << "TCP CONNECT scanning ..." << endl;

    if (Scanner::scan() == -1)
        return -1;

    // 从起始端口号遍历至终止端口号
    for (in_port_t dport = bport; dport <= eport; ++dport) {
        // 创建流式套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1) {
            perror("socket");
            continue;
        }

        // 连接目标主机
        struct sockaddr_in addr;
        socklen_t addrlen = sizeof(addr);
        bzero(&addr, addrlen);
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = daddr;
        addr.sin_port = htons(dport);
        if (connect(sockfd, (struct sockaddr*)&addr, addrlen) == -1)
            cout << ip << ':' << dport << " Closed" << endl;
        else
            cout << ip << ':' << dport << " Open" << endl;

        close(sockfd);
    }

    cout << "TCP CONNECT scanning OK!" << endl;
    return 0;
}

5. 声明SYNScanner类

// synscanner.h
// 声明SYNScanner类

#pragma once

#include "scanner.h"

// TCP SYN扫描器
class SYNScanner : public Scanner {
public:
    // 构造函数
    SYNScanner(const char* ip, const char* begin, const char* end);

    // 端口扫描
    int scan(void);
};

6. 实现SYNScanner类

// synscanner.cpp
// 实现SYNScanner类

#include <unistd.h>
#include <strings.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <iostream>
using namespace std;

#include "synscanner.h"

// 构造函数
SYNScanner::SYNScanner(const char* ip, const char* begin,
    const char* end) : Scanner(ip, begin, end) {}

// 端口扫描
int SYNScanner::scan(void) {
    cout << "TCP SYN scanning ..." << endl;

    if (Scanner::scan() == -1)
        return -1;

    // 从起始端口号遍历至终止端口号
    for (in_port_t dport = bport; dport <= eport; ++dport) {
        // 创建基于TCP协议的原始套接字
        int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
        if (sockfd == -1) {
            perror("socket");
            continue;
        }

        // 绑定源IP地址
        struct sockaddr_in addr;
        socklen_t addrlen = sizeof(addr);
        bzero(&addr, addrlen);
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = saddr;
        if (bind(sockfd, (struct sockaddr*)&addr, addrlen) == -1) {
            perror ("bind");
            close(sockfd);
            continue;
        }

        // 组织校验和结构
        uint8_t buf[1024];
        CHECKSUM* sum = (CHECKSUM*)buf;
        sum->saddr    = saddr;
        sum->daddr    = daddr;
        sum->useless  = 0;
        sum->protocol = IPPROTO_TCP;
        sum->length   = htons(sizeof(struct tcphdr));

        // 组织TCP包头
        struct tcphdr* tcph = (struct tcphdr*)(sum + 1);
        in_port_t sport = dport + 1024;
        tcph->source  = htons(sport);  // 源端口
        tcph->dest    = htons(dport);  // 目的端口
        tcph->seq     = htonl(123456); // 序号
        tcph->ack_seq = 0;             // 确认序号
        tcph->doff    = 5;             // 包头长度(4位)
        tcph->res1    = 0;             // 保留未用(4位)
        tcph->res2    = 0;             // 保留未用(2位)
        tcph->urg     = 0;             // URG位
        tcph->ack     = 0;             // ACK位
        tcph->psh     = 0;             // PSH位
        tcph->rst     = 0;             // RST位
        tcph->syn     = 1;             // SYN位
        tcph->fin     = 0;             // FIN位
        tcph->window  = htons(65535);  // 窗口大小
        tcph->check   = 0;             // 校验和
        tcph->urg_ptr = 0;             // 紧急指针

        // 计算校验和
        tcph->check = checksum(sum, sizeof(*sum) + sizeof(*tcph));

        // 发送SYN数据包
        addr.sin_addr.s_addr = daddr;
        addr.sin_port = htons(dport);
        if (sendto(sockfd, tcph, sizeof(*tcph), 0,
            (struct sockaddr*)&addr, addrlen) == -1) {
            perror("sendto");
            close(sockfd);
            continue;
        }

        // 接收应答数据包
        if (recvfrom(sockfd, buf, sizeof(buf), 0,
            (struct sockaddr*)&addr, &addrlen) <= 0) {
            perror("recvfrom");
            close(sockfd);
            continue;
        }

        // 检查应答数据包
        struct iphdr* iph = (struct iphdr*)buf;
        tcph = (struct tcphdr*)(buf + iph->ihl * 4);
        if (iph->saddr == daddr && iph->daddr == saddr &&
            ntohs(tcph->source) == dport && ntohs(tcph->dest) == sport) {
            if (tcph->ack == 1 && tcph->rst == 1)
                cout << ip << ':' << dport << " Closed" << endl;
            else
            if (tcph->ack == 1 && tcph->syn == 1)
                cout << ip << ':' << dport << " Open" << endl;
        }

        close(sockfd);
    }

    cout << "TCP SYN scanning OK!" << endl;
    return 0;
}

7. 声明FINScanner类

// finscanner.h
// 声明FINScanner类

#pragma once

#include "scanner.h"

// TCP FIN扫描器
class FINScanner : public Scanner {
public:
    // 构造函数
    FINScanner(const char* ip, const char* begin, const char* end);

    // 端口扫描
    int scan(void);
};

8. 实现FINScanner类

// finscanner.cpp
// 实现FINScanner类

#include <unistd.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/time.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <iostream>
using namespace std;

#include "finscanner.h"

// 构造函数
FINScanner::FINScanner(const char* ip, const char* begin,
    const char* end) : Scanner(ip, begin, end) {}

// 端口扫描
int FINScanner::scan(void) {
    cout << "TCP FIN scanning ..." << endl;

    if (Scanner::scan() == -1)
        return -1;

    // 从起始端口号遍历至终止端口号
    for (in_port_t dport = bport; dport <= eport; ++dport) {
        // 创建基于TCP协议的原始套接字
        int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
        if (sockfd == -1) {
            perror("socket");
            continue;
        }

        // 绑定源IP地址
        struct sockaddr_in addr;
        socklen_t addrlen = sizeof(addr);
        bzero(&addr, addrlen);
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = saddr;
        if (bind(sockfd, (struct sockaddr*)&addr, addrlen) == -1) {
            perror ("bind");
            close(sockfd);
            continue;
        }

        // 组织校验和结构
        uint8_t buf[1024];
        CHECKSUM* sum = (CHECKSUM*)buf;
        sum->saddr    = saddr;
        sum->daddr    = daddr;
        sum->useless  = 0;
        sum->protocol = IPPROTO_TCP;
        sum->length   = htons(sizeof(struct tcphdr));

        // 组织TCP包头
        struct tcphdr* tcph = (struct tcphdr*)(sum + 1);
        in_port_t sport = dport + 1024;
        tcph->source  = htons(sport);  // 源端口
        tcph->dest    = htons(dport);  // 目的端口
        tcph->seq     = htonl(123456); // 序号
        tcph->ack_seq = 0;             // 确认序号
        tcph->doff    = 5;             // 包头长度(4位)
        tcph->res1    = 0;             // 保留未用(4位)
        tcph->res2    = 0;             // 保留未用(2位)
        tcph->urg     = 0;             // URG位
        tcph->ack     = 0;             // ACK位
        tcph->psh     = 0;             // PSH位
        tcph->rst     = 0;             // RST位
        tcph->syn     = 0;             // SYN位
        tcph->fin     = 1;             // FIN位
        tcph->window  = htons(65535);  // 窗口大小
        tcph->check   = 0;             // 校验和
        tcph->urg_ptr = 0;             // 紧急指针

        // 计算校验和
        tcph->check = checksum(sum, sizeof(*sum) + sizeof(*tcph));

        // 发送FIN数据包
        addr.sin_addr.s_addr = daddr;
        addr.sin_port = htons(dport);
        if (sendto(sockfd, tcph, sizeof(*tcph), 0,
            (struct sockaddr*)&addr, addrlen) == -1) {
            perror("sendto");
            close(sockfd);
            continue;
        }

        // 将套接字设置为非阻塞模式
        if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
            perror("fcntl");
            close(sockfd);
            continue;
        }

        // 接收循环
        struct timeval start;
        gettimeofday(&start, NULL);
        for (;;) {
            // 接收应答数据包
            if (recvfrom(sockfd, buf, sizeof(buf), 0,
                (struct sockaddr*)&addr, &addrlen) > 0) {
                // 检查应答数据包
                struct iphdr* iph = (struct iphdr*)buf;
                tcph = (struct tcphdr*)(buf + iph->ihl * 4);
                if (iph->saddr == daddr && iph->daddr == saddr &&
                    ntohs(tcph->source) == dport &&
                    ntohs(tcph->dest) == sport &&
                    tcph->ack == 1 && tcph->rst == 1) {
                    cout << ip << ':' << dport << " Closed" << endl;
                    break;
                }
            }

            // 超时该端口开放
            struct timeval now;
            gettimeofday(&now, NULL);
            if (((now.tv_sec - start.tv_sec) * 1e6 +
                now.tv_usec - start.tv_usec) / 1e6 > 5) {
                cout << ip << ':' << dport << " Open" << endl;
                break;
            }
        }

        close(sockfd);
    }

    cout << "TCP FIN scanning OK!" << endl;
    return 0;
}

9. 声明UDPScanner类

// udpscanner.h
// 声明UDPScanner类

#pragma once

#include "scanner.h"

// UDP扫描器
class UDPScanner : public Scanner {
public:
    // 构造函数
    UDPScanner(const char* ip, const char* begin, const char* end);

    // 端口扫描
    int scan(void);
};

10. 实现UDPScanner类

// udpscanner.cpp
// 实现UDPScanner类

#include <unistd.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/time.h>
#include <netinet/udp.h>
#include <netinet/ip_icmp.h>
#include <stdio.h>
#include <iostream>
using namespace std;

#include "udpscanner.h"

// 构造函数
UDPScanner::UDPScanner(const char* ip, const char* begin,
    const char* end) : Scanner(ip, begin, end) {}

// 端口扫描
int UDPScanner::scan(void) {
    cout << "UDP scanning ..." << endl;

    if (Scanner::scan() == -1)
        return -1;

    // 从起始端口号遍历至终止端口号
    for (in_port_t dport = bport; dport <= eport; ++dport) {
        // 创建基于ICMP协议的原始套接字
        int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
        if (sockfd == -1) {
            perror("socket");
            return -1;
        }

        // 自己组织IP包头
        int on = 1;
        if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on,
            sizeof(on)) == -1) {
            perror("setsockopt");
            close(sockfd);
            return -1;
        }

        // 组织校验和结构
        uint8_t buf[1024];
        CHECKSUM* sum = (CHECKSUM*)(buf + sizeof(struct iphdr) -
            sizeof(CHECKSUM));
        sum->saddr    = saddr;
        sum->daddr    = daddr;
        sum->useless  = 0;
        sum->protocol = IPPROTO_UDP;
        sum->length   = htons(sizeof(struct udphdr));

        // 组织UDP包头
        struct udphdr* udph = (struct udphdr*)(sum + 1);
        in_port_t sport = dport + 1024;
        udph->source = htons(sport);         // 源端口
        udph->dest   = htons(dport);         // 目的端口
        udph->len    = htons(sizeof(*udph)); // 包长度
        udph->check  = 0;                    // 校验和

        // 计算校验和
        udph->check = checksum(sum, sizeof(*sum) + sizeof(*udph));

        // 组织IP包头
        struct iphdr* iph = (struct iphdr*)buf;
        iph->version  = 4;                 // 版本(4位)
        iph->ihl      = 5;                 // 包头长度(4位)
        iph->tos      = 16;                // 服务类型
        iph->tot_len  = htons(
            sizeof(*iph) + sizeof(*udph)); // 包长度
        iph->frag_off = 0;                 // 标志(3位)和偏移(13位)
        iph->ttl      = 69;                // 生存时间
        iph->protocol = IPPROTO_UDP;       // 上层协议
        iph->check    = 0;                 // 校验和
        iph->saddr    = saddr;             // 源IP地址
        iph->daddr    = daddr;             // 目的IP地址

        // 发送UDP数据包
        struct sockaddr_in addr;
        socklen_t addrlen = sizeof(addr);
        bzero(&addr, addrlen);
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = daddr;
        addr.sin_port = htons(dport);
        if (sendto(sockfd, buf, sizeof(*iph) + sizeof(*udph), 0,
            (struct sockaddr*)&addr, addrlen) == -1) {
            perror("sendto");
            close(sockfd);
            continue;
        }

        // 将套接字设置为非阻塞模式
        if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
            perror("fcntl");
            close(sockfd);
            continue;
        }

        // 接收循环
        struct timeval start;
        gettimeofday(&start, NULL);
        for (;;) {
            // 接收应答数据包
            if (recvfrom(sockfd, buf, sizeof(buf), 0,
                (struct sockaddr*)&addr, &addrlen) > 0) {
                // 检查应答数据包
                struct iphdr* iph = (struct iphdr*)buf;
                struct icmphdr* icmph = (struct icmphdr*)(buf + iph->ihl * 4);
                if (iph->saddr == daddr && iph->daddr == saddr &&
                    icmph->type == ICMP_DEST_UNREACH &&
                    icmph->code == ICMP_PORT_UNREACH) {
                    cout << ip << ':' << dport << " Closed" << endl;
                    break;
                }
            }

            // 超时该端口开放
            struct timeval now;
            gettimeofday(&now, NULL);
            if (((now.tv_sec - start.tv_sec) * 1e6 +
                now.tv_usec - start.tv_usec) / 1e6 > 5) {
                cout << ip << ':' << dport << " Open" << endl;
                break;
            }
        }

        close(sockfd);
    }

    cout << "UDP scanning OK!" << endl;
    return 0;
}

11. 测试Scanner类及其子类

// scanner_test.cpp
// 测试Scanner类及其子类

#include <stdlib.h>
#include <string.h>
#include <iostream>
using namespace std;

#include "tcpscanner.h"
#include "synscanner.h"
#include "finscanner.h"
#include "udpscanner.h"

int main(int argc, char* argv[]) {
    if (argc < 5)
        goto escape;

    // 扫描器
    Scanner* scanner;

    if (! strcmp(argv[1], "-c"))
        // TCP CONNECT扫描器
        scanner = new TCPScanner(argv[2], argv[3], argv[4]);
    else
    if (! strcmp(argv[1], "-s"))
        // TCP SYN扫描器
        scanner = new SYNScanner(argv[2], argv[3], argv[4]);
    else
    if (! strcmp(argv[1], "-f"))
        // TCP FIN扫描器
        scanner = new FINScanner(argv[2], argv[3], argv[4]);
    else
    if (! strcmp(argv[1], "-u"))
        // UDP扫描器
        scanner = new UDPScanner(argv[2], argv[3], argv[4]);
    else
        goto escape;

    // 端口扫描
    if (scanner->scan() == -1)
        return EXIT_FAILURE;

    // 返回成功
    return EXIT_SUCCESS;

escape:
    // 打印命令行帮助信息并返回失败
    cerr << "Usage: " << argv[0] <<
        " -c|s|f|u <ip> <begin> <end>" << endl;
    return EXIT_FAILURE;
}

12. 测试Scanner类及其子类构建脚本

# scanner_test.mak
# 测试Scanner类及其子类构建脚本

PROJ   = scanner_test
OBJS   = scanner_test.o \
         tcpscanner.o \
         synscanner.o \
         finscanner.o \
         udpscanner.o \
         scanner.o
CXX    = g++
LINK   = g++
RM     = rm -rf
CFLAGS = -c -g -Wall -I.

$(PROJ): $(OBJS)
    $(LINK) $^ -o $@

.cpp.o:
    $(CXX) $(CFLAGS) $^

clean:
    $(RM) $(PROJ) $(OBJS)

8.3 扩展提高

8.3.1 扩展ICMP扫描

传统ICMP扫描容易被防火墙过滤,扩展ICMP扫描可以绕开防火墙探测其后的主机。

根据TCP/IP协议,如果在通信过程中出现错误,接收端将向发送端响应一个ICMP错误报文以告知对方具体发生了什么错误,这些错误报文一般不会被防火墙拦截:

此外,扩展ICMP扫描还有两种其它功能:

8.3.2 扩展TCP扫描

前面提到的TCP FIN扫描属于秘密扫描的一种,即在扫描过程中无需建立TCP连接,因此不会被目标主机系统察觉,也不会留下任何日志记录。

1. 理论基础

当一个包头中带有不同标志位的TCP报文,到达一个处于监听或关闭状态的端口时,网络协议栈将以不同的标志位做出响应或不响应。如下表所示:

STATE URG ACK PSH RST SYN FIN NULL
LISTEN —— RST —— —— ACK+SYN —— ——
CLOSED RST —— RST —— ACK+RST ACK+RST RST

端口扫描程序即根据目标主机是否响应,以及响应包头中的标志位,判断被扫描端口的当前状态。

2. 其它扫描方式

1) ACK扫描

向目标主机的被扫描端口,发送一个仅包含ACK标志位的TCP包头:

也可以用这种方法判断目标主机防火墙是执行简单的分组过滤,还是执行基于状态的过滤。

2) NULL扫描

向目标主机的被扫描端口,发送一个不包含任何标志位的TCP包头:

3) URG+PSH+FIN扫描

向目标主机的被扫描端口,发送一个同时包含URG、PSH和FIN标志位的TCP包头:

3. 平台相关性

秘密扫描虽然种类繁多,但大都依赖于系统平台网络协议栈的实现细节,因此同一种扫描方法未必对所有的操作系统都有效。只有选择那些与特定目标平台相适应的扫描方法,才能达到预期的效果。

4. IP欺骗

为了规避目标主机对特定IP地址集的防护性过滤,扫描程序可以将报文IP包头中的源IP地址字段,设置为某个被目标主机信任的IP地址,冒充该地址所表示的主机,达到隐藏自身,欺骗目标的目的。

8.3.3 漏洞扫描

漏洞扫描最初是一种黑客攻击手段。黑客通过自己编写或利用他人开发的软件工具,检测被扫描主机系统中是否存在某些已知的漏洞,然后专门针对这些漏洞发起致命的攻击。

随着扫描技术的发展和漏洞资料库的日趋完善,逐渐出现了专业化的安全评估工具——漏洞扫描器。借助漏洞扫描器,网络安全人员或设备制造商可以提前发现系统中存在的潜在风险,及时修补,防患于未然。

1. 主机型漏洞扫描

主机型漏洞扫描亦称被动扫描,即通过一系列非破坏性的方法对系统的用户权限、文件属性、服务配置、应用程序乃至安全补丁等进行扫描检测,最后向用户提供检测报告。

主机型漏洞扫描通常需要较高的用户权限,因此它能够准确定位系统中存在的各种安全隐患,所能发现的问题比较全面,检测到的漏洞也比较彻底。

2. 网络型漏洞扫描

网络型漏洞扫描通过定制化的脚本,模拟黑客对被扫描系统发起攻击,并对结果进行分析,检查其是否与漏洞数据库中的已知规则相匹配,若匹配则说明存在安全漏洞。

目前大部分漏洞扫描产品都是基于网络型的,同时它们也借鉴了主机型的很多优点,既可以对网络上的服务器系统进行远程漏洞检测,也可以对本地主机系统进行更深层次的安全检测。

网络漏洞扫描器的工作过程一般是这样的:

8.3.4 基于Nmap的网络探测和安全审计

1. Nmap简介

网络映射器(Network mapper, Nmap)是一款开源网络探测和安全审计实用工具。系统管理人员可以利用该工具:

Nmap支持多种类型的扫描:

2. Nmap安装

在Ubuntu上安装Nmap非常简单:

$ sudo apt install nmap

3. Nmap使用

Nmap的命令语法如下:

nmap [Scan Type(s)][Options]{target specification}
1) 扫描类型(Scan Type(s))
Scan Type(s) 扫描类型
-sT TCP CONNECT扫描
-sS TCP SYN扫描
-sF TCP FIN扫描
-sA TCP ACK扫描
-sN TCP NULL扫描
-sM TCP Maimon扫描
-sW TCP Window扫描
-sX TCP XmasTree扫描
-sU UDP扫描
-sO IP扫描
-b FTP弹跳扫描
2) 功能选项(Options)

不同的扫描类型会有不同的功能选项,它们可以组合使用,但并非所有组合都是合理的。对不支持的功能组合,Nmap会向用户发出警告。

Namp的功能选项非常多,详情请参见:https://nmap.org/book/man-briefoptions.html

3) 目标说明(target specification)

目标说明用于指定扫描的对象,一个IP地址、一个域名或一个CIDR风格的地址范围。

例如:对本地主机执行TCP SYN扫描

$ sudo nmap -sS 127.0.0.1

Starting Nmap 7.60 ( https://nmap.org ) at 2019-04-04 15:01 CST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000012s latency).
Not shown: 998 closed ports
PORT     STATE SERVICE
631/tcp  open  ipp
8000/tcp open  http-alt

Nmap done: 1 IP address (1 host up) scanned in 1.85 seconds

例如:对IP地址介于192.168.1.0到192.168.1.255之间的所有主机执行TCP SYN扫描

$ sudo nmap -sS 192.168.1.0/24

其中,24表示被扫描主机IP地址的前24位与192.168.1.0相同。

例如:对IP地址介于192.168.1.1到192.168.1.254之间的所有主机执行TCP SYN扫描

$ sudo nmap -sS 192.168.1.1-254

其中,1-254表示范围。范围形式的表示不仅限于IP地址的最后八位,例如:

$ sudo nmap -sS 192.168.1-10.1-254

达内集团◇C++教研部◇闵卫