假期计划-masscan改造计划(一)

2022-02-28 5,595

本篇文章介绍扫描端口,分为masscan原理分析以及重写改造两大部。下篇文章介绍生产者消费者以及锁,无锁环形队列如何移植到python的实现。下下篇介绍如何移植用户态tcp到我们的扫描程序,加快扫描指纹。

masscan用作端口扫描,优点在于扫描速度快。但是缺点也很明显,那就是扫描不准确,经常出现漏报误报的问题。恰好自己也需要大批量端口扫描,于是放假期间研究了一下masscan。

原理分析

为了方便分析,代码我将使用python代码代替。学过计算机网络的人都知道,要想上网,必须要经过网关转发数据。如下图

对于主机来讲,怎么确定报文是否需要通过网关转发?那就需要用到掩码与网络地址。将目标地址与掩码做and运算,计算出目标地址的网络地址。如果目标地址的网络地址不是本网络地址的话,那么将报文交由网关转发,也就是该报文的数据链路层帧的目标mac地址是路由器的mac地址。这个就是扫描端口最基础的知识。对于masscan来讲,他绕过了系统tcp/ip的所有组件,直接在网卡上收发报文。也就是说,masscan自己写了小小的tcp/ip协议。ip地址与mac地址在tcp/ip中用来标识本机。既然已经绕过系统的tcp/ip,那么ip地址与mac地址就可以自己指定,并不需要系统的ip地址。如果我们的小TCP/IP协议与系统tcp/ip使用相同的ip地址的话,在收发某些报文的时候很有可能与系统冲突。

所以我们看到在masscan在输入参数的时候,需要输入ip地址,当然,如果不使用这个参数的话,那么使用系统的ip地址。既然masscan是用户态ip,那么还需要网关地址。网关的IP地址用来通过arp请求,获取到网关的mac地址。由以下几个函数获取,这里不再详细赘述

高速包捕获技术

我们需要开始接管网卡,绕过网卡直接收发数据。当然linux中提供rawsocket的方式可以自由组装报文。Linux网络协议栈是处理网络数据包的典型系统,它包含了从物理层直到应用层的全过程。

但是相对于pcap,pfring相比,效率还是很低。pcap,pfring提供直接在网卡中收发数据。并且使用例如DMA,零拷贝技术。将网卡收到的数据高效快捷地发送给用户态程序,也就是masscan。几种技术的简单对比,当然对于我们来讲pcap是最简单的方案。我们的程序将运行在vps中,pcap兼容性好,在linux中pcap默认安装。对于我们扫描端口来讲,pcap就可以很好地完成任务。

在c语言中,调用pcap初始化的时候,根据网卡名字打开网卡设备。masscan中代码注释如下

打开成功后将会返回pcap的指针,稍后我们通过该网卡发送,接收报文的时候,需要使用该指针(你可以把他想象成面向对象的对象的this指针) 这块没有什么难度,不会的看看文档。在这里也不需要例如dpdk等技术,所以就加快了我们的开发速度。

在这里我们还需要了解链路类型到底是什么,因为不光有以太网,还有VPN等等不同种类的链路类型。不同种类的链路类型将决定是否使用arp协议去获取mac地址以及如何组装二层的数据链路层帧

存储待扫描ip

我们在小内存vps上使用nmap扫描超大网段,例如A,B网段的时候,可能会发现nmap直接闪退。在我们不考虑广播地址,网络地址的情况下,一个A类网段有16777214个ip地址。在内存中我们使用无符号型32位int类型存储,在不考虑存储其他数据结构的情况下,需要16777214*4 = 67108856个字节,约63M内存去存储待扫描目标。如果我们同时扫描多个A段,那么小内存VPS可能无法承受。

同时我们知道,如果按照顺序扫描网段,从第一个ip地址一直扫描到最后一个ip。很容易触发对方防火墙的规则策略。最好的办法是将待扫描网段随机化处理。这样可以很好地规避该问题。

大家可以把这个问题抽象为洗牌算法,即保证手里的牌是随机的,同时也要保证不能重复。当然我们扫描端口,并不需要解决随机洗牌算法的每个元素等概率随机的难题。但是难点在于,首先要存储所有目标IP才能使用随机洗牌,并且最好的唐纳德洗牌算法的时间复杂度为O(n),从时间复杂度与空间复杂度的角度来讲并不是很好的选择。

masscan使用了非常巧妙的设计,从python生成器的角度实现随机从某个区间不重复地取出元素的方法,根据介绍,称为BlackRock算法。但是这个有个比较大的难题,漏扫与多扫的情况十分多。

我们将这段代码抠出来,运行一下便知。极端情况下,扫描一个c段,可能漏扫现象也十分严重。python版blackhold算法如下




image.png

简易用户态TCP/IP

在这里我们暂时不考虑vlan的情况,因为一台主机的接口为trunk的情况实在是太少了。即使接口为trunk模式,如果我们的报文不设置vlan的话,那么交换机将会使用native vlan转发。

arp协议

