前言
很多人都会好奇为什么ZMAP能够做到45分钟,扫遍整个互联网,遂去看了下ZMAP的源码,顺便分享知识.zmap源码解读之zmap扫描快的原因
引言
ZMAP是目前比较出色的端口扫描器,他的45分钟扫遍整个互联网的能力令人咋舌.然后去网上检索ZMAP扫描快的原因,都没有相关解释,遂只能自己动手,丰衣足食.
整体架构
ZMAP整体函数调用图如下所示.
通过图我们可以直观的看到整个程序调用的过程.ZMAP在启动时候,先获取环境信息,如IP,网关等.然后读取配置文件选择使用哪种扫描方式,然后在Probe_modules切换到对应的模块,然后启动.
本文侧重分析SYN扫描这个模块.整个执行的过程中,会有一个线程专门负责发送,另外有一个使用libpcap组件抓包.发送和接收就独立开来.
SYN扫描
如下图所示,客户端在发送一个SYN包的时候,如果对方端口开放,就会发送一个SYN-ACK,那么就表明这个端口开放,这时候我们发送RST包,防止占用对方资源.如果对方端口不开放,那么我们就会收到对方主机的RST包
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和sport1
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*)(ð_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可以将状态信息存在数据包里.避免了记录了状态信息.