扫描探测工具识别

网络扫描探测通常是发起网络入侵的第一步,攻击者可以利用扫描探测工具获取网络中的主机系统、TCP/UDP 端口的开放情况、子域名、网站指纹、WAF、CDN、中间件类别等重要信息,识别出存在安全漏洞的主机或系统,从而发起有针对性的网络入侵行为。此外,一些扫描工具同时具备漏洞利用的能力。因此,对网络扫描探测行为进行识别和研究,有利于及时发现网络攻击的前兆,发现网络攻击行为,快速定位网络服务中存在的漏洞,对网络安全防护工作十分有意义。

本文以下列三个常见扫描器为代表,探究扫描器的特有指纹信息,编写 Demo 进行扫描器的识别。

Zmap

抓包分析

ZMap 被设计用来针对整个 IPv4 地址空间或其中的大部分实施综合扫描的工具。

默认情况下,ZMap 会对于指定端口实施尽可能大速率的 TCP SYN 扫描。如下图所示,客户端在发送一个 SYN 包的时候,如果对方端口开放,就会发送一个 SYN-ACK,那么就表明这个端口开放,这时候我们发送 RST 包,防止占用对方资源;如果对方端口不开放,那么我们就会收到对方主机的 RST 包。

较为保守的情况下,对 10,000 个随机的地址的 80 端口以 10Mbps 的速度扫描,如下所示:

在生成的 csv 结果文件中,以下 IP 地址的 80 端口开放:

47.243.139.246
20.205.204.152
121.36.193.65
156.245.39.71
13.238.233.150
142.234.31.240
68.183.75.244
185.48.122.237
52.25.116.123
104.127.1.181
185.248.102.245
95.217.201.8
3.125.24.134
23.15.117.202

抓包结果如下所示,Zmap 向随机的 10,000 个 IP 的 80 端口发送 SYN 数据包。

如果 IP 的 80 端口开放,以 47.243.139.246 为例,筛选出的数据包如下图所示,具体解释为:

  1. 向 47.243.139.246 的 80 端口发送 SYN 数据包
  2. 接收到 47.243.139.246 的 80 端口的 SYN/ACK 包,证明该 IP 的 80 端口可用
  3. 向 47.243.139.246 的 80 端口发送 RST 数据包,防止占用对方资源

如果 IP 的 80 端口不开放,以 44.102.170.124 为例,筛选出的数据包如下图所示。Zmap 向其发送 SYN 请求后没有得到应答,故判断该 IP 的 80 端口不可用。

查看 Zmap 向哪些 IP 发送了 RST 数据包,则证明这些 IP 的 80 端口可用。筛选结果如下图所示,目的地址与上述的 csv 结果文件一致。

源码分析

Zmap 整体函数调用图如下所示。

通过图我们可以直观的看到整个程序调用的过程。Zmap 在启动时候,先获取环境信息,如 IP、网关等。然后读取配置文件选择使用哪种扫描方式,然后在 Probe_modules 切换到对应的模块,然后启动。

下面侧重分析 SYN 扫描这个模块,整个执行的过程中,会有一个线程专门负责发送,另外有一个使用 libpcap 组件抓包,发送和接收就独立开来。

zmap/src/probe_modules/module_tcp_synscan.c 是用于执行 TCP SYN 扫描的探测模块,在初始化阶段的 synscan_init_perthread 函数中,依次调用 make_ip_header 函数和 make_tcp_header 函数进行数据包 header 的封装。

static int synscan_init_perthread(
    void *buf, macaddr_t *src, macaddr_t *gw,
    port_h_t dst_port,
    UNUSED void **arg_ptr)
{
    struct ether_header *eth_header = (struct ether_header *)buf;
    make_eth_header(eth_header, src, gw);
    struct ip *ip_header = (struct ip *)(&eth_header[1]);
    uint16_t len = htons(sizeof(struct ip) + ZMAP_TCP_SYNSCAN_TCP_HEADER_LEN);
    make_ip_header(ip_header, IPPROTO_TCP, len);
    struct tcphdr *tcp_header = (struct tcphdr *)(&ip_header[1]);
    make_tcp_header(tcp_header, dst_port, TH_SYN);
    set_mss_option(tcp_header);
    return EXIT_SUCCESS;
}

这两个函数编写于 zmap/src/probe_modules/packet.c 中。分析 make_ip_header 函数可知,在下示第 7 行,IP 的 identification number 被设置为固定的 54321。

