IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第三章 网络工程-原始套接字与嗅探(2)解码IP包 -> 正文阅读

[网络协议]黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第三章 网络工程-原始套接字与嗅探(2)解码IP包

黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第三章 网络工程-原始套接字与嗅探(2)解码IP包


写在前面

在当前的模式下,我们的嗅探器接收所有IP头,以及任何更高的协议,如TCP、UDP或ICMP。信息以二进制形式打包,如前所示,很难理解。接下来,我们将对数据包的IP部分进行解码,以便从中提取有用的信息,例如协议类型(TCP、UDP或ICMP)以及源/目标IP地址。这将为以后进一步的协议解析奠定基础。
如果要检查网络上实际的数据包的样子,我们应当了解我们需要如何解码传入的数据包。IP头的组成如下图所示。
在这里插入图片描述
我们将会解压整个IP头部(不包括可选字段区域),并提取协议类型、源/目的IP地址。这意味着我们将直接跟二进制打交道,并且我们将通过一些措施用python来区分IP头的各个部分。
在Python中,与很多方式获取外部二进制数据到数据结构中。你可以使用ctypes模块或者struct模块来定义数据结构。对于Python来说,ctypes是一个外部函数库,提供了跟基于C的语言的交互,这使我们能够在共享库中使用与C兼容的数据类型和调用函数。另外,struct模块在Python值和C结构之间的转换。总之,ctypes模块处理二进制数据类型,并且提供一些其它的功能;struct模块主要处理二进制数据。本节中我们将会展示如何用他们从网络读取一个IPv4头部。

ctypes模块

下面的代码段定义了一个新的类IP,能够读取一个数据包,并将头部解析成单独分开的域。

from ctypes import *
import socket
import struct

class IP(Structure):
    _fields_ = [
        ("ihl",             c_ubyte,   4),  # 4 bit unsigned char
        ("version",         c_ubyte,   4),  # 4 bit unsigned char
        ("tos",             c_ubyte,   8),  # 1 byte char
        ("len",             c_ushort,  16), # 2 byte unsigned short
        ("id",              c_ushort,  16), # 2 byte unsigned short
        ("offset",          c_ushort,  16), # 2 byte unsigned short
        ("ttl",             c_ubyte,   8),  # 1 byte char
        ("protocol_num",    c_ubyte,   8),  # 1 byte shar
        ("sum",             c_ushort,  16), # 2 byte unsigned short
        ("src",             c_uint,    32), # 4 byte unsigned int
        ("dst",             c_uint,    32), # 2 byte unsigned int
    ]
    def __new__(cls, socket_buffer=None):
        return cls.from_buffer_copy(socket_buffer)
    
    def __init__(self, socket_buffer=None):
        # human readable IP addresses
        self.src_address = socket.inet_ntoa(struct.pack("<L", self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst))

上述IP类中创建了一个名为_fields_的结构,用来描述IP头中的各个部分。结构中使用了在ctypes模块中定义的C类型。例如,c_ubyte类型是一个无符号字符,c_ushort类型是一个无符号短整形。你会发现定义的结构跟上图中描述的IP头的各个部分是对应的。结构中的每一个域包含三个属性:域的名称、取值的类型、以及二进制位的宽度。能够指定位的宽度还是很方便的,因为它提供了指定所需长度的自由,而不仅仅是在字节级别。
IP类继承自ctypes模块中的Structure类,Structure指定在创建任何对象之前必须具有已定义的_fields_结构。为了填充_fields_结构,structure类使用__new__方法,它将类引用为第一个参数,创建并返回类的对象,该对象传递给__init__方法。当我们创建IP对象时,Python调用__new__,它在创建对象之前立即填充_fields_数据结构(当调用__init__方法时)。只要您事先定义了结构,就可以将__new__方法传递给外部网络数据包的数据,这些字段应该神奇地显示为对象的属性。
现在您已经了解了如何将C数据类型映射到IP头值。在转换为Python对象时,使用C代码作为参考可能很有用,因为转换为纯Pythons是无缝的。有关使用此模块的完整详细信息,请参阅类型文档。

