syn无状态扫描程序开发(三)

系列 - syn无状态扫描程序开发

先说一下大概的流程

  1. 初始化一个libpcap嗅探器
  2. 设置数据包过滤器,过滤出ack和rst的tcp包
  3. 开始捕获数据包
  4. 处理捕获的数据包,校验ack和dport

具体内容请看以下代码,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int recv_init(void) {
	char errbuf[PCAP_ERRBUF_SIZE];
	int snaplen = 96; // 参数定义捕获数据的最大字节数

	// 创建一个嗅探的描述符
	pc = pcap_open_live(pconf.iface, snaplen, PCAP_PROMISC, PCAP_TIMEOUT, errbuf);
	if (pc == NULL) {
		fprintf(stderr, "could not open device %s: %s", pconf.iface, errbuf);
		return -1;
	}
	switch (pcap_datalink(pc)) {
		case DLT_EN10MB:
			pconf.data_link_size = sizeof(struct ether_header);
			break;
		case DLT_RAW:
			pconf.data_link_size = 0;
			break;
			#if __linux__
		case DLT_LINUX_SLL:
			pconf.data_link_size = SLL_HDR_LEN;
			break;
			#endif
		default:
			fprintf(stderr, "unknown data link layer");
			return -1;
	}

	struct bpf_program bpf;
	char *pcap_filter = "tcp && tcp[13] & 4 != 0 || tcp[13] == 18"; // 接收syn-ack ack-rst rst
	if (pcap_compile(pc, &bpf, pcap_filter, 1, 0) < 0) { // 编译filter过滤器 pcap_compile()
		fprintf(stderr, "couldn't compile filter");
		return -1;
	}
	if (pcap_setfilter(pc, &bpf) < 0) { // 设置filter过滤器 pcap_setfilter()
		fprintf(stderr, "couldn't install filter");
		return -1;
	}

	// set pcap_dispatch to not hang if it never receives any packets
	// this could occur if you ever scan a small number of hosts as
	// documented in issue #74.
	if (pcap_setnonblock(pc, 1, errbuf) == -1) { // pcap_dispatch非阻塞
		fprintf(stderr, "pcap_setnonblock error:%s", errbuf);
		return -1;
	}

	return 0;
}

由于设置lipcap_dispatch为非阻塞,所以需要while循环不停的捕获派发数据包,数据包派发给packet_cb函数进行处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void packet_cb(u_char __attribute__((__unused__)) *user,
			   const struct pcap_pkthdr *p, const u_char *bytes) {
	if (p == NULL) {
		return;
	}
	if (precv.success_total >= pconf.max_results) {
		// Libpcap can process multiple packets per pcap_dispatch;
		// we need to throw out results once we've
		// gotten our --max-results worth.
		return;
	}

	// length of entire packet captured by libpcap
	uint32_t buflen = (uint32_t) p->caplen;
	handle_packet(buflen, bytes);
}

void recv_packets() {
	/**
     * 1.函数名称:int pcap_dispatch(pcap_t *p, int cnt,pcap_handler callback, u_char *user)
     * 函数功能:捕获并处理数据包。
     * 参数说明:cnt 参数指定函数返回前所处理数据包的最大值。
     * cnt=-1表示在一个缓冲区中处理所有的数据包。
     * cnt=0表示处理所有数据包,直到产生以下错误之一:读取 到EOF;超时读取。
     * callback参数指定一个带有三个参数的回调函数,这三个参数为:
     * 一个从pcap_dispatch()函数传递过来的 u_char指针,
     * 一个pcap_pkthdr结构的指针,
     * 和一个数据包大小的u_char指针。
     * 如果成功则返回读取到的字节数。读取到EOF时则返回零 值。
     * 出错时则返回-1,此时可调用pcap_perror()或pcap_geterr()函数获取错误消息。
     */
	int ret = pcap_dispatch(pc, -1, packet_cb, NULL);
	if (ret == -1) {
		fprintf(stderr, "pcap_dispatch error, err=%s", pcap_geterr(pc));
	} else if (ret == 0) {
		usleep(1000);
	}
}

