Python网络管理

网络

网络可以将多台主机进行连接,使得网络中的主机可以相互通信。在网络通信中,使用最广泛的通信协议是TCP/IP协议簇,因此,Python也提供了相应的应用程序接口(API), 使得工程师可以在Python程序中创建网络连接、进行网络通信。
计算机之间可以相互通信以后,就开始涉及网络安全问题。现如今网络情况复杂安全环境恶劣。

2017年5月12日起,全球范围内爆发基于Windows网络共享协议进行攻击传播的蠕虫恶意代码,这是不法分子通过改造之前泄露的NSA黑客武器库中“永恒之蓝”攻击程序发起的网络攻击事件。五个小时内,包括英国、俄罗斯、整个欧洲以及中国国内多个高校校内网、大型企业内网和政府机构专网中招,被勒索支付高额赎金才能解密恢复文件,对重要数据造成严重损失。
被袭击的设备被锁定,并索要300美元比特币赎金。要求尽快支付勒索赎金,否则将删除文件,甚至提出半年后如果还没支付的穷人可以参加免费解锁的活动。原来以为这只是个小范围的恶作剧式的勒索软件,没想到该勒索软件大面积爆发,许多高校学生中招,愈演愈烈。

Python是一门应用领域非常广泛的语言,除了在科学计算、大数据处理、自动化运维等领域广泛应用以外,在计算机网络领域中使用也非常广泛。这主要得益于Python语言的开发效率高、入门门槛低、功能强大等优点。工程师可以使用Python语言管理网络,计算机黑客可以使用Python语言或者Python语言编写的安全工具进行渗透测试、网络分析、安全防范等。
在这章中,我们将介绍Python在网络方面的应用,包括网络通信、网络管理和网络安全。我们首先介绍如何使用Python语言列出网络上所有活跃的主机;然后介绍一个 端口扫描工具;接着介绍如何使用IPy方便地进行IP地址管理;随后,介绍了一个DNS工具包;最后,我们介绍了一个非常强大的网络嗅探工具。

一、列出网络上所有活跃的主机

在这一小节中,我们将会学习如何在shell脚本中调用ping命令得到网络上活跃的主机列表,随后,我们使用Python语言改造这个程序,以此支持并发的判断。

1、使用ping命令判断主机是否活跃

ping命令是所有用户都应该了解的最基础的网络命令,ping命令可以探测主机到主机之间是否能够通信,如果不能ping到某台主机,则表明不能和这台主机进行通信。ping命令最常使用的场景是验证网络上两台主机的连通性以及找出网络上活跃的主机。
为了检査网络上两台主机之间的连通性,ping命令使用互联网控制协议(ICMP)中的 ECHO_REQUEST数据报,网络设备收到该数据报后会做出回应。ping命令可以通过网络设备的回复得知两台主机的连通性以及主机之间的网络延迟。
ping命令的使用非常简单,直接使用主机名、域名或IP地址作为参数调用ping命令即可。如下所示:

[root@bogon ~]# ping 192.168.1.10
PING 192.168.1.10 (192.168.1.10) 56(84) bytes of data.
64 bytes from 192.168.1.10: icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from 192.168.1.10: icmp_seq=2 ttl=64 time=0.034 ms
64 bytes from 192.168.1.10: icmp_seq=3 ttl=64 time=0.033 ms
64 bytes from 192.168.1.10: icmp_seq=4 ttl=64 time=0.039 ms

ping命令会连续发送包,并将结果打印到屏幕终端上。如果主机不可达,ping将会显示“Destination Host Unreachable”的错误信息。如下所示:

[root@bogon ~]# ping www.baidu.com
PING www.a.shifen.com (220.181.38.150) 56(84) bytes of data.
From bogon (192.168.1.10) icmp_seq=1 Destination Host Unreachable
From bogon (192.168.1.10) icmp_seq=2 Destination Host Unreachable
From bogon (192.168.1.10) icmp_seq=3 Destination Host Unreachable
From bogon (192.168.1.10) icmp_seq=4 Destination Host Unreachable

除了检查网络上两台主机之间的连通性外,ping命令还可以粗略地估计主机之间的网络延迟情况。在ping命令的输出结果中,time字段的取值表示网络上两台主机之间的往返时间,它是分组从源主机到B的主机一个来回的时间,单位是毫秒。我们可以通过这个时间粗略估计网络的速度以及监控网络状态。例如,有这样一个使用ping命令解决线上问题的案例。当时的情况是应用程序使用我们提供的数据库服务,在每个整点时都会出现应用程序建立数据库连接失败的情况。通过前期排査,可以确定的是应用的请求已成功发出,数据库的压力并不是特别大,数据库连接也没有满。因此,问题很有可能出在网络上面。为此,我们增加了一个ping延迟监控。通过监控发现,在每个整点时ping的网络延迟变大,甚至大到了不可接受的程度。有了这个线索以后,接着排查网络问题。通过分析定位,发现是因为宿主机上有定时任务,导致每个整点宿主机的cpu压力增加,从而引发了前面所说的建立数据库连接失败的错误。
默认情况下,ping命令会不停地发送ECHO_REQUEST数据报并等待回复,到按下Ctrl+C为止。我们可以用选项-c限制所发送的ECHO_REQUEST数据报数量。用法如下:

[root@bogon ~]# ping -c 2 192.168.1.10
PING 192.168.1.10 (192.168.1.10) 56(84) bytes of data.
64 bytes from 192.168.1.10: icmp_seq=1 ttl=64 time=0.030 ms
64 bytes from 192.168.1.10: icmp_seq=2 ttl=64 time=0.047 ms

--- 192.168.1.10 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.030/0.038/0.047/0.010 ms

在这个例子中,ping命令发送了 2个ECHO_REQUEST数据报就停止发送,这个功能对于脚本中检查网络的连通性非常有用。
ping命令将结果打印到屏幕终端,我们通过ping命令的输出结果判断主机是否可达, 这种方式虽然直观,但是不便于程序进行处理。在程序中可以通过ping命令的返回码判断主机足否可达,当主机活跃时,ping命令的返回码为0,当主机不可达时,ping命令的返回码非0。
有了上面的基础以后,要判断网络上活跃的主机就非常容易了。我们只需要ping每一台主机,然后通过ping命令的返回值判断主机是否活跃。下面这段Shell程序就是用来判断网络中的主机是否可达:

for ip in 'cat ips.txt'
do
	if ping $ip -c 2 & > /dev/null
	then
		echo "$ip is alive"
	else
		echo "$ip is unreachable"
	fi
done

在这个例子中,我们首先将主机地址以每行一个地址的形式保存到ips.txt文件中,然后通过cat命令读取ips.txt文件中的内容,使用for循环迭代ips.txt中保存的主机。
为了减少视觉杂讯,使用输出重定向的方式将ping命令的结果输出到/dev/null中,以此避免信息在终端上打印。为了简化起见,我们直接在if语句中调用ping命令,Shell脚本能够根据ping命令的返回码判断命令执行成功还是失败。

2、使用Python判断主机是否活跃