Struct模块

Struct模块提供格式字符,可用于指定二进制数据的结构。在下面的示例中,我们将再次定义一个IP类来保存IP头信息。不过,这次我们将使用格式字符来表示IP头的各个部分。

from email import header
import ipaddress
import struct

class IP:
    def __init__(self, buff=None):
        header = struct.unpack('<BBHHHBBH4s4s', buff)
        self.ver = header[0] >> 4
        self.ihl = header[0] & 0xF

        self.tos = header[1]
        self.len = header[2]
        self.id = header[3]
        self.offset = header[4]
        self.ttl = header[5]
        self.protocol_num = header[6]
        self.sum = header[7]
        self.src = header[8]
        self.dst = header[9]

        # human readable IP addresses
        self.src_address = ipaddress.ip_address(self.src)
        self.dst_address = ipaddress.ip_address(self.dst)

        # map protocol constants to their names
        self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}

第一个格式字符(在我们的例子中是<)总是指定数据的尾数,或二进制数中字节的顺序。C类型以机器的原始格式和字节顺序表示。在本示例中,我们在Kali(x64)上展示,kali是小字节序(低字节序)。在小字节序机器中,最低有效字节存储在较低地址,最高有效字节存储于最高地址。
接下来的格式字符表示IP头的各个部分。struct模块提供了几个格式字符。对于IP头,我们只需要格式字符B(1字节无符号字符)、H(2字节无符号短整形)和s(需要字节宽度规范的字节数组;4s表示4字节字符串)。注意我们的格式字符串与IP头示意图的结构相匹配。
需要注意的是,用ctypes时,我们需要指定IP头的各个部分的位宽;使用struct时,没有nybble格式字符(一个4bit单元,也被称为nibble,半字节),因此我们必须进行一些操作才能从IP头的第一部分获取ver和hdrlen变量。
在我们接收到的IP头数据的第一个字节中,我们想为ver变量只分配字节高位的nybble(字节中的第一个半字节)。获取字节高位的典型方法是将字节右移四位,这相当于在字节前面加上四个零,导致最后四位脱落。这只剩下原始字节的第一个半字节。Python代码基本上执行以下操作:
在这里插入图片描述
我们想给hdrlen变量赋值低位的nybble(半字节),或叫字节的最后四位。获取字节第二个nybble的典型方法是跟0xF(00001111)进行逻辑AND运算。Python代码对字节的处理操作如下所示。
在这里插入图片描述
实际上,解析IP头的时候,读者不需要对二进制操作了解太多,但是这里将会看到诸如在探索其他黑客代码时反复使用的shifts和and的方式,这些技术值得了解。
在这种需要移位的情况下,解码二进制数据需要一些努力。但对于许多情况(如读取ICMP消息),设置非常简单:ICMP消息的每个部分都是1字节的整数倍,struct模块提供的格式字符也是1字节的整数倍,因此无需将字节拆分为半字节。在下图所示的ICMP协议的Echo Reply消息中,可以看到ICMP头的每个参数都可以用一个现有格式字母(BBHHH)在结构中定义。
在这里插入图片描述
解析此消息的一种快速方法是简单地为前两个属性分配1个字节,为后三个属性分配2个字节:

class ICMP:
    def __init__(self, buff):
        header = struct.unpack('<BBHHH', buff)
        self.type = header[0]
        self.code = header[1]
        self.sum = header[2]
        self.id = header[3]
        self.seq = header[4]

