從 Masscan, Zmap 原始碼分析到開發實踐

酷酷的曉得哥發表於2019-10-14

作者:w7ay@知道創宇404實驗室
日期:2019年10月12日

Zmap和Masscan都是號稱能夠快速掃描網際網路的掃描器,十一因為無聊,看了下它們的程式碼實現,發現它們能夠快速掃描,原理其實很簡單,就是實現兩種程式,一個傳送程式,一個抓包程式,讓傳送和接收分隔開從而實現了速度的提升。但是它們識別的準確率還是比較低的,所以就想了解下為什麼準確率這麼低以及應該如何改善。

Masscan原始碼分析

首先是看的 的原始碼,在readme上有它的一些設計思想,它指引我們看 main.c中的入口函式 main(),以及傳送函式和接收函式 transmit_thread()receive_thread(),還有一些簡單的原理解讀。

理論上的6分鐘掃描全網

在後面自己寫掃描器的過程中,對Masscan的掃描速度產生懷疑,目前Masscan是號稱6分鐘掃描全網,以每秒1000萬的發包速度。

但是255^4/10000000/60 ≈ 7.047 ???

之後瞭解到,預設模式下Masscan使用 pcap傳送和接收資料包,它在Windows和Mac上只有30萬/秒的發包速度,而Linux可以達到150萬/秒,如果安裝了PF_RING DNA裝置,它會提升到1000萬/秒的發包速度(這些前提是硬體裝置以及頻寬跟得上)。

注意,這只是按照掃描 一個埠的計算。

PF_RING DNA裝置瞭解地址:

那為什麼Zmap要45分鐘掃完呢?

在Zmap的 上說明了

