添加新的协议
在Scapy中添加新的协议(或者是更加的高级:新的协议层)是非常容易的。所有的魔法都在字段中,如果你需要的字段已经有了,你就不必对这个协议太伤脑筋,几分钟就能搞定了。
简单的例子
每一个协议层都是Packet类的子类。协议层背后所有逻辑的操作都是被Packet类和继承的类所处理的。一个简单的协议层是被一系列的字段构成,他们关联在一起组成了协议层,解析时拆分成一个一个的字符串。这些字段都包含在名为fields_desc的属性中。每一个字段都是一个field类的实例:
在这个例子中,我们的协议层有三个字段,第一个字段是一个2个字节的短整型字段,名字为mickey,默认值是5,第二个字段是1个自己的整形字段,名字为minnie,默认值是3,普通的ByteField和XByteField之间的唯一不同的就是首选的字段值是十六进制。最后一个字段是一个4个字节的整数字段,名字为donald,他不同于普通的IntField类型的是他有一些更小的值供选择,类似于枚举类型,比如说,如果他的值是3的话则显示angry。此外,如果’cool‘值被关联到这个字段上,他将会明白接受的是2.
如果你的协议像上面这么简单,他已经可以用了:
本章讲解了用Scapy如何构建一个新的协议,这里有两个目标:
分解:这样做是为了当接收到一个数据包时(来自网络或者是文件)能够被转化成Scapy的内部结构。
构建:当想发送一个新的数据包时,有些填充数据需要被自动的额调整。
协议层
在深入剖析之前,让我们来看看数据包是怎样组织的。
我们很感兴趣这两个内部的字段类Packet:
p.underlayer
p.payload
这里是主要的“伎俩”。你不必在乎数据包,只关注协议层,一个堆在另一个上面。
一个可以通过协议层的名字容易的访问协议层:p[TCP]返回的是TCP和下面的协议,这是p.getlayer(TCP)的一个快捷方式。
注意:这里有一个可选的参数nb,用来返回所需协议的第几层协议层。
让我们将所有的放在一起,玩玩这个TCP协议层:
不出所料,tcp.underlayer指向的是我们IP数据包的开始,而tcp.payload是他的有效载荷。
构建一个新的协议层
非常简单!一个协议层就是由一系列的字段构成。让我们来看看UDP的定义:
为了方便,内部已经定义了许多字段,看看文档“W”的源码Phil会告诉你的。(这句我也不知道原文是什么意思)。
所以,定义一个协议层就是简单的组合一系列的字段,现在的目标是为每个字段提供有限的默认值,所以当用户构建数据包的时候不必给他们赋值。
主要的机制是基于Field结构,要牢记协议层就只是一系列的字段,不用记得太多。
所以,为了理解协议层是怎样工作的,一个就是需要快速的看看字段是怎么被处理的。
操作数据包==操作他们的字段
一个字段应该被考虑到有多种状态
i (internal) :这是Scapy怎样操作它们的方法。
m (machine) :这是真正的数据,这就是他们在网络上的样子。
h (human) :如何将数据展示给人们看。
这解释了一些神秘的方法i2h(),i2m(),m2i()可以用于每一个字段:他们都是将一种状态转换成另一种状态,用于特殊的用途。
其他特殊的方法有:
any2i():猜测输入的状态装换为internal状态。
i2repr():比i2h()更好。
然而,所有的这些都是底层的方法。用于添加或提取字段到当前的协议的方法是:
addfield(self, pkt, s, val):复制网络上原始的字段值val(属于pkt协议层的)到原始的字符串数据包s:
getfield(self, pkt, s):从原始的数据包s中提取出属于协议层pkt的字段值。他返回一个序列,第一个元素是移除了被抽取的字段值的原始的数据包字符串,第二个元素是被抽取字段的internal的表示状态:
当定义你自己的协议层,你通常只需要定义一些*2*()方法,有时候也会有addfield()和getfield()方法。
示例:可变长度的数值
在协议中经常使用可变长度的数值的方法来表示整数,例如处理信号进程(MIDI)。
每一个数值的字节的MSB编码被设置为1,除了最后一个字节。比如,0x123456将会编码为0xC8E856:
我们将定义一个字段,该字段将自动计算相关联的字符串的长度,但会使用该编码格式:
现在用这种字段定义一个协议层:
这里,len不必被计算,默认值会被直接显示的,这是目前我们协议层internal的表示,让我们强行来计算一下:
show2()方法显示这些字段被发送到网络中时的值,但是为了人类阅读方便,我们看到len=129。最后但并非最不重要,让我们来看看machine的表示:
前两个字节是\x81\x01,是129编码后的结果。
剖析
协议层只是一系列的字段,但是每一个字段之间使用什么连接的,协议层之间呢?这一节我们将解释这个秘密。
基本的填充数据
剖析数据包的核心的方法是Packet.dissect()。
当被调用时,s是一个将要被剖析的字符串,self指向当前协议层。
Packet.dissect()被调用了三次:
1.解析”A”*20为IPv4头
2.解析”B”*32为TCP头
3.到此为止数据包还有12个字节,他们将被解析成原始”Raw”的数据(有一些是默认的协议层类型)。
当传入一个协议层的时候,一切都很简单:
pre_dissect()在协议层之前被调用。
do_dissect()执行协议层真正的解析。
post_dissection()当解析时需要更新输入的时候被调用(比如说,解密,解压缩)
extract_padding()是一个重要的方法,应该被每一层所调用控制他们的大小,所以他可以被用来区分真正相关联的协议层的有效载荷,并且什么将被视为额外的填充字节。
do_dissect_payload()方法主要负责剖析有效载荷(如果有的话)。它基于guess_payload_class()(见下文),一旦是已知类型的有效荷载,该有效载荷将会以新的类型绑定到当前协议层:
最后,数据包中所有的协议层都被解析了,并和已知的类型关联在一起。
剖析字段
所有协议层和它的字段之间的魔法方法是do_dissect()。如果你已经理解了协议层的不同的表示,你也应该理解剖析一个协议层就是将构建它的字段从machine表示转换到internal表示。
猜猜是什么?这正是do_dissect()干的事:
所以,他接受原始的字符串数据包,并进入每一个字段,只要还有数据或者字段剩余:
当编写FOO(“\xff\xff”+”B”*8)的时候,他调用do_dissect()方法。第一个字段是
VarLenQField,因此他接收字节,只要MSB设置过,因此,一直到(包括)第一个”B”。这个映射做到了多亏了VarLenQField.getfield(),并且可以进行交叉检查:
然后,下一个字段以相同的方法被提取,直到2097090个字节都放进FOO.data中(或者更少,如果2097090是不可用的)。
如果当剖析完后还剩下一些字节,他们将以相同的方式映射到下一步要处理的(默认是Raw):
因此,现在我们该理解协议层是怎样被绑定在一起的。
绑定协议层
Scapy在解析协议层时一个很酷的特性是他试图猜测下一层协议是什么。连接两个协议层官方的方法是bind_layers():
比如说,如果你有一个HTTP类,你可能会认为所有的接受或者发送的数据包都是80端口的,你将会这样解码,下面是简单的方式:
这就是所有的啦!现在所有和80端口相关联的数据包都将被连接到HTTP协议层上,不管他是从pcap文件中读取的,还是从网络中接收到的。
guess_payload_class()的方法
有时候,猜测一个有效载荷类不是像定义一个单一的端口那么简单。比如说,他可能依赖于当前协议传入的一个字节值。有两个方法是必须的:
guess_payload_class()必须返回有效载荷的guessed类(下一层协议层的)。默认情况下,它使用类之间已有的关联通过bind_layer()放到合适的位置。
default_payload_class()返回默认的值。这个方法在Packet类中定义返回Raw,但是他能被重载。
比如说,解码802.11的变化取决于他是否被加密:
这里有需要的几点意见:
这些事是使用bind_layer()不可能完成的,因为测试中应该是”field==value”,但是这里我们测试的字段值比单一的字节要发杂。
如果测试失败了,没有这种假设,我们会回到默认的机制调用Packet.guess_payload_class()。
大多数时间,定义一个guess_payload_class()方法是没有必要的,可以从bind_layers()得到相同的结果。
改变默认的行为
如果你不喜欢Scapy得到协议层后的行为,你也可以通过调用split_layer()来改变或者禁用这些行为。比如说你不想要UDP/53绑定到DNS协议,只需要添加代码“
split_layers(UDP, DNS, sport=53)
”,现在所有源端口是53的数据包都不会当做DNS协议处理了,但是什么东西你要做特殊处理。
在后台:将所有的东西都放在一起
事实上,每一个协议层都有一个字段的guess_payload。当你使用bind_layers()的方式,他将定义的下一个添加到该列表中。
然后,当他需要猜测下一个协议层类,他调用默认的方法Packet.guess_payload_class(),该方法通过
payload_guess序列的
每一个元素,每一个元素都是一个元组:
第一个值是一个字段,我们用(‘dport’:2000)测试
第二个值是guessed类,如果他匹配到Skinny
所以,默认的guess_payload_class()尝试序列中所有的元素,知道偶一个匹配到,如果没发现一个元素,他将调用default_payload_class()。如果你重新定义了这个方法,你的方法将会被调用,否则,默认的方法会被调用,Raw将会被返回。
Packet.guess_payload_class()
测试字段中有什么guess_payload
调用被重载的guess_payload_class()
构建
构建一个数据包跟构建每一个协议层一样简单,然后一些魔法的事情发生了当关联一切的时候,让我们来试一试这些魔法。
基本的填充数据
首先要明确,构建是什么意思?正如我们所看到的,一个协议层能被不同的方法所表示
(human, internal, machine),构建的意思是转换到machine格式。
第二个要理解的事情是什么时候一个协议层将会被构建。答案不是那么明显,但是当你需要machine表示的时候,协议层就被构建了:当数据包在网络上被丢弃或者写入一个文件,当他装换成一个字符串,。。。事实上,machine表示应该被视为附加了协议层的大字符串。
调用str()构建这个数据包:
没有实例化的字段设置他们的默认值
长度自动更新
计算校验和
等等
事实上,使用str()而不是show2()不是一个随机的选择,就像所有的函数构建数据包都要调用Packet.__str__(),然而,__str__()调用了另一个函数:build():
重要的也是要理解的是,通常你不必关心machine表示,这就是为什么human和internal也在这里。
所以,核心的函数式build()(改代码被缩短了只保留了相关的部分):
所以,他通过构建当前协议层开始,然后是有效载荷,并且post_build()被调用更新后期的一些评估字段(像是校验和),最后将填充数据添加到数据包的尾部。
当然,构建一个协议层和构建它的字段是一样的,这正是do_build()干的事。
构建字段
构建每一个协议层的每一个字段都会调用Packet.do_build():
构建字段的核心函数是getfield(),他接收internal字段视图,并将它放在p的后面。通常,这个方法会调用i2m()并返回一些东西,如p.self.i2mval(val)(在val=self.getfieldval(f)处)。
如果val设置了,i2m()只是一个必须使用的格式化的方法,不如,如果预期是一个字节,struct.pack(‘B’,val)是在正确转化他的方法。
然而,如果val没有被设置,事情将会变得更加复杂,这就意味着不能简单的提供默认值,然后这些字段现在或者以后需要计算一些“填充数据”。
“刚刚好”意味着多亏了i2m(),如果所有的信息将是可用的。如果你必须处理一个长度直到遇到一个分隔符。
比如说:计算一个长度直到遇到一个分隔符:
在这个例子中,在i2m()中,如果x已经有一个值,他将装换为十六进制。如果没有提供任何值,将会返回0。
关联由Packet.do_build()提供,他为协议层的每一个字段调用Field.addfield()并以此调用Field.i2m():如果值是有效的,协议层将会被构建。
处理默认值:do_build()
字段给定的默认值有时候也不知道或者不可能知道什么时候将字段放在一起。比如说,如果我们在协议层中使用预先定义的XNumberField,我们希望当他被构建是被设置一个被给定的值,然后如果他没有被设置i2m()不会返回任何值。
这个问题的答案是Packet.post_build()。
当这个方法被调用,数据包已经被构建了,但是有些字段还是需要被计算,一个典型的例子就是需要计算检验和或者是长度。这是每一个字段每次都取决于一些东西,而不是当前需要的。所以,让我们假设我们有一个有XNumberField的数据包来看看他的构建过程:
当post_build()被调用的时候,p是当前的协议层,pay是有效载荷,这就已经构建好了,我们想要我们的长度是将所有的数据都放到分隔符之后的全部长度,所以我们在post_build()中添加他们的计算。
len现在正确的被计算:
而且machine也是期望的那样。
处理默认值:自动计算
像我们向前看到的那样,剖析机制是建立在程序员创造的协议层之间的连接之上的。然而,他也可以用在构建的过程中。
在协议层Foo(),我们第一个字节的类型是在下面定义的,比如说,如果type=0,下一层协议层是Bar0,如果是1,下一层是协议层是Bar1,以此类推。我们希望字段在下面自动设置。
如果我们除此之外没有做其他的事情,我们在解析数据包的时候将会有麻烦,不会有任何的Bar*绑定在Foo协议层,甚至是当我们通过调用show2()函数明确的构建数据包时也没有。
问题:
1.type还是等于0当我们将它设置为1的时候,我们当然可以通过p=Foo(type=1)/Bar0(val=1337)来构建p,但是这样不方便。
2.当Bar1注册为Raw的时候,数据包将会被错误的解析。这是因为Foo()和Bar*()之间没有设置任何的连接。
为了理解我们应该怎么做才能获得适当的行为,我们必须看看协议层是怎么组装的。当两个独立的数据包实例Foo()和Bar1(val=1337)通过分隔符”/”连接在一起的时候,将产生一个新的数据包,先前的实例被克隆了(也就是说这来了明确的对象构造不同,但是持有相同的值)。
操作符右边的是左边的有效载荷,这种行为是通过调用add_payload()完成的。最后返回一个新的数据包。
我们可以观察到,如果other是一个字符串而不是一个数据包,Raw将会从payload实例化得来。就像下面的例子:
这样的话add_payload()该执行什么?只是将两个数据包关联在一起吗?不仅仅是这样,在我们的例子中,该方法会适当的设置当前的值给type。
本能的我们可以感觉到上层协议(‘/’右边的协议层)能收集值设置给下层协议(‘/’左边的协议层)。看看向前的解释,这有一个方便的机制来指定两个相邻协议层之间的绑定。
再一次,这些信息必须提供给bind_layer(),内部将调用bind_top_down()让这些字段被重载,在这种情况下,我们需要指定这些:
然后,add_payload()遍历上面数据包
(payload)
的overload_fields,得到这些字段相关联的底层数据包(通过他们的type)并插入她们到overloaded_fields。
现在,当这个地段的值被请求,getfieldval()将返回插到overloaded_fields中的值。
字段被处理有三个方向:
fields:明确被设置的字段值,像是pdst在TCP中是(pdst=’42’)
overloaded_fields:重载字段
default_fields:所有的字段都是他们的默认值。(这些字段根据fields_desc的初始化构造函数调用init_fields())
在下面代码中,我们可以观察到一个字段是如何选择的并且看到他的返回值:
字段被插入到fields有更高的权限,然后是overloaded_fields,最后是default_fields,因此如果字段的type在overloaded_fields中设置,他的值将会被返回而不是在
default_fields中获取。
现在我们理解了背后的所有的魔法了!
我们的两个问题都解决了,而没有发太多的功夫。
理解底层:把所有的东西放在一起
最后但不是不重要,理解当构建数据包的时候每一个函数什么时候被调用是很重要的。
正如你所看到的,他首先运行序列的每一个字段,然从头开始构建,一旦所有的协议层构建好了,他们从头开始调用post_build()。
字段
这里列出了一些Scapy支持的字段。
简单的数据类型
表示:
X — 十六进制表示
LE — 小端(默认是大端)
Signal — 有符号的(默认是无符号的)
枚举
字段的值可能来自枚举
字符串
序列和定长长度
可变长度字段
这是关于Scapy怎么处理字段的可变长度的。这些字段通常可以从另外的字段那知道他们的长度,我们称他们为可变字段和定长字段。其思想是让每一个字段都引用另一个字段,这样当数据包被剖析时,可变就可以从定长字段那知道自己的长度,如果数据包时被组合的,你不必填充满定长字段,直接可以从可变长度推测他的值。
问题出现在你意识到可变长度字段和定长字段之间的关系并不总是明确的。有时候定长字段指示了字节长度,有时候是对象的数量。有时候长度包含首部部分,所以你必须减去固定的头部长度来推算出可变字段的长度。有时候长度不是以字节而是以16位来表示的,有时候相同的不变字段被两个不同的可变字段使用,有时候相同的可变字段引用不同的不可变字段,一个是一个字节,一个是来那个字节。
定长字段
首先,一个定长字段是用FieldLenField定义的(或者是他的派生)。当组装数据包的时候如果他的值是空,他的值将会从引用他的可变长度字段推倒出来。引用用了其他的length_of参数或者count_of参数,count_of参数只有当可变字段拥有一个序列(PacketListField或者FieldListField)的时候才会有意义。该值将用可变长度字段命名,作为一个字符串。根据那个参数使用i2len()或者 i2count()方法将会在不可变字段值找个调用。返回的值将会被函数调整提供给合适的参数。调整将适用于两个参数:i2len()或者i2count()返回的数据包实例和值。默认情况下,调整是不会做什么事的:
比如说,如果the_varfield是一个序列:
或者如果他是16位的:
可变长度字段
可变长度有:StrLenField, PacketLenField, PacketListField, FieldListField, …
这有两个第一,当一个数据包被剖析时,他们的长度会从已经已经解析的定长字段长度推到来,连接通络使用length_from参数,应用到一个函数,适用于被解析的数据包的一部分,返回一个字节的长度,例如:
或者:
对于PacketListField和FieldListField和他们的派生,当需要长度的时候,工作内容和他们的一样。如果你需要大量的元素,
length_from参数必须被忽略并且
count_from参数必须被替代,比如说:
例子
测试FieldListField类:
特殊的
TCP/IP
802.11
DNS
ASN.1
其他协议