现在目标,网卡都搞定了,我们该扫描端口了。首先我们要通过arp协议,解析到网关的mac地址。根据arp协议的说明,我们发送广播报就行,该包被称为arp请求包。ARP 报文格式如图所示。ARP 报文总长度为 28 字节,MAC 地址长度为 6 字节,IP 地址长度为 4 字节。

其中,每个字段的含义如下。

  • 硬件类型:指明了发送方想知道的硬件接口类型,以太网的值为 1。
  • 协议类型:表示要映射的协议地址类型。它的值为 0x0800,表示 IP 地址。
  • 硬件地址长度和协议长度:分别指出硬件地址和协议的长度,以字节为单位。对于以太网上 IP 地址的ARP请求或应答来说,它们的值分别为 6 和 4。
  • 操作类型:用来表示这个报文的类型,ARP 请求为 1,ARP 响应为 2,RARP 请求为 3,RARP 响应为 4。
  • 发送方 MAC 地址:发送方设备的硬件地址。
  • 发送方 IP 地址:发送方设备的 IP 地址。
  • 目标 MAC 地址:接收方设备的硬件地址。
  • 目标 IP 地址:接收方设备的IP地址。

ARP 数据包分为请求包和响应包,对应报文中的某些字段值也有所不同。

  • ARP 请求包报文的操作类型(op)字段的值为 request(1),目标 MAC 地址字段的值为 Target 00:00:00_00:00:00(00:00:00:00:00:00)(广播地址)。
  • ARP 响应包报文中操作类型(op)字段的值为 reply(2),目标 MAC 地址字段的值为目标主机的硬件地址。

�相关的代码如图,其实就是按byte组装串,交给网卡发送就行。相关代码在stack_arp_resolve

既然我们是用户态TCP/IP,那么对于我们的arp请求同样要回复。如果我们不回复,那么对方不知道我们的mac地址,很有可能导致报文无法正常传递。

相关代码在stack_arp_incoming_request

理论上讲我们可以设置任何mac地址用来发包,但是因为我们要处理arp响应,所以尽量使用真实mac地址。在某些特殊的环境中,比如wifi环境下,假如你乱修改mac地址,那么路由器根本就不给你这个信道发送报文!

在masscan中,默认会把所有的数据包都交给网关转发。假如需要扫描本地网段的话是不需要将报文交给网关转发的。所以,路由器在这个时候会发送tcmp重定向报文。但是masscan并不会响应该报文。

所以,masscan是无法扫描同网段IP和本机!

IP报文

我们还需要构造IP报头,才可以将数据包正常转发。IP报头的格式如下具体代码在_template_init

TCP扫描

这块是扫描的重中之重。扫描端口,确切的来讲是与待开放端口成功地建立tcp连接。我们先回顾tcp建立连接的三次握手

发送

我们只需要发送一个tcp syn包,假如对方的端口的确开放,那么他会回复tcp syn+ack报文。假如对方端口未开放,那么会回复tcp rst报文。这就叫tcp半开放扫描。

传统扫描技术,需要我们在发送完tcp syn包的同时,等待对方返回相应的包。而发送功能呢,同样需要等待对方响应。这也就是说我们为什么需要多线程和协程。

但是在pcap中,调用网卡发送完数据后,程序立即响应,并不会阻塞在等待中。接收数据包需要我们自己处理。




image.png

接收

调用pcap_next_ex即可收报。在masscan中使用pcap_next收报。但是该函数存在很多问题,例如收报不及时等。官方推荐**pcap_next_ex函数。**这块我们顺手改成这个函数即可。注意,开启抓包模式的情况下,可能也会捕获系统tcp/ip的报文。

既然我们一个线程发送,一个线程接收。如何从响应包中确定开放端口呢?回到tcp syn中,发送一个seq为x的syn包。对方响应ack+syn,ack序号为syn包的x+1。那么我们将目标ip,目标端口,我们的ip,我们的端口做一个简单的hash,结果设为syn包的seq。这样我们只需要对接收到的报文同样做hash运算,即可确定是我们发送的扫描包的响应。如图为了防止重复多个报文,我们使用bloom过滤器。相对于hashmap,bloom过滤器可以很好的应对小内存与大数据量的去冲。

注意,在解析ip报文的时候一定要考虑options选项!




image.png

控制发包速度

现在万事俱备直接发包就行。但是我们要控制发送速度,太快或者太慢都不好。在这里我们移植masscan的代码,名为Throttler,也就是化油器。

在ip层是尽力而为,也就是说我们发包速度太快,路由器很有可能并不会转发,直接丢弃。这也就是我们为什么需要控制发包速度的原因。