用PF_RING驅動,可以在5分鐘掃描全網,而預設模式才是45分鐘,Masscan的預設模式計算一下也是45分鐘左右才掃描完,這就是宣傳的差距嗎 (-

歷史記錄

觀察了readme的歷史記錄 

之前構建時會提醒安裝 libpcap-dev,但是後面沒有了,從releases上看,是將靜態編譯的 libpcap改為了動態載入。

C10K問題

c10k也叫做client 10k,就是一個客戶端在硬體效能足夠條件下如何處理超過1w的連線請求。Masscan把它叫做C10M問題。

Masscan的解決方法是不透過系統核心呼叫函式,而是直接呼叫相關驅動。

主要透過下面三種方式:

  • 定製的網路驅動
    • Masscan可以直接使用PF_RING DNA的驅動程式,該驅動程式可以直接從使用者模式向網路驅動程式傳送資料包而不經過系統核心。
  • 內建tcp堆疊
    • 直接從tcp連線中讀取響應連線,只要記憶體足夠,就能輕鬆支援1000萬併發的TCP連線。但這也意味著我們要手動來實現tcp協議。
  • 不使用互斥鎖
    • 鎖的概念是使用者態的,需要經過CPU,降低了效率,Masscan使用 rings來進行一些需要同步的操作。與之對比一下Zmap,很多地方都用到了鎖。
      • 為什麼要使用鎖?
        • 一個網路卡只用開啟一個接收執行緒和一個傳送執行緒,這兩個執行緒是不需要共享變數的。但是如果有多個網路卡,Masscan就會開啟多個接收執行緒和多個傳送執行緒,這時候的一些操作,如列印到終端,輸出到檔案就需要鎖來防止衝突。
      • 多執行緒輸出到檔案
        • Masscan的做法是每個執行緒將內容輸出到不同檔案,最後再集合起來。在 src/output.c中,

隨機化地址掃描

在讀取地址後,如果進行順序掃描,虛擬碼如下

for (i = 0; i < range; i++) {
    scan(i);}

但是考慮到有的網段可能對掃描進行檢測從而封掉整個網段,順序掃描效率是較低的,所以需要將地址進行隨機的打亂,用演算法描述就是設計一個 打亂陣列的演算法,Masscan是設計了一個加密演算法,虛擬碼如下

range = ip_count * port_count;for (i = 0; i < range; i++) {
    x = encrypt(i);
    ip   = pick(addresses, x / port_count);
    port = pick(ports,     x % port_count);
    scan(ip, port);}

隨機種子就是 i的值,這種加密演算法能夠建立一種一一對應的對映關係,即在[1...range]的區間內透過 i來生成[1...range]內不重複的隨機數。同時如果中斷了掃描,只需要記住 i的值就能重新啟動,在分散式上也可以根據 i來進行。

  • 如果對這個加密演算法感興趣可以看   這篇論文。

無狀態掃描的原理

回顧一下tcp協議中三次握手的前兩次

  1. 客戶端在向伺服器第一次握手時,會組建一個資料包,設定syn標誌位,同時生成一個數字填充seq序號欄位。
  2. 服務端收到資料包,檢測到了標誌位的syn標誌,知道這是客戶端發來的建立連線的請求包,服務端會回覆一個資料包,同時設定syn和ack標誌位,伺服器隨機生成一個數字填充到seq欄位。並將客戶端傳送的seq資料包+1填充到ack確認號上。

在收到syn和ack後,我們返回一個rst來結束這個連線,如下圖所示

Masscan和Zmap的掃描原理,就是利用了這一步,因為seq是我們可以自定義的,所以在傳送資料包時填充一個特定的數字,而在返回包中可以獲得相應的響應狀態,即是無狀態掃描的思路了。 接下來簡單看下Masscan中發包以及接收的程式碼。

發包

main.c中,前面說的隨機化地址掃描

接著生成cookie併傳送

uint64_tsyn_cookie( unsigned ip_them, unsigned port_them,
            unsigned ip_me, unsigned port_me,
            uint64_t entropy){
    unsigned data[4];
    uint64_t x[2];
    x[0] = entropy;
    x[1] = entropy;
    data[0] = ip_them;
    data[1] = port_them;
    data[2] = ip_me;
    data[3] = port_me;
    return siphash24(data, sizeof(data), x);}

看名字我們知道,生成cookie的因子有源ip,源埠,目的ip,目的埠,和entropy(隨機種子,Masscan初始時自動生成),siphash24是一種高效快速的雜湊函式,常用於網路流量身份驗證和針對雜湊dos攻擊的防禦。

組裝tcp協議 template_set_target(),部分程式碼

case Proto_TCP:
        px[offset_tcp+ 0] = (unsigned char)(port_me >> 8);
        px[offset_tcp+ 1] = (unsigned char)(port_me & 0xFF);
        px[offset_tcp+ 2] = (unsigned char)(port_them >> 8);
        px[offset_tcp+ 3] = (unsigned char)(port_them & 0xFF);
        px[offset_tcp+ 4] = (unsigned char)(seqno >> 24);
        px[offset_tcp+ 5] = (unsigned char)(seqno >> 16);
        px[offset_tcp+ 6] = (unsigned char)(seqno >>  8);
        px[offset_tcp+ 7] = (unsigned char)(seqno >>  0);
        xsum += (uint64_t)tmpl->checksum_tcp
                + (uint64_t)ip_me
                + (uint64_t)ip_them
                + (uint64_t)port_me
                + (uint64_t)port_them
                + (uint64_t)seqno;
        xsum = (xsum >> 16) + (xsum & 0xFFFF);
        xsum = (xsum >> 16) + (xsum & 0xFFFF);
        xsum = (xsum >> 16) + (xsum & 0xFFFF);
        xsum = ~xsum;
        px[offset_tcp+16] = (unsigned char)(xsum >>  8);
        px[offset_tcp+17] = (unsigned char)(xsum >>  0);
        break;

發包函式

/*************************************************************************** * wrapper for libpcap's sendpacket * * PORTABILITY: WINDOWS and PF_RING * For performance, Windows and PF_RING can queue up multiple packets, then * transmit them all in a chunk. If we stop and wait for a bit, we need * to flush the queue to force packets to be transmitted immediately. ***************************************************************************/intrawsock_send_packet(
    struct Adapter *adapter,
    const unsigned char *packet,
    unsigned length,
    unsigned flush){
    if (adapter == 0)
        return 0;
    /* Print --packet-trace if debugging */
    if (adapter->is_packet_trace) {
        packet_trace(stdout, adapter->pt_start, packet, length, 1);
    }
    /* PF_RING */
    if (adapter->ring) {
        int err = PF_RING_ERROR_NO_TX_SLOT_AVAILABLE;
        while (err == PF_RING_ERROR_NO_TX_SLOT_AVAILABLE) {
            err = PFRING.send(adapter->ring, packet, length, (unsigned char)flush);
        }
        if (err < 0)
            LOG(1, "pfring:xmit: ERROR %d\n", err);
        return err;
    }
    /* WINDOWS PCAP */
    if (adapter->sendq) {
        int err;
        struct pcap_pkthdr hdr;
        hdr.len = length;
        hdr.caplen = length;
        err = PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);
        if (err) {
            rawsock_flush(adapter);
            PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);
        }
        if (flush) {
            rawsock_flush(adapter);
        }
        return 0;
    }
    /* LIBPCAP */
    if (adapter->pcap)
        return PCAP.sendpacket(adapter->pcap, packet, length);
    return 0;}

