Skip to content

Latest commit

 

History

History
405 lines (266 loc) · 13.6 KB

File metadata and controls

405 lines (266 loc) · 13.6 KB

第七章:使用 Python 进行网络嗅探

在本章中,我们将涵盖以下内容:

  • Python 中的数据包嗅探器

  • 解析数据包

  • PyShark

介绍

嗅探器是一个可以拦截网络流量并嗅探数据包以进行分析的程序。随着数据流在网络上流动,嗅探器可以捕获每个数据包,解码数据包的原始数据以获取数据包头部中各个字段的值,并根据适当的规范分析其内容。网络数据包嗅探器可以用 Python 编写。

Python 中的数据包嗅探器

可以使用 socket 模块创建一个简单的 Python 数据包嗅探器。我们可以使用原始套接字类型来获取数据包。原始套接字提供对支持套接字抽象的底层协议的访问。由于原始套接字是互联网套接字 API 的一部分,它们只能用于生成和接收 IP 数据包。

准备工作

由于 socket 模块的一些行为取决于操作系统套接字 API,并且在不同操作系统下使用原始套接字没有统一的 API,我们需要使用 Linux 操作系统来运行此脚本。因此,如果您使用的是 Windows 或 macOS,请确保在虚拟 Linux 环境中运行此脚本。此外,大多数操作系统需要 root 访问权限才能使用原始套接字 API。

操作步骤...

以下是使用socket模块创建基本数据包嗅探器的步骤:

  1. 创建一个名为basic-packet-sniffer-linux.py的新文件,并在编辑器中打开它。

  2. 导入所需的模块:

import socket 
  1. 现在我们可以创建一个INET原始套接字:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP) 

读取和写入原始套接字都需要先创建一个原始套接字。这里我们使用INET族的原始套接字。套接字的族参数描述了套接字的地址族。以下是地址族常量:

    • AF_LOCAL:用于本地通信
  • AF_UNIX:Unix 域套接字

  • AF_INET:IP 版本 4

  • AF_INET6:IP 版本 6

  • AF_IPX:Novell IPX

  • AF_NETLINK:内核用户界面设备

  • AF_X25:保留给 X.25 项目

  • AF_AX25:业余无线电 AX.25

  • AF_APPLETALK:Appletalk DDP

  • AF_PACKET:低级数据包接口

  • AF_ALG:与内核加密 API 的接口

传递的下一个参数是套接字的类型。以下是套接字类型的可能值:

    • SOCK_STREAM:流(连接)套接字
  • SOCK_DGRAM:数据报(无连接)套接字

  • SOCK_RAW:原始套接字

  • SOCK_RDM:可靠交付的消息

  • SOCK_SEQPACKET:顺序数据包套接字

  • SOCK_PACKET:用于在开发级别获取数据包的 Linux 特定方法

最后一个参数是数据包的协议。此协议号由互联网数字分配机构IANA)定义。我们必须了解套接字的族;然后我们才能选择协议。由于我们选择了AF_INET(IPV4),我们只能选择基于 IP 的协议。

  1. 接下来,开始一个无限循环,从套接字接收数据:
while True: 
  print(s.recvfrom(65565)) 

套接字模块中的recvfrom方法帮助我们从套接字接收所有数据。传递的参数是缓冲区大小;65565是最大缓冲区大小。

  1. 现在用 Python 运行程序:
sudo python3 basic-packet-sniffer-linux.py 

结果将如下所示:

解析数据包

现在我们可以尝试解析我们嗅探到的数据,并解包头部。要解析数据包,我们需要了解以太网帧和 IP 数据包头部。

以太网帧结构如下:

前六个字节是目标 MAC地址,接下来的六个字节是源 MAC。最后两个字节是以太网类型。其余部分包括数据CRC 校验和。根据 RFC 791,IP 头部如下所示:

IP 头部包括以下部分:

  • 协议版本(四位):前四位。这代表了当前的 IP 协议。

  • 头部长度(四位):IP 头部的长度以 32 位字为单位表示。由于这个字段是四位,允许的最大头部长度为 60 字节。通常值为5,表示五个 32 位字:5 * 4 = 20 字节

  • 服务类型(八位):前三位是优先位,接下来的四位表示服务类型,最后一位未使用。

  • 总长度(16 位):这表示 IP 数据报的总长度(以字节为单位)。这是一个 16 位字段。IP 数据报的最大大小为 65,535 字节。

  • 标志(三位):第二位表示不分段位。当设置了这一位时,IP 数据报永远不会被分段。第三位表示更多分段位。如果设置了这一位,则表示一个被分段的 IP 数据报,在它之后还有更多分段。

  • 生存时间(八位):这个值表示 IP 数据报在被丢弃之前经过的跳数。

  • 协议(八位):这表示将数据传递给 IP 层的传输层协议。

  • 头部校验和(16 位):这个字段有助于检查 IP 数据报的完整性。

  • 源 IP 和目标 IP(每个 32 位):这些字段分别存储源地址和目标地址。

