添加新协议

添加新协议(或更正确地说:新协议 图层 )在骗局中很容易。所有的魔法都在田野里。如果您需要的字段已经存在,并且协议没有太大的脑损伤,这应该是几分钟的事情。

简单的例子

层是 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" } ) ]

在这个例子中,我们的层有三个字段。第一个是名为 mickey 默认值为5。第二个是名为 minnie 默认值为3。香草的区别 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 |>

本章介绍如何在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'

我们对班上的2个“内部”字段感兴趣 Packet

  • p.underlayer

  • p.payload

这里是主要的“把戏”。您不关心数据包,只关心层,一个接一个地堆叠。

您可以通过以下名称轻松访问层: p[TCP] 返回 TCP 以及以下几层。这是一个快捷方式 p.getlayer(TCP) .

注解

有一个可选参数 (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 它的有效载荷。

构建新层

很容易!图层主要是字段列表。让我们来看一看 UDP 定义:

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

你完了!为了方便起见,已经定义了许多字段,如Phil所说,查看文档``w``源。

所以,定义一个层就是简单地将字段收集到一个列表中。这里的目标是为每个字段提供有效的默认值,这样用户在构建数据包时就不必给出这些值。

主要机制基于 Field 结构。请始终记住,一个层只比字段列表多一点,但不多。

因此,要理解层是如何工作的,需要快速查看字段是如何处理的。

操纵数据包==操纵其字段

应在不同的状态下考虑一个字段:

  • i (内部):这是scapy处理它的方式。

  • m (胆碱)这就是真相所在,这就是真相所在。

    在网络上。

  • h (乌曼):包裹是如何显示给我们的眼睛的。

这就解释了神秘的方法 i2h()i2m()m2i() 在每个领域中都有这样的应用:它们是从一种状态转换到另一种状态,适应特定的用途。

其他特殊功能:

  • any2i() 猜测输入表示并返回内部表示。

  • i2repr() a nicer 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() .

示例:可变长度数量

有一种方法可以在协议中经常使用的可变长度数量上表示整数,例如在处理信号处理(例如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。

解剖

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

基本的东西

解剖的核心功能是 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个字节,所以它们被分解为“原始”数据(这是某种默认的层类型)。

对于给定的层,一切都非常简单:

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

最后,对包中的所有层进行解剖,并将其与已知类型粘合在一起。

解剖场

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

写作时 FOO("\xff\xff"+"B"*8) 它叫 do_dissect() . 第一个字段是varlenqfield。因此,只要设置了最高位,就需要字节,因此直到(包括)第一个“b”。由于 VarLenQField.getfield() 可以交叉检查:

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

然后,以同样的方式提取下一个字段,直到放入2097090字节为止。 FOO.data (如果2097090字节不可用,则小于此值,如此处所示)。

如果在分析当前层之后还剩下一些字节,那么它将以相同的方式映射到下一个期望的字节。 (Raw 默认情况下):

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

因此,我们现在需要了解层是如何绑定在一起的。

结合层

解剖层时,scapy的一个很酷的特点是它试图为我们猜测下一层是什么。连接两层的官方方法是 bind_layers() 功能。

内部可用 packet 模块,此功能可用于以下用途:

bind_layers(ProtoA, ProtoB, FieldToBind=Value)

每次一个包 ProtoA()/ProtoB() 将被创建, FieldToBind 属于 ProtoA 将等于 Value .

例如,如果您有一个类 HTTP ,您可能期望来自或将要到端口80的所有数据包都将按此方式解码。这样做很简单:

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

全是伙计们!现在,与端口80相关的每个数据包都将与层相关联 HTTP ,无论是从PCAP文件读取还是从网络接收。

这个 guess_payload_class() 方式

有时,猜测有效负载类不像定义单个端口那么简单。例如,它可以依赖于当前层中给定字节的值。需要的两种方法是:

  • guess_payload_class() 它必须返回负载的猜测类(下一层)。默认情况下,它使用已由 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() .

更改默认行为

如果不喜欢给定层的scapy行为,可以通过调用 split_layers() . 例如,如果不希望UDP/53与 DNS ,只需添加代码:

split_layers(UDP, DNS, sport=53)

现在,每个带有源端口53的数据包都不会作为DNS处理,而是按您指定的方式处理。

在引擎盖下面:把所有东西放在一起

事实上,每层都有一个场有效载荷。当使用bind_layers()方法时,它会将定义的下一层添加到该列表中。

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

然后,当需要猜测下一个层类时,它调用默认方法 Packet.guess_payload_class() . 此方法运行在列表有效负载的每个元素中,每个元素都是一个元组:

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

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

所以,违约 guess_payload_class() 尝试列表中的所有元素,直到其中一个匹配。如果找不到元素,则调用 default_payload_class() . 如果您重新定义了此方法,那么将调用您的方法,否则将调用默认方法,并且 Raw 返回类型。

Packet.guess_payload_class()

  • 现场测试什么 guess_payload

  • 调用重载 guess_payload_class()

建筑物

构建一个包和构建每一层一样简单。然后,一些神奇的东西会粘上所有的东西。那我们就变魔术吧。

基本的东西

首先要确定的是:“构建”是什么意思?正如我们所看到的,一个层可以用不同的方式表示(人、内部、机器)。构建意味着进入机器格式。

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

>>> 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. ..|..
调用 raw() 生成数据包:
  • 非实例字段设置为其默认值

  • 长度自动更新

  • 计算校验和

  • 等等。

事实上,使用 raw() 而不是 show2() 或者任何其他方法都不是随机选择,因为构建包调用的所有函数 Packet.__str__() (或) Packet.__bytes__() 在python 3下)。然而, __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() 做。

建筑领域

层的每个字段的构建都被调用 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 如果没有设置,这意味着之前没有提供默认值,因此该字段需要立即或稍后计算一些“stuff”。

“现在”是指感谢 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() :如果值可用,则生成层。

处理默认值: 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

而机器的表示是期望的。

处理默认值:自动计算

正如我们之前看到的,解剖机制是建立在程序员创建的层之间的链接上的。但是,它也可以在建筑过程中使用。

层中 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")
          ]