可以看到它是分三種模式發包的, PF_RING, WinPcap, LibPcap,如果沒有裝相關驅動的話,預設就是pcap發包。如果想使用PF_RING模式,只需要加入啟動引數 --pfring

接收

在接收執行緒看到一個關於cpu的程式碼

大意是鎖住這個執行緒執行的cpu,讓傳送執行緒執行在雙數cpu上,接收執行緒執行在單數cpu上。但程式碼沒怎麼看懂

接收原始資料包

int rawsock_recv_packet(
    struct Adapter *adapter,
    unsigned *length,
    unsigned *secs,
    unsigned *usecs,
    const unsigned char **packet){
    if (adapter->ring) {
        /* This is for doing libpfring instead of libpcap */
        struct pfring_pkthdr hdr;
        int err;
        again:
        err = PFRING.recv(adapter->ring,
                        (unsigned char**)packet,
                        0,  /* zero-copy */
                        &hdr,
                        0   /* return immediately */
                        );
        if (err == PF_RING_ERROR_NO_PKT_AVAILABLE || hdr.caplen == 0) {
            PFRING.poll(adapter->ring, 1);
            if (is_tx_done)
                return 1;
            goto again;
        }
        if (err)
            return 1;
        *length = hdr.caplen;
        *secs = (unsigned)hdr.ts.tv_sec;
        *usecs = (unsigned)hdr.ts.tv_usec;
    } else if (adapter->pcap) {
        struct pcap_pkthdr hdr;
        *packet = PCAP.next(adapter->pcap, &hdr);
        if (*packet == NULL) {
            if (is_pcap_file) {
                //pixie_time_set_offset(10*100000);
                is_tx_done = 1;
                is_rx_done = 1;
            }
            return 1;
        }
        *length = hdr.caplen;
        *secs = (unsigned)hdr.ts.tv_sec;
        *usecs = (unsigned)hdr.ts.tv_usec;
    }
    return 0;}

主要是使用了PFRING和PCAP的api來接收。後面便是一系列的接收後的處理了。在 mian.c757行

後面還會判斷是否為源ip,判斷方式不是相等,是判斷某個範圍。

int is_my_port(const struct Source *src, unsigned port){
    return src->port.first <= port && port <= src->port.last;}

接著後面的處理

if (TCP_IS_SYNACK(px, parsed.transport_offset)
    || TCP_IS_RST(px, parsed.transport_offset)) {
    // 判斷是否是syn+ack或rst標誌位
  /* 獲取狀態 */
  status = PortStatus_Unknown;
  if (TCP_IS_SYNACK(px, parsed.transport_offset))
    status = PortStatus_Open; // syn+ack 說明埠開放
  if (TCP_IS_RST(px, parsed.transport_offset)) {
    status = PortStatus_Closed; // rst 說明埠關閉
  }
  /* verify: syn-cookies 校驗cookie是否正確 */
  if (cookie != seqno_me - 1) {
    LOG(5, "%u.%u.%u.%u - bad cookie: ackno=0x%08x expected=0x%08x\n",
        (ip_them>>24)&0xff, (ip_them>>16)&0xff,
        (ip_them>>8)&0xff, (ip_them>>0)&0xff,
        seqno_me-1, cookie);
    continue;
  }
  /* verify: ignore duplicates  校驗是否重複*/
  if (dedup_is_duplicate(dedup, ip_them, port_them, ip_me, port_me))
    continue;
  /* keep statistics on number received 統計接收的數字*/
  if (TCP_IS_SYNACK(px, parsed.transport_offset))
    (*status_synack_count)++;
  /*   * This is where we do the output   * 這是輸出狀態了   */
  output_report_status(
    out,
    global_now,
    status,
    ip_them,
    6, /* ip proto = tcp */
    port_them,
    px[parsed.transport_offset + 13], /* tcp flags */
    parsed.ip_ttl,
    parsed.mac_src
  );
  /*   * Send RST so other side isn't left hanging (only doing this in   * complete stateless mode where we aren't tracking banners)   */
  // 傳送rst給服務端,防止服務端一直等待。
  if (tcpcon == NULL && !Masscan->is_noreset)
    tcp_send_RST(
    &parms->tmplset->pkts[Proto_TCP],
    parms->packet_buffers,
    parms->transmit_queue,
    ip_them, ip_me,
    port_them, port_me,
    0, seqno_me);}

