天天看點

scapy-整體設計和資料發送流程

scapy涉及了pf_packet套結字程式設計,路由以及面向對象設計等諸技術,它本身使用python編寫,熟悉python的家夥這下有福了,可以目睹這個優秀的軟體而無需下載下傳編譯源碼之類的了。

scapy中到處都是class,不過要說起來,使用class表示協定棧是再友善不過的了,協定棧本身實作的每一個層就是一個class,而每一個經過該層的資料包就是該class的一個對象,一個class擁有很多屬性,比如針對ip層來講,它有源位址,目的位址,ttl等屬性,同時它還有send,receive等方法,而每一個經過的資料包隻是将這一切具體化了而已,比如一個ip層的資料包可以是這樣子的:[src=1.2.3.4,dst=4.3.2.1,ttl=100,...],然後它可以使用send方法被發送給下層,或者說發送給對方的對等層,從這個意義上講,我們完全可以簡單的使用oo的技術寫出一個使用者态的協定棧,資料包的來源來自pcap抓取的包,而我們也可以自己使用上述的oo技術構造一個資料包,逐層封裝(一直到鍊路層)後使用packet技術發出去,如果實在不想使用packet技術,也可以搞一個很簡單的硬體,比如序列槽,轉接于雙絞線(此含義即一條線的一端為rj45口,插入以太網卡,另一端為序列槽,插入機器序列槽),這樣的話,從序列槽讀出的就是裸資料了,然後進入我們的使用者态使用oo實作的協定棧,解析之後傳給我們自己的應用或者再次通過此技術forward出去,注意,我們雖然無法将解析後的資料傳給别的應用,因為别的應用使用核心的協定棧,其socket是工作于核心的,然而我們可以代理别的應用,此技術就是本地代理。

     簡單并且直接的說,scapy就是一個簡單的使用者态協定棧實作,雖然它還要使用核心的路由功能。我本身非常欣賞scapy這個東西的整體設計,而它的使用也是非常簡單,隻需先在Debian上apt-get install之,然後直接在指令行輸入scapy即可,會出現下列提示符:

Welcome to Scapy (0.9.17.1beta)

>>> 

此時我們輸入一行資料回車之:

>>> a=IP(src="192.168.40.246",dst="193.168.40.34")/TCP(sport=1111,dport=2222)

這樣我們就等于構造好了一個ip資料包了,然後我們再輸入下列行回車之:

>>> sr(a)

資料包a即被發送出去了,這一切的操作就是這麼簡單。上述的IP(...)和TCP(...)中的IP和TCP實際上就是類,圓括号内的資料即可以構造出兩個對象,一個IP的,另一個TCP的,兩個對象堆列在一起即可形成一個ip資料包,其上是tcp資料,然後當我們調用sr的時候,資料被發送,sr實際上是一個全局的函數,不屬于任何類的,熟悉python的一定明白這一點。

def sr(x,filter=None, iface=None, *args,**kargs):

    if not kargs.has_key("timeout"):

        kargs["timeout"] = -1

    s = conf.L3socket(filter=filter, iface=iface)

    a,b,c=sndrcv(s,x,*args,**kargs)

    s.close()

    return a,b

核心之處在于構造s和發送s,s由L3socket構造,而其也是一個類對象,它的實作為:

class L3PacketSocket(SuperSocket):

    def __init__(self, type = ETH_P_ALL, filter=None, promisc=None, iface=None): #構造方法

        self.type = type

    #下面将構造出一個packet協定族的socket,并且還是raw的,用于接收資料,使用packet實為自己解析之友善

        self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type))

        self.ins.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2**30)

        ...#下面将構造出一個packet協定族的socket,并且還是raw的,用于發送自己構造的資料

        self.outs = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type))

        ...

        if iface is None: #得到所有機器上的網卡

            self.iff = get_if_list()

        ...#設定網卡為promisc模式用于抓取任意資料包

    def close(self):

    def recv(self, x): #接收方法

        pkt, sa_ll = self.ins.recvfrom(x) #首先通過作業系統的packet套結字的接收方法接收資料

        pkt = cls(pkt)

    ...

        return pkt

    def send(self, x):

        if hasattr(x,"dst"):

        #根據構造ip包時的dst屬性值來決定往哪個網卡口發送這個自己構造好的資料包,這是通過路由得到的,在scapy初始化的時候,路由即已經被讀入了記憶體中,然後我們這裡隻是根據dst來查找該dst所對應的路由,最終将之解析(python的struct unpack操作)成iff-網卡,gw-網關等資料。

            iff,a,gw = conf.route.route(x.dst)

        sdto = (iff, self.type) #得到類似c語言的sockaddr_ll結構體

        self.outs.bind(sdto) #将packet套結字bind到該網卡上(c語言中的sockaddr_ll結構體則是bind的參數類型)

        sn = self.outs.getsockname()

        self.outs.sendto(str(x), sdto) #最終通過作業系統的packet套結字發送出去

