原理
首先需要了解下TCP连接过程,经典的三次握手,如下:
1、扫描端向目标端发送SYN请求建立连接 2、目标端收到请求后,回复ACK同意连接并同意发送SYN请求建立连接 3、扫描端收到后,发送ACK同意,此时三次握手完成,以此来判断端口是否存活
SYN扫描过程为:
1. 扫描端向目标发起SYN请求建立连接。 2. 目标同意SYN请求建立连接。 3. 扫描端接收后,发送RST断开连接。
扫描端发送SYN数据包后需要通过原始套接字调用recvfrom获取数据包状态。
端口状态的判断:
收到服务器返回数据以及说明如下:
1. ACK + SYN : 表示端口开启 2. ACK ,无SYN : 一般也表示开启 3. ACK + RST : 端口关闭,但是主机存活 4. 无数据 : 防火墙过滤活IP无法访问活服务器不存活
构造数据包注意点:
在构造TCP数据包时,一般有两种方式,
-
伪造IP + TCP报文
这种需要设置socket为原始套接字,并且设置选项为IP_HDRINCL,这样在调用sendto时就不会自动填充IP数据包首部。
int onVal; setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &oneVal, sizeof(oneVal));
2. 伪造TCP报文
由上可知,sendto函数会自动伪造IP首部,因此构造数据时,不用构造IP头,只需要构造tcp报文即可。
实现
直接上代码吧:
#include <arpa/inet.h> #include <ctype.h> #include <errno.h> #include <inttypes.h> #include <limits.h> #include <math.h> #include <netdb.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <pthread.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <time.h> #include <net/if.h> #include <sys/ioctl.h> //定义TCP伪报头 struct pseudo_header { //Needed for checksum calculation unsigned int source_address; unsigned int dest_address; unsigned char placeholder; unsigned char protocol; unsigned short tcp_length; struct tcphdr tcp; }; /** * 计算校验和 */ unsigned short checksum(unsigned short *addr,int len){ int nleft=len; int sum=0; unsigned short * w=addr; unsigned short answer=0; while (nleft>1) { sum+=*w++; nleft-=2; } if (nleft==1) { *(unsigned char *)(&answer)=*(unsigned char *)w; sum+=answer; } sum=(sum>>16)+(sum & 0xffff); sum+=(sum>>16); answer=(short)~sum; return(answer); } void get_local_ip(char* localip) { int sock; struct sockaddr_in sin; struct ifreq ifr; sock = socket(AF_INET,SOCK_DGRAM,0); if(sock == -1){ exit(0); } strncpy(ifr.ifr_name,"eth0",IFNAMSIZ); ifr.ifr_name[IFNAMSIZ-1] = 0; if(ioctl(sock,SIOCGIFADDR,&ifr)<0){ close(sock); exit(0); } strcpy(localip,inet_ntoa(((struct sockaddr_in*)&ifr.ifr_addr)->sin_addr)); close(sock); return; } void prepare_header(char* datagram,struct sockaddr_in dest,int src_port, int dst_port,int rst,int syn) { struct tcphdr* tcph = (struct tcphdr*)datagram; //TCP header struct pseudo_header psh; char source_ip[INET6_ADDRSTRLEN]; get_local_ip(source_ip); //TCP Header tcph->source = htons(src_port); tcph->dest = htons(dst_port); tcph->seq = htonl(134235232); tcph->ack_seq = 0; tcph->doff = sizeof(struct tcphdr) / 4; tcph->fin = 0; tcph->syn = syn; tcph->rst = rst; tcph->psh = 0; tcph->ack = 0; tcph->urg = 0; tcph->window = htons(14600); tcph->check = 0; tcph->urg_ptr = 0; // 构造psh计算checksum psh.source_address = inet_addr(source_ip); psh.dest_address = dest.sin_addr.s_addr; psh.placeholder = 0; psh.protocol = IPPROTO_TCP; psh.tcp_length = htons(sizeof(struct tcphdr)); memcpy(&psh.tcp,tcph,sizeof(struct tcphdr)); tcph->check = checksum((unsigned short*)&psh, sizeof(struct pseudo_header)); } int syn_scan(char *target_ip,int target_port){ struct sockaddr_in target; int sockfd ; target.sin_family = AF_INET ; target.sin_addr.s_addr = inet_addr(target_ip) ; sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); char dataheader [4096]; memset(dataheader, 0, 4096); srand((unsigned)time(NULL)); int source_port = rand()%(32767-20000+1) + 20000; prepare_header(dataheader,target,source_port,target_port,0,1); // int ret = sendto(sockfd, dataheader, sizeof(struct tcphdr),0,(struct sockaddr*)&target,sizeof(target)) ; if (ret < 0){ return; } //接收的返回的数据包 struct tcphdr* tcph ; struct sockaddr_in tmp; char msg[1024] ; int len = sizeof(tmp); int count ,size; while (1){ memset(msg,0,1024); size = recvfrom(sockfd,msg,sizeof(msg),0,(struct sockaddr*)&tmp,&len) ; if (size == -1) return ; // recvfrom 接收到原始套接字包含ip + tcp tcph = (struct tcphdr*)( msg+ sizeof(struct ip)) ; if (tcph->syn == 1 && tcph->ack == 1 && tcph->dest == htons(source_port)) { printf("IP : %s, Port %d open\n",inet_ntoa(tmp.sin_addr), ntohs(tcph->source)); break; } } close(sockfd); } int main(){ // 发送请求数据包 char * target_ip = "8.8.8.8"; int target_port = 53; syn_scan(target_ip,target_port); }
效果如下:
[root@VM-0-17-centos scan]# gcc cscan.c -o main [root@VM-0-17-centos scan]# ./main IP : 8.8.8.8, Port 53 open
tcpdump抓包为:
[root@VM-0-17-centos ~]# tcpdump host 8.8.8.8 -vv tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 20:11:41.322280 IP (tos 0x0, ttl 64, id 47479, offset 0, flags [DF], proto TCP (6), length 40) VM-0-17-centos.26350 > dns.google.domain: Flags [S], cksum 0xb9c8 (correct), seq 1105024978, win 14600, length 0 20:11:41.389558 IP (tos 0xa0, ttl 100, id 61621, offset 0, flags [none], proto TCP (6), length 44) dns.google.domain > VM-0-17-centos.26350: Flags [S.], cksum 0x93bd (correct), seq 913248537, ack 1105024979, win 65535, options [mss 1394], length 0 20:11:41.389606 IP (tos 0xa0, ttl 64, id 47538, offset 0, flags [DF], proto TCP (6), length 40) VM-0-17-centos.26350 > dns.google.domain: Flags [R], cksum 0xf2cd (correct), seq 1105024979, win 0, length 0
这里有一个疑点,代码本身并没有发送RST数据包关闭连接,抓包数据中却有RST数据包的发送,查阅了一些资料,RST是内核主动发起,可以通过iptables进行关闭。
iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 10.0.0.1 -j DROP