Zmap原始碼分析

Zmap官方有一篇 ,講述了Zmap的原理以及一些實踐。上文說到Zmap使用的發包技術和Masscan大同小異,高速模式下都是呼叫pf_ring的驅動進行,所以對這些就不再敘述了,主要說下其他與Masscan不同的地方,paper中對丟包問題以及掃描時間段有一些研究,簡單整理下

  1. 傳送多個探針:結果表明,傳送8個SYN包後,響應主機數量明顯趨於平穩
  2. 哪些時間更適合掃描
    1. 我們觀察到一個±3.1%的命中率變化依賴於日間掃描的時間。最高反應率在美國東部時間上午7時左右,最低反應率在美國東部時間下午7時45分左右。
    2. 這些影響可能是由於整體網路擁塞和包丟失率的變化,或者由於只間斷連線到網路的終端主機的總可用性的日變化模式。在不太正式的測試中,我們沒有注意到任何明顯的變化

還有一點是Zmap只能掃描單個埠,看了一下程式碼,這個儲存埠變數的作用也只是在最後接收資料包用來判斷srcport用,不明白為什麼還沒有加上多埠的支援。

寬頻限制

相比於Masscan用 rate=10000作為限制引數,Zmap用 -B 10M的方式來限制

我覺得這點很好,因為不是每個使用者都能明白每個引數代表的原理。實現細節

發包與解包

Zmap不支援Windows,因為Zmap的發包預設用的是socket,在window下可能不支援tcp的組包(猜測)。相比之下Masscan使用的是pcap發包,在win/linux都有支援的程式。Zmap接收預設使用的是pcap。

在構造tcp包時,附帶的狀態資訊會填入到seq和srcport中

在解包時,先判斷返回dstport的資料

再判斷返回的ack中的資料

用go寫埠掃描器

在瞭解完以上後,我就準備用go寫一款類似的掃描器了,希望能解決丟包的問題,順便學習go。

在上面分析中知道了,Masscan和Zmap都使用了pcap,pfring這些元件來原生髮包,值得高興的是go官方也有原生支援這些的包  ,而且完美符合我們的要求。

介面沒問題,在實現了基礎的無狀態掃描功能後,接下來就是如何處理丟包的問題。

丟包問題

按照tcp協議的原理,我們傳送一個資料包給目標機器,埠開放時返回 ack標記,關閉會返回 rst標記。

但是透過掃描一臺外網的靶機,發現掃描幾個埠是沒問題的,但是掃描大批次的埠(1-65535),就可能造成丟包問題。而且不存在的埠不會返回任何資料。

控制速率

剛開始以為是速度太快了,所以先控制下每秒傳送的頻率。因為傳送和接收都是啟動了一個goroutine,目標的傳入是透過一個channel傳入的(go的知識點)。

所以控制速率的虛擬碼類似這樣

rate := 300 // 每秒速度var data = []int{1, 2, 3, 4, 5, 6,...,65535} // 埠陣列ports := make(chan int, rate)go func() {
        // 每秒將data資料分配到ports
        index := 0
        for {
            OldTimestap := time.Now().UnixNano() / 1e6 // 取毫秒
            for i := index; i < index+rate; i++ {
                if len(datas) <= index {
                    break
                }
                index++
                distribution <- data[i]
            }
            if len(datas) <= index {
                break
            }
            Timestap := time.Now().UnixNano() / 1e6
            TimeTick := Timestap - OldTimestap
            if TimeTick < 1000 {
                time.Sleep(time.Duration(1000-TimeTick) * time.Millisecond)
            }
        }
        fmt.Println("傳送完畢..")
    }()

