Adding new protocols

在Scapy中添加新协议(或更正确地说是一个新 )非常容易. 所有的魔力都在田野里. 如果您需要的字段已经存在,并且协议没有对大脑造成太大损害,那么这应该只需几分钟.

Simple example

层是Packet类的子类. Packet类别保留了层操作背后的所有逻辑,这些逻辑将被继承. 一个简单的层由一系列字段组成,这些字段在组装层时将被串联在一起,或者在分解字符串时将被一一分开. 字段列表保存在名为fields_desc的属性中. 每个字段都是字段类的一个实例:

class Disney(Packet):
    name = "DisneyPacket "
    fields_desc=[ ShortField("mickey",5),
                 XByteField("minnie",3) ,
                 IntEnumField("donald" , 1 ,
                      { 1: "happy", 2: "cool" , 3: "angry" } ) ]

In this example, our layer has three fields. The first one is a 2-byte integer field named mickey and whose default value is 5. The second one is a 1-byte integer field named minnie and whose default value is 3. The difference between a vanilla ByteField and an XByteField is only the fact that the preferred human representation of the field’s value is in hexadecimal. The last field is a 4-byte integer field named donald. It is different from a vanilla IntField by the fact that some of the possible values of the field have literate representations. For example, if it is worth 3, the value will be displayed as angry. Moreover, if the “cool” value is assigned to this field, it will understand that it has to take the value 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 |>

本章说明如何在Scapy中构建新协议. 有两个主要目标:

  • 剖析:这是在收到(从网络或文件中)数据包并将其转换为Scapy的内部信息时完成的.

  • 建筑物:当要发送这样一个新数据包时,需要在其中自动调整一些内容.

Layers

在深入剖析解剖本身之前,让我们看一下数据包的组织方式.

>>> 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'

我们对Packet类的2个" inside"字段感兴趣:

  • p.underlayer

  • p.payload

这是主要的"技巧". 您不必关心数据包,而只关心一个接一个地堆叠的层.

可以通过其名称轻松访问层: p[TCP]返回TCP及其后续层. 这是p.getlayer(TCP)的快捷方式.

Note

有一个可选参数( nb ),它返回所需协议的第nb层.

现在让我们将所有内容放在一起,并使用TCP层:

>>> tcp=p[TCP]
>>> tcp.underlayer
<IP  frag=0 proto=TCP |<TCP  |<Raw  load='AAAA' |>>>
>>> tcp.payload
<Raw  load='AAAA' |>

正如预期的那样, tcp.underlayer指向IP数据包的开头, tcp.payload指向其有效负载.

Building a new layer

VERY EASY! A layer is mainly a list of fields. Let’s look at UDP definition:

class UDP(Packet):
    name = "UDP"
    fields_desc = [ ShortEnumField("sport", 53, UDP_SERVICES),
                    ShortEnumField("dport", 53, UDP_SERVICES),
                    ShortField("len", None),
                    XShortField("chksum", None), ]

您完成了! 为了方便起见,已经定义了许多字段,请看Phil所说的doc ^ W来源.

因此,定义层只是在列表中收集字段. 这里的目标是为每个字段提供有效的默认值,以便用户在构建数据包时不必提供它们.

主要机制基于Field结构. 始终牢记,图层仅比字段列表多一点,但不多.

因此,要了解各层如何工作,需要快速查看如何处理这些字段.

Manipulating packets == manipulating its fields

字段应以不同的状态考虑:

  • i (nternal) : this is the way Scapy manipulates it.

  • m (achine)this is where the truth is, that is the layer as it is

    在网络上.

  • h (uman):如何将包显示给我们的人眼.

这就解释了每个领域都可以使用的神秘方法i2h()i2m()m2i()等:它们是从一种状态到另一种状态的转换,适用于特定的用途.

其他特殊功能:

  • any2i()猜测输入表示形式并返回内部表示形式.

  • i2repr()更好的i2h()

但是,所有这些都是"低级"功能. 向当前层添加或提取字段的功能是:

  • addfield(self, pkt, s, val) :将字段val的网络表示(属于pkt层)复制到原始字符串包s

    class StrFixedLenField(StrField):
        def addfield(self, pkt, s, val):
            return s+struct.pack("%is"%self.length,self.i2m(pkt, val))
    
  • getfield(self, pkt, s) :从原始数据包s提取属于pkt层的字段值. 它返回一个列表,第一个元素是删除提取字段后的原始数据包字符串,第二个元素是内部表示形式的提取字段本身:

    class StrFixedLenField(StrField):
        def getfield(self, pkt, s):
            return s[self.length:], self.m2i(pkt,s[:self.length])
    