void make_ip_header(struct ip *iph, uint8_t protocol, uint16_t len)
{
    iph->ip_hl = 5;  // Internet Header Length
    iph->ip_v = 4;   // IPv4
    iph->ip_tos = 0; // Type of Service
    iph->ip_len = len;
    iph->ip_id = htons(54321); // identification number
    iph->ip_off = 0;	   // fragmentation flag
    iph->ip_ttl = MAXTTL;      // time to live (TTL)
    iph->ip_p = protocol;      // upper layer protocol => TCP
    // we set the checksum = 0 for now because that's
    // what it needs to be when we run the IP checksum
    iph->ip_sum = 0;
}

分析 make_tcp_header 函数可知,在下示第 10 行,TCP 的 window 被设置为固定的 65535。

void make_tcp_header(struct tcphdr *tcp_header, port_h_t dest_port,
		     uint16_t th_flags)
{
    tcp_header->th_seq = random();
    tcp_header->th_ack = 0;
    tcp_header->th_x2 = 0;
    tcp_header->th_off = 5; // data offset
    tcp_header->th_flags = 0;
    tcp_header->th_flags |= th_flags;
    tcp_header->th_win = htons(65535); // largest possible window
    tcp_header->th_sum = 0;
    tcp_header->th_urp = 0;
    tcp_header->th_dport = htons(dest_port);
}

查看抓取的 SYN 数据包,如下图所示,IP 的 ID 和 TCP 的 window 确实为 54321 和 65535,所以这两个固定值可作为扫描器特征。

Angry IP Scanner

抓包分析

Angry IP Scanner(简称 angryip) 是一款开源跨平台的网络扫描器,主要用于扫描 IP 地址和端口。

angryip 默认使用 Windows ICMP 方法扫描各个 ip 地址,扫描每个 IP 的 80、443 和 8080 端口。以 IP 范围 123.56.104.200~123.56.104.250 为例,扫描结果如下图所示,红色代表 IP 不可用,蓝色代表 IP 可用端口不可用,绿色代表 IP 和端口均可用。

在捕获的数据包中,以 123.56.104.218 为例,该 IP 被标记为绿色,下面是与它有关的数据包抓取结果。

图中第一个红框处 angryip 与 123.56.104.218 进行了 3 次 ping,且都予以回复,说明该 IP 可用。第二个红框处 angryip 分别测试 123.56.104.218 的 80、443 和 8080 端口,其中 80 和 443 端口予以回复,说明这两个端口可用。

在不可用的 IP 中,以 123.56.104.204 为例,与它相关的数据包抓取结果如下。angryip 向其发送 3 次 ping 请求,都没有得到回复,则判断其 IP 不可用,也没有向其端口发送数据包。

源码分析

因为无论 IP 和端口是否可用,angryip 都会先发送 ping 数据包,所以通过 ping 阶段的源码分析其工具的特征。

分析 ipscan/test/net/azib/ipscan/core/net/ICMPSharedPingerTest.java 源码,该测试类调用 pinger.ping()方法 3 次,并计算平均时长。

public class ICMPSharedPingerTest {
    @Test @Ignore("this test works only under root")
    public void testPing() throws Exception {
	Pinger pinger = new ICMPSharedPinger(1000);
	PingResult result = pinger.ping(new ScanningSubject(InetAddress.getLocalHost()), 3);
	assertTrue(result.getAverageTime() >= 0);
	assertTrue(result.getAverageTime() < 50);
	assertTrue(result.getTTL() >= 0);
    }
}

该方法在 ipscan/test/net/azib/ipscan/core/net/WindowsPinger.java 中,源码如下所示,判断 IP 类型,并调用 IPv6 和 IPv4 对应的方法。

public PingResult ping(ScanningSubject subject, int count) throws IOException {
    if (subject.isIPv6())
	return ping6(subject, count);
    else
	return ping4(subject, count);
}

以 IPv4 为例,方法中定义了数据包的数据大小为 32,即 sendDataSize = 32。后续使用 Memory()方法创建 SendData 对象,并未对其进行赋值,故默认值应全为 0。

private PingResult ping4(ScanningSubject subject, int count) throws IOException {
    Pointer handle = dll.IcmpCreateFile();
    if (handle == null) throw new IOException("Unable to create Windows native ICMP handle");

    int sendDataSize = 32;
    int replyDataSize = sendDataSize + (new IcmpEchoReply().size()) + 10;
    Pointer sendData = new Memory(sendDataSize);
    sendData.clear(sendDataSize);
    Pointer replyData = new Memory(replyDataSize);

    PingResult result = new PingResult(subject.getAddress(), count);
    try {
	IpAddrByVal ipaddr = toIpAddr(subject.getAddress());
	for (int i = 1; i <= count && !currentThread().isInterrupted(); i++) {
            int numReplies = dll.IcmpSendEcho(handle, ipaddr, sendData, (short) sendDataSize, null, replyData, replyDataSize, timeout);
	    IcmpEchoReply echoReply = new IcmpEchoReply(replyData);
	    if (numReplies > 0 && echoReply.status == 0 && Arrays.equals(echoReply.address.bytes, ipaddr.bytes)) {
		result.addReply(echoReply.roundTripTime);
		result.setTTL(echoReply.options.ttl & 0xFF);
	    }
	}
    }
    finally {
	dll.IcmpCloseHandle(handle);
    }
    return result;
}

