多线程端口扫描器

Python 实现

Created on 2017-09-25 22:33

近两日,闲来无事,就写了些端口扫描器,重温TCP/IP协议栈的部分原理。

端口扫描器

所谓的端口扫描器,其实是用来检测目标服务器有哪些端口开放所使用的工具,一般是管理员 用来进行安全加固,检测是否有无意开放的端口;或者是恶意攻击的人员在进行攻击前的 准备工作。所以综述上下,端口扫描器是用来确定目标机器 (本地机器或者远程机器)的 特定服务的可用性

端口扫描原理

上面提到过,端口扫描器是用来确定目标机器的服务的可用性的;那么具体是怎么确定的呢? 如果还没有答案的话,可以换个角度来思考这个问题。假如你想确定邻居家的妹子是否 在家,你会怎么办?这不简单么,问一下不就清楚了么?对阿,对于服务器的端口也可以 适用这样的方法嘛。端口扫描的原理都是“问一下”,只是问的方法不一样而已,就好像你 是决定直接过去敲邻居门,还是打电话过去一样,殊途同归,方法是没有对错的之分,差异 只是方法的优劣。

TCP连接扫描

这是最简单的一种方法,一般被称为连接扫描,即利用 socket 对目标机器进行连接尝试, 如果能够成功建立三次握手连接,那就说明你用 socket 连接的端口是开放的;然后你就 可以断开连接,扫描下一个目标端口了 (如果不断开连接,这就是一种 DDOS攻击了). 只不过TCP连接扫描不是很常用,不仅是因为容易被发现,而且你的IP地址也可能会被目标地址 记录下来的 (对于攻击者来说,隐藏身份是很重要的)

代码解析:

	def scan(self, args):
		host, port = args
		try:
			# Create a TCP socket and try to connect
			# AF_INET for ipv4,AF_INET6 for ipv6
			sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			sock.connect((host, port))
			sock.close()
			return host, port, True
		except (socket.timeout, socket.error):
			return host, port, False

因为原理很简单,所以核心代码也是很简洁的,只是建立 socket 然后进行连接,如果 连接不上,就很大几率说明端口是关闭的 (并不是绝对的,例如socket超时的异常可能 就是因为网络异常,不一定是目标机器的缘故)

SYN扫描

再回顾一下TCP的三次握手:

TCP三次握手

TCP建立连接时,首先客户端和服务器处于close状态。然后客户端发送SYN同步位,此时 客户端处于SYN-SEND状态,服务器处于lISTEN状态,当服务器收到SYN以后,向客户端发 送同步位SYN和确认码ACK,然后服务器变为SYN-RCVD,客户端收到服务器发来的SYN和ACK 后,客户端的状态变成ESTABLISHED(已建立连接),客户端再向服务器发送ACK确认码,服 务器接收到以后也变成ESTABLISHED。然后服务器客户端开始数据传输,如图:

图来源于Google

SYN扫描原理

SYN+ACK

那么现在再回到SYN扫描上来.如果在发送第一次握手的 SYN flag 时,目标机器回 复了SYN+ACK, 这不就说明笔者发送的TCP包中的目标端口是开放的么!如果不开放, 服务器就不会期待第三次握手了,也不会给笔者发送 SYN+ACK 了;如图:

图来自 source

RST

如果第二次握手的时候,目标机器回复的不是 SYN+ACK, 而是 RST, 就说明TCP 包中的目标端口在目标机器上是关闭的;如图

图来自 source

Filtered

上面提及了目标端口的开放和关闭两种状态,那么,还有没有其他状态呢?什么,还有 其他状态? 如果就SYN扫描而言,就还有filtered 即被过滤之一说,如果还有加上其他扫描技术, 就还有其他状态了。回到SYN扫描,当返回的不是服务器想建立第二次握手的包,而是 ICMP的包就有可能被过滤,例如响应信息是ICMP错误信息类型3代码3(无法到达目标: 端口不可达)这里出现的端口不可达,可能就是被防火墙过滤了,如果是类型3代码13 (无法到达目标:通信被管理员禁止),那也是被过滤了。更多信息就要查询ICMP的官方文档

代码解释

	def scan(self, args):
		dst_ip, dst_port = args
		src_port = RandShort()
		answered, unanswered = sr(IP(dst=dst_ip) / TCP(sport=src_port,
														dport=dst_port, flags="S"),
						timeout=self.timeout, verbose=False)
		for packet in unanswered:
			return packet.dst, packet.dport, "Filtered"

		for (send, recv) in answered:
			if(recv.haslayer(TCP)):
				flags = recv.getlayer(TCP).sprintf("%flags%")
				if(flags == "SA"):
					# set RST to server in case of ddos attack
					send_rst = sr(IP(dst=dst_ip) / TCP(sport=src_port,
														dport=dst_port, flags="R"),
						timeout=self.timeout, verbose=True)
					return dst_ip, dst_port, "Open"
				elif (flags == "RA" or flags == "R"):
					return dst_ip, dst_port, "Closed"
			elif(recv.haslayer(ICMP)):
				icmp_type = recv.getlayer(ICMP).type
				icmp_code = recv.getlayer(ICMP).code
				if(icmp_type == ICMP_TYPE_DESTINATION_UNREACHABLE and icmp_code in ICMP_CODE):
					return dst_ip, dst_port, "Filtered"
			else:
				return dst_ip, dst_port, "CHECK"

核心代码很简单,就是发送建立连接的握手请求,然后根据不同的返回结果判断不同的状态。 如果端口确定是开放,那就发送 R flag给目标机器结束握手 (如果不结束握手的话, 那就是DDOS,这也是DDOS最常用的手段);因为这次不是使用操作系统原生的 socket, 而是自行构造发送 IP数据包,所以需要使用一个很强大的构造 操作各种数据包的工具 -- scapy (顺便说一下,如果在 Windows下安装 scapy,需要非常多的步骤,如果是Unix/Linux,只需 几行命令:) )

后话

简单的扫描器就已经完成了,加上多线程的功能提高性能。很想吐嘈一下,真的对 Python 的多线程恨铁不成钢,只好换成多进程;也给 Python2 Python3 API的改变折腾 得够呛,不禁让笔者怀念起Java:( 其实正如笔者开头所言的,你确定隔壁家妹子是否在 家的方法有很多,你扫描端口的方法也有很多:例如 XMAS scan(TCP圣诞树扫描),FIN scan, Null scan,ACK scan,Window scan, UDP scan等。当然你如果不想针对各种扫描都 写一个扫描器,你可以使用 nmap 这个地球最强大的扫描器, 没有之一. 在 Python也已经 有与nmap整合的强大的包 python-nmap

扫描器完整代码地址 PortScanner


参考