RSS Feed
更好更安全的互联网

从 Masscan, Zmap 源码分析到开发实践

2019-10-14

作者:w7ay@知道创宇404实验室 
日期:2019年10月12日

Zmap和Masscan都是号称能够快速扫描互联网的扫描器,十一因为无聊,看了下它们的代码实现,发现它们能够快速扫描,原理其实很简单,就是实现两种程序,一个发送程序,一个抓包程序,让发送和接收分隔开从而实现了速度的提升。但是它们识别的准确率还是比较低的,所以就想了解下为什么准确率这么低以及应该如何改善。

Masscan源码分析

首先是看的Masscan的源码,在readme上有它的一些设计思想,它指引我们看main.c中的入口函数main(),以及发送函数和接收函数transmit_thread()receive_thread(),还有一些简单的原理解读。

理论上的6分钟扫描全网

在后面自己写扫描器的过程中,对Masscan的扫描速度产生怀疑,目前Masscan是号称6分钟扫描全网,以每秒1000万的发包速度。

image-20191010142518478

但是255^4/10000000/60 ≈ 7.047 ???

之后了解到,默认模式下Masscan使用pcap发送和接收数据包,它在Windows和Mac上只有30万/秒的发包速度,而Linux可以达到150万/秒,如果安装了PF_RING DNA设备,它会提升到1000万/秒的发包速度(这些前提是硬件设备以及带宽跟得上)。

注意,这只是按照扫描一个端口的计算。

PF_RING DNA设备了解地址:http://www.ntop.org/products/pf_ring/

那为什么Zmap要45分钟扫完呢?

在Zmap的主页上说明了

image-20191010151936899