定义自己的图层时,通常只需要定义一些*2*()方法,有时还需要定义addfield()getfield() .

Example: variable length quantities

有一种方法可以在协议中经常使用的可变长度数量上表示整数,例如在处理信号处理(例如MIDI)时.

除最后一个字节外,数字的每个字节均使用MSB设置为1进行编码. 例如,0x123456将被编码为0xC8E856:

def vlenq2str(l):
    s = []
    s.append( hex(l & 0x7F) )
    l = l >> 7
    while l>0:
        s.append( hex(0x80 | (l & 0x7F) ) )
        l = l >> 7
    s.reverse()
    return "".join(chr(int(x, 16)) for x in s)

def str2vlenq(s=""):
    i = l = 0
    while i<len(s) and ord(s[i]) & 0x80:
        l = l << 7
        l = l + (ord(s[i]) & 0x7F)
        i = i + 1
    if i == len(s):
        warning("Broken vlenq: no ending byte")
    l = l << 7
    l = l + (ord(s[i]) & 0x7F)

    return s[i+1:], l

我们将定义一个字段,该字段自动计算关联字符串的长度,但使用该编码格式:

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)

现在,使用这种字段定义一个层:

class FOO(Packet):
    name = "FOO"
    fields_desc = [ VarLenQField("len", None, "data"),
                    StrLenField("data", "", "len") ]

    >>> f = FOO(data="A"*129)
    >>> f.show()
    ###[ FOO ]###
      len= 0
      data=    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

此处, len尚未计算,仅显示默认值. 这是我们层的当前内部表示. 让我们现在强制计算:

>>> f.show2()
###[ FOO ]###
  len= 129
  data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

show2()方法显示字段及其值,因为它们将被发送到网络,但是以一种人类可读的方式,因此我们看到len=129 . 最后但并非最不重要的一点,让我们现在看一下机器表示:

>>> raw(f)
'\x81\x01AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

前2个字节为\x81\x01 ,在此编码中为129.

Dissecting

层仅是字段列表,但是每个字段之间以及之后的每个层之间的粘合是什么. 这些是本节中解释的奥秘.

The basic stuff

解剖的核心功能是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))

调用时, 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' |>>>

Packet.dissect()被调用3次:

  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() (请参见下文). 一旦知道有效负载的类型,就用这种新类型将有效负载绑定到当前层:

    def do_dissect_payload(self, s):
        cls = self.guess_payload_class(s)
        p = cls(s, _internal=1, _underlayer=self)
        self.add_payload(p)
    

最后,将数据包中的所有层都解剖,并用它们的已知类型粘合在一起.

Dissecting fields

在图层及其字段之间具有所有魔力的方法是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

因此,只要有剩余的数据或字段,它就会接收原始字符串包,并向其提供每个字段:

>>> FOO("\xff\xff"+"B"*8)
<FOO  len=2097090 data='BBBBBBB' |>

When writing FOO("\xff\xff"+"B"*8), it calls do_dissect(). The first field is VarLenQField. Thus, it takes bytes as long as their MSB is set, thus until (and including) the first ‘B’. This mapping is done thanks to VarLenQField.getfield() and can be cross-checked:

>>> vlenq2str(2097090)
'\xff\xffB'

然后,以相同的方式提取下一个字段,直到将2097090字节放入FOO.dataFOO.data (如果2097090字节不可用,则将其更少,如此处所示).

如果在当前层的剖析之后还剩下一些字节,则以相同的方式将其映射到预期的下一个字节(默认为Raw ):

>>> FOO("\x05"+"B"*8)
<FOO  len=5 data='BBBBB' |<Raw  load='BBB' |>>

因此,我们现在需要了解如何将图层绑定在一起.

Binding layers

剖析图层时,Scapy的一项很酷的功能是它试图为我们猜测下一层是什么. 链接2层的官方方法是使用bind_layers()函数.

packet模块内部可用,此功能可以按以下方式使用:

bind_layers(ProtoA, ProtoB, FieldToBind=Value)

每次创建数据包ProtoA()/ProtoB()时, FieldToBindProtoA将等于Value .

例如,如果您具有类HTTP ,则可能希望来自端口80或来自端口80的所有数据包都将被解码. 就是这样完成的:

bind_layers( TCP, HTTP, sport=80 )
bind_layers( TCP, HTTP, dport=80 )

那就是所有人! 现在,与端口80相关的每个数据包都将与HTTP层相关联,无论是从pcap文件中读取还是从网络接收.

The guess_payload_class() way