如需获取使用该模块的详细信息,可以阅读struct的文档(https://docs.python.org/3/library/struct.html)。
读者可以使用ctypes模块或struct模块来读取和解析二进制数据。无论采用哪种方法,都将进行如下所示的实例化类:

mypacket = IP(buff)
print(f'{mypacket.src_address} -> {mypacket.dst_address}')

在本示例中,我们使用变量buff中的包数据实例化IP类。

编写IP解码器

接下来我们将IP解码逻辑创建到名为sniffer_IP_header_decode.py的文件中。

import ipaddress
import os
import socket
import struct
import sys

class IP:
    def __init__(self, buff=None):
        header = struct.unpack('<BBHHHBBH4s4s', buff)
        self.ver = header[0] >> 4
        self.ihl = header[0] & 0xF

        self.tos = header[1]
        self.len = header[2]
        self.id = header[3]
        self.offset = header[4]
        self.ttl = header[5]
        self.protocol_num = header[6]
        self.sum = header[7]
        self.src = header[8]
        self.dst = header[9]

        # human readable IP addresses
        self.src_address = ipaddress.ip_address(self.src)
        self.dst_address = ipaddress.ip_address(self.dst)

        # map protocol constants to their names
        self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except Exception as e:
            print('%s No protocol for %s' % (e, self.protocol_num))
            self.protocol = str(self.protocol_num)
    
    def sniff(host):
        # should look familiar from previous example
        if os.name == 'nt':
            socket_protocol = socket.IPPROTO_IP
        else:
            socket_protocol = socket.IPPROTO_ICMP
        
        sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)
        sniffer.bind((host, 0))
        sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

        if os.name == 'nt':
            sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
        
        try:
            while True:
                # read a packet
                raw_buffer = sniffer.recvfrom(65535)[0]
                # create an IP header from the first 20 bytes
                ip_header = IP(raw_buffer[0:20])
                # print the detected protocol and hosts
                print('Protocol: %s %s -> %s' % (ip_header.protocol, ip_header.src_address, ip_header.dst_address))
        except KeyboardInterrupt:
            # if we're on Windows, turn off proniscuous mode
            if os.name == 'nt':
                sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
            sys.exit()

if __name__ == '__main__':
    if len(sys.argv) == 2:
        host = sys.argv[1]
    else:
        host = '192.168.65.141'
    sniff(host)

上述代码中,我们首先完成了IP类的定义,这是一个python结构,将接收到的buffer的前20个字节友好的映射到IP头。我们识别的所有字段都与IP头的结构很好地匹配。接下来,我们做了一些内部处理,以生成可读的输出,展示正在使用的协议和连接中涉及的IP地址。我们使用新创建的IP结构,编写逻辑来持续读取数据包并解析其信息。然后读入数据包,传递前20个字节用来初始化IP结构。接下来,简单地打印出我们捕获的信息。下面我们运行一下。

小试牛刀

运行前面的代码,看看我们从发送的原始数据包中提取了什么样的信息。强烈建议在Windows机器上进行此测试,这将能够看到TCP、UDP和ICMP,并允许进行一些非常整洁的测试(比如打开浏览器)。如果读者手边只有Linux,那么执行前面的ping测试以查看它的实际运行情况。我们分别在windows下和linux下运行一下。

在windows下

打开一个命令行窗口,用python运行一下上面编写的脚本。能够得到预期的输出。
在这里插入图片描述

在linux下

在命令终端运行上述脚本(注意使用sudo,否则提示无权限),然后从另一个主机上ping一下linux主机,也可以得到预期的输出,如下图。
在这里插入图片描述

说明

这里有点小问题,原书中的代码,打印出来在Protocol后面跟着的是协议名称,比如ICMP、UDP、TCP(如下图),可是我写的代码打印出来是协议的代号,即上面代码中定义的1、6、17。
在这里插入图片描述
偷懒一下,直接用BeyondCompare比对一下看看哪里的问题。找到原因了,init函数中的最后一行代码,应该是在try的except里面执行,而不是在外面,缩进一下,然后重新执行,结果正常了,如下图。
在这里插入图片描述
到这里,我们也看到了工具的局限性:我们只看到了ICMP协议的响应,因为我们有意构建用于主机发现的扫描器,这是完全可以接受的。接下来的章节中,我们将使用与解码IP头相同的技术来解码ICMP消息。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 21:14:47  更:2022-10-08 21:15:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/6 15:31:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码