用PF_RING驱动,可以在5分钟扫描全网,而默认模式才是45分钟,Masscan的默认模式计算一下也是45分钟左右才扫描完,这就是宣传的差距吗 (-

历史记录

观察了readme的历史记录 https://github.githistory.xyz/robertdavidgraham/Masscan/blob/master/README.md

之前构建时会提醒安装libpcap-dev,但是后面没有了,从releases上看,是将静态编译的libpcap改为了动态加载。

C10K问题

c10k也叫做client 10k,就是一个客户端在硬件性能足够条件下如何处理超过1w的连接请求。Masscan把它叫做C10M问题。

Masscan的解决方法是不通过系统内核调用函数,而是直接调用相关驱动。

主要通过下面三种方式:

  • 定制的网络驱动
    • Masscan可以直接使用PF_RING DNA的驱动程序,该驱动程序可以直接从用户模式向网络驱动程序发送数据包而不经过系统内核。
  • 内置tcp堆栈
    • 直接从tcp连接中读取响应连接,只要内存足够,就能轻松支持1000万并发的TCP连接。但这也意味着我们要手动来实现tcp协议。
  • 不使用互斥锁
    • 锁的概念是用户态的,需要经过CPU,降低了效率,Masscan使用rings来进行一些需要同步的操作。与之对比一下Zmap,很多地方都用到了锁。
      • 为什么要使用锁?
        • 一个网卡只用开启一个接收线程和一个发送线程,这两个线程是不需要共享变量的。但是如果有多个网卡,Masscan就会开启多个接收线程和多个发送线程,这时候的一些操作,如打印到终端,输出到文件就需要锁来防止冲突。
      • 多线程输出到文件
        • Masscan的做法是每个线程将内容输出到不同文件,最后再集合起来。在src/output.c中,

随机化地址扫描

在读取地址后,如果进行顺序扫描,伪代码如下

但是考虑到有的网段可能对扫描进行检测从而封掉整个网段,顺序扫描效率是较低的,所以需要将地址进行随机的打乱,用算法描述就是设计一个打乱数组的算法,Masscan是设计了一个加密算法,伪代码如下

随机种子就是i的值,这种加密算法能够建立一种一一对应的映射关系,即在[1...range]的区间内通过i来生成[1...range]内不重复的随机数。同时如果中断了扫描,只需要记住i的值就能重新启动,在分布式上也可以根据i来进行。

无状态扫描的原理

回顾一下tcp协议中三次握手的前两次

  1. 客户端在向服务器第一次握手时,会组建一个数据包,设置syn标志位,同时生成一个数字填充seq序号字段。
  2. 服务端收到数据包,检测到了标志位的syn标志,知道这是客户端发来的建立连接的请求包,服务端会回复一个数据包,同时设置syn和ack标志位,服务器随机生成一个数字填充到seq字段。并将客户端发送的seq数据包+1填充到ack确认号上。

在收到syn和ack后,我们返回一个rst来结束这个连接,如下图所示

image-20191003223330374
image-20191003230816536

Masscan和Zmap的扫描原理,就是利用了这一步,因为seq是我们可以自定义的,所以在发送数据包时填充一个特定的数字,而在返回包中可以获得相应的响应状态,即是无状态扫描的思路了。 接下来简单看下Masscan中发包以及接收的代码。

发包

main.c中,前面说的随机化地址扫描

image-20191003232846484

接着生成cookie并发送

image-20191003233102015

看名字我们知道,生成cookie的因子有源ip,源端口,目的ip,目的端口,和entropy(随机种子,Masscan初始时自动生成),siphash24是一种高效快速的哈希函数,常用于网络流量身份验证和针对散列dos攻击的防御。

组装tcp协议template_set_target(),部分代码

发包函数

可以看到它是分三种模式发包的,PF_RING,WinPcap,LibPcap,如果没有装相关驱动的话,默认就是pcap发包。如果想使用PF_RING模式,只需要加入启动参数--pfring

接收

在接收线程看到一个关于cpu的代码

image-20191004003419241

大意是锁住这个线程运行的cpu,让发送线程运行在双数cpu上,接收线程运行在单数cpu上。但代码没怎么看懂

接收原始数据包

主要是使用了PFRING和PCAP的api来接收。后面便是一系列的接收后的处理了。在mian.c757行

image-20191004004238243

后面还会判断是否为源ip,判断方式不是相等,是判断某个范围。

接着后面的处理

Zmap源码分析

Zmap官方有一篇paper,讲述了Zmap的原理以及一些实践。上文说到Zmap使用的发包技术和Masscan大同小异,高速模式下都是调用pf_ring的驱动进行,所以对这些就不再叙述了,主要说下其他与Masscan不同的地方,paper中对丢包问题以及扫描时间段有一些研究,简单整理下

  1. 发送多个探针:结果表明,发送8个SYN包后,响应主机数量明显趋于平稳
  2. 哪些时间更适合扫描
    1. 我们观察到一个±3.1%的命中率变化依赖于日间扫描的时间。最高反应率在美国东部时间上午7时左右,最低反应率在美国东部时间下午7时45分左右。
    2. 这些影响可能是由于整体网络拥塞和包丢失率的变化,或者由于只间断连接到网络的终端主机的总可用性的日变化模式。在不太正式的测试中,我们没有注意到任何明显的变化

还有一点是Zmap只能扫描单个端口,看了一下代码,这个保存端口变量的作用也只是在最后接收数据包用来判断srcport用,不明白为什么还没有加上多端口的支持。

宽带限制

相比于Masscan用rate=10000作为限制参数,Zmap用-B 10M的方式来限制

image-20191010154942162

我觉得这点很好,因为不是每个使用者都能明白每个参数代表的原理。实现细节

image-20191010155045099
image-20191010155334018

发包与解包

Zmap不支持Windows,因为Zmap的发包默认用的是socket,在window下可能不支持tcp的组包(猜测)。相比之下Masscan使用的是pcap发包,在win/linux都有支持的程序。Zmap接收默认使用的是pcap。

在构造tcp包时,附带的状态信息会填入到seq和srcport中

image-20191010161356014

在解包时,先判断返回dstport的数据

image-20191012110543094

再判断返回的ack中的数据

image-20191012110655331

用go写端口扫描器

在了解完以上后,我就准备用go写一款类似的扫描器了,希望能解决丢包的问题,顺便学习go。

在上面分析中知道了,Masscan和Zmap都使用了pcap,pfring这些组件来原生发包,值得高兴的是go官方也有原生支持这些的包 https://github.com/google/gopacket,而且完美符合我们的要求。

image-20191012111724556

接口没问题,在实现了基础的无状态扫描功能后,接下来就是如何处理丢包的问题。

丢包问题

按照tcp协议的原理,我们发送一个数据包给目标机器,端口开放时返回ack标记,关闭会返回rst标记。

但是通过扫描一台外网的靶机,发现扫描几个端口是没问题的,但是扫描大批量的端口(1-65535),就可能造成丢包问题。而且不存在的端口不会返回任何数据。

控制速率

刚开始以为是速度太快了,所以先控制下每秒发送的频率。因为发送和接收都是启动了一个goroutine,目标的传入是通过一个channel传入的(go的知识点)。

所以控制速率的伪代码类似这样

本地状态表

即使将速度控制到了最小,也存在丢包的问题,后经过一番测试,发现是防火墙的原因。例如常用的iptables,其中拒绝的端口不会返回信息。将端口放行后再次扫描,就能正常返回数据包了。

此时遇到的问题是有防火墙策略的主机如何进行准确扫描,一种方法是扫描几个端口后就延时一段时间,但这不符合快速扫描的设想,所以我的想法是维护一个本地的状态表,状态表中能够动态修改每个扫描结果的状态,将那些没有返回包的目标进行重试。

Ps:这是针对一个主机,多端口(1-65535)的扫描策略,如果是多个IP,Masscan的随机化地址扫描策略就能发挥作用了。

设想的结构如下

初始数据时status为0,当发送数据时,将status变更为1,同时记录发送时间time,接收数据时通过返回的标记,dstport,seq等查找到本地状态表相应的数据结构,变更status为2,同时启动一个监控程序,监控程序每隔一段时间对所有的状态进行检查,如果发现stauts为1并且当前时间-发送时间大于一定值的时候,可以判断这个ip+端口的探测包丢失了,准备重发,将retry+1,重新设置发送时间time后,将数据传入发送的channel中。

概念验证程序

因为只是概念验证程序,而且是自己组包发送,需要使用到本地和网关的mac地址等,这些还没有写自动化程序获取,需要手动填写。mac地址可以手动用wireshark抓包获得。

如果你想使用该程序的话,需要修改全局变量中的这些值

整个go语言源程序如下,单文件。

运行结果如下

image-20191012135527477

但这个程序并没有解决上述说的防火墙阻断问题,设想很美好,但是在实践的过程中发现这样一个问题。比如扫描一台主机中的1000个端口,第一次扫描后由于有防火墙的策略只检测到了5个端口,剩下995个端口会进行第一次重试,但是重试中依然会遇到防火墙的问题,所以本质上并没有解决这个问题。

Top端口

这是Masscan源码中一份内置的Top端口表

可以使用--top-ports = n来选择数量。

这是在写完go扫描器后又在Masscan中发现的,可能想象到Masscan可能也考虑过这个问题,它的方法是维护一个top常用端口的排行来尽可能减少扫描端口的数量,这样可以覆盖到大多数的端口(猜测)。

总结

概念性程序实践失败了,所以再用go开发的意义也不大了,后面还有一个坑就是go的pcap不能跨平台编译,只能在Windows下编译windows版本,mac下编译mac版本。

但是研究了Masscan和Zmap在tcp协议下的syn扫描模式,还是有很多收获,以及明白了它们为什么要这么做,同时对网络协议和一些更低层的细节有了更深的认识。

这里个人总结了一些tips:

  • Masscan源码比Zmap读起来更清晰,注释也很多,基本上一看源码就能明白大致的结构了。
  • Masscan和Zmap最高速度模式都是使用的pfring这个驱动程序,理论上它两的速度是一致的,只是它们宣传口径不一样?
  • 网络宽带足够情况下,扫描单个端口准确率是最高的(通过自己编写go扫描器的实践得出)。
  • Masscan和Zmap都能利用多网卡,但是Zmap线程切换用了锁,可能会消耗部分时间。
  • 设置发包速率时不仅要考虑自己带宽,还要考虑目标服务器的承受情况(扫描多端口时)

参考链接


Paper

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1052/

作者:吴烦恼 | Categories:安全研究 | Tags:

发表评论