本地狀態表

即使將速度控制到了最小,也存在丟包的問題,後經過一番測試,發現是防火牆的原因。例如常用的 iptables,其中拒絕的埠不會返回資訊。將埠放行後再次掃描,就能正常返回資料包了。

此時遇到的問題是有防火牆策略的主機如何進行準確掃描,一種方法是掃描幾個埠後就延時一段時間,但這不符合快速掃描的設想,所以我的想法是維護一個本地的狀態表,狀態表中能夠動態修改每個掃描結果的狀態,將那些沒有返回包的目標進行重試。

Ps:這是針對一個主機,多埠(1-65535)的掃描策略,如果是多個IP,Masscan的 隨機化地址掃描策略就能發揮作用了。

設想的結構如下

// 本地狀態表的資料結構type ScanData struct {
    ip     string
    port   int
    time   int64 // 傳送時間
    retry  int   // 重試次數
    status int   // 0 未傳送 1 已傳送 2 已回覆 3 已放棄}

初始資料時 status為0,當傳送資料時,將 status變更為1,同時記錄傳送時間 time,接收資料時透過返回的標記, dstport, seq等查詢到本地狀態表相應的資料結構,變更 status為2,同時啟動一個監控程式,監控程式每隔一段時間對所有的狀態進行檢查,如果發現 stauts為1並且當前時間-傳送時間大於一定值的時候,可以判斷這個ip+埠的探測包丟失了,準備重發,將 retry+1,重新設定傳送時間 time後,將資料傳入傳送的channel中。

概念驗證程式

因為只是概念驗證程式,而且是自己組包傳送,需要使用到本地和閘道器的mac地址等,這些還沒有寫自動化程式獲取,需要手動填寫。mac地址可以手動用wireshark抓包獲得。

如果你想使用該程式的話,需要修改全域性變數中的這些值