上面就是scapy中的核心套結字class,它封裝了套結字的幾乎所有行為,那麼可想而知套結字操作的資料則被封裝成了另一個核心的class,這就是Packet,然而該Packet作為抽象父類出現,任何特定協定的資料包都是一個它的子類,對于最簡單的以太幀來講,它是:

class Ether(Packet):

    name = "Ethernet"

    fields_desc = [ DestMACField("dst"),

                    SourceMACField("src"),

                    XShortField("type", 0x0000) ]

    def hashret(self):

        return struct.pack("H",self.type)+self.payload.hashret()

    def answers(self, other):

        if isinstance(other,Ether):

            if self.type == other.type:

                return self.payload.answers(other.payload)

        return 0

    def mysummary(self):

        return "%s > %s (%04x)" % (self.src, self.dst, self.type)

sr函數的作用就是用一個SOCKET将一個PACKET發送出去,之是以使用大寫就是因為這都是scapy中的類對象,而不是内建的資料類型或者系統的資料類型,既然SOCKET和PACKET都是類,那麼也就是因為它們是類,是以很容易通過繼承擴充出很多不同種類的SOCKET和PACKET,比如鍊路層socket,網絡層socket這些不同層次的SOCKET(它們的send/recv方法實作當然不同),以及以太幀,ip資料報,tcp資料段這些不同層次的PACKET,總之,SOCKET定義了資料的發送接收流程和發送接收方式,而PACKET定義了資料包的格式,可以想象,将SOCKET和PACKET都定義成頂層的抽象超類,然後在不同的協定層定義不同的SOCKET和PACKET子類即可,注意每一個協定層的SOCKET子類和PACKET子類并不是一一對應的,對于每個層次來水SOCKET子類應該很少,而PACKET卻很多,簡單的說,該層支援那些具體協定,就有幾個PACKET子類。事實上,scapy就是這麼設計的。

     上述的L3PacketSocket表示一個網絡層的socket,它的定義如下,以SuperSocket作為父類:

class L3PacketSocket(SuperSocket)

而上述的Ether表示一個鍊路層的packet,它的定義如下,以Packet作為父類:

class Ether(Packet)

有了上述的Socket和Packet,整個發送接收流程就可以運作了,接下來最後需要幾個全局的方法用于發送不同層次的資料包,這就是sr,srp等方法了,在scapy的終端,敲入下列指令可以看到幫助資訊:

>>> help('sr') #類似的還有srp(發送二層資料幀),sr1等等

Help on function sr in module __main__:

sr(x, filter=None, iface=None, nofilter=0, *args, **kargs)

    Send and receive packets at layer 3

    nofilter: put 1 to avoid use of bpf filters

    retry:    if positive, how many times to resend unanswered packets

              if negative, how many times to retry when no more packets are answered

    timeout:  how much time to wait after the last packet has been sent

    verbose:  set verbosity level

    multi:    whether to accept multiple answers for the same stimulus

    filter:   provide a BPF filter

    iface:    listen answers only on the given interface

整個流程是:

1.scapy啟動,全局讀取路由資訊,供以後發送三層以及三層以上資料包時确定網卡出口時使用,要知道,建立一個af_packet類型的socket需要bind一個sockaddr_ll結構體,而此結構體需要指定一個确定的網卡資訊,比如eth0,eth1等:

struct sockaddr_ll sl;

int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

sl.sll_family = PF_PACKET;

struct ifreq req;

strcpy(req.ifr_name, "eth0");

ioctl(fd, SIOCGIFINDEX, &req);

sl.sll_ifindex = req.ifr_ifindex;

sl.sll_protocol = htons(ETH_P_ALL);

bind(fd, (struct sockaddr *)&sl, sizeof(sl));

bind網卡的時候需要的eth?資訊就是通過scapy初始化時得到的全局路由得到的,到時候,會根據IP(...,dst="xxx",...)中的dst資訊來在全局路由中得到ethN資訊,進而可以将socket進行bind;

2.使用者輸入一個諸如a=IP(src="192.168.40.246",dst="192.168.40.34")/TCP(sport=1111,dport=2222)之類的建立一個對象,該對象擁有dst屬性:192.168.40.34;

3.使用者調用sr(a)發送資料包,此時初始化一個L3PacketSocket對象,并且調用L3PacketSocket對象的send函數,後者将原生socket bind到一個ethN上(由路由确定),然後調用原生socket将資料發出;

4.讀取傳回。

5.以上的1-4僅僅是三層資料的發送過程,sr函數隻能發送ip層以及ip攜帶的資料

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