在最之前有icmp 源抑制报文,源站抑制报文旨在请求发送方降低发往路由器或主机的报文发送速率。在接收的过程中,当接收方没有足够的接收缓冲区来处理接收到的报文,或者接收这个报文会导致临近其本身的缓冲区限制时,就会触发源站抑制报文。数据被从一个或一群主机高速地发往网络上的一个路由器,虽然路由器有缓冲机制,但是路由器的缓冲区大小通常(由于物理内存有限的原因)被限制。因此,如果路由器的通信量过大,路由器最终会(由于内存耗尽,导致必须丢弃掉接收到的数据报)无法继续处理超过输入缓冲区限制的部分数据,直到路由器缓冲队列有空余空间可以存放新的数据报。但是由于网络层(Network Layer)缺乏确认消息(ACK)机制,因此客户端无法获知数据是否成功抵达接收方。所以研究者提出了源站抑制这一补救措施来解决这一问题:当路由器发现流入数据速率远远高于流出数据速率时,会发送ICMP源站抑制报文给源站,通知源站应该降低其数据传输速度或等待一定时间后再尝试发送更多数据。当源站接收到ICMP源站抑制报文时会减慢数据发送的速度,或者在再次尝试发送数据前等待一定的时间,使得路由器能够(在处理完当前接收到的数据之后)清空输入缓冲队列。但是因为有研究表明“源站抑制是一种无效的(不公平的)补救措施“,所以路由的源站抑制报文已在1995年被RFC 1812弃用。此外,(路由)转发和回应任何形式的源站抑制报文已在2012年被RFC 6633 弃用。

核心思想是使用令牌桶的原理,学过QOS流量的网工大佬都知道令牌桶限速算法。每秒钟给多少个令牌,然后发送即可

重写

为什么选择重写?降低开发难度,解决bug,添加指纹识别功能。masscan的基础功能在很多种场景不适合我们。我们团队中懂c开发的同学非常少,所以我们只能选择使用python重写。万幸的是masscan的代码质量很高,重写起来效率非常高。使用python编写例如目标输入,结果输出,等非核心代码。pcap发包函数等,直接将masscan的代码移植到python中即可。

c与python传递指针

在masscan中存在大量的指针操作。我们如何将c指针传递到python,在c语言中获取python传递的指针。在python中使用Capsule对象即可包装一个指针成为c对象,用法十分简单,示例代码

/* Destructor function for points */
static void del_Point(PyObject *obj) {
  free(PyCapsule_GetPointer(obj,"Point"));
}
 
static PyObject *PyPoint_FromPoint(Point *p, int must_free) {
  /* 胶囊和C指针类似。在内部,它们获取一个通用指针和一个名称,可以使用
  PyCapsule_New() 函数很容易的被创建。 另外,一个可选的析构函数能被
绑定到胶囊上,用来在胶囊对象被垃圾回收时释放底层的内存*/

  return PyCapsule_New(p, "Point", must_free ? del_Point : NULL);
}

/* Utility functions */
static Point *PyPoint_AsPoint(PyObject *obj) {
  return (Point *) PyCapsule_GetPointer(obj, "Point");
}


计数引用

由于我们的代码在c中,例如生成的对象引用等,python虚拟机根本就管不到。很有可能造成内存溢出的难题。所以我们一定要对计数引用了如指掌。

什么时候不需要调用INCREF

1.对于函数中的局部变量,这些局部变量如果是PyObject对象的指针,没有必要增加这些局部对象的引用计数。理论上,当有一个变量指向对象的时候,对象的引用计数会被+1,同时在变量离开作用域时,对象的引用计数会被-1,而这两个操作是相互抵消的,最终对象的引用数没有改变。使用引用计数真正的原因是防止对象在有变量指向它的时候被提前销毁。

什么时候需要调用INCREF

如果有任何的可能在某个对象上调用DECREF,那么就需要保证该对象不能处于unprotected状态。1) 如果一个引用处于unprotected,可能会引起微妙的bug。一个常见的情况是,从list中取出元素对象,继续操作它,但是不增加它的引用计数。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它)。导致 item 成为一个悬垂指针。2) 传递PyObject对象给函数,一般都是假设传递过来的对象的引用计数已经是protected,因此在函数内部不需要调用Py_INCREF。不过,如果想要参数存活到函数退出,可以调用Py_INCREF。

编译产物

在这里使用python build就可以完成。但是我是用cmake编译,脚本如下

include_directories(/opt/homebrew/opt/python@3.9/Frameworks/Python.framework/Headers)

add_link_options( -undefined dynamic_lookup  -Wl,-headerpad,0x1000)
message(${PROJECT_NAME})

add_library(pyh SHARED  ${superscan})
set_target_properties(pyh PROPERTIES SUFFIX "so")
set_target_properties(pyh PROPERTIES PREFIX "")
set_target_properties(pyh PROPERTIES OUTPUT_NAME "SuperScan_C.cpython-39-darwin.")

调用

我们需要写一份接口文件,方便类型推断。

存储扫描结果

我们可以预先生成扫描结果,将其存储在磁盘中,在读取的时候利用生成器的思想读取即可。

测试

代码截图 发送函数抓包函数

由于python GIL锁的问题,我们最终达到每秒发送50万 tcp syn包的成绩。网卡每秒发送80-100MB的流量。扫描千万级别IP的端口开放仅需半小时。

内存占用15MB左右。相对比使用命令行启动masscan,大大提升了性能。并且开发简单~


本文作者:宽字节安全

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/174006.html

Tags:
评论  (0)
快来写下你的想法吧!

宽字节安全

文章数:26 积分: 75

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号