zmap
Usage
Do upnp protocol scan - sudo zmap -p 1900 --probe-module="upnp" -O json -r 10000
We need more specific information, so try to figure out what’s else zmap can offer us - zmap --probe-module="upnp -O json --list-output-fields"
/
use -f options to add what we are interested - sudo zmap -p 1900 --probe-module="upnp" -O json -r 10000 -f "saddr, classifiction, location"
zmap实现原理
ip地址扫描
在ip地址扫描策略上,zmap进行了特别的优化,主要在三个方面:
-
相对于nmap逐个按顺序的扫描ip地址,zmap进行散列式的分组随机扫描,以保障目标地址网段不会出现饱和拥塞。
-
nmap在扫描时需要维护每次扫描时建立的连接状态,以避免重复扫描,而zmap通过使用
cyclie multiplictive group
的算法,使用三个整形变量实现随机无重复的覆盖扫描。 -
探测优化,使用raw socket编程,重载每个发送包中没有使用到的字段来区分接收到的包,实现无状态的不重复的扫描,并且每个目标地址都只发送固定数量的探测包(默认是1个)。
ip散列算法
zmap中ip散列分布扫描的算法基于Primitive root modulo n数学理论,关于这个理论本身的定义 和证明我们暂不关心,我们关心其特性的运用价值。
IPv4的地址空间范围是0~2^32,为了实例简单易懂,我们假设我们需要扫描的范围是(0,7),那么n就是7,7的primitive root有3和5,下面的表说明为什么3是7的 本源根:
和3类似,5也可以通过上面计算验证为是7的本源根,下面通过一张示意图来说明如何本源根的特性:
结合上图,假设我们第一个扫描的ip地址整数值是3,那么通过3 * 5 mod 7 = 1
我们得到下一个我们应该扫描的ip地址是1,然后再通过1 * 5 mod 7 = 5
得到下一个ip地址是5,因此类推,我们就随机的扫描了所有的ip地址。
zmap中n值是(2^32 + 15 = 4,294,967,311),即比2^32大的最小质数,这个质数的本源根恰好也是3。这样zmap通过三个整形变量(本源根、第一个IP地址、当前的IP地址)就可以维持ip地址扫描的状态。但扫描到的下一个IP地址和第一个IP地址相等时,说明我们已经扫描完整个ipv4地址了。
无状态跟踪实现代码
我们知道,正常的TCP通信都需要三次握手,在此期间需要维护记录与对方交互的状态,这种状态记录开销是很大的,所以zmap不进行三次握手,只发送一个SYN,然后 等待对方回复SYN-ACK,之后就发送RST取消连接。虽然nmap中也支持这种扫描,但关键性的问题是对回复的SYN-ACK进行seq number的校验。正常的做法是需要记录状态,而zmap是将目标ip地址进行hash,将其保存到探测包中未使用的字段中,这样当SYN-ACK回来时,就可以根据其携带的hash信息来进行校验,免去了状态存储。 具体代码实现如下:
//send.c
/* one sender thread */
int send_run(sock_t st, shard_t *s)
{
...
/* actually send a packet */
for(int i = 0; i < zconf.packet_streams; i++)
{
size_t length = zconf.probe_module->packet_lenght;
uint32_t validation[VALIDATE_BYTES / sizeof(uint32_t)];
...
validate_gen(src_ip, current_ip, (uint8_t *)validation);
zconf.probe_module->make_packet(buf, &length, src_ip, current_ip, validation, i, probe_data);
...
}
}
上面代码是在实际发送一个packet时的组包处理,validate_gen()函数将源ip地址src_ip和目的ip地址current_ip进行哈希,哈希得到的结果存入validation变量 传入probe module中实际组包的回调函数,make_packet回调函数会加哈希结果写到可以重载的字段。不同的probe module发送的包处于不同的协议层,因此重载的字段不一样,下面以icmp echo这个probe module为例:
//
static int icmp_echo_make_packet(void *buf, size_t *buf_len, ipaddr_n_t src_ip, ipaddr_n_t dst_ip, uint32_t *validation, ...)
{
struct ether_header *ether_header = (struct ether_header *)buf;
struct ip *ip_header = (struct ip*)(ðer_headr[1]);
struct icmp *icmp_header = (struct icmp *)(&ip_headr[1]);
...
icmp_header->icmp_id = validation[1] & 0xffff;
icmp_header->icmp_seq = validation[2] & 0xffff;
...
}
icmp报文的格式如下:
具体到icmp echo报文,格式如下:
通过报文格式和前面的ping和traceroute程序的实现,可见,icmp_id和icmp_seq字段是可以重载而不会影响通信的。
下面再来看看tcp syncscan的probe module的make packet函数实现: //module_tcp_synscan.c
static int synscan_make_packet(*buf, size_t *buf_len, ...)
{
struct ether_header *ether_header = (struct ether_header *)buf;
struct ip *ip_header = (struct ip*)(ðer_headr[1]);
struct tcphdr *tcp_header = (struct tcphdr *)(ip_header[1]);
...
tcp_header->th_sport = htons(get_src_port(num_ports, probe_num, validation));
tcp_header->th_seq = validation[0];
...
}
//packet.h
static 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);
}
因为在zmap 的tcp synscan机制中,tcp协议字段中的源端口sport和seq是没有作用的,所以将哈希值重载到了th_sport字段。
在接收报文时通过解析对应的被重载过的字段,来判断接收到的包是不是对于我们scan的回应,这样就实现了不需要再scan时通过保存状态信息来识别 对应的response。
Packet Transmission and Receipt
Zgrab
Usage
sudo zmap -p 443 --output-field=* -O csv -r 10000 | ztee results.csv | ./zgrab --port 443 --tls --http="/" --output-file=banners.json