有时,猜测有效载荷类并不像定义单个端口那样简单. 例如,它可以取决于当前层中给定字节的值. 所需的2种方法是:

  • guess_payload_class() which must return the guessed class for the payload (next layer). By default, it uses links between classes that have been put in place by bind_layers().

  • default_payload_class()返回默认值. 在Packet类中定义的此方法返回Raw ,但是可以重载.

例如,解码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)

这里需要一些评论:

  • 这不能使用bind_layers()来完成,因为测试应该是" field==value ",但是在这里,我们测试字段值中的单个位会更加复杂.

  • 如果测试失败,则不做任何假设,我们重新插入调用Packet.guess_payload_class()的默认猜测机制.

大多数时候,定义方法guess_payload_class()并不是必需的,因为可以从bind_layers()获得相同的结果.

Changing the default behavior

如果您不喜欢Scapy对于给定图层的行为,则可以通过调用split_layers()来更改或禁用它. 例如,如果您不希望UDP / 53与DNS绑定,只需添加代码:

split_layers(UDP, DNS, sport=53)

现在,每个带有源端口53的数据包都不会被视为DNS,而是您指定的任何内容.

Under the hood: putting everything together

实际上,每一层都有一个有效负载字段. 当使用bind_layers()方法时,它将定义的下一层添加到该列表中.

>>> p=TCP()
>>> p.payload_guess
[({'dport': 2000}, <class 'scapy.Skinny'>), ({'sport': 2000}, <class 'scapy.Skinny'>), ... )]

然后,当需要猜测下一个图层类时,它将调用默认方法Packet.guess_payload_class() . 此方法遍历列表payload_guess的每个元素,每个元素都是一个元组:

  • 第一个值是要测试的字段( 'dport': 2000

  • 如果匹配( Skinny ),则第二个值是猜测的类别

因此,默认的guess_payload_class()尝试列表中的所有元素,直到一个匹配. 如果未找到任何元素,则调用default_payload_class() . 如果您已重新定义此方法,则将调用your方法,否则,将调用默认方法,并返回Raw类型.

Packet.guess_payload_class()

  • 测试字段中的内容guess_payload

  • 调用重载的guess_payload_class()

Building

构建数据包就像构建每个层一样简单. 然后,发生了一些魔术,将所有东西粘合在一起. 那我们去做魔术吧.

The basic stuff

首先要确定的是:"建造"是什么意思? 如我们所见,可以用不同的方式(人,内部,机器)来表示层. 构建意味着要使用机器格式.

要理解的第二件事是"何时"构建层. 答案不是那么明显,但是只要您需要机器表示,就可以构建层:当数据包在网络上被丢弃或写入文件时,或者当它被转换为字符串时,……实际上,机器表示形式应该被认为是一个大字符串,并且所有层次都附加在一起.

>>> 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. ..|..
Calling raw() builds the packet:
  • 非实例字段设置为其默认值

  • 长度会自动更新

  • 计算校验和

  • 等等.

实际上,使用raw()而不是show2()或任何其他方法并不是一个随机选择,因为构建数据包的所有函数都调用Packet.__str__() (或Python 3下的Packet.__bytes__() ). 但是, __str__()调用了另一个方法: build()

def __str__(self):
    return next(iter(self)).build()

同样重要的是要理解的是,通常您并不关心机器表示,这就是为什么这里有人员表示和内部表示的原因.

因此,核心方法是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

因此,首先从构建当前层开始,然后是有效负载,然后post_build()来更新一些较晚评估的字段(如校验和). 最后,将填充添加到数据包的末尾.

当然,构建层与构建其每个字段相同,这正是do_build()所做的.

Building fields

Packet.do_build()调用层的每个字段的构建:

def do_build(self):
    p=""
    for f in self.fields_desc:
        p = f.addfield(self, p, self.getfieldval(f))
    return p

构建字段的核心功能是addfield() . 它采用该字段的内部视图并将其放在p的末尾. 通常,此方法调用i2m()并返回类似p.self.i2m(val) (其中val=self.getfieldval(f) ).

如果设置了val ,则i2m()只是按照必须的方式格式化该值. 例如,如果需要一个字节,则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])

在此示例中,在i2m() ,如果x已经具有一个值,则将其转换为其十六进制值. 如果没有给出值,则返回长度" 0".

胶水是由Packet.do_build()提供的,它为该层中的每个字段调用Field.addfield() ,而后者又依次调用Field.i2m() :如果值可用,则构建该层.

Handling default values: post_build

给定字段的默认值有时是未知的,或者在将字段放在一起时无法计算. 例如,如果我们使用了先前在层中定义的XNumberField ,则我们希望在构建数据包时将其设置为给定值. 但是,如果未设置,则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

