zmap源码解读之zmap扫描快的原因

前言

很多人都会好奇为什么ZMAP能够做到45分钟,扫遍整个互联网,遂去看了下ZMAP的源码,顺便分享知识.

zmap源码解读之zmap扫描快的原因

  • 整体架构: ZMAP整体框架;
  • 源码分析: 详细的解释扫描的原理;

引言

  ZMAP是目前比较出色的端口扫描器,他的45分钟扫遍整个互联网的能力令人咋舌.然后去网上检索ZMAP扫描快的原因,都没有相关解释,遂只能自己动手,丰衣足食.

整体架构

  ZMAP整体函数调用图如下所示.
PS-PNG-work
  通过图我们可以直观的看到整个程序调用的过程.ZMAP在启动时候,先获取环境信息,如IP,网关等.然后读取配置文件选择使用哪种扫描方式,然后在Probe_modules切换到对应的模块,然后启动.
  本文侧重分析SYN扫描这个模块.整个执行的过程中,会有一个线程专门负责发送,另外有一个使用libpcap组件抓包.发送和接收就独立开来.

SYN扫描

  如下图所示,客户端在发送一个SYN包的时候,如果对方端口开放,就会发送一个SYN-ACK,那么就表明这个端口开放,这时候我们发送RST包,防止占用对方资源.如果对方端口不开放,那么我们就会收到对方主机的RST包
SYN扫描

SYN扫描分析

  ZMAP扫描在单端口的情况下比NMAP快,哪怕他们都是采用SYN,这是因为,ZMAP使用了无状态的扫描方式.无状态,就是在扫描过程中,不会记录发包的状态,不用记住那些包发了没,这样减少了用于存储状态的空间,相对的提高了扫描的速度.

粗略解释

  ZMAP虽然不存储状态信息,但是这并不意味他不需要状态信息,而是他把状态信息存储在数据包里,这样通过返回的数据包,就能够直观的知道,这个包,是不是在我本次发送包的范围之内.在发送SYN包的时候,程序会将IP信息hash化,将其存储在sport,seq.在接收包的时候只要检验IPhash化的结果是否和dport和ack两个字段的内容相符合就行了.

详细解释

  程序在初始化的时候,会使用rijndaelKeySetupEnc函数产生一个key,用于校验,具体的rijndael算法,参考AES.

1
void validate_init()
{
    for (int i=0; i < AES_BLOCK_WORDS; i++) {
        aes_input[i] = 0;
    }
    uint8_t key[AES_KEY_BYTES];
    if (!random_bytes(key, AES_KEY_BYTES)) {
        log_fatal("validate", "couldn't get random bytes");
    }
    if (rijndaelKeySetupEnc(aes_sched, key, AES_KEY_BYTES*8) != AES_ROUNDS) {
        log_fatal("validate", "couldn't initialize AES key");
    }
    inited = 1;
}

void validate_gen(const uint32_t src, const uint32_t dst,
                uint8_t output[VALIDATE_BYTES])
{
    assert(inited);
    aes_input[0] = src;
    aes_input[1] = dst;
    rijndaelEncrypt(aes_sched, AES_ROUNDS, (uint8_t *)aes_input, output);
}

validate_init是用于生成key,validate_gen是用于生成密文来校验.
在发送SYN包的时候,程序会将生成的密文分两部分,一个存储在seq和sport

1
int synscan_make_packet(void *buf, ipaddr_n_t src_ip, ipaddr_n_t dst_ip,
        uint32_t *validation, int probe_num, __attribute__((unused)) void *arg)
{
    struct ether_header *eth_header = (struct ether_header *)buf;
    struct ip *ip_header = (struct ip*)(&eth_header[1]);
    struct tcphdr *tcp_header = (struct tcphdr*)(&ip_header[1]);
    uint32_t tcp_seq = validation[0];

    ip_header->ip_src.s_addr = src_ip;
    ip_header->ip_dst.s_addr = dst_ip;

    tcp_header->th_sport = htons(get_src_port(num_ports,
                probe_num, validation));
    tcp_header->th_seq = tcp_seq;
    tcp_header->th_sum = 0;
    tcp_header->th_sum = tcp_checksum(sizeof(struct tcphdr),
            ip_header->ip_src.s_addr, ip_header->ip_dst.s_addr, tcp_header);

    ip_header->ip_sum = 0;
    ip_header->ip_sum = zmap_ip_checksum((unsigned short *) ip_header);

    return EXIT_SUCCESS;
}

而端口的怎么分配的,是通过get_src_port函数对应的,函数通过取模的方式,将信息存储在port里.

1


static __attribute__((unused)) inline uint16_t get_src_port(int num_ports,
                int probe_num, uint32_t *validation)
{
    return zconf.source_port_first + ((validation[1] + probe_num) % num_ports);
}

在接受包的时候,通过同样的方式

1
static __attribute__((unused)) inline int check_dst_port(uint16_t port,
                int num_ports, uint32_t *validation)
{
    if (port > zconf.source_port_last
                    || port < zconf.source_port_first) {
        return -1;
    }
    int32_t to_validate = port - zconf.source_port_first;
    int32_t min = validation[1] % num_ports;
    int32_t max = (validation[1] + zconf.packet_streams - 1) % num_ports;

    return (((max - min) % num_ports) >= ((to_validate - min) % num_ports));
}

来检测这个包是不是我想要的包.
整个校验包的函数如下

1
int synscan_validate_packet(const struct ip *ip_hdr, uint32_t len,
        __attribute__((unused))uint32_t *src_ip,
        uint32_t *validation)
{
    if (ip_hdr->ip_p != IPPROTO_TCP) {
        return 0;
    }
    if ((4*ip_hdr->ip_hl + sizeof(struct tcphdr)) > len) {
        // buffer not large enough to contain expected tcp header
        return 0;
    }
    struct tcphdr *tcp = (struct tcphdr*)((char *) ip_hdr + 4*ip_hdr->ip_hl);
    uint16_t sport = tcp->th_sport;
    uint16_t dport = tcp->th_dport;
    // validate source port
    if (ntohs(sport) != zconf.target_port) {
        return 0;
    }
    // validate destination port
    if (!check_dst_port(ntohs(dport), num_ports, validation)) {
        return 0;
    }
    // validate tcp acknowledgement number
    if (htonl(tcp->th_ack) != htonl(validation[0])+1) {
        return 0;
    }
    return 1;
}

程序会首先看目标的端口是不是,本次我在探测的端口,其次是看dport是不是符合取模运算,最后比较接收的数据包的ACK字段是不是符合本次加密的密文第一部分.

通过这样的一个过程,使得ZMAP可以将状态信息存在数据包里.避免了记录了状态信息.

文章目录
  1. 1. 前言
    1. 1.1. zmap源码解读之zmap扫描快的原因
  2. 2. 引言
  3. 3. 整体架构
  4. 4. SYN扫描
  5. 5. SYN扫描分析
    1. 5.1. 粗略解释
    2. 5.2. 详细解释
|
Fork me on GitHub