SCAPY官方教程九
要完成任务,请按照以下步骤操作:
步骤 1: 定义新的 Protocol 类
创建一个新的 Python 文件 my_protocol.py 并在其中定义一个新的 Protocol 类 MyProtocol:
`python
from scapy.all import *
class MyProtocol(Packet):
def init(self, name, srcip, dstip):
super().init(name=name)
self.srcip = srcip
self.dstip = dstip
@classmethod
def name(cls, arg, x=0):
if cls == arg:
return x
if cls.find(args[0]) == -1:
return None
return cls.name
def fields_desc(self):定义该协议使用的字段描述
return [
ByteField("srcip", self.srcip),
ByteField("dstip", self.dstip),
ByteField("length", 0)
]
步骤 2: 添加该 Protocol 到 Scapy 的模块 将上述文件导入 Scapy 的模块,并添加到 scipy/contrib/protocol.py 中:python
在 scipy/contrib/protocol.py 中添加以下代码:
from . import common, fieldset
class MyProtocol(Packet):
def init(self, name, srcip, dstip):
super().init(name=name)
self.srcip = srcip
self.dstip = dstip
@classmethod
def name(cls, arg, x=0):
if cls == arg:
return x
if cls.find(args[0]) == -1:
return None
return cls.name
def fields_desc(self):定义该协议使用的字段描述
return [
ByteField("srcip", self.srcip),
ByteField("dstip", self.dstip),
ByteField("length", 0)
]
AI助手`
步骤 3: 测试你的 Protocol
现在你可以创建并捕获你的新
如何通过Scapy添加新协议
使用Scapy实现新的协议(或说是新增的一层)轻而易举地完成。所有这些魔法遍布各个领域。如果您的需求已存在且方案不算过于复杂,则整个流程只需几分钟就能完成。
一、简单的例子
该层作为Packet类的一个子类进行扩展。所有层操作的逻辑均由同一个父类对象承载,并得以继承子类特有的功能特性。一个简单的层通常包含一组字段,在设计之初就需要考虑其后续扩展的可能性。在组装过程中会被连接起来,在解码过程中则逐个被解析。为了方便管理与维护,在设计之初就应该对各个功能模块进行明确划分与标识管理。每个字段都是其所属类型的一个实例:通过这种方式能够实现功能的一致性和可维护性
class Disney(Packet):
name = "DisneyPacket "
fields_desc=[ ShortField("mickey",5),
XByteField("minnie",3) ,
IntEnumField("donald" , 1 ,
{ 1: "happy", 2: "cool" , 3: "angry" } ) ]
AI助手
在这一实例中,我们设计了三层结构,每个结构都包含特定的配置参数.第一层由命名为mickey的2字节整数组成,其初始设置值设定为5;第二层则由命名为minnie的1字节整数组成,初始值设为3.对比vanilla ByteField和XByteField的主要区别在于前者的人工表示采用十六进制形式,而后者则保留十进制特性.最后一层由名为Donald的四字节整数类型构成,其与传统的IntField不同之处在于允许部分数值以文字形式呈现:例如当数值等于3时,系统会将其标记为'生气'.此外,若向该属性注入'cool'字符串型数据,系统会将其转换并强制赋值为数字2
如果您的协议如此简单,则可以使用:
>>> d=Disney(mickey=1)
>>> ls(d)
mickey : ShortField = 1 (5)
minnie : XByteField = 3 (3)
donald : IntEnumField = 1 (1)
>>> d.show()
###[ Disney Packet ]###
mickey= 1
minnie= 0x3
donald= happy
>>> d.donald="cool"
>>> raw(d)
’\x00\x01\x03\x00\x00\x00\x02’
>>> Disney(_)
<Disney mickey=1 minnie=0x3 donald=cool |>
AI助手
本章解释了如何在 Scapy 中构建一个新协议。有两个主要目标:
剖析:该系统在接收数据包(源自于网络流或本地文件)时完成任务,并且必须将该数据包转换为Scapy内部结构。
构建:每当一个人希望发送一个新的数据包时,在其中自动进行相应的优化操作。
二、网络层级
在深入剖析本身之前,让我们看看数据包是如何组织的。
>>> p = IP()/TCP()/"AAAA"
>>> p
<IP frag=0 proto=TCP |<TCP |<Raw load='AAAA' |>>>
>>> p.summary()
'IP / TCP 127.0.0.1:ftp-data > 127.0.0.1:www S / Raw'
AI助手
我们对类的 2 个“内部”字段感兴趣Packet:
p.underlayer
p.payload
这是主要的“技巧”。你不关心数据包,只关心层,一个接一个地堆叠。
可以通过名称轻松访问层:p[TCP]返回TCP 和后续层。这是p.getlayer(TCP).
有一个可选参数 ( nb) 返回nb所需协议的第 th 层。
现在让我们把所有东西放在一起,玩TCP图层:
>>> tcp=p[TCP]
>>> tcp.underlayer
<IP frag=0 proto=TCP |<TCP |<Raw load='AAAA' |>>>
>>> tcp.payload
<Raw load='AAAA' |>
AI助手
按照预期的情况来看,在 tcp.underlayer 中存放着该 IP 数据包的开头部分的信息,在 tcp.payload 中则存储着其有效负载部分的信息。
构建新层
很容易!层主要是字段列表。我们看一下UDP定义:
class UDP(Packet):
name = "UDP"
fields_desc = [ ShortEnumField("sport", 53, UDP_SERVICES),
ShortEnumField("dport", 53, UDP_SERVICES),
ShortField("len", None),
XShortField("chksum", None), ]
AI助手
你完成了!为方便起见,已经定义了许多字段,请查看 Phil 所说的 doc^W 源。
因此,在数据定义层中只是负责整合字段信息。其主要目标是为每个字段设置合理的默认值,这样用户在构建数据包时无需用户在打包时自行配置。
主要机制是基于Field结构。永远记住,一个层只是一个字段列表,但不多。
因此,要了解图层的工作方式,需要快速了解字段的处理方式。
操作数据包 == 操作其字段
一个字段应该在不同的状态下被考虑:
i(nternal) :这是 Scapy 操作它的方式。
m(achine)这就是真相所在,那是层的本来面目
在网络上。
h(uman) : 数据包如何显示给我们的肉眼。
该系统阐述了每个领域中可获取的方法包括i2h(), i2m(), 以及m2i()等函数:这些方法实现了从一种状态到另一种状态的状态转换,并且适用于特定的应用场景。
其他特殊功能:
any2i()猜测输入表示并返回内部表示。
i2repr()更好的i2h()
但是,所有这些都是“低级”功能。向当前层添加或提取字段的功能是:
addfield(self, pkt, s, val)
该字段(属于layer)将被用来将(pkt)的数据包转换为目标字符串数据包(s)。
1. class StrFixedLenField(StrField):
2. def addfield(self, pkt, s, val):
3. return s+struct.pack("%is"%self.length,self.i2m(pkt, val))
AI助手
getfield(self, pkt, s):从原始数据包中获取s所属该层的字段值pkt。它会返回一个列表,在第一个位置上记录着删除该字段后的原始数据包字符串,在第二个位置上则是内部表示中所获取的该字段本身。
1. class StrFixedLenField(StrField):
2. def getfield(self, pkt, s):
3. return s[self.length:], self.m2i(pkt,s[:self.length])
AI助手
在定义自己的层时,一般而言只需要实现几个*2*()接口方法,另外还需要实现addfield()和getfield()两种功能.
示例:可变长度数量
一种常用的方法会在协议中常用的可变长度量上编码为整数。其中,在信号处理领域(如 MIDI技术中)常被采用。
除了最后一个字节之外,在对其他每个字节进行编码时,默认会将最高有效位(MSB)设置为 1。
def vlenq2str(l):
s = []
s.append(l & 0x7F)
l = l >> 7
while l > 0:
s.append( 0x80 | (l & 0x7F) )
l = l >> 7
s.reverse()
return bytes(bytearray(s))
def str2vlenq(s=b""):
i = l = 0
while i < len(s) and ord(s[i:i+1]) & 0x80:
l = l << 7
l = l + (ord(s[i:i+1]) & 0x7F)
i = i + 1
if i == len(s):
warning("Broken vlenq: no ending byte")
l = l << 7
l = l + (ord(s[i:i+1]) & 0x7F)
return s[i+1:], l
AI助手
我们将定义一个自动计算相关字符串长度的字段,但使用该编码格式:
class VarLenQField(Field):
""" variable length quantities """
__slots__ = ["fld"]
def __init__(self, name, default, fld):
Field.__init__(self, name, default)
self.fld = fld
def i2m(self, pkt, x):
if x is None:
f = pkt.get_field(self.fld)
x = f.i2len(pkt, pkt.getfieldval(self.fld))
x = vlenq2str(x)
return raw(x)
def m2i(self, pkt, x):
if s is None:
return None, 0
return str2vlenq(x)[1]
def addfield(self, pkt, s, val):
return s+self.i2m(pkt, val)
def getfield(self, pkt, s):
return str2vlenq(s)
AI助手
现在,使用这种字段定义一个层:
class FOO(Packet):
name = "FOO"
fields_desc = [ VarLenQField("len", None, "data"),
StrLenField("data", "", length_from=lambda pkt: pkt.len) ]
>>> f = FOO(data="A"*129)
>>> f.show()
###[ FOO ]###
len= None
data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
AI助手
在本阶段中,在此位置上尚未完成对变量len的长度计算,默认值为零。这是我们的这一层当前所使用的数据表示形式,请我们这一层立即执行计算操作:
>>> f.show2()
###[ FOO ]###
len= 129
data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
AI助手
该函数show2()用于展示字段及其对应的值。值得注意的是,在将这些数据传输至网络的同时,在确保人机-readable的同时我们能够观察到字符串长度为129。同样重要的是,请注意以下几点:通过机器表示的形式。
>>> raw(f)
'\x81\x01AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
AI助手
前 2 个字节是\x81\x01,在此编码中为 129。
三、深入剖析
网络图层仅包含字段列表这一基本构成要素,但它们之间的连接机制却是一个需要深入探讨的核心问题。各个层级之间以及后续层级之间的连接机制到底是什么?这些内容正是本节所要阐述的核心内容。
基本的东西
解剖的核心功能是Packet.dissect():
def dissect(self, s):
s = self.pre_dissect(s)
s = self.do_dissect(s)
s = self.post_dissect(s)
payl,pad = self.extract_padding(s)
self.do_dissect_payload(payl)
if pad and conf.padding:
self.add_payload(Padding(pad))
AI助手
调用时,s是一个字符串,其中包含将要剖析的内容。self指向当前层。
>>> p=IP("A"*20)/TCP("B"*32)
WARNING: bad dataofs (4). Assuming dataofs=5
>>> p
<IP version=4L ihl=1L tos=0x41 len=16705 id=16705 flags=DF frag=321L ttl=65 proto=65 chksum=0x4141
src=65.65.65.65 dst=65.65.65.65 |<TCP sport=16962 dport=16962 seq=1111638594L ack=1111638594L dataofs=4L
reserved=2L flags=SE window=16962 chksum=0x4242 urgptr=16962 options=[] |<Raw load='BBBBBBBBBBBB' |>>>
AI助手
Packet.dissect()被调用 3 次:
剖析"A"*20为 IPv4 标头
剖析"B"*32TCP 标头
考虑到数据包中剩余了12个字节,在此情况下我们将其划分为‘Raw’数据(属于一种预设的层级结构)
对于给定的层,一切都非常简单:
pre_dissect()被调用来准备层。
do_dissect()执行层的真正解剖。
post_dissection()在解析输入并进行必要的更新时会被调用,并主要用于处理诸如解密和解压缩等操作。
extract_padding()扮演着关键角色,在神经网络架构设计中占据重要地位。每个包含自身规模的层级都应该启动该函数,在处理有效负载时,该函数能够分辨出与当前层直接相关的数据以及其他可能被视为填充字节的部分。
do_dissect_payload() 主要负责在存在时解析有效载荷的功能。它根据 guess_payload_class() (见下文)。当识别出有效载荷的类型时,该系统会将其以这种方式绑定到当前层:
1. def do_dissect_payload(self, s):
2. cls = self.guess_payload_class(s)
3. p = cls(s, _internal=1, _underlayer=self)
4. self.add_payload(p)
AI助手
最后,包中的所有层都被剖析,并与它们已知的类型粘合在一起。
字段解析
该方法能够整合图层及其字段间的全部特殊功能,并由函数 do_dissect() 实现。若已掌握某一层的不同表现形式,则就应明白"分解"一个层实际上是在对其各个字段进行详细解析,并从机器层面一直到其内部表示的过程。
你猜怎么着?这正是这样do_dissect()做的:
def do_dissect(self, s):
flist = self.fields_desc[:]
flist.reverse()
while s and flist:
f = flist.pop()
s,fval = f.getfield(self, s)
self.fields[f] = fval
return s
AI助手
当有数据或字段剩余时(即存在未被处理的数据项),该过程会接收原始字符串数据包,并将其分配给各个字段。
>>> FOO("\xff\xff"+"B"*8)
<FOO len=2097090 data='BBBBBBB' |>
AI助手
在编写代码时,我们使用了函数调用FOO("FF FF"+"00000000"+"B"*8)。该函数被调用。返回的第一个字段属于VarLenQField类型。当设置标志位MSB时,请注意该字段需要占用一定数量的字节。计算至并包含第一个字符'B'的位置。此映射关系由以下步骤实现:首先通过调用$VarLenQField.getfield()获取所需数据,并对获取的数据进行交叉验证。
>>> vlenq2str(2097090)
'\xff\xffB'
AI助手
随后,在同样的方法基础上依次提取后续字段,并按上述步骤将其加载至内存中的2,147,483,648字节FOO.data(当无法获取全部该数值时,默认仅读取部分数据,请参考此处)
当对当前层进行分析后仍存在剩余数据时,则采用同样的策略将这些数据对应到下一预期的目标内容中(其中,默认情况下使用Raw参数):
>>> FOO("\x05"+"B"*8)
<FOO len=5 data='BBBBB' |<Raw load='BBB' |>>
AI助手
因此,我们现在需要了解层是如何绑定在一起的。
绑定层
当我们在进行网络分析时,在剖面层中使用Scapy软件包能够有效地识别并推测下一层次的技术或协议。而在2层之间建立连接的官方方法是通过调用bind_layers()函数实现的。
在模块内部可用packet,此功能可按如下方式使用:
bind_layers(ProtoA, ProtoB, FieldToBind=Value)
AI助手
每次ProtoA()/ProtoB()创建数据包时,FieldToBindof ProtoA将等于Value。
当您拥有一个名为 HTTP 的类时,
或许希望所有来自或前往端口 80 的数据包都被解码。
这通常可以通过以下方式实现:
bind_layers( TCP, HTTP, sport=80 )
bind_layers( TCP, HTTP, dport=80 )
AI助手
在互联网时代,我们坚信所有人都是平等的。通过统一开放的协议HTTP,所有与端口 80 相关的数据包均可以通过 pcap 文件或网络接口获取。
基于**guess_payload_class()**机制
有时候,在估算有效的负载类别时,并非仅仅依赖于单一端口的定义。比如,在某些情况下,这个值可能会受到当前层中指定字节数量的影响。具体来说,则需要采用两种不同的方法。
guess_payload_class()负责返回有效载荷的猜测类(下一层)。在默认情况下,默认行为是通过bind_layers()函数决定的。
该函数由$default_payload_class()实现,并返回预设的默认值。\n\n包内定义的方法名称为Packet,并返回类型Raw。\n\n通过重载功能可以在原有基础上进行功能扩展
例如,解码 802.11 取决于它是否被加密:
class Dot11(Packet):
def guess_payload_class(self, payload):
if self.FCfield & 0x40:
return Dot11WEP
else:
return Packet.guess_payload_class(self, payload)
AI助手
这里需要几个评论:
由于无法直接调用 bind_layers() 函数进行验证,默认情况下我们期望每个字段都与预期值相等(即 field == value)。然而,在当前场景中情况更为复杂
当检测到测试出现故障时,则不进行任何假设,并且将默认的猜测机制纳入流程继续处理,并且调用Packet.guess_payload_class()。
通常情况默认下,在不使用的情况下,默认下
更改默认行为
如果需要更改或禁用Scapy对某个指定层级的行为,请通过调用该层级的配置函数来实现此功能。例如,在不希望UDP/53绑定DNS的情况下,请编写以下代码块:
split_layers(UDP, DNS, sport=53)
AI助手
现在的配置使得源端口 53 的所有数据包将不作为 DNS 处理 而是被您所指定的所有内容处理
Under the hood:把一切都放在一起
请注意,在每一个层级都包含一个字段paylaod_guess的情况下,当采用bind_layers()的方式时, 该列表会包含下一个层级的信息
>>> p=TCP()
>>> p.payload_guess
[({'dport': 2000}, <class 'scapy.Skinny'>), ({'sport': 2000}, <class 'scapy.Skinny'>), ... )]
AI助手
随后,在需要猜测下一层分类的时候,该函数会执行默认的方法Packet.guess_payload_class()。此函数会遍历列表中的每一个元素,并且这些元素都是一对有序的项目
第一个值是要测试的字段 ( )'dport': 2000
第二个值是猜测的类,如果它匹配 ( Skinny)
因此,默认情况下会遍历列表中的每一个元素以寻找匹配项。如果未发现任何匹配项,则会调用默认函数default_payload_class(). 如果您对这个函数进行了重定义,则会使用您的版本;否则将按照既定流程执行并返回指定的类型信息。
Packet.guess_payload_class()
测试现场有什么guess_payload
调用重载guess_payload_class()
四、Build 构建
构建一个数据包如同搭建每一个部分一样容易。随后,那些魔法使得所有部分自动结合在一起。试试这项神奇的技术吧!
基本的东西
首先需要明确"建立"的概念是什么?通过观察可知,在这个层次中存在多种表示方法:人与组织内部的动态关系以及由自动化系统构成的体系结构。构建的过程则要求我们将该层转化为与机器兼容的形式。
除了第一点之外的第二件事就是确定"何时"建立图层的问题。这个答案其实并不明显;不过只要在您的应用场景中需要用到机器表示的话就需要创建层级:每当数据包在网络中被丢弃或者被写入文件的时候;还有在被转换为字符串的时候也一样......实际上,在这种情况下, 机器表示可以被视为一个巨大的字符串, 所有的层级都会连接在一起
>>> p = IP()/TCP()
>>> hexdump(p)
0000 45 00 00 28 00 01 00 00 40 06 7C CD 7F 00 00 01 E..(....@.|.....
0010 7F 00 00 01 00 14 00 50 00 00 00 00 00 00 00 00 .......P........
0020 50 02 20 00 91 7C 00 00 P. ..|..
AI助手
调用raw()构建数据包:
非实例化字段设置为其默认值
长度自动更新
计算校验和
等等。
事实上,在选择raw()函数而非其他如show2()等方法时,并非随意之举。因为这涉及构建数据包调用的全部功能模块——包括所有相关的Packet.__str__()(在Python 3环境中)或Packet.__bytes__()方法。然而,在这种情况下,默认情况下会触发另一个机制:当__str__()被调用时,则会触发另一个机制build()的执行。
def __str__(self):
return next(iter(self)).build()
AI助手
同样需要明确的概念是,在这一领域中,机器的表示方式通常被视为一个已知的事实。这正是人类与系统交互的核心基础所在。
因此,核心方法是build() (代码已被缩短以仅保留相关部分):
def build(self,internal=0):
pkt = self.do_build()
pay = self.build_payload()
p = self.post_build(pkt,pay)
if not internal:
pkt = self
while pkt.haslayer(Padding):
pkt = pkt.getlayer(Padding)
p += pkt.load
pkt = pkt.payload
return p
AI助手
进而生成当前层级的数据结构,并对负载进行优化分配机制。在完成所有构建后调用该函数以更新后续评估指标(例如校验和)。最后一步是将新数据填入数据包尾部。
显然,在实现上构建一个层与其各个字段相同,这就是do_build()所负责的任务。
build 字段
层的每个字段的构建被调用Packet.do_build():
def do_build(self):
p=""
for f in self.fields_desc:
p = f.addfield(self, p, self.getfieldval(f))
return p
AI助手
构建字段的关键在于实现一个名为addfield()的功能。该方法获取字段的内部视图,并将其赋值给变量p。在常规操作中,此过程通常涉及调用 i2m() 函数,并返回类似于 p.self.i2m(val) 的结果(其中 val=self.getfieldval(f))。
如果val存在,则该函数只需将值以必要的方式格式化. 例如, 若要一个字节, 则将其转换为正确的方法. struct.pack("B", val)
但是,在未设置的情况下事情会变得更为复杂 val。这表明该字段必须在立即或稍后的时间段内进行计算,并且因为缺少了预设默认值而无法自动填充数据。为了确保数据的一致性与完整性,在此情况下该字段必须在立即或稍后的时间段内进行计算。
当前表达感谢i2m()函数的使用。当所有相关信息均可用时,请您考虑将数据输入至某个指定的界限位置。
例如:计算长度直到分隔符
class XNumberField(FieldLenField):
def __init__(self, name, default, sep="\r\n"):
FieldLenField.__init__(self, name, default, fld)
self.sep = sep
def i2m(self, pkt, x):
x = FieldLenField.i2m(self, pkt, x)
return "%02x" % x
def m2i(self, pkt, x):
return int(x, 16)
def addfield(self, pkt, s, val):
return s+self.i2m(pkt, val)
def getfield(self, pkt, s):
sep = s.find(self.sep)
return s[sep:], self.m2i(pkt, s[:sep])
AI助手
在示例中,在i2m()函数内部实现时(或在i2m()函数内部),如果变量x已经存在一个赋值,则该赋值会被转换为其对应的十六进制数值表示。如果没有指定具体数值,则该函数将返回一个空字符串(即长度为零)。
该胶水基于Packet.do_build()函数实现,并通过调用Field.addfield()层中的每个字段提供相应的功能;同时会反向触发Field.i2m()方法:当该值存在时,则会生成相应的结构层。
处理默认值:post_build
当字段相邻时,在某些情况下(如字段未预先定义),给定字段可能没有预设值可取用。举例来说,在层中引用前定义的a变量,并将其传递到构建数据包的过程中是有必要的。然而,在这种情况下,默认设置将无法应用(如未使用i2m()函数),因此结果将是无效或缺失的信息。
这个问题的答案是Packet.post_build()。
在调用该方法的过程中(即调用此方法时),数据包已预先构建完毕,并且仍需对某些字段进行计算。这些计算通常用于生成校验和或其他相关参数。实际上,在某些情况下(例如当某个字段的值依赖于尚未获取的数据项时),这一操作是必不可少的。
所以让我们假设有这样一个数据包包含着XNumberField并对其构建过程进行详细分析
class Foo(Packet):
fields_desc = [
ByteField("type", 0),
XNumberField("len", None, "\r\n"),
StrFixedLenField("sep", "\r\n", 2)
]
def post_build(self, p, pay):
if self.len is None and pay:
l = len(pay)
p = p[:1] + hex(l)[2:]+ p[2:]
return p+pay
AI助手
在函数post_build()被调用的时候,在这里p代表当前层级;pay即为有效的负载量,则表示已构建内容的总量。为了得到所需的长度值,在此情况下应覆盖放置于分隔符后方全部数据的总跨度;因此我们需要将这一数值计算结果整合进`.
>>> p = Foo()/("X"*32)
>>> p.show2()
###[ Foo ]###
type= 0
len= 32
sep= '\r\n'
###[ Raw ]###
load= 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
AI助手
len现在正确计算:
>>> hexdump(raw(p))
0000 00 32 30 0D 0A 58 58 58 58 58 58 58 58 58 58 58 .20..XXXXXXXXXXX
0010 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX
0020 58 58 58 58 58 XXXXX
AI助手
机器表示是预期的。
处理默认值:自动计算
基于我们之前的观察发现,在软件架构设计中, 剖析机制主要建立在不同层级之间形成的连接之上. 然而, 在开发过程中这种架构同样发挥着重要作用.
在layerFoo()中定义的第一个字节属于类型字段(type field),它决定了后续的行为模式:当type值为0时代表后续一层为Bar0类的对象实例;当type值为1时则代表后续一层为Bar1类的对象实例;依此类推。我们希望通过分析后续内容来自动配置该字段值以简化流程。
class Bar1(Packet):
fields_desc = [
IntField("val", 0),
]
class Bar2(Packet):
fields_desc = [
IPField("addr", "127.0.0.1")
]
AI助手
当我们仅使用这些类时,在解析数据包方面会遇到困难。即使我们手动构建了数据包以实现显式构建功能,并未成功将Foo层与倍数绑定show2()方法
>>> p = Foo()/Bar1(val=1337)
>>> p
<Foo |<Bar1 val=1337 |>>
>>> p.show2()
###[ Foo ]###
type= 0
len= 4
sep= '\r\n'
###[ Raw ]###
load= '\x00\x00\x059'
AI助手
问题:
当前type的值仍为 0,并非不可行地进行赋值使其变为 1。然而,在直接调用时可能会遇到不便之处。具体来说,在代码中我们可以表示为 p = \frac{Foo( type = 1 )}{ Bar_0( val = 1337 ) }
该数据包经过了彻底的解剖分析,并且其中的变量 Bar1 被视为 Raw. 在函数调用 Foo() 中出现的原因是因为在与另一个变量 Bar*() 之间未配置相应的链接.
为了掌握我们应该做什么以获得正确的行为,并必须分析这些层级是如何组合在一起的。当通过运算符将两个独立的数据包实例Foo()和Bar1(val=1337)结合时会生成一个新的数据包,在这种情况下会产生新的数据包……其中这两个原有的实例会被复制(即它们成为结构不同但拥有相同值的不同对象):
def __div__(self, other):
if isinstance(other, Packet):
cloneA = self.copy()
cloneB = other.copy()
cloneA.add_payload(cloneB)
return cloneA
elif type(other) is str:
return self/Raw(load=other)
AI助手
运算符右侧变成左侧的有效载荷这一现象是由于 调用add_payload()函数所导致 的
注意:我们能够注意到,在其他参数并非被实例化为 Packet 类型的字符串而是普通的字符串时,Raw 会自动创建相应的对象完成任务。例如,在下面的示例中我们可以看到这一过程:
>>> IP()/"AAAA"
<IP |<Raw load='AAAA' |>>
AI助手
那么add_payload()的功能是什么?仅仅是一个数据包间的连接吗?此外,在我们的案例中,该方法能够正确地将所需类型的数据赋值给相应的字段。
不言而喻地而言,在'/'右侧收集的值会被用来将字段设置至'/'左侧下方的位置。正如前面所述,在相邻层级之间建立了一种双向绑定关系能够实现数据在不同层级间的动态交互。
再次强调一下,在我们的案例中,则要求我们:相关的信息应由bind_layers()传递给bind_top_down()机制。该机制会负责整合数据并实现字段映射。
bind_layers( Foo, Bar1, {'type':1} )
bind_layers( Foo, Bar2, {'type':2} )
AI助手
然后,在这个过程中,
首先,
add\_payload() 执行以下操作:
- 对顶层数据包进行遍历操作;
- 收集与底层数据包相关的相关字段;
- 最后将这些相关字段插入到目标加载项中。
当下,在请求获取该字段值的过程中,函数getfieldval()会被系统所识别并赋值为overladed\_fields中的数据
这些字段在三个字典之间分派:
fields: 值已被显式设置的字段,如 pdstTCP ( pdst='42')
overloaded_fields: 重载字段
default_fields:具有默认值的所有字段(这些字段
fields_desc由构造函数通过调用init_fields())进行初始化。
在提供的代码示例中,我们能够查看字段如何被选中及其相应的值是如何返回的。
def getfieldval(self, attr):
for f in self.fields, self.overloaded_fields, self.default_fields:
if f.has_key(attr):
return f[attr]
return self.payload.getfieldval(attr)
AI助手
插入字段 fields 处于更高优先级位置;接着 overloads... 最后是 default_fields. 由此可见,在这种情况下,当该字段 type 设置为 overloaded_fields 时,则会返回其值而非 default_fields 包含的值.
我们现在能够理解它背后的所有魔力!
>>> p = Foo()/Bar1(val=0x1337)
>>> p
<Foo type=1 |<Bar1 val=4919 |>>
>>> p.show()
###[ Foo ]###
type= 1
len= 4
sep= '\r\n'
###[ Bar1 ]###
val= 4919
AI助手
我们的两个问题在我们没有做太多的情况下就解决了
Under the hood:把一切都放在一起
最后但同样重要的是,了解构建数据包时何时调用每个函数非常有用:
>>> hexdump(raw(p))
Packet.str=Foo
Packet.iter=Foo
Packet.iter=Bar1
Packet.build=Foo
Packet.build=Bar1
Packet.post_build=Bar1
Packet.post_build=Foo
AI助手
它依次遍历每个字段的列表,并构造它们。当完成整个层级结构的构建后,随后将执行相应的操作
五、字段
以下是 Scapy 开箱即用支持的字段列表:
1. 简单数据类型
Legend:
X- 十六进制表示
LE- 小端(默认为大端 = 网络字节顺序)
Signed- 有符号(默认为无符号)
ByteField
XByteField
ShortField
SignedShortField
LEShortField
XShortField
X3BytesField # three bytes as hex
LEX3BytesField # little endian three bytes as hex
ThreeBytesField # three bytes as decimal
LEThreeBytesField # little endian three bytes as decimal
IntField
SignedIntField
LEIntField
LESignedIntField
XIntField
LongField
SignedLongField
LELongField
LESignedLongField
XLongField
LELongField
IEEEFloatField
IEEEDoubleField
BCDFloatField # binary coded decimal
BitField
XBitField
BitFieldLenField # BitField specifying a length (used in RTP)
FlagsField
FloatField
AI助手
2. Enumerations
可能的字段值取自给定的枚举(列表、字典等),例如:
ByteEnumField("code", 4, {1:"REQUEST",2:"RESPONSE",3:"SUCCESS",4:"FAILURE"})
EnumField(name, default, enum, fmt = "H")
CharEnumField
BitEnumField
ShortEnumField
LEShortEnumField
ByteEnumField
IntEnumField
SignedIntEnumField
LEIntEnumField
XShortEnumField
AI助手
3. 字符串
StrField(name, default, fmt="H", remain=0, shift=0)
StrLenField(name, default, fld=None, length_from=None, shift=0):
StrFixedLenField
StrNullField
StrStopField
AI助手
4. 列表和长度
FieldList(name, default, field, fld=None, shift=0, length_from=None, count_from=None)
# A list assembled and dissected with many times the same field type
# field: instance of the field that will be used to assemble and disassemble a list item
# length_from: name of the FieldLenField holding the list length
FieldLenField # holds the list length of a FieldList field
LEFieldLenField
LenField # contains len(pkt.payload)
PacketField # holds packets
PacketLenField # used e.g. in ISAKMP_payload_Proposal
PacketListField
AI助手
可变长度字段
本节讨论如何利用Scapy工具处理可变长度的字段。这些字段通常由另一端指定其长度信息,并被我们称作变量长度域(varfield)和长度域(lenfield)。我们将这类字段分别称为变量长度域(varfield)和长度域(lenfield)。这种方法的核心思想是通过引用域来间接获取域的大小信息。具体而言,在解析数据包时,系统会利用varfield的值来推导出相应数据段的大小,并且这一过程无需显式填充lenfield值即可直接计算出其大小。
当您意识到lenfield与varfield之间的关系并非总是直接明了时,就会出现问题.有时,lenfield表示以字节为单位的长度,而有时它表示对象的数量.如果长度包含标题部分,您必须从总长度中减去固定的标题长度才能推导出varfield的长度.此外,有时长度是以16位字为单位计算的.另外,有时候不同的varfield会共享相同的lenfield.最后,同一个varfield可能会被多个不同的lenfield引用,其中有些引用基于字节单位,而另一些则基于16位字单位.
长度字段
首先,请定义一个名为 lenfield 的字段,并通过 FieldLenField 或其派生类进行声明。如果在数据包组装过程中该字段值未被赋值,则该字段的值将根据 varfield 进行推导。请注意以下几点:当 varfield 为包含列表(如 PacketListField) 的类型时,请通过指定 length\_of 或 count\_of 参数来完成对该字段的引用。特别地,在这种情形下,请确保所选参数能够正确反映相关属性的信息量大小关系:依据所选参数配置,请调用相应的 i2len() 或 i2count() 方法以获取所需信息。返回值将通过 adjust 参数中提供的函数进行调整。adjust 将应用于 两个参数:数据包实例和 i2len() or 返回的值 i2count(). 默认情况下, getDefault() 不做任何调整:
adjust=lambda pkt,x: x
AI助手
例如,如果the_varfield是一个列表
FieldLenField("the_lenfield", None, count_of="the_varfield")
AI助手
或者如果长度是 16 位字:
FieldLenField("the_lenfield", None, length_of="the_varfield", adjust=lambda pkt,x:(x+1)/2)
AI助手
可变长度字段
varfield 包含以下字段名:StrLen、PacketLength、PacketList、FieldsList等
在最初的两个阶段,在数据包被解析的过程中,其长度可由已解析的部分推导得出。通过length_from参数来建立链接关系这一机制工作原理是这样的:该参数基于一个函数作用于部分解析的数据包,并返回所对应的字段长度值(单位:字节)。举个例子来说:假设我们有一个数据包被部分解密后包含了子协议的信息,则该机制会根据这些信息计算出正确的字段长度并将其应用到链接设置中。
StrLenField("the_varfield", "the_default_value", length_from = lambda pkt: pkt.the_lenfield)
AI助手
或者
StrLenField("the_varfield", "the_default_value", length_from = lambda pkt: pkt.the_lenfield-12)
AI助手
对于PacketListField与FieldListField及其相关类别的字段,在涉及长度属性时,请参考前述说明。若这些字段涉及多个元素,则应在配置参数中避免使用length_from参数,并应采用count_from参数来替代。例如:
FieldListField("the_varfield", ["1.2.3.4"], IPField("", "0.0.0.0"), count_from = lambda pkt: pkt.the_lenfield)
AI助手
例子
class TestSLF(Packet):
fields_desc=[ FieldLenField("len", None, length_of="data"),
StrLenField("data", "", length_from=lambda pkt:pkt.len) ]
class TestPLF(Packet):
fields_desc=[ FieldLenField("len", None, count_of="plist"),
PacketListField("plist", None, IP, count_from=lambda pkt:pkt.len) ]
class TestFLF(Packet):
fields_desc=[
FieldLenField("the_lenfield", None, count_of="the_varfield"),
FieldListField("the_varfield", ["1.2.3.4"], IPField("", "0.0.0.0"),
count_from = lambda pkt: pkt.the_lenfield) ]
class TestPkt(Packet):
fields_desc = [ ByteField("f1",65),
ShortField("f2",0x4244) ]
def extract_padding(self, p):
return "", p
class TestPLF2(Packet):
fields_desc = [ FieldLenField("len1", None, count_of="plist",fmt="H", adjust=lambda pkt,x:x+2),
FieldLenField("len2", None, length_of="plist",fmt="I", adjust=lambda pkt,x:(x+1)/2),
PacketListField("plist", None, TestPkt, length_from=lambda x:(x.len2*2)/3*3) ]
AI助手
测试FieldListField类:
>>> TestFLF("\x00\x02ABCDEFGHIJKL")
<TestFLF the_lenfield=2 the_varfield=['65.66.67.68', '69.70.71.72'] |<Raw load='IJKL' |>>
AI助手
5. Special
Emph # Wrapper to emphasize field when printing, e.g. Emph(IPField("dst", "127.0.0.1")),
ActionField
ConditionalField(fld, cond)
# Wrapper to make field 'fld' only appear if
# function 'cond' evals to True, e.g.
# ConditionalField(XShortField("chksum",None),lambda pkt:pkt.chksumpresent==1)
# When hidden, it won't be built nor dissected and the stored value will be 'None'
PadField(fld, align, padwith=None)
# Add bytes after the proxified field so that it ends at
# the specified alignment from its beginning
BitExtendedField(extension_bit)
# Field with a variable number of bytes. Each byte is made of:
# - 7 bits of data
# - 1 extension bit:
# * 0 means that it is the last byte of the field ("stopping bit")
# * 1 means that there is another byte after this one ("forwarding bit")
# extension_bit is the bit number [0-7] of the extension bit in the byte
MSBExtendedField, LSBExtendedField # Special cases of BitExtendedField
AI助手
6. TCP/IP
IPField
SourceIPField
IPoptionsField
TCPOptionsField
MACField
DestMACField(MACField)
SourceMACField(MACField)
ICMPTimeStampField
AI助手
7. 802.1
Dot11AddrMACField
Dot11Addr2MACField
Dot11Addr3MACField
Dot11Addr4MACField
Dot11SCField
AI助手
8. 域名系统
DNSStrField
DNSRRCountField
DNSRRField
DNSQRField
AI助手
9. ASN.1
ASN1F_element
ASN1F_field
ASN1F_INTEGER
ASN1F_enum_INTEGER
ASN1F_STRING
ASN1F_OID
ASN1F_SEQUENCE
ASN1F_SEQUENCE_OF
ASN1F_PACKET
ASN1F_CHOICE
AI助手
10. 其他协议
NetBIOSNameField # NetBIOS (StrFixedLenField)
ISAKMPTransformSetField # ISAKMP (StrLenField)
TimeStampField # NTP (BitField)
AI助手
六、设计模式
一些模式类似于许多协议,因此可以在 Scapy 中以相同的方式描述。
以下部分将介绍实现新协议时可以遵循的几种模型和约定。
字段命名约定
目标是保持数据包的书写流畅和直观。基本说明如下:
避免在字段名称中使用`Packet.slots``列表中的任意一个值(如名称、时间或原始信息),因为这些数据专门用于Scapy内部操作。
使用倒驼峰式和常用缩写(例如 len、src、dst、dstPort、srcIp)。
建议在可能或相关的情况下优先采用规范中的术语;这些术语的使用有助于新用户方便地生成数据集。
向 Scapy 添加新协议
该协议能够进入 scapy/layers 或者 scapy/contrib 。位于 scapy/layers 中的协议通常可以在公共网络中发现;而属于 scapy/contrib 的协议通常是不常见或者是特定用途的应用。
正确地说,在网络协议架构中,
类型不应直接引入其他相关类型,
但支持同一层架构的其他类型
能够同时实现不同功能需求。
Scapy 包含了一个名为explore()的功能模块,该功能可被用于搜索可用的 layer 和 contrib 模块.由于这些 contrib 模块将反馈信息回 Scapy,因此每个 contrib 模块都必须支持提供与之相关的详细信息.有意地,这些反馈机制的存在是为了确保 Scapy 能够获取所有必要的元数据.
该处放置改写后的内容
该处放置改写后的内容
1. # scapy.contrib.description = [...]
2. # scapy.contrib.status = [...]
3. # scapy.contrib.name = [...] (optional)
AI助手
当contrib组件不含任何数据字段且不应出现在explore函数中时,请您进行配置。
# scapy.contrib.status = skip
AI助手
层 模块必须有一个文档字符串,其中第一行简短地描述了模块。
七、调用 Scapy 函数
本节提供了一些示例,展示如何在您自己的代码中从 Scapy 函数中受益。
UDP校验和
下面将提供一个具体的例子来演示如何手动计算_checksum函数以及UDP校验码的计算方法。
按照 RFC768 中的描述计算 UDP 伪标头
使用 p[UDP].chksum=0 使用 Scapy 构建一个 UDP 数据包
使用伪标头和 UDP 数据包调用 checksum()
1. from scapy.all import *
2.
3. # Get the UDP checksum computed by Scapy
4. packet = IP(dst="10.11.12.13", src="10.11.12.14")/UDP()/DNS()
5. packet = IP(raw(packet)) # Build packet (automatically done when sending)
6. checksum_scapy = packet[UDP].chksum
7.
8. # Set the UDP checksum to 0 and compute the checksum 'manually'
9. packet = IP(dst="10.11.12.13", src="10.11.12.14")/UDP(chksum=0)/DNS()
10. packet_raw = raw(packet)
11. udp_raw = packet_raw[20:]
12. # in4_chksum is used to automatically build a pseudo-header
13. chksum = in4_chksum(socket.IPPROTO_UDP, packet[IP], udp_raw) # For more infos, call "help(in4_chksum)"
14.
15. assert(checksum_scapy == chksum)
AI助手