调用post_build()p是当前层, pay有效载荷,即已经建立的有效载荷. 我们希望长度为分隔符之后的数据的全长,因此我们将其计算结果添加到post_build() .

>>> p = Foo()/("X"*32)
>>> p.show2()
###[ Foo ]###
  type= 0
  len= 32
  sep= '\r\n'
###[ Raw ]###
     load= 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

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

And the machine representation is the expected one.

Handling default values: automatic computation

如我们先前所见,解剖机制建立在程序员创建的各层之间的链接上. 但是,它也可以在构建过程中使用.

在该层中Foo()我们的第一个字节是类型,它定义随之而来的,例如,如果type=0 ,下一层是Bar0 ,如果它是1,下一层是Bar1 ,等等. 然后,我们希望根据接下来的内容自动设置此字段.

class Bar1(Packet):
    fields_desc = [
          IntField("val", 0),
          ]

class Bar2(Packet):
    fields_desc = [
          IPField("addr", "127.0.0.1")
          ]

如果不使用这些类,则在解剖数据包时会遇到麻烦,因为即使没有通过show2()调用显式构建数据包,也没有东西将Foo层与多个Bar*绑定在一起:

>>> 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'

Problems:

  1. 当我们希望将其自动设置为1时, type仍等于0.我们当然可以用p = Foo(type=1)/Bar0(val=1337)构建p ,但这不是很方便.

  2. 由于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)

操作员的右侧成为左侧的有效负载. 这是通过调用add_payload() . 最后,返回新的数据包.

注意:我们可以观察到,如果其他不是Packet而是字符串,则Raw类将实例化以形成有效负载. 像这个例子:

>>> IP()/"AAAA"
<IP  |<Raw  load='AAAA' |>>

好吧,应该实现哪些add_payload() ? 只是两个数据包之间的链接? 不仅在我们这种情况下,此方法还将适当地设置type的正确值.

本能地,我们认为上层(" /"的右侧)可以收集值以将字段设置为下层(" /"的左侧). 如前所述,有一种方便的机制可以指定两个相邻层之间在两个方向上的绑定.

同样,必须将这些信息提供给bind_layers() ,它将在内部调用bind_top_down()负责汇总字段以使其过载. 在我们的情况下,我们需要指定的是:

bind_layers( Foo, Bar1, {'type':1} )
bind_layers( Foo, Bar2, {'type':2} )

然后, add_payload()遍历上层数据包(有效负载)的overload_fields ,获取与下层数据包相关联的字段(按其类型),然后将它们插入到overloaded_fields字段中.

现在,当将请求该字段的值时, getfieldval()将返回插入overloaded_fields字段中的值.

字段在三个字典之间分配:

  • fields :字段,其值已被显式地设置,像pdst在TCP( pdst='42'

  • overloaded_fields :重载领域

  • default_fields: all the fields with their default value (these fields

    构造函数通过调用init_fields()根据fields_desc进行初始化.

在以下代码中,我们可以观察如何选择一个字段并返回其值:

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)

插入字段中的fields具有更高的优先级,然后是overloaded_fields fields ,最后是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

我们无需做很多事情就解决了我们的两个问题:太懒了:)

Under the hood: putting everything together

最后但并非最不重要的一点是,了解构建包时何时调用每个函数非常有用:

>>> 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

如您所见,它首先遍历每个字段的列表,然后从头开始构建它们. 构建post_build()所有层后,它将从末尾开始调用post_build() .

Fields

这是Scapy即时可用的字段列表:

Simple datatypes

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

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

Strings

StrField(name, default, fmt="H", remain=0, shift=0)
StrLenField(name, default, fld=None, length_from=None, shift=0):
StrFixedLenField
StrNullField
StrStopField

Lists and lengths

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

Variable length fields

这是关于如何使用Scapy处理具有可变长度的字段的方法. 这些字段通常从另一个字段知道它们的长度. 我们称它们为varfield和lenfield. 这样做的目的是使每个字段相互引用,以便在解剖数据包时,varfield可以在组装数据包时从lenfield知道其长度,而不必填充lenfield,这将直接从varfield值推导出其值.

当您意识到lenfield和varfield之间的关系并不总是那么简单时,就会出现问题. lenfield有时以字节为单位指示长度,有时是多个对象. 有时长度包括标头部分,因此您必须减去固定的标头长度才能得出varfield长度. 有时,长度不是以字节为单位,而是以16位字为单位. 有时,两个不同的varfield使用相同的lenfield. 有时,相同的varfield被两个lenfield引用,一个lenbytes一个字节,一个16bits字.

