作者:w7ay@知道创宇404实验室
日期:2019年10月12日
原文链接:https://paper.seebug.org/1052/
Zmap和Masscan都是号称能够快速扫描互联网的扫描器,十一因为无聊,看了下它们的代码实现,发现它们能够快速扫描,原理其实很简单,就是实现两种程序,一个发送程序,一个抓包程序,让发送和接收分隔开从而实现了速度的提升。但是它们识别的准确率还是比较低的,所以就想了解下为什么准确率这么低以及应该如何改善。
Masscan源码分析
首先是看的Masscan的源码,在readme上有它的一些设计思想,它指引我们看main.c中的入口函数main(),以及发送函数和接收函数transmit_thread()和receive_thread(),还有一些简单的原理解读。
理论上的6分钟扫描全网
在后面自己写扫描器的过程中,对Masscan的扫描速度产生怀疑,目前Masscan是号称6分钟扫描全网,以每秒1000万的发包速度。
但是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的主页上说明了
用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中,
随机化地址扫描
在读取地址后,如果进行顺序扫描,伪代码如下
for (i = 0; i < range; i++) {
scan(i);
}
但是考虑到有的网段可能对扫描进行检测从而封掉整个网段,顺序扫描效率是较低的,所以需要将地址进行随机的打乱,用算法描述就是设计一个打乱数组的算法,Masscan是设计了一个加密算法,伪代码如下
range = ip_count * port_count;
for (i = 0; i < range; i++) {
x = encrypt(i);
ip = pick(addresses, x / port_count);
port = pick(ports, x % port_count);
scan(ip, port);
}
随机种子就是i的值,这种加密算法能够建立一种一一对应的映射关系,即在[1…range]的区间内通过i来生成[1…range]内不重复的随机数。同时如果中断了扫描,只需要记住i的值就能重新启动,在分布式上也可以根据i来进行。
如果对这个加密算法感兴趣可以看 Ciphers with Arbitrary Finite Domains 这篇论文。
无状态扫描的原理
回顾一下tcp协议中三次握手的前两次
1、客户端在向服务器第一次握手时,会组建一个数据包,设置syn标志位,同时生成一个数字填充seq序号字段。
2、服务端收到数据包,检测到了标志位的syn标志,知道这是客户端发来的建立连接的请求包,服务端会回复一个数据包,同时设置syn和ack标志位,服务器随机生成一个数字填充到seq字段。并将客户端发送的seq数据包+1填充到ack确认号上。
在收到syn和ack后,我们返回一个rst来结束这个连接,如下图所示
Masscan和Zmap的扫描原理,就是利用了这一步,因为seq是我们可以自定义的,所以在发送数据包时填充一个特定的数字,而在返回包中可以获得相应的响应状态,即是无状态扫描的思路了。 接下来简单看下Masscan中发包以及接收的代码。
发包
在main.c中,前面说的随机化地址扫描
接着生成cookie并发送
uint64_t syn_cookie( unsigned ip_them, unsigned port_them,
unsigned ip_me, unsigned port_me,
uint64_t entropy) {
unsigned data[4];
uint64_t x[2];
x[0] = entropy;
x[1] = entropy;
data[0] = ip_them;
data[1] = port_them;
data[2] = ip_me;
data[3] = port_me;
return siphash24(data, sizeof(data), x); }
看名字我们知道,生成cookie的因子有源ip,源端口,目的ip,目的端口,和entropy(随机种子,Masscan初始时自动生成),siphash24是一种高效快速的哈希函数,常用于网络流量身份验证和针对散列dos攻击的防御。
组装tcp协议template_set_target(),部分代码
case Proto_TCP:
px[offset_tcp+ 0] = (unsigned char)(port_me >> 8);
px[offset_tcp+ 1] = (unsigned char)(port_me & 0xFF);
px[offset_tcp+ 2] = (unsigned char)(port_them >> 8);
px[offset_tcp+ 3] = (unsigned char)(port_them & 0xFF);
px[offset_tcp+ 4] = (unsigned char)(seqno >> 24);
px[offset_tcp+ 5] = (unsigned char)(seqno >> 16);
px[offset_tcp+ 6] = (unsigned char)(seqno >> 8);
px[offset_tcp+ 7] = (unsigned char)(seqno >> 0);
xsum += (uint64_t)tmpl->checksum_tcp
+ (uint64_t)ip_me
+ (uint64_t)ip_them
+ (uint64_t)port_me
+ (uint64_t)port_them
+ (uint64_t)seqno;
xsum = (xsum >> 16) + (xsum & 0xFFFF);
xsum = (xsum >> 16) + (xsum & 0xFFFF);
xsum = (xsum >> 16) + (xsum & 0xFFFF);
xsum = ~xsum;
px[offset_tcp+16] = (unsigned char)(xsum >> 8);
px[offset_tcp+17] = (unsigned char)(xsum >> 0);
break;
发包函数
/***************************************************************************
* wrapper for libpcap's sendpacket
*
* PORTABILITY: WINDOWS and PF_RING
* For performance, Windows and PF_RING can queue up multiple packets, then
* transmit them all in a chunk. If we stop and wait for a bit, we need
* to flush the queue to force packets to be transmitted immediately.
***************************************************************************/
int
rawsock_send_packet(
struct Adapter *adapter,
const unsigned char *packet,
unsigned length,
unsigned flush)
{
if (adapter == 0)
return 0;
/* Print --packet-trace if debugging */
if (adapter->is_packet_trace) {
packet_trace(stdout, adapter->pt_start, packet, length, 1);
}
/* PF_RING */
if (adapter->ring) {
int err = PF_RING_ERROR_NO_TX_SLOT_AVAILABLE;
while (err == PF_RING_ERROR_NO_TX_SLOT_AVAILABLE) {
err = PFRING.send(adapter->ring, packet, length, (unsigned char)flush);
}
if (err < 0)
LOG(1, "pfring:xmit: ERROR %d\n", err);
return err;
}
/* WINDOWS PCAP */
if (adapter->sendq) {
int err;
struct pcap_pkthdr hdr;
hdr.len = length;
hdr.caplen = length;
err = PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);
if (err) {
rawsock_flush(adapter);
PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);
}
if (flush) {
rawsock_flush(adapter);
}
return 0;
}
/* LIBPCAP */
if (adapter->pcap)
return PCAP.sendpacket(adapter->pcap, packet, length);
return 0;
}
可以看到它是分三种模式发包的,PF_RING,WinPcap,LibPcap,如果没有装相关驱动的话,默认就是pcap发包。如果想使用PF_RING模式,只需要加入启动参数–pfring
接收
在接收线程看到一个关于cpu的代码
大意是锁住这个线程运行的cpu,让发送线程运行在双数cpu上,接收线程运行在单数cpu上。但代码没怎么看懂
接收原始数据包
int rawsock_recv_packet(
struct Adapter *adapter,
unsigned *length,
unsigned *secs,
unsigned usecs,
const unsigned char packet) {
if (adapter->ring) {
/ This is for doing libpfring instead of libpcap /
struct pfring_pkthdr hdr;
int err;
again:
err = PFRING.recv(adapter->ring,
(unsigned char)packet,
0, / zero-copy /
&hdr,
0 / return immediately */
);
if (err == PF_RING_ERROR_NO_PKT_AVAILABLE || hdr.caplen == 0) {
PFRING.poll(adapter->ring, 1);
if (is_tx_done)
return 1;
goto again;
}
if (err)
return 1;
*length = hdr.caplen;
*secs = (unsigned)hdr.ts.tv_sec;
*usecs = (unsigned)hdr.ts.tv_usec;
} else if (adapter->pcap) {
struct pcap_pkthdr hdr;
*packet = PCAP.next(adapter->pcap, &hdr);
if (packet == NULL) {
if (is_pcap_file) {
//pixie_time_set_offset(10100000);
is_tx_done = 1;
is_rx_done = 1;
}
return 1;
}
*length = hdr.caplen;
*secs = (unsigned)hdr.ts.tv_sec;
*usecs = (unsigned)hdr.ts.tv_usec;
}
return 0;
}
主要是使用了PFRING和PCAP的api来接收。后面便是一系列的接收后的处理了。在mian.c757行
后面还会判断是否为源ip,判断方式不是相等,是判断某个范围。
int is_my_port(const struct Source *src, unsigned port)
{
return src->port.first <= port && port <= src->port.last;
}
接着后面的处理
if (TCP_IS_SYNACK(px, parsed.transport_offset)
|| TCP_IS_RST(px, parsed.transport_offset)) {
// 判断是否是syn+ack或rst标志位
/* 获取状态 */
status = PortStatus_Unknown;
if (TCP_IS_SYNACK(px, parsed.transport_offset))
status = PortStatus_Open; // syn+ack 说明端口开放
if (TCP_IS_RST(px, parsed.transport_offset)) {
status = PortStatus_Closed; // rst 说明端口关闭
}
/* verify: syn-cookies 校验cookie是否正确 */
if (cookie != seqno_me - 1) {
LOG(5, "%u.%u.%u.%u - bad cookie: ackno=0x%08x expected=0x%08x\n",
(ip_them>>24)&0xff, (ip_them>>16)&0xff,
(ip_them>>8)&0xff, (ip_them>>0)&0xff,
seqno_me-1, cookie);
continue;
}
/* verify: ignore duplicates 校验是否重复*/
if (dedup_is_duplicate(dedup, ip_them, port_them, ip_me, port_me))
continue;
/* keep statistics on number received 统计接收的数字*/
if (TCP_IS_SYNACK(px, parsed.transport_offset))
(*status_synack_count)++;
/*
* This is where we do the output
* 这是输出状态了
*/
output_report_status(
out,
global_now,
status,
ip_them,
6, /* ip proto = tcp */
port_them,
px[parsed.transport_offset + 13], /* tcp flags */
parsed.ip_ttl