scapy-协议数据组织结构与细节

是使用继承还是使用包含,这也许是OO中永远讨论不完的话题,其受到争议正是因为这两种方式都太灵活了,让人不能轻易以一个的优点作为根据迅速推翻另一者,正是因为二者都很好,scapy则同时使用了二者,恰巧正是这种scapy的使用方式将继承和包含结合在了一起,结束了它们的争吵。
     首先不管是tcp段,ip数据报还是以太帧,实现它们都是Packet,正因为如此,IP,TCP,Ether等各层的数据包(格式)类都继承于Packet这个父类,然而由于这些协议彼此是分层的,一个层次的协议头+payload在下一个层次将完全被看作payload,因此这些各个层次的数据包又是彼此包含的。协议栈数据的并列性-都是数据但是格式不同使得它们比较适合于继承于同一个父类,而协议栈本身的分层性使得这些数据又能彼此包含。在scapy中,Packet是一个所有层次“数据”的父类,然后从之派生出诸如TCP,UDP,IP,Ether之类的子类,然而IP对象中可以包含TCP或者UDP对象,同时也能被Ether对象所包含,这就是scapy的总体组织数据的方式。结合代码的话,我觉得最重要的就是一个静态的列表了,那就是layer_bonds:
layer_bonds = [ ( Dot3,   LLC,      { } ),
                ( PrismHeader, Dot11, { }),
                ( Dot11,  LLC,      { "type" : 2 } ),
                ( LLPPP,  IP,       { } ),
                ( Ether,  LLC,      { "type" : 0x007a } ),
                ( Ether,  Dot1Q,    { "type" : 0x8100 } ),
                ( Ether,  Ether,    { "type" : 0x0001 } ),
                ( Ether,  ARP,      { "type" : 0x0806 } ),
                ( Ether,  IP,       { "type" : 0x0800 } ),
                ( Ether,  EAPOL,    { "type" : 0x888e } ),
                ( Ether,  EAPOL,    { "type" : 0x888e, "dst" : "01:80:c2:00:00:03" } ),
                ( EAPOL,  EAP,      { "type" : EAPOL.EAP_PACKET } ),
                ( LLC,    STP,      { "dsap" : 0x42 , "ssap" : 0x42 } ),
                ( LLC,    SNAP,     { "dsap" : 0xAA , "ssap" : 0xAA } ),
                ( SNAP,   Dot1Q,    { "code" : 0x8100 } ),
                ( SNAP,   Ether,    { "code" : 0x0001 } ),
                ( SNAP,   ARP,      { "code" : 0x0806 } ),
                ( SNAP,   IP,       { "code" : 0x0800 } ),
                ( SNAP,   EAPOL,    { "code" : 0x888e } ),
                ( IPerror,IPerror,  { "proto" : socket.IPPROTO_IP } ),
                ( IPerror,ICMPerror,{ "proto" : socket.IPPROTO_ICMP } ),
                ( IPerror,TCPerror, { "proto" : socket.IPPROTO_TCP } ),
                ( IPerror,UDPerror, { "proto" : socket.IPPROTO_UDP } ),
                ( IP,     IP,       { "proto" : socket.IPPROTO_IP } ),
                ( IP,     ICMP,     { "proto" : socket.IPPROTO_ICMP } ),
                ( IP,     TCP,      { "proto" : socket.IPPROTO_TCP } ),
                ( IP,     UDP,      { "proto" : socket.IPPROTO_UDP } ),
                ( UDP,    DNS,      { "sport" : 53 } ),
                ( UDP,    DNS,      { "dport" : 53 } ),
                ( UDP,    DNS,      { "dport" : 53 } ),
                ( UDP,    ISAKMP,   { "sport" : 500, "dport" : 500 } ),
                ( UDP,    NTP,      { "sport" : 123, "dport" : 123 } ),
                ( UDP,    BOOTP,    { "sport" : 68, "dport" : 67 } ),
                ( UDP,    BOOTP,    { "sport" : 67, "dport" : 68 } ),
                ( Dot11, Dot11AssoReq,    { "type" : 0, "subtype" : 0 } ),
                ...
                ]