如果我们在没有其他东西的情况下使用这些类,那么在剖析数据包时会遇到麻烦,因为没有任何东西可以用多个 Bar* 即使我们通过调用 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'

问题:

  1. type 仍然等于0,而我们希望它自动设置为1。我们当然可以建造 p 具有 p = Foo(type=1)/Bar0(val=1337) 但这不太方便。

  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 :所有字段及其默认值(这些字段

    初始化依据 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)

插入的字段 fields 那就有更高的优先权 overloaded_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

我们的两个问题已经解决了,我们没有做太多的事情:懒惰是件好事。)

在引擎盖下面:把所有东西放在一起

最后,了解构建包时何时调用每个函数非常有用:

>>> 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() 从最后开始。

领域

以下是scapy开箱即用支持的字段列表:

简单数据类型

传说:

  • X -十六进制表示

  • LE -小endian(默认值为big endian=网络字节顺序)

  • 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
LELongField
XLongField
LELongField

IEEEFloatField
IEEEDoubleField
BCDFloatField       # binary coded decimal

BitField
XBitField

BitFieldLenField    # BitField specifying a length (used in RTP)
FlagsField
FloatField

枚举

可能的字段值取自给定的枚举(列表、字典等),例如:

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

字符串

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

列表和长度

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

可变长度字段

这是关于如何使用scapy处理长度可变的字段。这些字段通常从另一个字段知道它们的长度。我们称他们为瓦尔菲尔德和伦菲尔德。其思想是让每个字段引用另一个字段,这样当一个数据包被分解时,varfield就可以从lenfield知道它的长度。当一个数据包被组装时,你不必填充lenfield,它就可以直接从varfield值推导出它的值。

当你意识到Lenfield和Varfield之间的关系并不总是直接的时候,问题就会出现。有时,lenfield以字节表示长度,有时表示多个对象。有时,长度包括头段部分,因此必须减去固定头段长度来推导varfield长度。有时长度不以字节为单位,而是以16位字为单位。有时两个不同的varfield使用相同的lenfield。有时同一个varfield被两个lenfield引用,一个在字节中,一个在16位字中。

长度字段

首先,使用 FieldLenField (或衍生物)。如果在组装数据包时它的值为“无”,则它的值将从引用的varfield中推导出来。引用是使用 length_of 参数或 count_of count_of 只有当varfield是保存列表的字段时,参数才有意义 (PacketListFieldFieldListField )该值将是varfield的名称,作为字符串。根据使用的参数 i2len()i2count() 将对varfield值调用方法。返回的值将由adjust参数中提供的函数进行调整。Adjust将应用于2个参数:数据包实例和返回的值 i2len()i2count() . 默认情况下,调整不执行任何操作:

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)
可变长度字段

varfield可以是: StrLenFieldPacketLenFieldPacketListFieldFieldListField ,…

对于前两个,当一个包被解剖时,它们的长度是从已经解剖过的Lenfield推导出来的。链接是使用 length_from 参数,它接受一个应用于部分解析的数据包的函数,返回字段的长度(以字节为单位)。例如::

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

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

对于 PacketListFieldFieldListField 以及它们的衍生物,当它们需要一个长度时,它们的工作原理如上所述。如果需要多个元素,则必须忽略参数的长度,而必须使用参数的count。例如::

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

实例

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

特殊的

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

ASN.1

ASN1F_element
ASN1F_field
ASN1F_INTEGER
ASN1F_enum_INTEGER
ASN1F_STRING
ASN1F_OID
ASN1F_SEQUENCE
ASN1F_SEQUENCE_OF
ASN1F_PACKET
ASN1F_CHOICE

其他协议

NetBIOSNameField         # NetBIOS (StrFixedLenField)

ISAKMPTransformSetField  # ISAKMP (StrLenField)

TimeStampField           # NTP (BitField)

设计模式

有些模式类似于许多协议,因此可以用scapy的相同方式描述。

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

字段命名约定

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

  • 使用倒驼色大小写和常用缩写(例如len、src、dst、dstport、srcip)。

  • 在可能或相关的地方,最好使用规范中的名称。这旨在帮助新来者轻松伪造数据包。

向scapy添加新协议

新协议可以进入 scapy/layersscapy/contrib . 协议 scapy/layers 通常应该在公共网络上找到,而协议 scapy/contrib 应该是不常见的或具体的。

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

scapy提供了 explore() 函数,搜索可用的层/控件模块。因此,反馈给scapy的模块必须提供有关它们的信息,故意:

  • A 控制 模块必须已定义,靠近模块顶部(许可证标题下面是一个好位置) (没有括号) Example ::

    # scapy.contrib.description = [...]
    # scapy.contrib.status = [...]
    # scapy.contrib.name = [...] (optional)
    
  • 如果contrib模块不包含任何数据包,并且不应在 explore() ,则应改为设置:

    # scapy.contrib.status = skip
    
  • A 图层 模块必须有一个docstring,其中第一行简短地描述了模块。