前面的Shell脚本中,虽然所有的IP地址都是彼此独立,但是,我们的程序依然是顺序调用ping命令进行主机探活。由于每执行一次ping命令都要经历一段时间延迟(或者接收回应,或者等待回应超时),所以,当我们要检査大量主机是否处于活跃状态时需要很长的时间。对于这种情况可以考虑并发地判断主机是否活跃。
Shell脚本可以非常快速地解决简单的任务,但是,对于比较复杂的任务,Shell脚本就无能为力。如这里的并发判断主机是否活跃的需求。对于这种情况,可以使用Python语言编写并发的程序,以此加快程序的执行。如下所示:

#/usr/bin/python
#_*_ coding:utf-8 _*_

from __future__ import print_function
import subprocess
import threading


def is_reacheable(ip):
    if (subprocess.call(['ping', '-c', '1', ip])):
        print('{0} is alive'.format(ip))
    else:
        print('{0} is unreacheable'.format(ip))


def main():
    with open('ips.txt') as f:
        lines = f.readlines();
        threads = []
        for line in lines:
            thr = threading.Thread(target=is_reacheable, args=(line,))
            thr.start()
            threads.append(thr)

        for thr in threads:
            thr.join()

if __name__ == "__main__":
    main()

在这个例子中,我们首先打开ips.txt文件,并通过File对象的readlines函数将所有IP 地址读入内存。读入内存以后,IP地址保存在一个列表中,列表的每一项正好是一个地址。 对于每一个IP地址都创建一个线程。由于线程之间不共享任何数据,因此,不需要进行并发控制,这也使得整个程序变得简单。

在Python 中要判断两台主机是否可达有两种不同的方法,一种是在Python程序中调用ping命令实 现,另一种是使用socket编程发送ICMP数据报。为了简单起见,在我们这里的例子中使用前一种方法。为了调用系统中的ping命令,我们使用了subprocess模块。
我们的Python程序最终也是调用ping命令判断主机是否活跃,从思路上来说,和前面的Shell脚本是一样的。区别就在于Python程序使用并发加快了程序的执行效率。在我的测试环境中,测试了36个IP地址,其中,有12个IP地址不可达,需要等待网络超时才能返回,另外24个IP地址可达。使用Linux自带的time命令进行粗略计时,Shell脚本的执行时间是1分48秒,Python程序的执行时间是10秒。两个程序执行时间的差异,根据网络规模和网络环境将会显著不同。这里要表达的是,使用Python语言只需要很少的代码就能够将一个程序改造成并发的程序,通过并发来大幅提升程序的效率。

3、使用生产者消费者模型减少线程的数量

在前面的例子中,我们为每一个IP地址创建一个线程,这在IP地址较少的时候还算可行,但在IP地址较多时就会暴露出各种问题(如频繁的上下文切换)。因此,我们需要限制线程的数量。

列出网络上所有活跃主机的问题,其实是一个简单的生产者和消费者的问题。生产者 和消费者问题是多线程并发中一个非常经典的问题,该问题描述如下:

有一个或多个生产者在生产商品,这些商品将提供给若干个消费者去消费。为了使生产者和消费者能并发执行,在两者之间设置一个缓冲期,生产者将它生产的商品放入缓冲中,消费者可以从缓冲区中取走商品进行消费。生产者只需要关心这个缓冲区是否已满, 如果未满则向缓冲区中放入商品,如果已满,则需要等待。同理,消费者只需要关心緩冲区中是否存在商品,如果存在商品则进行消费,如果缓冲区为空,则需要等待。

生产者和消费者模型的好处是,生产者不需要关心有多少消费者、消费者何时消费、 以怎样的速度进行消费。消费者也不需要关心生产者,这就实现了程序模块的解耦。
我们这里的问题比生产者和消费者模型还要简单,只需要一次性将所有的IP地址读入到内存中,然后交由多个线程去处理。也就是说,我们一开始就生产好了所有的商品,只需要消费者消费完这些商品即可。如下所示:

#/usr/bin/python
#_*_ coding:utf-8 _*_

from __future__ import print_function
import subprocess
import threading

from sqlalchemy.util.queue import Empty, Queue


def call_ping(ip):
    if (subprocess.call(['ping', '-c', '1', ip])):
        print('{0} is alive'.format(ip))
    else:
        print('{0} is unreacheable'.format(ip))


def is_reacheable(q):
    try:
        while True:
            ip = q.get_nowait()
            call_ping(ip)
    except Empty:
        pass


def main():
    q = Queue()
    with open('ips.txt') as f:
        for line in f:
            q.put(line)

        threads = []

    for i in range(10):
        thr = threading.Thread(target=is_reacheable, args=(q,))
        thr.start()
        threads.append(thr)

    for thr in threads:
        thr.join()


if __name__ == "__main__":
    main()

在这个例子中创建了10个线程作为消费者线程,我们可以修改range函数的参数控制线程的个数。此外,我们还用到了一个新的数据结构,即Queue。引入Queue足因为多个消费者之间存在并发访问的问题,即多个消费者可能同时从缓冲区中获取商品。为了解决并发问题,我们使用了 Python标准库的Queue。Queue是标准库中线程安全的队列(FIFO) 实现,提供了一个适用于多线程编程的先进先出的数据结构,非常适合用于生产者和消费者线程之间的数据传递。
在这段程序中,我们首先将所有1P地址读入内存并放入Queue中,消费者不断从 Queue中获取商品。需要注意的是,如果我们使用Queue的get方法,当Queue中没有商品时,线程将会阻塞等待直到有新的商品为止。而在这个例子中不需要消费者阻塞等待, 因此,使用了Queue的get_nowait方法。该方法在冇商品时直接返回商品,没有商品时抛出Empty异常。消费者线程不断从Queue中获取IP地址,获取到IP地址以后调用call_ ping函数判断主机是否可达,直到没有商品以后退出线程。

二、端口扫描

仅仅知道网络上的主机是否可达还不够,很多情况下,我们需要的是一个端口扫描器。使用端口扫描器吋以进行安全检测与攻击防范。例如,在2017年5月12日,全球范围内爆发了基于Windows网络共享协议的永恒之蓝(Wannacry)勒索蠕虫。仅仅五个小时,包 括美国、中国、俄罗斯以及整个欧洲在内的100多个国家都不问程度地遭受永恒之蓝病毒攻击,尤其是高校、大型企业内网和政府机构专网,被攻击的电脑被勒索支付高额赎金才能解密恢复文件,对重要数据造成严重损失。永恒之蓝利用Windows系统的445端口进行蠕虫攻击,部分运营商已经在主干网络上封禁了 445端口,但是教育网以及大量企业内网并没有此限制,从而导致了永恒之蓝勒索蠕虫的泛滥。

所以作为工程师,一方面需要在日常维护养成良好的习惯,如配置防火墙、进行网络隔离、关闭不必要的服务、及时更新补丁;另一方面可以掌握一些安全相关的工具,在日常中进行安全防范,在紧急悄况下进行安全检测。在这一小节,我们将介绍如何使用Python进行端口扫描。有了端口扫描器,我们可以快速了解主机打开了哪些不必要的端口,以便及时消灭安全隐患。
在这一小节中,我们将使用Python语言编写一个端口扫描器,然后介绍大名鼎鼎的端 口扫描工具nmap,最后,通过python-nmap在Python代码中调用nmap进行端口扫描。