这个列表中的元素是一个个的元组,而元组的元素是类和字典,基本上这个列表铺就了整个协议栈的静态结构,基本上就是整个协议栈了,除了应用层的协议比较少之外它就是一个协议栈,列表元素是一个元组,解释如下:
(下层协议l,上层协议u,{上层协议的识别属性:u的识别值})
有了这个列表,数据的组织就简单多了,以IP类对象为例,我们构造一个数据报,其上是TCP类对象,还是以:
a=IP(src="192.168.40.246",dst="192.168.40.34")/TCP(sport=1111,dport=2222)
这个为例,它涉及到了两个对象-IP对象ip1和TCP对象tcp1的创建和一个运算符-/,显然tcp1的数据是ip1的payload,由于它们都是Packet的子类,因此在Packet的实现中就能看出一个完整的ip数据报是怎么构建出来的:
class Packet(Gen):
    name="abstract packet"
    fields_desc = []
    aliastypes = [] #用于确定上层payload的类型从而得到它的class并且将payload转化成相应的class对象
    overload_fields = {}
    underlayer = None
    payload_guess = []
    initialized = 0
    def __init__(self, pkt="", **fields):
        self.time  = time.time()
        self.aliastypes = [ self.__class__ ] + self.aliastypes
    ...
        if pkt: #如果构造的时候就已经传入了上层的包,那么就开始构造payload
            self.dissect(pkt)
        for f in fields.keys():
            self.fields[f] = self.fieldtype[f].any2i(self,fields[f])
    def add_payload(self, payload):
    ...
        else: #将payload参数加入到self成为self的payload
            if isinstance(payload, Packet):
                self.__dict__["payload"] = payload #为self的payload赋值,参数正式成为了self的载荷。
                payload.add_underlayer(self) #self作为payload的下层协议
        ...
    def remove_payload(self):
        ... # 重置self的payload字段和underlayer字段
    def add_underlayer(self, underlayer):
        self.underlayer = underlayer
    def remove_underlayer(self, underlayer):
        self.underlayer = None
    def copy(self): #完全克隆一个当前的对象
        clone = self.__class__()
        clone.fields = self.fields.copy()
        for k in clone.fields:
            clone.fields[k]=self.fieldtype[k].copy(clone.fields[k])
        clone.default_fields = self.default_fields.copy()
        clone.overloaded_fields = self.overloaded_fields.copy()
        clone.overload_fields = self.overload_fields.copy()
        clone.underlayer=self.underlayer
        clone.__dict__["payload"] = self.payload.copy()
        clone.payload.add_underlayer(clone)
        return clone
    def __getattr__(self, attr):
        ...
    def __setattr__(self, attr, val):
        ...
    def __delattr__(self, attr):
        ...
    def __repr__(self):
        ...
    def __str__(self):
        return self.__iter__().next().build() # 这里只是构造出该层的数据包,然后在build内部构造其上层数据的时候还是会调用上层的str函数的,从而一层一层将数据包构造好
    def __div__(self, other): # 除法的重载,例子中的IP(...)/TCP(...)就使用了这个重载,其实就是将tcp对象作为ip对象的payload加入到ip数据报中,从上面的构造函数__init__中可以看出,还有一种增加payload的方式是直接在构造的时候传入一个Packet子类对象的裸数据,然后构造函数会调用dissect函数"发现"这些裸数据是哪个Packet子类的对象,然后构造之,最终add_payload。而使用除法(如例子中的方式)更直接一些,你必须自己构造payload对象,也就是说你必须写明它是TCP的对象,用TCP(...)来构造,而不能仅仅写一些数据,这其实也是python语言的要求,当然你也可以写一些裸数据,但仅限于字符串,然后这些字符串将作为Packet的payload:
>>> a=IP(src="192.168.40.246",dst="192.168.40.34")/"aaaaaaaaaaa"。
        if isinstance(other, Packet): # 如果other是一个我们手工构造好的Packet,那么下面的很好懂
            cloneA = self.copy() 
            cloneB = other.copy()
            cloneA.add_payload(cloneB)
            return cloneA
        elif type(other) is str: # 如果是一个字符串,那么构造裸数据Packet对象和self相除
            return self/Raw(load=other)
        ...
    def __rdiv__(self, other):
        ...
    def __len__(self):
        return len(self.__str__())
    def do_build(self):
         p=""
        for f in self.fields_desc: # 构造协议头
            p = f.addfield(self, p, self.__getattr__(f))
        pkt = p+str(self.payload) # 构造载荷。此时有点递归的意思,因为payload本身也是一个Packet,而Packet类重写了str函数,在重写的str函数中又要调用payload的build函数,这样从下到上(Ether->应用层)将数据包(实际上是一个以太帧)构造好。
        return pkt
    def post_build(self, pkt): # 在子类中实现,实现各自层次的校验码的计算之类的
        return pkt
    def build(self): # 构造完整的数据包,只构造一层
        return self.post_build(self.do_build()) # 先根据已有的数据构造协议头和载荷,再计算协议头中没有填充的内容
    def extract_padding(self, s):
        return s,None
    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
        payl,pad = self.extract_padding(s)
        self.do_dissect_payload(payl)
        if pad and conf.padding:
            self.add_payload(Padding(pad))
    def do_dissect_payload(self, s):
        if s:
            cls = self.guess_payload_class() # "猜测"一下我们需要用哪个Packet的子类构造对象,毕竟通过构造函数传入的只是一些数据,这个猜测的依据就是下层协议指示上层协议的字段,比如IP协议头的proto
            try:
                p = cls(s) # 找到了s属于的类,那么该构造其对象了
            except:
                p = Raw(s) # 异常情况下构造成裸数据
            self.add_payload(p) # 将构造好的Packet子类对象作为payload加入self
    def dissect(self, s):
        return self.do_dissect(s)
    def guess_payload_class(self):
        for t in self.aliastypes: # 一般而言,t只有一个,那就是self类本身的地址
            for fval, cls in t.payload_guess: # t的payload_guess在scapy初始化的时候通过layer_bonds这个静态列表构造,它是一个类和和一个字典,此处为:注释0
                ok = 1
                for k in fval.keys(): # 遍历字典中的键值
                    if fval[k] != getattr(self,k): # 由于layer_bonds是静态构造好的,每一个可能的匹配上层协议是什么都是下层协议的某个字段指示的,字典中键值的value必须和其下层协议头中以键值为属性的值相等才可以,否则就不匹配
                        ok = 0
                        break
                if ok:
                    return cls
        return None
    def hide_defaults(self):
        ...
    def __iter__(self):
        ...#暂且不在这里注释这个,这个很重要,同时也很复杂
    def send(self, s, slp=0):
        for p in self:
            s.send(str(p))
            if slp:
                time.sleep(slp)
    def __gt__(self, other):
        ...
    def __lt__(self, other):
        ...
    def hashret(self):
        return self.payload.hashret()
    def answers(self, other): #如果other是self的回复包,则返回1,否则返回0,具体实现由子类重载
        return 0
    def haslayer(self, cls):
        if self.__class__ == cls:
            return 1
        return self.payload.haslayer(cls)
    def getlayer(self, cls):
        if self.__class__ == cls:
            return self
        return self.payload.getlayer(cls)
    def display(self, lvl=0):
        ...
    def sprintf(self, fmt, relax=1):
       ...
    def mysummary(self):
        ...
    def summaryback(self, smallname=0):
        ...
    def summary(self, onlyname=0):
        ...
    def lastlayer(self,layer=None):
        return self.payload.lastlayer(self)
Packet类定义了所有层次协议的公共操作,留下注入post_build这类协议相关的操作给子类实现。理解了scapy中协议数据包的分层构造以及payload如何加入低一层的数据包之后,接下来最最重要的就是将一个数据包发送出去,要发送出去就需要将一个数据包的所有层次都堆积累加起来,形成一个以太帧之类的二层数据。这一切在scapy中是最后实现的,也就是说是通过原生socket往外发送包的时候才实现这个累加过程的,以三层数据包为例,L3PacketSocket的send函数如下:
def send(self, x):
       if hasattr(x,"dst"):
           iff,a,gw = conf.route.route(x.dst)
       else:
           iff = conf.iface
       sdto = (iff, self.type)
       self.outs.bind(sdto)
       sn = self.outs.getsockname()
       if sn[3] == ARPHDR_PPP:
           sdto = (iff, ETH_P_IP)
       elif LLTypes.has_key(sn[3]):
           x = LLTypes[sn[3]]()/x  # 加入链路层,也就是将当前的三层数据包作为二层数据包的paylaod,注意这里利用的是除法的重载
       self.outs.sendto(str(x), sdto) # x是一个Packet(的子类)对象,最关键的是str函数,它可以被重载,而父类Packet就重载了它:
