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

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

Masscan源碼分析

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

理論上的6分鐘掃描全網

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

image-20191010142518478

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

之后了解到,默認模式下Masscan使用pcap發送和接收數據包,它在Windows和Mac上只有30萬/秒的發包速度,而Linux可以達到150萬/秒,如果安裝了PF_RING DNA設備,它會提升到1000萬/秒的發包速度(這些前提是硬件設備以及帶寬跟得上)。

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

PF_RING DNA設備了解地址:http://www.ntop.org/products/pf_ring/

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

在Zmap的主頁上說明了

image-20191010151936899

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

歷史記錄

觀察了readme的歷史記錄 https://github.githistory.xyz/robertdavidgraham/Masscan/blob/master/README.md

之前構建時會提醒安裝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中,image-20191011135408844

隨機化地址掃描

在讀取地址后,如果進行順序掃描,偽代碼如下

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來結束這個連接,如下圖所示

image-20191003223330374

image-20191003230816536

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

發包

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

image-20191003232846484

接著生成cookie并發送

image-20191003233102015

uint64_t
syn_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.
 ***************************************************************************/
int
rawsock_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的代碼

image-20191004003419241

大意是鎖住這個線程運行的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行

image-20191004004238243

后面還會判斷是否為源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官方有一篇paper,講述了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的方式來限制

image-20191010154942162

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

image-20191010155045099

image-20191010155334018

發包與解包

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

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

image-20191010161356014

在解包時,先判斷返回dstport的數據

image-20191012110543094

再判斷返回的ack中的數據

image-20191012110655331

用go寫端口掃描器

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

在上面分析中知道了,Masscan和Zmap都使用了pcap,pfring這些組件來原生發包,值得高興的是go官方也有原生支持這些的包 https://github.com/google/gopacket,而且完美符合我們的要求。

image-20191012111724556

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

丟包問題

按照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 main

import (
    "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)
    }
}

運行結果如下

image-20191012135527477

但這個程序并沒有解決上述說的防火墻阻斷問題,設想很美好,但是在實踐的過程中發現這樣一個問題。比如掃描一臺主機中的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線程切換用了鎖,可能會消耗部分時間。
  • 設置發包速率時不僅要考慮自己帶寬,還要考慮目標服務器的承受情況(掃描多端口時)

參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1052/