The length field

首先,使用FieldLenField (或派生变量)声明FieldLenField . 如果在组装数据包时其值为None,则将从参考的varfield中推导出其值. 基准是使用任一完成length_of参数或count_of参数. 仅当varfield是保存列表的字段( PacketListFieldFieldListField )时, count_of参数才有意义. 该值将作为字符串的varfield的名称. 根据使用哪个参数,将在varfield值上调用i2len()i2count()方法. 返回值将通过adjust参数中提供的功能进行调整. 将对两个参数应用adjust:数据包实例和i2len()i2count()返回的值. 默认情况下,adjust不执行任何操作:

adjust=lambda pkt,x: x

例如,如果the_varfield是一个列表

FieldLenField("the_lenfield", None, count_of="the_varfield")

或如果长度以16位字为单位:

FieldLenField("the_lenfield", None, length_of="the_varfield", adjust=lambda pkt,x:(x+1)/2)
The variable length field

varfield可以是: StrLenFieldPacketLenFieldPacketListFieldFieldListField ,…

对于两个第一,当一个包被解剖时,它们的长度是从已经被解剖的一个lenfield中推导出来的. 链接是使用length_from参数完成的,该参数具有一个函数,该函数应用于部分解剖的数据包,返回以字节为单位的字段长度. 例如:

StrLenField("the_varfield", "the_default_value", length_from = lambda pkt: pkt.the_lenfield)

or

StrLenField("the_varfield", "the_default_value", length_from = lambda pkt: pkt.the_lenfield-12)

对于PacketListFieldFieldListField及其派生类,当它们需要长度时,它们按上述方式工作. 如果它们需要许多元素,则必须忽略length_from参数,而必须使用count_from参数. 例如:

FieldListField("the_varfield", ["1.2.3.4"], IPField("", "0.0.0.0"), count_from = lambda pkt: pkt.the_lenfield)

Examples

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) ]

测试FieldListField类:

>>> TestFLF("\x00\x02ABCDEFGHIJKL")
<TestFLF  the_lenfield=2 the_varfield=['65.66.67.68', '69.70.71.72'] |<Raw  load='IJKL' |>>

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)


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

TCP/IP

IPField
SourceIPField

IPoptionsField
TCPOptionsField

MACField
DestMACField(MACField)
SourceMACField(MACField)

ICMPTimeStampField

802.11

Dot11AddrMACField
Dot11Addr2MACField
Dot11Addr3MACField
Dot11Addr4MACField
Dot11SCField

DNS

DNSStrField
DNSRRCountField
DNSRRField
DNSQRField

ASN.1

ASN1F_element
ASN1F_field
ASN1F_INTEGER
ASN1F_enum_INTEGER
ASN1F_STRING
ASN1F_OID
ASN1F_SEQUENCE
ASN1F_SEQUENCE_OF
ASN1F_PACKET
ASN1F_CHOICE

Other protocols

NetBIOSNameField         # NetBIOS (StrFixedLenField)

ISAKMPTransformSetField  # ISAKMP (StrLenField)

TimeStampField           # NTP (BitField)

Design patterns

一些模式类似于许多协议,因此可以在Scapy中以相同的方式进行描述.

以下部分将介绍实现新协议时可以遵循的几种模型和约定.

Field naming convention

目的是保持数据包的书写流畅和直观. 基本说明如下:

  • 使用驼峰式大小写和常用缩写(例如len,src,dst,dstPort,srcIp).

  • 只要有可能或相关,请使用规范中的名称. 这旨在帮助新手轻松伪造数据包.

Add new protocols to Scapy

新协议可以在scapy/layersscapy/contrib . scapy/layers协议通常应该在通用网络上找到,而scapy/contrib协议应该是不常见或特定的.

准确地说, scapy/layers协议不应导入scapy/contrib协议,而scapy/contrib协议可能同时导入scapy/contribscapy/layers协议.

Scapy提供了Explore explore()函数,以搜索可用的图层/贡献模块. 因此,回馈给Scapy的模块必须在知情的情况下提供有关它们的信息:

  • 的contrib模块必须已定义,靠近模块的顶部(许可证标题下方是个好地方)(没有括号) 实施例

    # scapy.contrib.description = [...]
    # scapy.contrib.status = [...]
    # scapy.contrib.name = [...] (optional)
    
  • 如果contrib模块不包含任何数据包,并且不应在Explore()中建立索引,则应设置:

    # scapy.contrib.status = skip
    
  • 图层模块必须具有文档字符串,其中第一行简短描述了该模块.