1、使用Python编写端口扫描工具

在Linux下,可以使用ping命令要判断一台主机是否可达,而判断一个端口是否打开可以使用telnet命令。我们可以模仿前面小节中并行ping的例子,在Python代码中调用 telnet命令判断一个端口是否打开。但是telnet命令存在一个问题,当我们telnet—个不可达的端口时,telnet需要很久才能够超时返回,并且telnet命令没有参数控制超时时间。 此外,如果Python标准库中有相应的模块,应该尽可能地使用Python的标准库,而不是在 Python代码中执行Linux命令。这一方面能够增加代码的可读性、可维护性,另一方面也能够保证程序跨平台运行。
为了使用Python编写端口扫描器,我们需要简单了解socket模块。socket模块为操作系统的socket连接提供了一个Python接口。有了 socket模块,我们可以完成任何使用 socket的任务。
socket模块提供了一个工厂函数socket,socket函数会返冋一个socket对象。我们可以给socket函数传递参数,以此创建不同网络协议和网络类塑的socket对象。默认情况下,socket函数会返回一个使用TCP协议的socket对象。如下所示:

In [1]: import socket

In [2]: s = socket.socket()

In [3]: s.connect(('47.100.98.242',80))

In [4]: s.send("GET/HTTP/1.0".encode())
Out[4]: 12

In [5]: print(s.recv(200))
b'HTTP/1.1 400 Bad Request\r\nServer: nginx\r\nDate: Sat, 29 Feb 2020 15:44:51
 GMT\r\nContent-Type: text/html\r\nContent-Length: 150\r\nConnection: close\r\
n\r\n<html>\r\n<head><title>400 Bad Request</title></head>\r\n<b'

In [6]: s.close()

在这个例子中,socket工厂函数以默认参数AF_INET和SOCK_STREAM创建了一个 名为s的socket对象,该对象可以在进程间进行TCP通信。创建完对象以后,我们使用connect函数连接到远程服务器的80端口,并发送一个HTTP请求到远程服务器,发送完 毕之后,接收服务器响应的前200个宇节。最后,调用socket对象的close方法关闭连接。
在这个例子中,我们用到了 socket工厂函数、socket的connect方法、send方法、recv 方法和close方法,这也是socket中最常使用的一些方法。

接下来,我们就看一下如何使用简单的socket接口编写一个端口扫描器。如下所示:

#/usr/bin/python
#_*_ coding:utf-8 _*_

from __future__ import print_function
from socket import *


def conn_scan(host, port):
    conn = socket(AF_INET, SOCK_STREAM)
    try:
        conn.connect((host, port))
        print(host, port, 'is avaliable')
    except Exception as e:
        print(host, port, 'is not avaliable')
    finally:
        conn.close()


def main():
    host = '47.100.98.242'
    for port in range(80, 5000):
        conn_scan(host, port)


if __name__ == '__main__':
    main()

在这个端口扫描的例子中,conn_scan用来判断端口是否可用。该函数尝试建立与目标主机和端口的连接,如果成功,打印一个端口开放的消息,否则,打印一个端口关闭的消息。
除广使用socket套接字编程的方式判断端口是否可用以外,还可以使用Python标准库的telnet模块。该模块中包含了一个Telnet类,该类的对象表示一个telnet的连接。创建一 个Telnet对象并不会建立到远程主机的连接,需要显式地使用open方法建立连接。open方法接受三个参数,分别是主机名、端口号和超时时间。如下所示:

#/usr/bin/python
#_*_ coding:utf-8 _*_


from __future__ import print_function
from socket import *
import telnetlib

def conn_scan(host, port):
    t = telnetlib.Telnet()
    try:
        t.open(host,port,timeout=1)
        print(host, port, 'is avaliable')
    except Exception as e:
        print(host, port, 'is not avaliable')
    finally:
        t.close()


def main():
    host = '47.100.98.242'
    for port in range(20, 5000):
        conn_scan(host, port)


if __name__ == '__main__':
    main()

对于上面这段程序,我们可以参考多线程的ping程序,以及使用生产者和消费者模型的ping程序,将这段程序扩展成多主机和多线程的端口扫描器。
与ping程序不同的是,端U扫描需要用到两个参数,即主机地址和端口号。当我们有了主机的列表和端口号的列表以后,如何能够快速地得到所有主机与端口号的组合呢?对于这个问题,有多种不同的方法。其中比较方便的是使用列表推导。如下所示:

In [1]: l1 = ('a','b','c')

In [2]: l2 = (22,80)

In [3]: list([(x,y) for x in l1 for y in l2])
Out[3]: [('a', 22), ('a', 80), ('b', 22), ('b', 80), ('c', 22), ('c', 80)]

使用列表推导虽然比较方便,但是,这个列表推导表达式本身比较复杂。因此,我们可以考虑使用itertools模块中的product函数。Python标准库的itertools模块提供了一组非常常用的函数,读者很有必要了解hertooU模块中提供的函数。在itertools模块中有一个名为product的函数,该函数用来返回多个可迭代对象的笛卡尔积。注意,product比前面的列表推导表达式更加通用,它可以返回多个可迭代对象的笛长尔积。这里的例子只需要计算两个可迭代对象的笛卡尔积。如下所示:

In [4]: from itertools import product

In [5]: list(product(l1,l2))
Out[5]: [('a', 22), ('a', 80), ('b', 22), ('b', 80), ('c', 22), ('c', 80)]

有了主机和端口的组合以后,我们可以参照生产者和消费者模型的例子,开发一个多线程的端口扫描器。但是我们并没有必要这么做,因为除了使用多线程编程编写端口扫描器以外,还可以使用Python-nmap模块更加方便地进行端口扫描。

2、使用nmap扫描端口