void *receiver(void *arg) {
	if (recv_init() != 0) {
		fprintf(stderr, "recv init error.");
		return NULL;
	}
	precv.recv_ready = 1;
	precv.start = now();
	do {
		recv_packets();
	} while (!(psend.complete &&
			   ((now() - psend.finish > precv.cooldown_secs) || precv.validation_passed == psend.sent)));

	// pcap关闭之前获取最终的统计数据
	recv_update_stats();

	// 清理
	recv_cleanup();

	return 0;
}

handle函数中传入的是嗅探到的一个数据包,但是也有可能,这个数据包原来的很大的,这里接收到的不完整,所以需要进行过滤以下。然后提取ip头 tcp头开始校验

  1. 过滤不完整的数据包
1
2
3
4
5
6
    // packet格式:以太网头+ip头+tcp头
if (len < sizeof(struct ip) + pconf.data_link_size) {
	// buffer not large enough to contain ethernet
	// and ip headers. further action would overrun buf
	return;
}
  1. 提取ip头,获取src ip
1
2
3
4
5
// 提取ip头
struct ip *ip_hdr = (struct ip *) &packet[pconf.data_link_size];

// 提取src ip
uint32_t src_ip = ip_hdr->ip_src.s_addr;
  1. 提取tcp头获取sport(这里的sport,表示send时候的dport)
1
2
3
struct tcphdr *tcp =
	(struct tcphdr *) ((char *) ip_hdr + 4 * ip_hdr->ip_hl);
port_h_t sport = ntohs(tcp->th_sport);
  1. 生成密文用于校验
1
2
3
4
// 生成密文,用于校验数据包来源
uint32_t validation[VALIDATE_BYTES / sizeof(uint32_t)];
// 接收到的src ip就是发送时候的dst ip
validate_gen(ip_hdr->ip_dst.s_addr, ip_hdr->ip_src.s_addr, (uint8_t *) validation);
  1. 校验send时候的src port,详细描述请看注释
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int check_dst_port(port_h_t port, int num_ports, uint32_t *validation) { // 相对于recv数据包的目的端口
	if (port > source_port_last || port < source_port_first) {
		return 0;
	}
	int32_t to_validate = port - source_port_first;
	int32_t min = validation[1] % num_ports;  // packet——stream下限相对取值
	int32_t max = (validation[1] + pconf.packet_streams - 1) % num_ports; // packet——stream上限相对取值

	return (((max - min) % num_ports) >= ((to_validate - min) % num_ports));
}
  1. 校验ack,区分ack包和rst包
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// We treat RST packets different from non RST packets
uint32_t th_ack = ntohl(tcp->th_ack);
if (tcp->th_flags == TH_RST) { // 接收到rst包
	// For RST packets, recv(ack) == sent(seq) + 0 or + 1
	if (th_ack != validation[0] &&
		th_ack != validation[0] + 1) { // 校验不通过,则表示rst包为其他来源的包
		return -1;
	} else {
		// todo 校验通过 ,表示rst包来自扫描ip, 扫描ip只会返回ack-rst而不是rst。。猜测可能是防火墙伪造的包,也可能这个条件永远不会到达。
		// 暂留坑
		assert(1);
	}
} else { // 接收到非rst包 主要是想接收到(syn-ack, ack-rst)
	// For non RST packets, recv(ack) == sent(seq) + 1
	if (th_ack != validation[0] + 1) { // 校验不通过,表示为其他来源的包
		return -1;
	} else {
		if (tcp->th_flags == (TH_ACK + TH_RST)) { // ack-rst 端口关闭
			return 0;
		} else if ((tcp->th_flags == (TH_SYN + TH_ACK))) { // ack-syn 端口开放
			return sport;
		} else { // unknow
			return -1;
		}

	}
}

到了这里,就是后期的数据处理问题了,无论是输出到文件,还是控制台,都很简单。只需要拿到上面syn-ack包校验通过的sport端口,就是目标机器open的端口,syn-rst包校验通过的sport端口就是close的端口,其他的则是fitler不可达端口

syn无状态扫描技术到此算是结束了,作者基于此开发的全端口扫描,在10m带宽下只需要3~4秒的时间。如果带宽足够的话,应该是可以一小时扫全球了。由于作者水平有限,文中难免存在疏漏,恳请各位读者批评指正!