天天看點

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,    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): # 在子類中實作,實作各自層次的校驗碼的計算之類的

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

            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) # 建構載荷

最核心的就是__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

        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:

這個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++一樣醜陋無比!

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271135