Python-nmap模块是对nmap命令的封装。nmap是知名的网络探测和安全扫描程序, 是Network Mapper的简称。nmap可以进行主机发现(Host Discovery)、端口扫描(Port Scanning)、版本侦测(Version Detection〉、操作系统侦测(Operating System Detection),nmap是网络管理员必用的软件之一。nmap因为功能强大、跨平台、开源、文档丰富等诸多优点,在安全领域使用非常广泛。
在使用之前,需要先安装nmap。如下所示:

[root@bogon ~]# yum install nmap

nmap的使用非常灵活,功能又很强大,因此nmap有很多命令行选项。使用nmap时, 首先需要确定要对哪些主机进行扫描,然后确定怎么进行扫描(如使用何种技术,对哪些端 口进行扫描)。
nmap具有非常灵活的方式指定需要扫描的主机,我们可以使用nmap命令的-sL选项 来进行测试。-sL选项仅仅打印IP列表,不会进行任何操作。如下所示:

[root@bogon ~]# nmap -sL 47.100.98.242/80

Starting Nmap 6.40 ( http://nmap.org ) at 2020-03-01 00:18 CST
Illegal netmask in "47.100.98.242/80". Assuming /32 (one host)
Nmap scan report for 47.100.98.242
Nmap done: 1 IP address (0 hosts up) scanned in 0.04 seconds

nmap提供了非常灵活的方式来指定主机,包括同时指定多个IP、通过网段指定主机、通过通配符指定主机等。如下所示:

nmap -sL 47.100.98.242 14.215.177.39
nmap -sL 47.100.98.*
nmap -sL 47.100.98.242,243,245
nmap -sL 47.100.98.242-250
nmap -sL 47.100.98.* --exclude 47.100.98.242
nmap -sL 47.100.98.242/30

除了上面指定主机的方式,我们也可以将IP地址保存到文本中,通过-iL选项读取文件中的IP地址。如下所示:

nmap -iL ip.list
(1)主机发现

端口扫描是nmap的重点,除此之外,我们也可以使用nmap检查网络上所有在线的主机,实现类似前边小节中列出网络上所有活跃的主机的功能。使用-sP或-sn选项可以告诉nmap不要进行端口扫描,仅仅判断主机是否可达。如下所示:

[root@bogon ~]# nmap -sP 47.100.98.*
Starting Nmap 6.40 ( http://nmap.org ) at 2020-03-01 00:25 CST
Nmap done: 256 IP addresses (0 hosts up) scanned in 206.44 seconds
    
[root@bogon ~]# nmap -sn 47.100.98.*
Starting Nmap 6.40 ( http://nmap.org ) at 2020-03-01 00:35 CST
Nmap done: 256 IP addresses (0 hosts up) scanned in 205.38 seconds
(2)端口扫描

端口扫描是nmap最基本,也是最核心的功能,用于确定目标主机TCP/UDP端口的开放情况。不添加任何参数便是对主机进行端口扫描。默认情况下,nmap将会扫描1000个最常用的端口号。如下所示:

[root@bogon ~]# nmap 14.215.177.39
Starting Nmap 6.40 ( http://nmap.org ) at 2020-03-01 00:42 CST
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.09 seconds

在进行端口扫描时,nmap提供了大M的参数控制端口扫描。包括端口扫描协议、端口扫描类型、扫描的端口号。如下所示:

端口扫描协议:T (TCP)、U (UDP)、S (SCTP>、P (IP);

端口扫描类型:-sS/sT/sA/sW/sM: TCP SYN/Connect()/ACK/Window/Maimon scans;

扫描的端口号:-p 80,443 -p 80-160

nmap中的端口扫描协议、扫描类型和端口号相关的选项,可以结合起来使用。如下所示:

-p22; -p1-65535; -p U:53,111,137,T:21-25,80,139,8080,S:9

nmap通过探测将端口划分为6个状态,下表给出了每个状态的含义。

端口状态 状态含义
open 端口是开放的
closed 端口是关闭的
filtered 端口被防火墙IDS/IPS屏蔽,无法确认其状态
unfiltered 端口没有被屏蔽,但是否开放需要进一步确定
open|filtered 端口是开放的或被屏蔽
closed|filtered 端口是关闭的或被屏蔽

在进行端口扫描时,可以使用不同的端口扫描类型。常见的端口扫描类型如下:

TCP SYNC SCAN:半开放扫描,这种类沏的扫描为发送一个SYN包,启动一个TCP会话,并等待响应的数据包。如果收到的是一个reset包,表明端口是关闭的; 如果收到的是一个SYNC/ACK包,则表示端口是打开的。
TCP NULL SCAN: NULL扫描把TCP头中的所有标志位都设置为NULL。如果收到的是一个RST包,则表示相应的端口是关闭的。
TCP FIN SCAN : TCP FIN扫描发送一个表示结束一个活跃的TCP连接的FIN包, 让对方关闭连接。如果收到了一个RST包,则表示相应的端口是关闭的。 TCPXMASSCAN: TCPXMAS扫描发送PSH、FIN、URG和TCP标志位被设置为1的数据包,如果收到一个RST包,则表示相砬端口是关闭的。
(3)版本侦测

nmap在进行端口扫描时,还可以进行版本侦测。版本监测功能用于确定开放端口上运行的应用程序及版本信息。如下所示:

nmap -sV 47.100.98.242
(4)操作系统监测

操作系统侦测用于监测主机运行的操作系统类型及设备类型等信息。nmap拥有丰富的系统数据库,可以识别2600多种操作系统与设备类型。如下所示:

nmap -sO 47.100.98.242

3、使用python-nmap进行端口扫描

我们在上一小节中,花f较多的篇幅介绍nmap。Python的Python-nmap仅仅趋对nmap的封装,因此,要使用Python-nmap,必须先了解nmap。Python-nmap相对于nmap, 主要的改进在于对输出结果的处理。Python-nmap将nmap的输出结果保存到宇典之中,我们只需要通过Python的字典就可以获取到nmap的输出信息,不用像Shell脚本一样通过字符串处理和正则表达式来解析nmap的结果。Python-nmap将nmap的强大功能与Python语言优秀的表达能力进行了完美的结合,使用Python语言丰富的数据结构保存结果,以便后续继续进行处理,如使用Python-nmap生成相关的报告。
Python-nmap是开源的库,因此,在使用之前需要手动进行安装。如下所示:

pip3 install python-nmap

Python-nmap的使用非常简单,我们只要创建一个PortScarmer对象,并调用对象的 scan方法就能够完成基本的nmap端口扫描。如下所示:

In [1]: import nmap                 

In [2]: nm = nmap.PortScanner()    

In [3]: nm.scan('192.168.79.129','22-1000')
Out[3]: 
{'nmap': {'command_line': 'nmap -oX - -p 22-1000 -sV 192.168.79.129',
  'scaninfo': {'tcp': {'method': 'syn', 'services': '22-1000'}},
  'scanstats': {'timestr': 'Mon Mar  2 16:31:17 2020',
   'elapsed': '6.33',
   'uphosts': '1',
   'downhosts': '0',
   'totalhosts': '1'}},
 'scan': {'192.168.79.129': {'hostnames': [{'name': '192.168.79.129',
     'type': 'PTR'}],
   'addresses': {'ipv4': '192.168.79.129'},
   'vendor': {},
   'status': {'state': 'up', 'reason': 'localhost-response'},
   'tcp': {22: {'state': 'open',
     'reason': 'syn-ack',
     'name': 'ssh',
     'product': 'OpenSSH',
     'version': '7.4',
     'extrainfo': 'protocol 2.0',
     'conf': '10',
     'cpe': 'cpe:/a:openbsd:openssh:7.4'},
    111: {'state': 'open',
     'reason': 'syn-ack',
     'name': 'rpcbind',
     'product': '',
     'version': '2-4',
     'extrainfo': 'RPC #100000',
     'conf': '10',
     'cpe': ''}}}}}            

当我们创建PortScanner对象时,Python-nmap会检査系统中是否已经安装了 nmap,如果没有安装,抛出PortScannerError异常。调用PortScanner对象的scan方法进行扫描以后就可以通过该类的其他方法获取本次扫描的信息。如命令行参数、主机列表、扫描的方法等。如下所示:

In [4]: nm.command_line()           
Out[4]: 'nmap -oX - -p 22-1000 -sV 192.168.79.129'

In [5]: nm.scaninfo()                
Out[5]: {'tcp': {'method': 'syn', 'services': '22-1000'}}

In [6]: nm.all_hosts()              
Out[6]: ['192.168.79.129']

Python-nmap还提供了以主机地址为键,获取单台主机的详细信息。包括获取主机网络状态、所有的协议、所有打开的端口号,端口号对应的服务等。如下所示:

In [7]: nm['192.168.79.129'].state()
Out[7]: 'up'

In [8]: nm['192.168.79.129'].all_protocols()
Out[8]: ['tcp']

In [9]: nm['192.168.79.129'].keys()  
Out[9]: dict_keys(['hostnames', 'addresses', 'vendor', 'status', 'tcp'])
    
In [10]: nm['192.168.79.129']['tcp'][22]
Out[10]: 
{'state': 'open',
 'reason': 'syn-ack',
 'name': 'ssh',
 'product': 'OpenSSH',
 'version': '7.4',
 'extrainfo': 'protocol 2.0',
 'conf': '10',
 'cpe': 'cpe:/a:openbsd:openssh:7.4'}

In [11]: nm['192.168.79.129']['tcp'][111]
Out[11]: 
{'state': 'open',
 'reason': 'syn-ack',
 'name': 'rpcbind',
 'product': '',
 'version': '2-4',
 'extrainfo': 'RPC #100000',
 'conf': '10',
 'cpe': ''}

Python-nmap是对nmap的Python封装,因此我们也可以通过Python-nmap指定nmap命令的复杂选项。如下所示:

nm.scan(hosts='192.168.79.129/24',arguments='-n -sP -PE -PA21,23,80,3389')

三、使用IPy进行IP管理

在网络设计中,首先要做的就是规划IP地址。IP地址规划的好坏直接影响路由算法的效率,包括网络性能和扩展性。在IP地址规划中,需要进行大量的IP地址计算,包括网段、网络掩码、广播地址、子网数、IP类型等计算操作。在大量的计算操作中,如果没有一个好的工具,计算IP地址是一个很无趣有容易出错的事情。在Perl语言中,可以使用NET::IP模块,在Python语言中,可以使用开源的IPy模块进行操作。

1、IPy模块介绍

IPy模块是一个处理IP地址的模块,它能够自动识别IP地址的版本、IP地址的类型。使用IPy模块,可以方便地进行IP地址的计算。

IPy模块是第三方的开源模块,因此,在使用之前需要进行安装。直接使用pip安装即可:

pip3 install ipy

2、IPy模块的基本使用

IPy模块有一个IP类,这个类几乎可以接受任何格式的IP地址和网段。如下所示:

In [1]: import IPy 
    
In [2]:from IPy import IP           

In [3]: IP(0x7f000001)               
Out[3]: IP('127.0.0.1')

In [4]: IP('127.0.0.1')               
Out[4]: IP('127.0.0.1')

In [5]: IP('127.0.0.0/30')             
Out[5]: IP('127.0.0.0/30')

In [6]: IP('1080:0:0:0:8:800:200C:417A')
Out[6]: IP('1080::8:800:200c:417a')

In [7]: IP('127.0.0.0-127.255.255.255')
Out[7]: IP('127.0.0.0/8')

IP类包含了许多的方法,用来进行灵活的IP地址操作。例如:

(1)version:获取IP地址的版本
In [9]: IP('127.0.0.0-127.255.255.255') 
Out[9]: IP('127.0.0.0/8')

In [10]: IP('10.0.0.0/8').version()  
Out[10]: 4

In [11]: IP('::1').version()        
Out[11]: 6
(2)len:得到子网IP地址的个数
In [12]: IP('127.0.0.0/30').len()    
Out[12]: 4
    
In [13]: IP('127.0.0.0/28').len()    
Out[13]: 16
(3)iptype:返回IP地址的类型
In [14]: IP('127.0.0.1').iptype()    
Out[14]: 'LOOPBACK'

In [15]: IP('8.8.8.8').iptype()      
Out[15]: 'PUBLIC'
(4)int:返回IP地址的整数形式
In [16]: IP('8.8.8.8').int()         
Out[16]: 134744072
(5)strHex:返回IP地址的十六进制形式
In [17]: IP('8.8.8.8').strHex()    
Out[17]: '0x8080808'
(6)strBin:返回IP地址的二进制形式
In [18]: IP('8.8.8.8').strBin()
Out[18]: '00001000000010000000100000001000'

有一个方便的函数能够将IP转换为不同的格式,在工作环境中将会非常有用。例如,以数宇的形式在数据库中存储IP地址,在数据库中存储IP地址有两种形式,第一种是以变长字符串的形式将IP地址保存到数据库中,另一种是将IP地址转换为整数以后保存到数据库中。将IP地址转换为整数进行存储能够有效地节省存储空间,提高数据库的存储效率和访问速度。因此,在最佳实践中,我们一般将IP地址以数字的形式保存到数据库中。需要 IP地址时,再将数字形式的IP地址转换为字符串格式的IP地址。这个需求十分常见,因 此,MySQL提供了两个函数,分别用以将字符串形式的IP地址转换为数据格式的IP地址,以及将数字格式的IP地址转换为字符串形式的IP地址。如下所示:

mysql> select INET_ATON('10.166.224.14');
+----------------------------+
| INET_ATON('10.166.224.14') |
+----------------------------+
|                  178708494 |
+----------------------------+
1 row in set (0.00 sec)

mysql> select INET_NTOA('178708494');
+------------------------+
| INET_NTOA('178708494') |
+------------------------+
| 10.166.224.14          |
+------------------------+
1 row in set (0.00 sec)

除了使用MySQL自带的函数以外,我们也可以使用IP类提供的int方法将字符串形式的IP地址转换为数字形式的IP地址。要将数字形式的IP地址转换会字符串形式的IP地址,可以直接使用数字的方式创建IP对象。如下所示:

In [9]: IP('178708494')             
Out[9]: IP('10.166.224.14')

In [10]: '{0}'.format(IP("178708494"))
Out[11]: '10.166.224.14'

3、网段管理

IP类的构造函数可以接受不同格式的IP地址,也可以接受网段。如下所示:

In [1]: from IPy import IP            

In [2]: IP('127.0.0.0/24')        
Out[2]: IP('127.0.0.0/24')

In [3]: IP('127.0.0.0-127.255.255.255')
Out[3]: IP('127.0.0.0/8')

In [4]: IP('127.0.0.0/127.255.255.255')
Out[4]: IP('127.0.0.0/31')

网段包含多个IP地址,我们可以直接使用len方法或者Python内置的len函数得到网段中IP地址的个数,也可以直接使用for循环迭代网段,以此遍历各个IP。如下所示:

In [1]: from IPy import IP            

In [2]: IP('127.0.0.0/24')            
Out[2]: IP('127.0.0.0/24')

In [3]: IP('127.0.0.0-127.255.255.255')
Out[3]: IP('127.0.0.0/8')

In [4]: IP('127.0.0.0/127.255.255.255')
Out[4]: IP('127.0.0.0/31')

In [5]: ips = IP('10.166.224.144/28')

In [6]: ips.len()                     
Out[6]: 16

In [7]: len(ips)                      
Out[7]: 16

In [8]: [ip for ip in ips]            
Out[8]: 
[IP('10.166.224.144'),
 IP('10.166.224.145'),
 IP('10.166.224.146'),
 IP('10.166.224.147'),
 IP('10.166.224.148'),
 IP('10.166.224.149'),
 IP('10.166.224.150'),
 IP('10.166.224.151'),
 IP('10.166.224.152'),
 IP('10.166.224.153'),
 IP('10.166.224.154'),
 IP('10.166.224.155'),
 IP('10.166.224.156'),
 IP('10.166.224.157'),
 IP('10.166.224.158'),
 IP('10.166.224.159')]

IP类有一个名为strNormal的方法,该方法接受一个wantprefixlen参数,参数的合法取值为0~3,每一个取值代表一种网段的显示方式。如下所示:

In [12]: ips.strNormal(0)          
Out[12]: '10.166.224.144'

In [13]: ips.strNormal(1)            
Out[13]: '10.166.224.144/28'

In [14]: ips.strNormal(2)            
Out[14]: '10.166.224.144/255.255.255.240'

In [15]: ips.strNormal(3)           
Out[15]: '10.166.224.144-10.166.224.159'

通过IP类,我们也可以方便地判断一个IP是否属于一个网段,判断子网是否包含于另一个网段中,以及两个网段是否有重叠。如下所示:

In [16]: '10.166.224.144' in IP('10.166.224.144/28')
Out[16]: True

In [17]: IP('10.166.224.144/29') in IP('10.166.224.144/28')
Out[17]: True

In [18]: IP('10.166.224.0/28').overlaps('10.166.224.144/28')
Out[18]: 0

对于网段,我们可以方便地获取网络地址掩码以及网络的广播地址。如下所示:

In [22]: ips.netmask()              
Out[22]: IP('255.255.255.240')

In [23]: ips.broadcast()            
Out[23]: IP('10.166.224.159')

四、使用dnspython解析DNS

1、dnspython简介与安装

dnspython是Python实现的一个DNS工具集,它支持几乎所有的记录类型,可以用于查询、传输并动态更新ZONE信息,同时支持TSIG(事务签名)验证消息和EDNS0(扩展DNS)。使用dnspython可以代替Linux命令行下的nslookup以及dig等工具。

dnspython是第三方的开源模块,因此,使用之前需要先进行安装:

pip3 install dnspython

2、使用dnspython进行域名解析

dnspython提供了丰富的API,其中,高层次的API根据名称和类型执行查询操作,低层次的API可以直接更新ZONE信息、消息、名称和记录。在所有的API中,最常使用的是域名查询。dnspython提供了一个DNS解析类resolver,使用它的query方法可以实现域名的查询功能。

dns.resolver.query(qname,rdtype=1,rdclass=1,tcp=False,source=None,raise_on_no_answer=True,source_port=0)

query方法各参数的含义如下:

qname:査询的域名;
rdtype:指定RR资源;

A:地址记录(Address),返回域名指向的IP地址;
NS:域名服务器记录(Name Server),返回保存下一级域名信息的服务器地址。该记录只能设罝为域名,不能设置为IP地址;
MX:邮件记录(Mail exchange),返回接收电子邮件的服务器地址;
CNAME:规范名称记录(Canonical Name),别名记录,实现域名间的映射;
PTR:逆向査询记录(Pointer Record),反向解析,与A记录相反,将IP地址转换为主机名。

rdclass:网络类型;
tcp:指定査询是否启用TCP协议;
source:査询源的地址;
source_port:査询源的端口 ;
raise_on_no_answer:指定査询无应答时是否触发异常,默认为True。

在使用dnspython查询DNS相关信息之前,我们先简单了解一下dig命令,以便对照查看Python程序的输出结果与dig命令的输出结果。
dig的全称是domain information groper,它是一个灵活探测DNS的工具,可以执行DNS査找,并显示从查询的名称服务器返回的答案。由于dig命令灵活易用、输出明确, 因此,大多数DNS管理员都使用dig解决DNS问题。
在我的主机上运行dig命令査找dnspython.org域名的信息。运行结果如下:

[root@192 ~]# dig qiniu.lexizhi.com

; <<>> DiG 9.9.4-RedHat-9.9.4-72.el7 <<>> qiniu.lexizhi.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35907
;; flags: qr rd ra; QUERY: 1, ANSWER: 12, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;qiniu.lexizhi.com.		IN	A

;; ANSWER SECTION:
qiniu.lexizhi.com.	5	IN	CNAME	www.lexizhi.com.qiniudns.com.
www.lexizhi.com.qiniudns.com. 5	IN	CNAME	dt003.china.line.qiniudns.com.
dt003.china.line.qiniudns.com. 5 IN	CNAME	tinychinacdnweb.qiniu.com.w.kunlunno.com.
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.231
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.234
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.232
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.233
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 219.147.157.66
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.228
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.235
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.229
tinychinacdnweb.qiniu.com.w.kunlunno.com. 5 IN A 150.138.180.230

;; Query time: 633 msec
;; SERVER: 192.168.79.2#53(192.168.79.2)
;; WHEN:302 17:29:51 CST 2020
;; MSG SIZE  rcvd: 300

在Python代码中,可以使用dnspython查询A记录。如下所示:

from __future__ import print_function
import dns.resolver

data = dns.resolver.query('www.lexizhi.com', 'A')
for item in data:
    print(item)

Python程序的输出结果如下:

47.100.98.242

使用dnspython实现NS记录,查询方法如下:

from __future__ import print_function
import dns.resolver

data = dns.resolver.query('dnspython.org', 'NS')
for item in data:
    print(item)

Python程序查询NS记录的结果如下:

ns-343.awsdns-42.com.
ns-518.awsdns-00.net.
ns-1253.awsdns-28.org.
ns-2020.awsdns-60.co.uk.

从输出结果来看,使用dig命令或dnspython模块都是一样的。如果在命令行操作,建议使用dig命令。如果要使用程序管理DNS或查询DNS的内容,则推荐使用dnspython模块。

五、网络嗅探器Scapy

Scapy是一个Python语言编写的工具,使用Scapy可以发送、嗅探、剖析和伪造网络数据报。Scapy涉及比较底层的网络协议,因此,不可避免地导致Scapy的接口复杂。虽然 Scapy的接口复杂,但整体思路却非常简单,就是发送数据报和接收数据报。在发送数据报时,Scapy提供了相关的辅助类来帮助我们构造数据报,在接收数据报时,Scapy也提供了相应的函数来帮助我们过滤和解析数据报。

在这一小节中,首先我们将介绍Scapy的功能和安装方式,然后介绍Scapy的基本使用,接着介绍如何使用Scapy发送数据报,并通过 —个DNS査询的例子演示Scapy发送数据报,最后介绍如何使用Scapy进行网络嗅探,并通过一个抓取敏感信息的例子来演示Scapy的网络嗅探。

1、Scapy简介与安装

Scapy是一个强大的交互式数据报处理程序,它能够伪造或者解码大量的网络协议数据报,能够发送、捕捉、匹配请求和回复数据报。Scapy可以轻松处理大多数经典任务,如端口扫描、路由跟踪、探测、攻击或网络发现等。使用Scapy可以替代hping, arpspoof,arp-sk, arping,p0f等功能,甚至可以替代nmap, tcpdump和tshark的部分功能。此外,Scapy 还有很多其他工具没有的优秀特性,如发送无效数据帧、注入修改的802.11数据帧、在 WEP上解码加密通道(VOIP)、ARP缓存攻击(VLAN)等。
Scapy是使用Python语言开发的丁.具,因此,我们可以直接使用pip安装:

pip3 install scapy

Scapy运行时要对网络接口进行控制,所以需要root权限。在这一小节的例子中, 我们都使用root用户来运行Scapy程序或与Scapy相关的Python程序。
Scapy提供了非常丰富的功能,不同的功能依赖不同的软件。启动Scapy命令行工具时,Scapy会进行相应的检査并给出提示。如下所示:

[root@192 ~]# scapy
INFO: Can't import matplotlib. Won't be able to plot.
INFO: Can't import PyX. Won't be able to use psdump() or pdfdump().
WARNING: No route found for IPv6 destination :: (no default route?)
INFO: Can't import python-cryptography v1.7+. Disabled WEP decryption/encryption. (Dot11)
INFO: Can't import python-cryptography v1.7+. Disabled IPsec encryption/authentication.
                                      
                     aSPY//YASa       
             apyyyyCY//////////YCa       |
            sY//////YSpcs  scpCY//Pp     | Welcome to Scapy
 ayp ayyyyyyySCP//Pp           syY//C    | Version 2.4.3
 AYAsAYYYYYYYY///Ps              cY//S   |
         pCCCCY//p          cSSps y//Y   | https://github.com/secdev/scapy
         SPPPP///a          pP///AC//Y   |
              A//A            cyP////C   | Have fun!
              p///Ac            sC///a   |
              P////YCpc           A//A   | Craft packets before they craft
       scccccp///pSP///p          p//Y   | you.
      sY/////////y  caa           S//P   |                      -- Socrate
       cayCyayP//Ya              pY/Ya   |
        sY/PsY////YCc          aC//Yp 
         sc  sccaCY//PCypaapyCP//YSs  
                  spCPY//////YPSps    
                       ccaacs         
                                       using IPython 7.12.0
>>>           

例如,使用Scapy生成图形化的示意图需要安装matplotlib库,但没有安装matplotlib并不影响Scapy的基本使用。

2、Scapy的基本使用

在本教程中,我们会介绍Scapy的一些基本用法,完整的使用方法可以参考Scapy的官方文档http://www.secdev.org/projects/scapy/doc/usage.html。

我们有两种方式运行Scapy,一种是直接启动Scapy进入一个交互式界面,另一种是在Python程序中调用Scapy提供的功能。与其他软件不同的是,Scapy的交互模式其实就是Python的交互模式。因此我们可以在Scapy的交互模式下导入Python的包,使用Python的语法,执行Python中的语句。如下所示:

>>> import sys                                                                                                              
>>> print(sys.version)                                                                                                      
3.8.1 (default, Jan 14 2020, 10:59:16) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

既然知道了Scapy的交换模式是Python的交换模式这个事实,那我们就可以轻易地将Scapy交换模式中的代码放置在Python的源文件中,使用Python程序的方式进行软件开发。要在Python程序中使用Scapy的功能,只需要导入scapy.all模块即可。如下所示:

>>> import sys                                                                                                              
>>> print(sys.version)                                                                                                      
3.8.1 (default, Jan 14 2020, 10:59:16) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
>>> from __future__ import print_function                                                                                   
>>> from scapy.all import *                                                                                                 
>>> print(ls())                                                                                                             
AH         : AH
AKMSuite   : AKM suite
ARP        : ARP
ASN1P_INTEGER : None
ASN1P_OID  : None
ASN1P_PRIVSEQ : None
ASN1_Packet : None

在Scapy的交互式工具中,我们可以通过ls()显示Scapy支持的所有协议、lsc()列出Scapy支持的所有命令、conf显示所有的配置信息、help(cmd)显示某一命令的使用帮助等。如下所示:

>>> ls()                      
AH         : AH
AKMSuite   : AKM suite
ARP        : ARP
ASN1P_INTEGER : None
ASN1P_OID  : None
ASN1P_PRIVSEQ : None
ASN1_Packet : None
    
>>> lsc()                         
IPID_count          : Identify IP id values classes in a list of packets
arpcachepoison      : Poison target's cache with (your MAC,victim's IP) couple
arping              : Send ARP who-has requests to determine which hosts are up
arpleak             : Exploit ARP leak flaws, like NetBSD-SA2017-002.
bind_layers         : Bind 2 layers on some specific fields' values.
bridge_and_sniff    : Forward traffic between interfaces if1 and if2, sniff and return
chexdump            : Build a per byte hexadecimal representation
computeNIGroupAddr  : Compute the NI group Address. Can take a FQDN as input parameter
corrupt_bits        : Flip a given percentage or number of bits from a string
corrupt_bytes       : Corrupt a given percentage or number of bytes from a string
defrag              : defrag(plist) -> ([not fragmented], [defragmented],
defragment          : defragment(plist) -> plist defragmented as much as possible 
dhcp_request        : Send a DHCP discover request and return the answer
dyndns_add          : Send a DNS add message to a nameserver for "name" to have a new "rdata"
dyndns_del          : Send a DNS delete message to a nameserver for "name"
etherleak           : Exploit Etherleak flaw
explore             : Function used to discover the Scapy layers and protocols.
fletcher16_checkbytes: Calculates the Fletcher-16 checkbytes returned as 2 byte binary-string.
fletcher16_checksum : Calculates Fletcher-16 checksum of the given buffer.
fragleak            : --
fragleak2           : --
fragment            : Fragment a big IP datagram
fuzz                : 
getmacbyip          : Return MAC address corresponding to a given IP address
getmacbyip6         : Returns the MAC address corresponding to an IPv6 address
hexdiff             : Show differences between 2 binary strings

>>> help(sniff)
Help on function sniff in module scapy.sendrecv:

sniff(*args, **kwargs)
    Sniff packets and return a list of packets.
    
    Args:
        count: number of packets to capture. 0 means infinity.
        store: whether to store sniffed packets or discard them
        prn: function to apply to each packet. If something is returned, it
             is displayed.
             --Ex: prn = lambda x: x.summary()
        session: a session = a flow decoder used to handle stream of packets.
                 e.g: IPSession (to defragment on-the-flow) or NetflowSession
        filter: BPF filter to apply. 
                                        
【按q退出】                                        

如果你对网络编程特别感兴趣,你一定会喜欢上Scapy。我们不但可以使用Scapy嗅探和发送数据报,甚至还可以使用Scapy学习计算机网络相关知识。例如,我们可以使用ls查看协议的详细格式。如下所示:

>>> ls(ARP)                          
hwtype     : XShortField                         = (1)
ptype      : XShortEnumField                     = (2048)
hwlen      : FieldLenField                       = (None)
plen       : FieldLenField                       = (None)
op         : ShortEnumField                      = (1)
hwsrc      : MultipleTypeField                   = (None)
psrc       : MultipleTypeField                   = (None)
hwdst      : MultipleTypeField                   = (None)
pdst       : MultipleTypeField                   = (None)

3、使用Scapy发送数据报

Scapy的数据报遵循了网络协议中经典的TCP/IP四层模型,即链路层、网络层、运输层和应用层。Scapy为每个层协议都提供了辅助类,我们要做的就是把这些类实例化并修改对象的取值,以此来构造数据报。每一层都可以通过类调用创建相应的数据报,如IP()、TCP()、UDP()等,不同层之间通过“/”来连接。如下所示:

>>> packet1 = IP(dst='10.166.244.14')
>>> packet2 = IP(dst='10.166.244.14')/TCP(dport=80)
>>> packet3 = IP(dst='10.166.244.14')/ICMP()                                           

display方法可以查看当前数据报的内容,即各个参数的取值情况。例如,下面就显示了我们构造的第一个数据报中各个字段的取值。

>>> packet1.display()                
###[ IP ]### 
  version= 4
  ihl= None
  tos= 0x0
  len= None
  id= 1
  flags= 
  frag= 0
  ttl= 64
  proto= hopopt
  chksum= None
  src= 192.168.79.129
  dst= 10.166.244.14
  \options\

字段都有默认值,如果我们建立一个类的实例,没有传给它任何参数,那么它的参数取值就是默认值。如果传递了相应的参数,就使用用户传递的参数。如果使用del删除了某个参数,就恢复了默认值。如下所示:

>>> packet1.dst                      
'10.166.244.14'
>>> packet1.ttl = 32                
>>> packet1.ttl                      
32
>>> del packet1.ttl                  
>>> packet1.ttl                      
64

如果我们没有提供相应的参数取值(user set fields),参数将使用默认值(default fields)。之后,如果我们在这个类的上层进行操作 (比如IP的上面定义TCP),那么,数据报的取值将由上层协议进行覆盖。

4、使用Scapy构造DNS查询请求

下面以DNS解析为例,介绍如何使用Scapy构造数据报并发送请求。假设我们使用的 DNS服务器地址为8.8,8.8,现在,我们需要获取百度( )的IP地址。 为了获取百度的IP地址,我们需要创建一个DNS的请求包。如下所示:

>>> dns=DNS(rd=1,qd=DNSQR(qname='www.baidu.com'))                                     

DNS是一个应用层协议,底层可以使用UDP或TCP协议。无论是TCP协议还是UDP 协议,都依赖IP协议进行网络报传输。因此,完整的DNS清求数据报如下所示:

>>> packet = sr1(IP(dst='8.8.8.8')/UDP()/dns)
Begin emission:
...Finished sending 1 packets.
..*
Received 6 packets, got 1 answers, remaining 0 packets

在这个例子中,我们使用sr1函数发送和接收数据报,sr1在三层发送数据报,并且接收第一个回复。收到回复后,我们可以使用show方法来查看数据报的详细内容。如下所示:

>>> packet[DNS].show()
###[ DNS ]### 
  id= 0
  qr= 1
  opcode= QUERY
  aa= 0
  tc= 0
  rd= 1
  ra= 1
  z= 0
  ad= 0
  cd= 0
  rcode= ok
  qdcount= 1
  ancount= 3
  nscount= 0
  arcount= 0
  \qd\
   |###[ DNS Question Record ]### 
   |  qname= 'www.baidu.com.'
   |  qtype= A
   |  qclass= IN
  \an\
   |###[ DNS Resource Record ]### 
   |  rrname= 'www.baidu.com.'
   |  type= CNAME
   |  rclass= IN
   |  ttl= 332
   |  rdlen= None
   |  rdata= 'www.a.shifen.com.'
   |###[ DNS Resource Record ]### 
   |  rrname= 'www.a.shifen.com.'
   |  type= CNAME
   |  rclass= IN
   |  ttl= 93
   |  rdlen= None
   |  rdata= 'www.wshifen.com.'
   |###[ DNS Resource Record ]### 
   |  rrname= 'www.wshifen.com.'
   |  type= A
   |  rclass= IN
   |  ttl= 58
   |  rdlen= None
   |  rdata= 103.235.46.39
  ns= None
  ar= None

DNS应答包里面包含了非常详细的信息。例如,在这个应答中我们可以看到,DNS支持递归查询(ra取值为1表示DNS服务器支持递归查询,ra取值为0表示不支持递归查询)。百度的域名解析给出了 3个结果(ancount取值为3)。在这个例子中,我们通过手动构造数据报的方式,正确发送了DNS请求,解析了百度的IP地址。
在构造数据报时,如果有应用的数据,数据部分可以直接使用字符。如下所示:

>>> a=Ether()/IP(dst='www.slashdot.org')/TCP()/"GET /index.html HTTP/1.0 \n\n"         
>>> hexdump(a)
0000  00 50 56 FE 43 5E 00 0C 29 58 4A 96 08 00 45 00  .PV.C^..)XJ...E.
0010  00 43 00 01 00 00 40 06 6C 12 C0 A8 4F 81 D8 69  .C....@.l...O..i
0020  26 0F 00 14 00 50 00 00 00 00 00 00 00 00 50 02  &....P........P.
0030  20 00 AF 0F 00 00 47 45 54 20 2F 69 6E 64 65 78   .....GET /index
0040  2E 68 74 6D 6C 20 48 54 54 50 2F 31 2E 30 20 0A  .html HTTP/1.0 .
0050  0A 

如果我们安装了PyX,还可以直接将数据报dump成一个PostScript或PDF文件。

5、使用Scapy进行网络嗅探

Scapy除了可以伪造数据报并接收响应结果以外,还可以用于数据报嗅探。对数据报进行嗅探的函数为sniff,sniff函数的详细使用方法如下:

sniff(filter="", iface="any", prn=function, count=N)

sniff函数的参数说明如下:

  1. filter:用来表示想要捕获数据报类型的过滤器,如只捕获ICMP数据报,则filter取值为“ICMP”,只捕获80端口的TCP数据报,则filter取值为“TCP and (port 80)";
  2. iface:设置嗅探器所要嗅探的网卡,默认对所有网卡进行嗅探;
  3. pm:指定嗅探到符合过滤器条件的数据报时所调用的回调函数,这个回调函数只接受一个参数,即收到的数据报。
  4. count:指定需要嗅探的数据报的个数。
def pack_callback(packet):
	print(packet.show())
sniff(prn=pack_calback, iface="ens33", count=1)

下面是一个非常简单的sniff使用示例。在这个例子中,我们仅仅捕获三个ICMP的数据报,并且直接打印数据报的信息。前面说过,Scapy的交互模式就是Python的交互模式,因此,我们可以直接使用Python的库、语法和语句。sniff要求prn是一个回调函数,因此,我们传递给prn参数的是一个Lambda函数。如下所示:

>>> a = sniff(filter="tcp", prn=lambda x:x.summary(), count=3) 
Ether / IP / TCP 192.168.79.1:65134 > 192.168.79.129:ssh PA / Raw
Ether / IP / TCP 192.168.79.129:ssh > 192.168.79.1:65134 PA / Raw
Ether / IP / TCP 192.168.79.129:ssh > 192.168.79.1:65134 PA / Raw

你可能感兴趣的:(Python)