我的整体思路如下:首先实现扫描一个ip的所有端口的功能,然后再获取本机局域网ip,检测局域网内的其他存活ip,通过端口扫描逐个扫描存活ip即可
实现扫描一个ip的scanner
connect扫描原理
TCP connect方式使用系统网络API connect向目标主机的端口发起连接,如果无法连接,说明该端口关闭。但是缺点是connect扫描速度较慢,所以接下来我采用connect扫描与多线程相结合的方式,提高扫描速度
connect扫描的多线程设置
我的大致思路是将要扫描的端口均分给每一个线程,通过并行来提高端口扫描的效率
首先通过宏定义初始化一些基本参数
char *addr=ip; //IP地址
int num_thread=NUM_THREAD; //线程个数
int port_front=PORT_START; //起始端口号
int port_end=PORT_END; //终止端口号
int num_port=port_end-port_front+1; //扫描的端口个数
后期如需更改扫描的范围或者测试效率,更改宏定义的数据即可
#define PACKET_SIZE 4096
#define NUM_THREAD 512
#define PORT_START 1
#define PORT_END 65535
#define NUM_IP 20
NUM_IP是扫描的局域网的ip个数,如果需要扫描整个局域网,一般局域网都是设置的24网段,那么只需要扫描255个端口即可,而且通常最后一个是广播地址,无需扫描,那么只用扫描254个即可
PORT_START 和 PORT_END 是要扫描的起始端口和结束端口,例如我这里扫描的所有端口,所以设置为1和65535
NUM_THREAD 是用的线程个数,其他宏定义后续再做解释
多线程传递多个参数要用结构体型数据传参,设置结构体类型如下:
struct THREAD_TEMP
{
char *addr;
int num_port;
int num_thread;
int head;
int end;
};
addr
是全局变量ip地址的指针,num_port是扫描的端口的个数,num_thread是线程个数,head是开始扫描的端口,end是结束扫描的端口
通过for循环创建NUM_THREAD个线程,同时将传参的数据存在结构体数组ranges中,通过调用scan_a_port线程子函数来达到端口来实现端口扫描,最后通过pthread_join()阻塞主函数,当所有子线程的函数全部返回后才结束
pthread_t id[num_thread];
struct THREAD_TEMP ranges[num_thread];
for(i=0;i<num_thread;i++){
ranges[i].addr=addr;
ranges[i].num_port=num_port;
ranges[i].num_thread=num_thread;
ranges[i].head=port_front+(num_port/num_thread)*i;
ranges[i].end=port_front+(num_port/num_thread)*(i+1)-1;
pthread_create(&id[i],NULL,scan_a_port,(void *)&ranges[i]);
}
for(i=0;i<num_thread;i++){
pthread_join(id[i],NULL);
}
子线程scan_a_port()函数
将主函数传递过来的结构体变量解析出来ip,端口号,然后直接调用connect函数即可实现功能
以端口号的解析为例:
port=((struct THREAD_TEMP*)arg)->head;
将数据填入地址中
memset(&serv_addr,0,sizeof(serv_addr));//初始化
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(((struct THREAD_TEMP*)arg)->addr); //IP
serv_addr.sin_port=htons(port); //port
上述的inet_addr函数是将str型数据转化为二进制形式存储在s_addr中
htons函数是将本地字节序转化为网络字节序,因为网络通信可能从不同种机器之间进行,有的大端序,有的小端序,为了实现统一,所以统一改为网络字节序(大端)存储
调用connect函数,如果返回为0,则该端口为开启端口
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==0){
flag=1;
printf("%d\t",ntohs(serv_addr.sin_port));
close(sock);
}
至此,已经完整实现了扫描一个ip的所有端口的功能
获取本机局域网ip
这里使用pcap库来实现本机ip、掩码、网桥等信息的获取
调用合适的网卡
devStr=pcap_lookupdev(errBuf); //获取合适的网卡
inet_sock = socket(AF_INET, SOCK_DGRAM, 0);
strcpy(ifr.ifr_name, devStr);
本机ip的获取:
ioctl(inet_sock, SIOCGIFADDR, &ifr);
strcpy(local_ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));
掩码获取:
ioctl(inet_sock,SIOCGIFNETMASK,&ifr);
strcpy(mask,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));
网桥获取:
ioctl(inet_sock,SIOCGIFBRDADDR,&ifr);
strcpy(broad_ip,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr));
检测局域网其他存活ip并扫描
首先这里就由提到之前说的网络字节序的问题了,在结构体in_addr中,ip地址以二进制形式存储,而且是大端序,例如,假设一个ip地址是0.128.1.255,那么存储为:
str: 0.128.1.255
二进制: 11111111 00000001 10000000 00000000
16进制: 0xff018000
如果是在24网段,我只需要将前8位修改为我想要扫描的ip即可
首先创建一个指向8位的指针,指向最前面一位,因为是大端序,所以实际上是从最后倒排,此时最前一位实际上是最后一位,所以+3
unsigned char *temp=(unsigned char*)&addr+3;
通常局域网ip的.1是路由器地址所以从.2开始扫描才能扫描其他主机,用初始化为0的int型数flag[255]来标记存活ip,如果存活才开始扫描
for(int i=2;i<=NUM_IP;i++){
*temp=i;
ip_after=inet_ntoa(addr);
if(livetest(ip_after)==1)
ip_flag[i]=1;
sleep(1);
}
for(int i=2;i<=NUM_IP;i++){
if(ip_flag[i]==1){
*temp=i;
ip_after=inet_ntoa(addr);
scan_ip(ip_after);
}
}
结果展示
单独测试检测本地存活ip:
测试扫描局域网内所有主机:
因为我家是开的店铺,所以有很多同种设备的网络摄像头,扫描出来很多ip开放端口一样不是程序错误,而是扫描到了同种设备,仅此而已
个人总结
首先整个实验能带给我的收益还挺多的,既是新知识的学习过程,也是对以前学习的一个复习与巩固。新的知识包括多线程的学习,socket的数据结构和相关函数等,旧知识就是计算机组成原理学的大端序小端序和位操作。当然我的实验也有不完善的地方,因为testlive函数没有实现完整的ping功能,所以效率比较低,但是完整的ping功能实现代码量其实挺大的,可能比我这个地方实验的代码还多,所以我也没实现完整功能。再一个就是多线程的优化,很早之前就听说线程池的高效率和简便性,但是自己也还没能弄懂,所以这里也就只能写了一个“伪线程池”结构。希望自己早日提高个人能力后,回过头来能重新优化一下自己的功能和效率。
然后很多比较基础的知识可能本篇中没有着重强调,但是我已经提前学习并写过文档了,可以点击下列文字参考:
Linux C 多线程学习笔记、Socket编程基础、for循环实现的“伪线程池“demo
完整源码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<pthread.h>
#include<time.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <pcap.h>
#define PACKET_SIZE 4096
#define NUM_THREAD 512
#define PORT_START 1
#define PORT_END 65535
#define NUM_IP 20
void * scan_a_port(void* arg);
int scan_ip(char *ip);
unsigned short cal_chksum(unsigned short *addr,int len);
int livetest(char* ip);
void get_localhost_infor(void);
void show_localhost_infor(void);
char local_ip[16];
char mask[32];
char broad_ip[16];
int ip_flag[256];
int flag=0;
struct THREAD_TEMP
{
char *addr;
int num_port;
int num_thread;
int head;
int end;
};
int main(){
get_localhost_infor();
show_localhost_infor();
char *ip_before=local_ip;
char *ip_after;
struct in_addr addr;
inet_aton(ip_before,&addr);
unsigned char *temp=(unsigned char*)&addr+3;
for(int i=2;i<=NUM_IP;i++){
*temp=i;
ip_after=inet_ntoa(addr);
if(livetest(ip_after)==1)
ip_flag[i]=1;
sleep(1);
}
for(int i=2;i<=NUM_IP;i++){
if(ip_flag[i]==1){
*temp=i;
ip_after=inet_ntoa(addr);
scan_ip(ip_after);
}
}
return 0;
}
//获取本机局域网IP/掩码/网关等information
void get_localhost_infor(){
int inet_sock;
struct ifreq ifr;
char errBuf[PCAP_ERRBUF_SIZE],*devStr;
devStr=pcap_lookupdev(errBuf); //获取合适的网卡
inet_sock = socket(AF_INET, SOCK_DGRAM, 0);
strcpy(ifr.ifr_name, devStr);
//本机ip
ioctl(inet_sock, SIOCGIFADDR, &ifr);
strcpy(local_ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));
//掩码
ioctl(inet_sock,SIOCGIFNETMASK,&ifr);
strcpy(mask,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));
//网桥ip
ioctl(inet_sock,SIOCGIFBRDADDR,&ifr);
strcpy(broad_ip,inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr));
//
}
void show_localhost_infor(){
printf("localhost_ip:\t%s\n",local_ip);
printf("ip_mask:\t%s\n",mask);
printf("broad_ip:\t%s\n",broad_ip);
}
//扫描一个制定ip的所有端口
int scan_ip(char *ip){
time_t time_start,time_end;
time_start=time(NULL); //记录起始时间
char *addr=ip; //IP地址
int num_thread=NUM_THREAD; //线程个数
int port_front=PORT_START; //起始端口号
int port_end=PORT_END; //终止端口号
int num_port=port_end-port_front+1; //扫描的端口个数
printf("\n");
printf("IP:%s\n",addr);
printf("Begin scan this ip --->\n");
printf("Up_Port:\t");
//将端口个数转换为线程个数的整数倍
while(1){
if(num_port%num_thread==0)
break;
else{
num_port++;
port_end++;
}
}
pthread_t id[num_thread];
struct THREAD_TEMP ranges[num_thread];
int i;
flag=0;
for(i=0;i<num_thread;i++){
ranges[i].addr=addr;
ranges[i].num_port=num_port;
ranges[i].num_thread=num_thread;
ranges[i].head=port_front+(num_port/num_thread)*i;
ranges[i].end=port_front+(num_port/num_thread)*(i+1)-1;
pthread_create(&id[i],NULL,scan_a_port,(void *)&ranges[i]);
}
for(i=0;i<num_thread;i++){
pthread_join(id[i],NULL);
}
if(flag==0){
printf("NULL");
}
time_end=time(NULL);
printf("\nFinish scan this ip --->\tUse_time:%d s\n",time_end-time_start);
return 0;
}
//多线程调用函数--扫描指定ip的指定端口
void * scan_a_port(void* arg){
int sock;
struct sockaddr_in serv_addr;
int port;
port=((struct THREAD_TEMP*)arg)->head;
int i;
for(i=0;i<((struct THREAD_TEMP*)arg)->num_port/((struct THREAD_TEMP*)arg)->num_thread;i++){
sock=socket(PF_INET,SOCK_STREAM,0);
if(sock==-1){
printf("%s\n","socket() error");
exit(0);
}
memset(&serv_addr,0,sizeof(serv_addr));//初始化
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(((struct THREAD_TEMP*)arg)->addr); //IP
serv_addr.sin_port=htons(port); //port
port++;
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==0){
flag=1;
printf("%d\t",ntohs(serv_addr.sin_port));
close(sock);
}
else {
close(sock);
continue;
}
}
}
//计算校验和
unsigned short cal_chksum(unsigned short *addr,int len){
int sum=0;
int nleft = len;
unsigned short *w = addr;
unsigned short answer = 0;
/* 把ICMP报头二进制数据以2字节为单位累加起来 */
while(nleft > 1){
sum += *w++;
nleft -= 2;
}
/*
* 若ICMP报头为奇数个字节,会剩下最后一字节。
* 把最后一个字节视为一个2字节数据的高字节,
* 这2字节数据的低字节为0,继续累加
*/
if(nleft == 1){
*(unsigned char *)(&answer) = *(unsigned char *)w;
sum += answer; /* 这里将 answer 转换成 int 整数 */
}
sum = (sum >> 16) + (sum & 0xffff); /* 高位低位相加 */
sum += (sum >> 16); /* 上一步溢出时,将溢出位也加到sum中 */
answer = ~sum; /* 注意类型转换,现在的校验和为16位 */
return answer;
}
//探测是否存在指定ip的活动主机
int livetest(char* ip){
char sendpacket[PACKET_SIZE]; /* 发送的数据包 */
char recvpacket[PACKET_SIZE]; /* 接收的数据包 */
pid_t pid;
int datalen = 56; /* icmp数据包中数据的长度 */
struct protoent *protocol;
protocol = getprotobyname("icmp");
int sockfd;
int size = 50*1024;
if((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0) {
perror("socket error");
}
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size) );
struct sockaddr_in dest_addr;
bzero(&dest_addr, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.s_addr = inet_addr(ip);
//send packet;
int packsize;
struct icmp *icmp;
struct timeval *tval;
icmp = (struct icmp*)sendpacket;
icmp->icmp_type = ICMP_ECHO; /* icmp的类型 */
icmp->icmp_code = 0; /* icmp的编码 */
icmp->icmp_cksum = 0; /* icmp的校验和 */
icmp->icmp_seq = 1; /* icmp的顺序号 */
icmp->icmp_id = pid; /* icmp的标志符 */
packsize = 8 + datalen; /* icmp8字节的头 加上数据的长度(datalen=56), packsize = 64 */
tval = (struct timeval *)icmp->icmp_data; /* 获得icmp结构中最后的数据部分的指针 */
gettimeofday(tval, NULL); /* 将发送的时间填入icmp结构中最后的数据部分 */
icmp->icmp_cksum = cal_chksum((unsigned short *)icmp, packsize);/*填充发送方的校验和*/
if(sendto(sockfd, sendpacket, packsize, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0){
perror("sendto error");
}
//printf("send %d, send done\n",1 );
int n;
struct sockaddr_in from;
int fromlen = sizeof(from);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
struct timeval timeo = {1,0};
fd_set set;
FD_ZERO(&set);
FD_SET(sockfd, &set);
//read , write;
int retval = select(sockfd+1, &set, NULL, NULL, &timeo);
if(retval == -1) {
printf("select error\n");
return 0;
}else if(retval == 0 ) {
// printf("timeout\n");
return 0;
}else{
if( FD_ISSET(sockfd, &set) ){
// printf("host is live\n");
return 1;
}
return 0;
}
}