var (
    SrcIP  string           = "10.x.x.x" // 源IP
    DstIp  string           = "188.131.x.x" // 目標IP
    device string           = "en0" // 網路卡名稱
    SrcMac net.HardwareAddr = net.HardwareAddr{0xf0, 0x18, 0x98, 0x1a, 0x57, 0xe8} // 源mac地址
    DstMac net.HardwareAddr = net.HardwareAddr{0x5c, 0xc9, 0x99, 0x33, 0x37, 0x80} // 閘道器mac地址)

整個go語言源程式如下,單檔案。

package mainimport (
    "fmt"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
    "log"
    "net"
    "sync"
    "time")var (
    SrcIP  string           = "10.x.x.x" // 源IP
    DstIp  string           = "188.131.x.x" // 目標IP
    device string           = "en0" // 網路卡名稱
    SrcMac net.HardwareAddr = net.HardwareAddr{0xf0, 0x18, 0x98, 0x1a, 0x57, 0xe8} // 源mac地址
    DstMac net.HardwareAddr = net.HardwareAddr{0x5c, 0xc9, 0x99, 0x33, 0x37, 0x80} // 閘道器mac地址)// 本地狀態表的資料結構type ScanData struct {
    ip     string
    port   int
    time   int64 // 傳送時間
    retry  int   // 重試次數
    status int   // 0 未傳送 1 已傳送 2 已回覆 3 已放棄}func recv(datas *[]ScanData, lock *sync.Mutex) {
    var (
        snapshot_len int32         = 1024
        promiscuous  bool          = false
        timeout      time.Duration = 30 * time.Second
        handle       *pcap.Handle
    )
    handle, _ = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)
    // Use the handle as a packet source to process all packets
    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    scandata := *datas
    for {
        packet, err := packetSource.NextPacket()
        if err != nil {
            continue
        }
        if IpLayer := packet.Layer(layers.LayerTypeIPv4); IpLayer != nil {
            if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
                tcp, _ := tcpLayer.(*layers.TCP)
                ip, _ := IpLayer.(*layers.IPv4)
                if tcp.Ack != 111223 {
                    continue
                }
                if tcp.SYN && tcp.ACK {
                    fmt.Println(ip.SrcIP, " port:", int(tcp.SrcPort))
                    _index := int(tcp.DstPort)
                    lock.Lock()
                    scandata[_index].status = 2
                    lock.Unlock()
                } else if tcp.RST {
                    fmt.Println(ip.SrcIP, " port:", int(tcp.SrcPort), " close")
                    _index := int(tcp.DstPort)
                    lock.Lock()
                    scandata[_index].status = 2
                    lock.Unlock()
                }
            }
        }
        //fmt.Printf("From src port %d to dst port %d\n", tcp.SrcPort, tcp.DstPort)
    }}func send(index chan int, datas *[]ScanData, lock *sync.Mutex) {
    srcip := net.ParseIP(SrcIP).To4()
    var (
        snapshot_len int32 = 1024
        promiscuous  bool  = false
        err          error
        timeout      time.Duration = 30 * time.Second
        handle       *pcap.Handle
    )
    handle, err = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)
    if err != nil {
        log.Fatal(err)
    }
    defer handle.Close()
    scandata := *datas
    for {
        _index := <-index
        lock.Lock()
        data := scandata[_index]
        port := data.port
        scandata[_index].status = 1
        dstip := net.ParseIP(data.ip).To4()
        lock.Unlock()
        eth := &layers.Ethernet{
            SrcMAC:       SrcMac,
            DstMAC:       DstMac,
            EthernetType: layers.EthernetTypeIPv4,
        }
        // Our IPv4 header
        ip := &layers.IPv4{
            Version:    4,
            IHL:        5,
            TOS:        0,
            Length:     0, // FIX
            Id:         0,
            Flags:      layers.IPv4DontFragment,
            FragOffset: 0,  //16384,
            TTL:        64, //64,
            Protocol:   layers.IPProtocolTCP,
            Checksum:   0,
            SrcIP:      srcip,
            DstIP:      dstip,
        }
        // Our TCP header
        tcp := &layers.TCP{
            SrcPort:  layers.TCPPort(_index),
            DstPort:  layers.TCPPort(port),
            Seq:      111222,
            Ack:      0,
            SYN:      true,
            Window:   1024,
            Checksum: 0,
            Urgent:   0,
        }
        //tcp.DataOffset = 5 // uint8(unsafe.Sizeof(tcp))
        _ = tcp.SetNetworkLayerForChecksum(ip)
        buf := gopacket.NewSerializeBuffer()
        err := gopacket.SerializeLayers(
            buf,
            gopacket.SerializeOptions{
                ComputeChecksums: true, // automatically compute checksums
                FixLengths:       true,
            },
            eth, ip, tcp,
        )
        if err != nil {
            log.Fatal(err)
        }
        //fmt.Println("\n" + hex.EncodeToString(buf.Bytes()))
        err = handle.WritePacketData(buf.Bytes())
        if err != nil {
            fmt.Println(err)
        }
    }}func main() {
    version := pcap.Version()
    fmt.Println(version)
    retry := 8
    var datas []ScanData
    lock := &sync.Mutex{}
    for i := 20; i < 1000; i++ {
        temp := ScanData{
            port:   i,
            ip:     DstIp,
            retry:  0,
            status: 0,
            time:   time.Now().UnixNano() / 1e6,
        }
        datas = append(datas, temp)
    }
    fmt.Println("target", DstIp, " count:", len(datas))
    rate := 300
    distribution := make(chan int, rate)
    go func() {
        // 每秒將ports資料分配到distribution
        index := 0
        for {
            OldTimestap := time.Now().UnixNano() / 1e6
            for i := index; i < index+rate; i++ {
                if len(datas) <= index {
                    break
                }
                index++
                distribution <- i
            }
            if len(datas) <= index {
                break
            }
            Timestap := time.Now().UnixNano() / 1e6
            TimeTick := Timestap - OldTimestap
            if TimeTick < 1000 {
                time.Sleep(time.Duration(1000-TimeTick) * time.Millisecond)
            }
        }
        fmt.Println("傳送完畢..")
    }()
    go recv(&datas, lock)
    go send(distribution, &datas, lock)
    // 監控
    for {
        time.Sleep(time.Second * 1)
        count_1 := 0
        count_2 := 0
        count_3 := 0
        var ids []int
        lock.Lock()
        for index, data := range datas {
            if data.status == 1 {
                count_1++
                if data.retry >= retry {
                    datas[index].status = 3
                    continue
                }
                nowtime := time.Now().UnixNano() / 1e6
                if nowtime-data.time >= 1000 {
                    datas[index].retry += 1
                    datas[index].time = nowtime
                    ids = append(ids, index)
                    //fmt.Println("重發id:", index)
                    //distribution <- index
                }
            } else if data.status == 2 {
                count_2++
            } else if data.status == 3 {
                count_3++
            }
        }
        lock.Unlock()
        if len(ids) > 0 {
            time.Sleep(time.Second)
            increase := 0
            interval := 60
            for _, v := range ids {
                distribution <- v
                increase++
                if increase > 1 && increase%interval == 0 {
                    time.Sleep(time.Second)
                }
            }
        }
        fmt.Println("status=1:", count_1, "status=2:", count_2, "status=3:", count_3)
    }}