有关 IP 头部的更多详细信息,请参考 RFC 791 文档:tools.ietf.org/html/rfc791

如何做到...

以下是解析数据包的步骤:

  1. 创建一个名为basic-parse-packet-packet-linux.py的新文件,并导入解析数据包所需的模块:
from struct import * 
import sys 
  1. 现在我们可以创建一个函数来解析以太网头部:
def ethernet_head(raw_data): 
    dest, src, prototype = struct.unpack('! 6s 6s H', raw_data[:14])  
    dest_mac = get_mac_addr(dest) 
    src_mac = get_mac_addr(src) 
    proto = socket.htons(prototype) 
    data = raw_data[14:] 
    return dest_mac, src_mac, proto, data  

在这里,我们使用struct模块中的unpack方法来解包头部。从以太网帧结构中,前六个字节是目标 MAC 地址,接下来的 6 个字节是源 MAC 地址,最后的无符号短整型是以太网类型。最后,剩下的是数据。因此,这个函数返回目标 MAC 地址、源 MAC 地址、协议和数据。

  1. 现在我们可以创建一个主函数,在ethernet_head()中解析这个函数并获取详细信息:
def main(): 
    s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(3))  
    while True: 
        raw_data, addr = s.recvfrom(65535) 
        eth = ethernet(raw_data) 
        print('\nEthernet Frame:') 
        print('Destination: {}, Source: {}, Protocol: {}'.format(eth[0], eth[1], eth[2])) 

main() 
  1. 现在我们可以检查以太网帧中的数据部分并解析 IP 头部。我们可以创建另一个函数来解析ipv4头部:
def ipv4_head(raw_data): 
    version_header_length = raw_data[0] 
    version = version_header_length >> 4 
    header_length = (version_header_length & 15) * 4 
    ttl, proto, src, target = struct.unpack('! 8x B B 2x 4s 4s', raw_data[:20]) 
    data = raw_data[header_length:] 
    return version, header_length, ttl, proto, src, target, data 

根据 IP 头部,我们将使用struct中的unpack方法来解包头部,并返回版本头部长度TTL、协议源和目标 IP。

  1. 现在更新main()以打印 IP 头部:
def main(): 
    s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(3))  
    while True: 
        raw_data, addr = s.recvfrom(65535) 
        eth = ethernet(raw_data) 
        print('\nEthernet Frame:') 
        print('Destination: {}, Source: {}, Protocol: {}'.format(eth[0], eth[1], eth[2]))  
        if eth[2] == 8: 
            ipv4 = ipv4(ethp[4]) 
            print( '\t - ' + 'IPv4 Packet:') 
            print('\t\t - ' + 'Version: {}, Header Length: {}, TTL:{},'.format(ipv4[1], ipv4[2], ipv4[3])) 
            print('\t\t - ' + 'Protocol: {}, Source: {}, Target: {}'.format(ipv4[4], ipv4[5], ipv4[6])) 
  1. 目前,打印出的 IP 地址不是可读格式,因此我们可以编写一个函数来格式化它们:
def get_ip(addr): 
    return '.'.join(map(str, addr)) 

确保更新ipv4_head函数通过在返回输出之前添加以下行来格式化 IP 地址:

src = get_ip(src) 
target = get_ip(target) 
  1. 现在我们已经解包了网络层,接下来要解包的是传输层。我们可以从 IP 头部的协议 ID 中确定协议。以下是一些协议的协议 ID:
  • TCP:6

  • ICMP:1

  • UDP:17

  • RDP:27

  1. 接下来,我们可以创建一个函数来解包 TCP 数据包:
def tcp_head( raw_data): 
    (src_port, dest_port, sequence, acknowledgment, offset_reserved_flags) = struct.unpack( 
        '! H H L L H', raw_data[:14]) 
    offset = (offset_reserved_flags >> 12) * 4 
    flag_urg = (offset_reserved_flags & 32) >> 5 
    flag_ack = (offset_reserved_flags & 16) >> 4 
    flag_psh = (offset_reserved_flags & 8) >> 3 
    flag_rst = (offset_reserved_flags & 4) >> 2 
    flag_syn = (offset_reserved_flags & 2) >> 1 
    flag_fin = offset_reserved_flags & 1 
    data = raw_data[offset:] 
    return src_port, dest_port, sequence, acknowledgment, flag_urg, flag_ack, flag_psh, flag_rst, flag_syn, flag_fin, data 

TCP 数据包根据 TCP 数据包头的结构进行解包:

  1. 现在我们可以更新main()以打印 TCP 头部的详细信息。在ipv4部分内添加以下行:
if ipv4[4] == 6:  
    tcp = tcp_head(ipv4[7]) 
    print(TAB_1 + 'TCP Segment:') 
    print(TAB_2 + 'Source Port: {}, Destination Port: {}'.format(tcp[0], tcp[1])) 
    print(TAB_2 + 'Sequence: {}, Acknowledgment: {}'.format(tcp[2], tcp[3])) 
    print(TAB_2 + 'Flags:') 
    print(TAB_3 + 'URG: {}, ACK: {}, PSH:{}'.format(tcp[4], tcp[5], tcp[6])) 
    print(TAB_3 + 'RST: {}, SYN: {}, FIN:{}'.format(tcp[7], tcp[8], tcp[9]))  
    if len(tcp[10]) > 0: 
         # HTTP 
        if tcp[0] == 80 or tcp[1] == 80: 
             print(TAB_2 + 'HTTP Data:') 
                 try: 
                    http = HTTP(tcp[10]) 
                    http_info = str(http[10]).split('\n') 
                    for line in http_info: 
                       print(DATA_TAB_3 + str(line)) 
                 except: 
                       print(format_multi_line(DATA_TAB_3, tcp[10])) 
                 else: 
                      print(TAB_2 + 'TCP Data:') 
                      print(format_multi_line(DATA_TAB_3, tcp[10])) 
  1. 类似地,更新函数以解包 UDP 和 ICMP 数据包。

数据包根据数据包头结构进行解包。以下是 ICMP 的数据包头结构:

根据图表,我们可以使用以下代码解包数据包:

elif ipv4[4] == 1: 
    icmp = icmp_head(ipv4[7]) 
    print('\t -' + 'ICMP Packet:') 
    print('\t\t -' + 'Type: {}, Code: {}, Checksum:{},'.format(icmp[0], icmp[1], icmp[2])) 
    print('\t\t -' + 'ICMP Data:') 
    print(format_multi_line('\t\t\t', icmp[3])) 

以下是 UDP 的数据包头结构:

就像我们对 ICMP 所做的那样,我们可以按照以下方式解包 UDP 数据包头:

elif ipv4[4] == 17: 
    udp = udp_head(ipv4[7]) 
    print('\t -' + 'UDP Segment:') 
    print('\t\t -' + 'Source Port: {}, Destination Port: {}, Length: {}'.format(udp[0], udp[1], udp[2])) 

现在保存并以所需权限运行脚本:

sudo python3 basic-parse-packet-linux.py  

输出将打印所有被嗅探到的数据包。因此,它会一直打印,直到我们用键盘中断停止。输出如下:

PyShark

PyShark 是 Wireshark CLI(TShark)的包装器,因此我们可以在 PyShark 中拥有所有 Wireshark 解码器。我们可以使用 PyShark 来嗅探接口,或者分析pcap文件。

准备就绪

在使用此模块时,请确保在系统上安装 Wireshark 并使用pip命令安装pyshark

pip3 install pyshark  

还要确保在计算机上安装了 TShark。TShark 是基于终端的 Wireshark,PyShark 用于数据包捕获功能。

在这里了解更多关于 TShark 的信息:www.wireshark.org/docs/wsug_html_chunked/AppToolstshark.html

如何做...

让我们尝试一些 PyShark 的例子。确保在系统中安装了 TShark。

  1. 为了更好地理解,我们可以使用 Python 交互式终端并查看 PyShark 的功能。请注意,这些命令也可以包含在脚本中。唯一的依赖是 TShark。

  2. 导入pyshark模块:

>>> import pyshark 
  1. 现在将pcap文件加载到pyshark中:
>>> cap = pyshark.FileCapture('sample.pcap') 

我们可以使用以下命令从实时接口进行嗅探:

 >>> cap = pyshark.LiveCapture(interface='wlp3s0b1')
                  >>> cap.sniff(timeout=3)

这将嗅探接口的下一个 3 秒

  1. 现在您可以从cap变量中获取数据包的详细信息。

要打印出第一个数据包的详细信息,我们可以使用以下命令:

>>> print(cap[0]) 

输出如下:

您可以使用dir()查看所有可能的选项:

>>> print(dir(cap[0])) 

为了以漂亮的格式查看它们,我们可以使用pprint模块:

>>> import pprint 
>>> pprint.pprint(dir(cap[0])) 

这将打印 PyShark 中数据包的所有可能选项。输出如下:

  1. 您可以按如下方式迭代每个数据包:
for pkt in cap: print(pkt.highest_layer)

  1. 我们可以按如下方式获取经过筛选的数据包流到pyshark
cap = pyshark.LiveCapture(interface='en0', bpf_filter='ip and tcp port 80')  
cap.sniff(timeout=5) 

这将过滤数据包,除了 TCP/IP 到端口80