Packet的str函数如下:
def __str__(self):
    return self.__iter__().next().build()
这里的self其实是一个以太帧(不考虑其它链路层协议),也就是一个Ether类的对象,而Ether是Packet的子类且没有重写iter,因此它的__iter__方法也就是Packet的方法,该iter在枚举的时候要用到,比如for x in P(P为一个Packet对象)。就是这个__iter__方法一下子就把所有协议层次数据都堆叠好了。Packet的__iter__比较复杂,但是原理很简单,就是两层遍历,第一层遍历协议层,第二层遍历该协议层协议头中的所有字段,方向是从下往上,然后对于每一个协议层次调用build方法:
def build(self):
    return self.post_build(self.do_build()) #对于每一层最后调用post_build
def do_build(self):
        p=""
        for f in self.fields_desc: # 构建字段
            p = f.addfield(self, p, self.__getattr__(f))
        pkt = p+str(self.payload) # 构建载荷
        return pkt
最核心的就是__iter__()了:
def __iter__(self):
  def loop(todo, done, self=self):
   if todo:
    eltname = todo.pop() # pop出单个协议的协议头字段
    elt = self.__getattr__(eltname)
    if not isinstance(elt, Gen):
     if self.fieldtype[eltname].islist:
      elt = SetGen([elt])
     else:
      elt = SetGen(elt)
    for e in elt:
     done[eltname]=e
     for x in loop(todo[:], done):
      yield x # x是一个特定层的Packet数据包,yield是python里面使用的,和生成器有关
   else:
    if isinstance(self.payload,NoPayload):
     payloads = [None]
    else:
     payloads = self.payload
     for payl in payloads:
      done2=done.copy()
      for k in done2:
       if isinstance(done2[k], RandNum):
        done2[k] = int(done2[k])
       pkt = self.__class__(**done2)
       pkt.underlayer = self.underlayer
       pkt.overload_fields = self.overload_fields.copy()
       if payl is None:
        yield pkt
       else:
        yield pkt/payl # 仍然是除法将pay1作为pkt的载荷
 return loop(map(lambda x:str(x), self.fields.keys()), {})
Ether的__iter__方法返回了一个迭代器,同样它之上的IP的__iter__也返回了一个迭代器,TCP也一样,并且我们最终的包是一个Ether对象,设为eth,它里面包含了IP对象ip1,而ip1对象中又包含了TCP对象tcp1,因此当调用eth的__iter__方法时,得到的迭代器是eth->ip1->tcp1,进一步eth的__str__方法被调用的时候,调用__iter__().next().build(),这也就构建好了以太头,然后在其build函数中调用str(self.payload)(我们暂且忽略post_build),而str的参数是eth的payload,也就是ip1,接下来调用ip1的__str__方法,因为ip1的迭代器为ip1->tcp1,因此这次取出了ip1,调用其build方法,构建好了ip协议头,再接下来调用str(ip1.payload),而这是就是tcp1了,依照上面的流程,tcp头也构建好了,这样一个整个的以太帧就封装好了,以上的过程很优美,这在于python语言的简洁和优美,这些迭代器是如何建立并工作的呢?这就是上面的__iter__函数。
     该函数内部定义了子函数,子函数使用了yield,也就是这个子函数返回一个迭代器或者说这个迭代器是这个子函数返回的,yield可以返回一个迭代器是不假,懂python的都知道它产生一个生成器,然而在scapy的实现,巧就巧在这个生成器函数使用了递归,这样在子函数中使用递归,最终却返回一个迭代器,这就是精妙所在。loop子函数首先在if todo判断中pop出当前协议层定义的Packet子类的属性字段,直到最后没有字段可供pop的时候最终会进入:
else:
    if isinstance(self.payload,NoPayload):
这个else分支,然后在for payl in payloads中又会调用payload的__iter__函数,开始上一层的协议层数据包的构建。loop子函数由一个if-else组成,第一个if后的分支解决本层的数据,else分支解决上面各层的数据,依次类推。函数的调用路径是:
eth[iter-loop]-->ip1[iter-loop]--...(ip头的字段数个ip1[])...-->tcp1[iter-loop]--...(tcp头的字段数个tcp1[])...-->ip1[iter-loop]->eth[iter-loop]
     可能也只有python能写出如此优美的代码了,然而如果不是高手,可能既是使用python写出的代码也和c++一样丑陋无比!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值