執行結果如下

但這個程式並沒有解決上述說的防火牆阻斷問題,設想很美好,但是在實踐的過程中發現這樣一個問題。比如掃描一臺主機中的1000個埠,第一次掃描後由於有防火牆的策略只檢測到了5個埠,剩下995個埠會進行第一次重試,但是重試中依然會遇到防火牆的問題,所以本質上並沒有解決這個問題。

Top埠

這是Masscan原始碼中一份內建的Top埠表

staticconstunsignedshorttop_tcp_ports[]={1,3,4,6,7,9,13,17,19,20,21,22,23,24,25,26,30,32,33,37,42,43,49,53,70,79,80,81,82,83,84,85,88,89,90,99,100,106,109,110,111,113,119,125,135,139,143,144,146,161,163,179,199,211,212,222,254,255,256,259,264,280,301,306,311,340,366,389,406,407,416,417,425,427,443,444,445,458,464,465,481,497,500,512,513,514,515,524,541,543,544,545,548,554,555,563,587,593,616,617,625,631,636,646,648,666,667,668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800,801,808,843,873,880,888,898,900,901,902,903,911,912,981,987,990,992,993,995,999,1000,1001,1002,1007,1009,1010,1011,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1102,1104,1105,1106,1107,1108,1110,1111,1112,1113,1114,1117,1119,1121,1122,1123,1124,1126,1130,1131,1132,1137,1138,1141,1145,1147,1148,1149,1151,1152,1154,1163,1164,1165,1166,1169,1174,1175,1183,1185,1186,1187,1192,1198,1199,1201,1213,1216,1217,1218,1233,1234,1236,1244,1247,1248,1259,1271,1272,1277,1287,1296,1300,1301,1309,1310,1311,1322,1328,1334,1352,1417,1433,1434,1443,1455,1461,1494,1500,1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687,1688,1700,1717,1718,1719,1720,1721,1723,1755,1761,1782,1783,1801,1805,1812,1839,1840,1862,1863,1864,1875,1900,1914,1935,1947,1971,1972,1974,1984,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2013,2020,2021,2022,2030,2033,2034,2035,2038,2040,2041,2042,2043,2045,2046,2047,2048,2049,2065,2068,2099,2100,2103,2105,2106,2107,2111,2119,2121,2126,2135,2144,2160,2161,2170,2179,2190,2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381,2382,2383,2393,2394,2399,2401,2492,2500,2522,2525,2557,2601,2602,2604,2605,2607,2608,2638,2701,2702,2710,2717,2718,2725,2800,2809,2811,2869,2875,2909,2910,2920,2967,2968,2998,3000,3001,3003,3005,3006,3007,3011,3013,3017,3030,3031,3052,3071,3077,3128,3168,3211,3221,3260,3261,3268,3269,3283,3300,3301,3306,3322,3323,3324,3325,3333,3351,3367,3369,3370,3371,3372,3389,3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689,3690,3703,3737,3766,3784,3800,3801,3809,3814,3826,3827,3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000,4001,4002,4003,4004,4005,4006,4045,4111,4125,4126,4129,4224,4242,4279,4321,4343,4443,4444,4445,4446,4449,4550,4567,4662,4848,4899,4900,4998,5000,5001,5002,5003,5004,5009,5030,5033,5050,5051,5054,5060,5061,5080,5087,5100,5101,5102,5120,5190,5200,5214,5221,5222,5225,5226,5269,5280,5298,5357,5405,5414,5431,5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678,5679,5718,5730,5800,5801,5802,5810,5811,5815,5822,5825,5850,5859,5862,5877,5900,5901,5902,5903,5904,5906,5907,5910,5911,5915,5922,5925,5950,5952,5959,5960,5961,5962,5963,5987,5988,5989,5998,5999,6000,6001,6002,6003,6004,6005,6006,6007,6009,6025,6059,6100,6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565,6566,6567,6580,6646,6666,6667,6668,6669,6689,6692,6699,6779,6788,6789,6792,6839,6881,6901,6969,7000,7001,7002,7004,7007,7019,7025,7070,7100,7103,7106,7200,7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777,7778,7800,7911,7920,7921,7937,7938,7999,8000,8001,8002,8007,8008,8009,8010,8011,8021,8022,8031,8042,8045,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8093,8099,8100,8180,8181,8192,8193,8194,8200,8222,8254,8290,8291,8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651,8652,8654,8701,8800,8873,8888,8899,8994,9000,9001,9002,9003,9009,9010,9011,9040,9050,9071,9080,9081,9090,9091,9099,9100,9101,9102,9103,9110,9111,9200,9207,9220,9290,9415,9418,9485,9500,9502,9503,9535,9575,9593,9594,9595,9618,9666,9876,9877,9878,9898,9900,9917,9929,9943,9944,9968,9998,9999,10000,10001,10002,10003,10004,10009,10010,10012,10024,10025,10082,10180,10215,10243,10566,10616,10617,10621,10626,10628,10629,10778,11110,11111,11967,12000,12174,12265,12345,13456,13722,13782,13783,14000,14238,14441,14442,15000,15002,15003,15004,15660,15742,16000,16001,16012,16016,16018,16080,16113,16992,16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221,20222,20828,21571,22939,23502,24444,24800,25734,25735,26214,27000,27352,27353,27355,27356,27715,28201,30000,30718,30951,31038,31337,32768,32769,32770,32771,32772,32773,32774,32775,32776,32777,32778,32779,32780,32781,32782,32783,32784,32785,33354,33899,34571,34572,34573,35500,38292,40193,40911,41511,42510,44176,44442,44443,44501,45100,48080,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49163,49165,49167,49175,49176,49400,49999,50000,50001,50002,50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055,55056,55555,55600,56737,56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389};