在实际抓包中,每个发出的 ICMP 请求中,Data 的大小均为 32 字节,且全为 0,所以可将它作为 angryip 的特征。

Masscan

抓包分析

Masscan 默认使用 SYN 扫描,以 IP 123.56.104.218 为例,扫描其 1~600 端口,结果如下所示。

抓包结果如下所示,Masscan 向 123.56.104.218 的 1~600 端口进行随机化扫描,发出 SYN 请求。

查看 80 端口的数据包,下图可知 80 端口向 Masscan 回复,说明该端口可用。

查看 81 端口的数据包,发现并没有数据包回复,说明该端口不可用。

筛选收到的 SYN/ACK 数据包,得到 22、443 和 80 端口,说明 123.56.104.218 的 1~600 中这 3 个端口可用。

源码分析

观察抓包分析中结果可以发现,所有发出的 SYN 请求中,窗口大小都是 1024。

在 Masscan 的主函数 masscan/src/main.c 文件中,默认使用以下代码初始化 TCP 数据包的模板。

template_packet_init(
    parms->tmplset,
    parms->source_mac,
    parms->router_mac_ipv4,
    parms->router_mac_ipv6,
    masscan->payloads.udp,
    masscan->payloads.oproto,
    stack_if_datalink(masscan->nic[index].adapter),
    masscan->seed);

该函数位于 masscan/src/templ.pkt.c 中,其中对于 TCP 的初始化代码如下所示。

/* [TCP] */
_template_init(&templset->pkts[Proto_TCP],
               source_mac, router_mac_ipv4, router_mac_ipv6,
               default_tcp_template,
               sizeof(default_tcp_template)-1,
               data_link);
templset->count++;

其中调用的 default_tcp_template 定义在该文件头部,下述 7 行指定 IP 的 length 为 40,下述 10 行指定 TLL 为 255,下述 18 行指定 ack 为 0,下述 21 行指定 window 的大小为 1024,可以将这些指标视为 Masscan 的特征。

static unsigned char default_tcp_template[] =
    "\0\1\2\3\4\5"  /* Ethernet: destination */
    "\6\7\x8\x9\xa\xb"  /* Ethernet: source */
    "\x08\x00"      /* Ethernet type: IPv4 */
    "\x45"          /* IP type */
    "\x00"
    "\x00\x28"      /* total length = 40 bytes */
    "\x00\x00"      /* identification */
    "\x00\x00"      /* fragmentation flags */
    "\xFF\x06"      /* TTL=255, proto=TCP */
    "\xFF\xFF"      /* checksum */
    "\0\0\0\0"      /* source address */
    "\0\0\0\0"      /* destination address */

    "\0\0"          /* source port */
    "\0\0"          /* destination port */
    "\0\0\0\0"      /* sequence number */
    "\0\0\0\0"      /* ack number */
    "\x50"          /* header length */
    "\x02"          /* SYN */
    "\x04\x0"        /* window fixed to 1024 */
    "\xFF\xFF"      /* checksum */
    "\x00\x00"      /* urgent pointer */
    "\x02\x04\x05\xb4"  /* added options [mss 1460] */
;

Demo 设计与实现

经过抓包分析和源码分析后,可以总结三个扫描器的特征如下表所示。

总体来看,三个扫描器工具都是基于单包的头部信息进行识别,且经过源码确认,属于强特征。那么识别的具体设计也就很容易了,对 pcap 文件的每个数据包进行分类,判断其是否满足上述三个指纹,核心识别流程图如下图所示。

具体实现使用 Python 的 Scrapy 包解析 pcap,进行相关操作,代码很短,核心部分如下。

for data in packets:
    if 'TCP' in data:
        # 识别 Zmap
        if (data['TCP'].window == 65535) and (data['IP'].id == 54321):
            isZmap = True
        # 识别 Masscan
        if data['TCP'].window == 1024 and data['TCP'].ack == 0 \
                and data['IP'].ttl == 255 and data['IP'].len == 40:
            isMasscan = True
    # 识别 Angry IP Scanner
    if 'ICMP' in data:
        if 'Raw' in data:
            items = processStr(data['Raw'].load)
            if len(data['Raw']) == 32 and items == ANGRYIP_FLAG:
                isAngryip = True

代码放置于 GitHub


参考: