Linux小知识--原始套接字(raw socket)之模拟TCP握手
用rawsocket模拟tcp握手,实现多线程tcp端口扫描
使用背景
今天继续介绍一下原始套接字的一种用法,就是用来模拟TCP的握手连接过程,不过作为客户端,用户可以直接使用connect函数来进行连接操作,那么为何还要用RawSocket来模拟呢?其实就涉及到一种使用场景,就是扫描端口。听起来就像干坏事
其实端口扫描是用来发现风险,并且规避风险的重要手段,知己知彼啊
TCP端口扫描的实现形式
- SYN扫描
双方并没有建立起一条完整的连接,而是扫描着先向被扫描的目的端口发出一个SYN包,如果从目标端口返回一个syn/ack包,就可以断定该端口处于监听状态(即端口是开放的),如果返回rst包,则表明端口不在监听状态,是关闭的。
优点:速度快,如果不被防火墙过滤的话,基本都能收到应答包
缺点:扫描行为容易被发现,并且它是不可靠的,容易丢包
- FIN扫描
主动结束的一方发送FIN包,当我们发送一个FIN包给一个非监听的端口时,会有RST应答,反之,发给一个正在监听的端口时,不会有任何回应。
优点:隐蔽性好,速度快
缺点:只能用于Linux系统,Windows系统下无效
就常见的端口服务来说,大概有5000种左右,这个我们可以通过查看/etc/services文件看到目前常见的服务端口
显然,如果我们需要快速扫描一个设备的开放端口,那么如果扫描每个端口,通过connect函数的方式,将会特别的慢,因为connect默认的超时时间是75秒,那么扫描一台设备,将会耗用大量的时间
所以我们需要一种快速的方式,即前面引用中描述的,发送syn报文,然后看能否收到syn/ack应答即可。
如果我们需要快速扫描全部端口,建议通过多线程的方式,一个线程向被扫描设备所有端口发送syn消息,然后另一个线程开始接收应答,收到哪个端口的应答,即可认为该端口是开放的。
关键步骤
- 创建socket(发送线程)
sockfd_s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); //接收套接字
if(sockfd_s < 0)
{
TCP_LOG_ERR("create sockfd_s failed\n");
p2_run--;
pthread_exit(NULL);
}
int one = 1;
if(setsockopt(sockfd_s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0)
{ //定义套接字不添加IP首部,代码中手工添加
TCP_LOG_ERR("setsockopt failed!\n");
close(sockfd_s);
p2_run--;
pthread_exit(NULL);
}
这里的核心操作主要是第二步骤,要自己控制IP头部内容,所以这里不用自动填写,我们自己掌控局面
- 构建IP及TCP首部
char pkg_buffer[TCP_BUF_SIZE];
struct ip *ip_header; //ip首部指针
struct tcphdr *tcp_header; //tcp首部指针
unsigned int ip_datagram_len;
ip_datagram_len= sizeof(struct ip) + sizeof(struct tcphdr); //ip数据报长度
// 初始化ip数据报 ip_datagram(IP首部+TCP首部+TCP数据部分)
memset(pkg_buffer,0,TCP_BUF_SIZE);
// 构建IP首部和TCP首部指针
ip_header = (struct ip *)pkg_buffer;
tcp_header = (struct tcphdr *)(pkg_buffer + sizeof(struct ip));//ip首部后面就是tcp报文段了
/*封装ip首部*/
// 版本 4
ip_header->ip_v = IPVERSION;
// 首部长度 4
ip_header->ip_hl = sizeof(struct ip)>>2;
// 服务类型(types of service) 8
ip_header->ip_tos = 0;
// 总长度 16
ip_header->ip_len = htons(ip_datagram_len);
// 标识 16
ip_header->ip_id = 0;
// 标志+偏移 16
ip_header->ip_off = 0;
// 生存时间 8
ip_header->ip_ttl = 0;
// 协议 8
ip_header->ip_p = IPPROTO_TCP;
// 首部检验和 16
ip_header->ip_sum = 0;
//源地址 32
ip_header->ip_src.s_addr =inet_addr(local_ipaddr);
// 目的地址 32
ip_header->ip_dst.s_addr = inet_addr(dest_ipaddr);
// tcp源端口
tcp_header->source = htons(SEND_TCPMQ[i].sport);
// 目的端口 16
tcp_header->dest = htons(i);
// 序号 32
tcp_header->seq = 0;
// 确认号 32
tcp_header->ack_seq = 0;
// 数据偏移 4
//tcp_header->res1 = 0;
// 保留 4
tcp_header->doff = 5; // 这里从wireshark来看是指的是数据偏移,resl和doff的位置反了,不知道是头文件有问题还是什么的,应该不是大小端问题。
//res2+urg+ack+psh+rst+syn+fin 8
//tcp_header->res2 = 0;
//tcp_header->urg = 0;
//tcp_header->ack = 0;
//tcp_header->psh = 0;
//tcp_header->rst = 0;
tcp_header->syn = 1;
//tcp_header->fin = 0;
// 窗口 16
//tcp_header->window = 0;
// 检验和 16
tcp_header->check = 0;
// 紧急指针 16
//tcp_header->urg_ptr = 0;
/*计算tcp校验和*/
ip_header->ip_ttl = 0;
tcp_header->check = 0;
// ip首部的校验和,内核会自动计算,可先作为伪首部,存放tcp长度
ip_header->ip_sum = htons(sizeof(struct tcphdr));
// 计算tcp校验和,从伪首部开始
tcp_header->check = check_sum((unsigned short *)pkg_buffer+4,ip_datagram_len-8);
ip_header->ip_ttl = MAXTTL;
- 发送
if ((n = sendto(sockfd_s, pkg_buffer, ip_datagram_len, 0, (struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in)))< 0)
{
printf("发送TCP失败n = %d", n);
}
- 接收部分,采用了select方式,通过配置超时时间,例如两秒,超时没有收到响应,那么认为已经处理完毕
FD_ZERO(&fds);
FD_SET(sockfd_r, &fds);
timeout.tv_sec = 2;
timeout.tv_usec = 0;
memset(buf, 0, 1024);
len = sizeof(client_addr);
ret = select(sockfd_r+1, &fds, NULL, NULL, &timeout);
if(ret == 0)
{
printf("select time out#######continue\n");
break;
}
else if(ret == -1)
{
printf("select error");
continue;
}
else
{
if(FD_ISSET(sockfd_r,&fds))
{
int n = 0;
if((n = recvfrom(sockfd_r, buf, 1024, 0, (struct sockaddr*)&client_addr, &len)) < 0)
{
continue;
}
else
{
struct tcphdr *tcp_header= NULL;
struct ip * ip = NULL;
int iphdrlen;
ip = (struct ip *)buf;
iphdrlen = ip->ip_hl << 2;
tcp_header = (struct tcphdr*)(buf+iphdrlen);
unsigned short formport=0;
if (n < (sizeof(struct ip)+sizeof(struct tcphdr)))
{
printf("TCP packets length is less than 8\n\r");
}
formport=htons(tcp_header->source) ;
if(SEND_TCPMQ[formport].used)
{
if((SEND_TCPMQ[formport].sport)==htons(tcp_header->dest))
{
if((tcp_header->syn == 1)&&(tcp_header->ack == 1)&&(!SEND_TCPMQ[formport].sended))
{
printf("[SYN]faxian[%d]",formport);
SEND_TCPMQ[formport].sended=true;
}
}
}
}
}
}
注意这里有一个判断来源端口与发送端口是否一致的过程,因为我们的sokcet并没有绑定任何端口,所以所有数据都会过来,可能类似于抓包的结果,所以需要判断一下这个syn/ack来源是不是我们发送的响应结果。
下面是完整代码,接收线程
#include <netinet/in.h> // for sockaddr_in
#include <netinet/tcp.h> // for tcp
#include <netinet/ip.h> // for ip
#define TCP_BUF_SIZE 256
#define TCP_PORTFILE "/etc/services"
#define TCP_LOG(fmt...) printf(fmt);
#define TCP_LOG_ERR(fmt...) printf(fmt);
typedef struct _p2_tcp_ser
{
bool used;
int sport;
bool sended;
}TCP_MQ;
static TCP_MQ SEND_TCPMQ[65536];
static char dest_ipaddr[32]={0};
static char local_ipaddr[32]={0};
static int p2_run=2;
static unsigned short check_sum(unsigned short *buffer, int size)
{
//建议将变量放入寄存器, 提高处理效率.
int len = size;
//16bit
unsigned short *p = buffer;
//32bit
unsigned int sum = 0;
//16bit求和
while( len >= 2)
{
sum += *(p++)&0x0000ffff;
len -= 2;
}
//最后的单字节直接求和
if( len == 1){
sum += *((unsigned char *)p);
}
//高16bit与低16bit求和, 直到高16bit为0
while((sum&0xffff0000) != 0){
sum = (sum>>16) + (sum&0x0000ffff);
}
return (unsigned short)(~sum);
}
/* get dst address */
static int tcp_dst_addr(const char *addrHost, struct sockaddr_in * dst_addr,int port)
{
struct hostent * host = NULL;
unsigned long inaddr = 0;
bzero(dst_addr, sizeof(struct sockaddr_in));
dst_addr->sin_family = AF_INET;
if ((inaddr = inet_addr(addrHost)) == INADDR_NONE)
{
if ((host = gethostbyname(addrHost)) == NULL)
{
TCP_LOG_ERR("gethostbyname error.\n\r");
return (-1);
}
memcpy((char *) &dst_addr->sin_addr, host->h_addr, host->h_length);
}
else
{
memcpy((char *) &dst_addr->sin_addr, (char *) &inaddr, sizeof(dst_addr->sin_addr));
}
dst_addr->sin_port = htons(port);
return 0;
}
static void SEND_TCPMQ_INIT(void)
{
int i=0;
FILE *fp = NULL;
char values[1024]={0};
for (i = 0; i < 65536; ++i)
{
SEND_TCPMQ[i].used=false;
SEND_TCPMQ[i].sended=false;
}
if((fp = fopen(TCP_PORTFILE,"r")) == NULL)
{
TCP_LOG_ERR("!!!open[%s] is error!",TCP_PORTFILE);
return;
}
fseek(fp,0,SEEK_SET);
//开始解析内容
while(!feof(fp))
{
int oport_int=0;
unsigned short oport_short=0;
char service[128]={0};
int scanres=0;
memset(values,0,1024);
fgets(values,1024,fp);
if((strlen(values)<2))
{
continue;
}
clean_newline(values);
scanres=sscanf(values,"%s\t%d",service,&oport_int);
if(scanres==2)
{
oport_short=(unsigned short)oport_int;
SEND_TCPMQ[oport_short].used=true;
SEND_TCPMQ[oport_short].sport=random()%65535;;
}
}
fclose(fp);
}
void* p2_tcp_send_main(void* para)
{
int sockfd_s;
int i=0;
sockfd_s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); //接收套接字
if(sockfd_s < 0)
{
TCP_LOG_ERR("create sockfd_s failed\n");
p2_run--;
pthread_exit(NULL);
}
int one = 1;
if(setsockopt(sockfd_s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0)
{ //定义套接字不添加IP首部,代码中手工添加
TCP_LOG_ERR("setsockopt failed!\n");
close(sockfd_s);
p2_run--;
pthread_exit(NULL);
}
for (i = 0; i < 65536; ++i)
{
if(SEND_TCPMQ[i].used)
{
struct sockaddr_in dst_addr;
int n=0;
char pkg_buffer[TCP_BUF_SIZE];
struct ip *ip_header; //ip首部指针
struct tcphdr *tcp_header; //tcp首部指针
unsigned int ip_datagram_len;
ip_datagram_len= sizeof(struct ip) + sizeof(struct tcphdr); //ip数据报长度
// 初始化ip数据报 ip_datagram(IP首部+TCP首部+TCP数据部分)
memset(pkg_buffer,0,TCP_BUF_SIZE);
// 构建IP首部和TCP首部指针
ip_header = (struct ip *)pkg_buffer;
tcp_header = (struct tcphdr *)(pkg_buffer + sizeof(struct ip));//ip首部后面就是tcp报文段了
/*封装ip首部*/
// 版本 4
ip_header->ip_v = IPVERSION;
// 首部长度 4
ip_header->ip_hl = sizeof(struct ip)>>2;
// 服务类型(types of service) 8
ip_header->ip_tos = 0;
// 总长度 16
ip_header->ip_len = htons(ip_datagram_len);
// 标识 16
ip_header->ip_id = 0;
// 标志+偏移 16
ip_header->ip_off = 0;
// 生存时间 8
ip_header->ip_ttl = 0;
// 协议 8
ip_header->ip_p = IPPROTO_TCP;
// 首部检验和 16
ip_header->ip_sum = 0;
//源地址 32
ip_header->ip_src.s_addr =inet_addr(local_ipaddr);
// 目的地址 32
ip_header->ip_dst.s_addr = inet_addr(dest_ipaddr);
// tcp源端口
tcp_header->source = htons(SEND_TCPMQ[i].sport);
// 目的端口 16
tcp_header->dest = htons(i);
// 序号 32
tcp_header->seq = 0;
// 确认号 32
tcp_header->ack_seq = 0;
// 数据偏移 4
//tcp_header->res1 = 0;
// 保留 4
tcp_header->doff = 5; // 这里从wireshark来看是指的是数据偏移,resl和doff的位置反了,不知道是头文件有问题还是什么的,应该不是大小端问题。
//res2+urg+ack+psh+rst+syn+fin 8
//tcp_header->res2 = 0;
//tcp_header->urg = 0;
//tcp_header->ack = 0;
//tcp_header->psh = 0;
//tcp_header->rst = 0;
tcp_header->syn = 1;
//tcp_header->fin = 0;
// 窗口 16
//tcp_header->window = 0;
// 检验和 16
tcp_header->check = 0;
// 紧急指针 16
//tcp_header->urg_ptr = 0;
/*计算tcp校验和*/
ip_header->ip_ttl = 0;
tcp_header->check = 0;
// ip首部的校验和,内核会自动计算,可先作为伪首部,存放tcp长度
ip_header->ip_sum = htons(sizeof(struct tcphdr));
// 计算tcp校验和,从伪首部开始
tcp_header->check = check_sum((unsigned short *)pkg_buffer+4,ip_datagram_len-8);
ip_header->ip_ttl = MAXTTL;
tcp_dst_addr(dest_ipaddr, &dst_addr,i);
if ((n = sendto(sockfd_s, pkg_buffer, ip_datagram_len, 0, (struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in)))< 0)
{
printf("发送TCP失败n = %d", n);
}
}
}
close(sockfd_s);
p2_run--;
pthread_exit(NULL);
}
int p2_tcp_ping_send_process(void)
{
int ret=0;
pthread_t main_process;
pthread_attr_t attr;
pthread_attr_init (&attr);
/* 设置线程的属性为分离的 */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&main_process, &attr, p2_tcp_send_main, NULL);
if (ret!= 0)
{
printf("线程创建失败(%d)", ret);
pthread_attr_destroy(&attr);
return -1;
}
return 0;
}
void* p2_tcp_revice_main(void* para)
{
int ret;
int sockfd_r;
struct timeval timeout;
fd_set fds;
struct sockaddr_in client_addr;
char buf[1024];
socklen_t len;
sockfd_r = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); //接收套接字
if(sockfd_r < 0)
{
printf("create sockfd_r failed\n");
p2_run--;
pthread_exit(NULL);
}
while(1)
{
FD_ZERO(&fds);
FD_SET(sockfd_r, &fds);
timeout.tv_sec = 2;
timeout.tv_usec = 0;
memset(buf, 0, 1024);
len = sizeof(client_addr);
ret = select(sockfd_r+1, &fds, NULL, NULL, &timeout);
if(ret == 0)
{
printf("select time out#######continue\n");
break;
}
else if(ret == -1)
{
printf("select error");
continue;
}
else
{
if(FD_ISSET(sockfd_r,&fds))
{
int n = 0;
if((n = recvfrom(sockfd_r, buf, 1024, 0, (struct sockaddr*)&client_addr, &len)) < 0)
{
continue;
}
else
{
struct tcphdr *tcp_header= NULL;
struct ip * ip = NULL;
int iphdrlen;
ip = (struct ip *)buf;
iphdrlen = ip->ip_hl << 2;
tcp_header = (struct tcphdr*)(buf+iphdrlen);
unsigned short formport=0;
if (n < (sizeof(struct ip)+sizeof(struct tcphdr)))
{
printf("TCP packets length is less than 8\n\r");
}
formport=htons(tcp_header->source) ;
if(SEND_TCPMQ[formport].used)
{
if((SEND_TCPMQ[formport].sport)==htons(tcp_header->dest))
{
if((tcp_header->syn == 1)&&(tcp_header->ack == 1)&&(!SEND_TCPMQ[formport].sended))
{
printf("[SYN]faxian[%d]",formport);
SEND_TCPMQ[formport].sended=true;
}
}
}
}
}
}
}
close(sockfd_r);
p2_run--;
pthread_exit(NULL);
}
int p2_tcp_ping_revice_process(void)
{
int ret=0;
pthread_t main_process;
pthread_attr_t attr;
pthread_attr_init (&attr);
/* 设置线程的属性为分离的 */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&main_process, &attr, p2_tcp_revice_main, NULL);
if (ret!= 0)
{
printf("线程创建失败(%d)", ret);
pthread_attr_destroy(&attr);
return -1;
}
return 0;
}
void p2_tcp_ping(char* remote_ip,char* local_ip)
{
p2_run=2;
strcpy(dest_ipaddr,remote_ip);
strcpy(local_ipaddr,local_ip);
//初始化数组,随机生成每个请求的源端口
SEND_TCPMQ_INIT();
//启动接收线程
p2_tcp_ping_revice_process();
//启动发送线程
p2_tcp_ping_send_process();
while(p2_run!=0)
{
usleep(100);
}
}
主函数void p2_tcp_ping(char* remote_ip,char* local_ip),需要传递远端IP和本地IP,进行端口扫描。
如果我们不想拘泥于常见端口,那么可以不通过/etc/services函数的指示,直接扫描每个端口,一共65535个,时间基本相差无几。
前面的引用中还提到了一种方式,发送FIN包,如果收到RST的回应,则端口关闭,否则端口开放。通过抓包也发现了这种情况,但是Socket并没有接收到数据,原因还在查。
最近升级了win11,大家有没有发现打开文件夹延迟巨大,感觉都有3-4秒的延迟,这对于我们这些思维敏捷的少年来说,简直是酷刑
还好网上找到了解决方法,
亲测,果然好用,放弃了退回版本的想法啊。毕竟11还是颜值更高。
更多推荐
所有评论(0)