可以使用 --top-ports = n來選擇數量。

這是在寫完go掃描器後又在Masscan中發現的,可能想象到Masscan可能也考慮過這個問題,它的方法是維護一個top常用埠的排行來儘可能減少掃描埠的數量,這樣可以覆蓋到大多數的埠(猜測)。

總結

概念性程式實踐失敗了,所以再用go開發的意義也不大了,後面還有一個坑就是go的pcap不能跨平臺編譯,只能在Windows下編譯windows版本,mac下編譯mac版本。

但是研究了Masscan和Zmap在tcp協議下的syn掃描模式,還是有很多收穫,以及明白了它們為什麼要這麼做,同時對網路協議和一些更低層的細節有了更深的認識。

這裡個人總結了一些tips:

  • Masscan原始碼比Zmap讀起來更清晰,註釋也很多,基本上一看原始碼就能明白大致的結構了。
  • Masscan和Zmap最高速度模式都是使用的pfring這個驅動程式,理論上它兩的速度是一致的,只是它們宣傳口徑不一樣?
  • 網路寬頻足夠情況下,掃描單個埠準確率是最高的(透過自己編寫go掃描器的實踐得出)。
  • Masscan和Zmap都能利用多網路卡,但是Zmap執行緒切換用了鎖,可能會消耗部分時間。
  • 設定發包速率時不僅要考慮自己頻寬,還要考慮目標伺服器的承受情況(掃描多埠時)


參考連結

如需轉載,請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2659926/,如需轉載,請註明出處,否則將追究法律責任。

相關文章