分享

原始套接字编程(AF_PACKET+SOCK_RAW)模拟一个PING

 补丁牛仔裤 2024-05-14 发布于广东

1. 背景

最近看一个客户的代码片段,发现他在用原始套接字编程,一般学习套接字都是流式套接字和数据报套接字,本来也不是搞网络的,原始套接字了解得很少,借着这次机会,自己来学习一下原始套接字编程。

2. 原始套接字是什么

我去翻看<Linux_Unix系统编程手册>第58章(TCP/IP网络基础)中只有一个泛泛的介绍:

 

 但是这个描述并不完全,于是我参考: 信息安全课程9:raw socket编程 - 知乎

另外还了解到原始套接字在socket的创建上有不同的组合,例如: AF_INET+SOCK_RAW最多只能允许用户层与IP层直接通信,而AF_PACKET+SOCK_RAW就可以允许用户层与数据链路层直接通信了(这一点也是Linux_Unix系统编程手册说得不准确的地方)

 

 另外,关于AF_PACKET+SOCK_RAW可以参考man packet:

3. 封装与PING包格式

同样参考 <Linux_Unix系统编程手册>中的封装基本概念:

 我自己用wireshark抓了一个ping包的格式如下:

上图是执行ping 192.168.0.103 -c 1抓的包,可以看到一个ping操作实际分为两个包,一个是由本机发出的包(echo ping request),另一个是收到对端发来的ack包(echo ping reply),不论怎样,这两个包组成都是相同的: 

98byte = EthernetII(以太网头部14byte)+Internet Protocol Version4(IP包20byte)+ICMP(64byte)

4. 实验代码

实验代码实际上是受到下面博客的启发:Linux 网络编程——原始套接字实例:MAC 地址扫描器_siocgifhwaddr_Mike江的博客-CSDN博客

在这篇博客中,作者用AF_PACKET+SOCK_RAW的原始套接字在数据链路层模拟了一个地址解析协议的操作(Address Resolution Protocol),其中作者没有使用繁杂的包数据结构去构造发送数据,转而使用了直接赋值的方式,非常直观与暴力,可以对着wireshark的数据来构造自己的数据包,非常便于理解与学习,所以我自己模仿了一个PING操作,代码如下:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <strings.h>
  5. #include <errno.h>
  6. #include <net/if.h> //struct ifreq
  7. #include <sys/ioctl.h> //ioctl、SIOCGIFADDR
  8. #include <sys/socket.h>
  9. #include <netinet/ether.h> //ETH_P_ALL
  10. #include <netpacket/packet.h> //struct sockaddr_ll
  11. #include <netinet/in.h>
  12. #include <netinet/ip_icmp.h>
  13. #include <unistd.h>
  14. #include <sys/types.h>
  15. #include <sys/stat.h>
  16. #include <fcntl.h>
  17. #define BUFFSIZE (1024)
  18. #define IPPACKETLEN (20)
  19. #define ICMPHEADERLEN (16)
  20. #define ICMPDATALEN (48)
  21. #define PINGPACKETLEN (98)
  22. //#define DEBUG
  23. static void test(void)
  24. {
  25. unsigned short a = 0x3623; /* in memory should be 0x23 0x36 */
  26. unsigned short b = htons(a);
  27. printf("network order b = %#x\n", b); /* in memory should be 0x36 0x23 */
  28. printf("local order b' = %#x\n", ntohs(b));
  29. return;
  30. }
  31. /* 2 bytes was a group
  32. * totally lens group
  33. */
  34. static unsigned short calc_checksum(unsigned short *addr, int len)
  35. {
  36. int i;
  37. unsigned int sum = 0;
  38. unsigned short tmp = 0;
  39. /* in this algorithm,
  40. * we use ntohs to convert the already constructed big endian data
  41. */
  42. for (i = 0; i < len; i++) {
  43. tmp = ntohs(*addr);
  44. sum += tmp;
  45. addr++;
  46. }
  47. sum = (sum >> 16) + (sum & 0xffff);
  48. sum += (sum >>16);
  49. tmp = ~sum;
  50. return tmp;
  51. }
  52. int main(int argc,char *argv[])
  53. {
  54. int i = 1;
  55. #ifdef DEBUG
  56. test();
  57. return 0;
  58. #endif
  59. unsigned char send_msg[BUFFSIZE] = {
  60. //--------------Ethernet II------------------------14--------------------------
  61. 0xf8, 0x94, 0xc2, 0xdb, 0x72, 0x43, //dst_mac: F8-94-C2-DB-72-43
  62. 0x00, 0x0c, 0x29, 0x7e, 0x2f, 0x86, //src_mac: 00-0C-29-7E-2F-86
  63. 0x08, 0x00, //type: 0x0800 IPV4
  64. //--------------Internet Protocol Version 4--------20--------------------------
  65. 0x45, //0100-version4, 0101-5*4byte=20byte
  66. 0x00, //DSCP:0, ECN-Not-ECT
  67. 0x00, 0x54, //total length: 0x54==84==20(IP)+64(ICMP)
  68. 0x36, 0x23, //identification: 0x36 0x23
  69. 0x40, 0x00, //flags: 0x4000, do not fragment
  70. 0x40, //time to live(TTL): 64
  71. 0x01, //protocol: ICMP(1)
  72. 0x00, 0x00, //checksum(need to calc later)
  73. 192, 168, 0, 104, //src ip addr
  74. 192, 168, 0, 103, //dst ip addr
  75. //--------------ICMP-Header------------------------16--------------------------
  76. 0x08, //type: 8(echo ping request)
  77. 0x00, //code:0
  78. 0x00, 0x00, //checksum(need to calc later)
  79. 0x20, 0x09, //identifier: 0x20 0x09
  80. 0x00, 0x00, //sequence number
  81. 0x12, 0xfc, 0xd4, 0x64,
  82. 0x00, 0x00, 0x00, 0x00, //timestamp from icmp data
  83. //--------------ICMP-Data--------------------------48--------------------------
  84. 0x12, 0x34, 0x56, 0x78,
  85. 0x12, 0x34, 0x56, 0x78,
  86. 0x12, 0x34, 0x56, 0x78,
  87. 0x12, 0x34, 0x56, 0x78,
  88. };
  89. unsigned short checksum = 0;
  90. unsigned short checksum_be = 0;
  91. /* handle checksum of IP packet */
  92. checksum = calc_checksum((unsigned short *)(&send_msg[14]), IPPACKETLEN/2);
  93. printf("IP packet checksum = %#x\n", checksum);
  94. checksum_be = htons(checksum);
  95. memcpy(&send_msg[24], &checksum_be, sizeof(checksum_be));
  96. /* handle checksum of ICMP packet */
  97. checksum = calc_checksum((unsigned short *)(&send_msg[34]), (ICMPHEADERLEN + ICMPDATALEN)/2);
  98. printf("ICMP packet checksum = %#x\n", checksum);
  99. checksum_be = htons(checksum);
  100. memcpy(&send_msg[36], &checksum_be, sizeof(checksum_be));
  101. /* socket create failed, why: Operation not permitted
  102. * should have root permission to create the socket raw
  103. */
  104. int sock_raw_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  105. if (sock_raw_fd < 0) {
  106. printf("socket create failed, why: %s\n", strerror(errno));
  107. return -1;
  108. }
  109. printf("socket create success.\n");
  110. struct sockaddr_ll sll;
  111. struct ifreq req;
  112. strncpy(req.ifr_name, "ens33", IFNAMSIZ);
  113. ioctl(sock_raw_fd, SIOCGIFINDEX, &req);
  114. bzero(&sll, sizeof(sll));
  115. sll.sll_ifindex = req.ifr_ifindex;
  116. int len = sendto(sock_raw_fd, send_msg, PINGPACKETLEN, 0 , (struct sockaddr *)&sll, sizeof(sll));
  117. if (len == -1) {
  118. printf("sendto failed, why: %s\n", strerror(errno));
  119. close(sock_raw_fd);
  120. return -1;
  121. }
  122. printf("\n%d bytes sended.\n", len);
  123. unsigned char recv_msg[BUFFSIZE] = {0};
  124. len = recvfrom(sock_raw_fd, recv_msg, sizeof(recv_msg), 0, NULL, NULL);
  125. if (len < 0) {
  126. printf("recvfrom failed, why: %s\n", strerror(errno));
  127. close(sock_raw_fd);
  128. return -1;
  129. }
  130. printf("\n%d bytes received, check the wireshark.\n", len);
  131. close(sock_raw_fd);
  132. return 0;
  133. }

需要注意的是send_msg的数据全部是依据wireshark包来构造的,自己机器的ip和mac,对端机器的ip和mac需要自行适配,另外,IP包和ICMP包是有 checksum需要计算的,一个偷懒的办法是先胡乱写一个checksum进去,发包的时候wireshark会识别到错误并告诉你正确的checksum是什么,再回填进去即可,通常,IP包的checksum wireshark不检查(显示validation disabled),但是如果构造不对的话是收不到echo ping reply包的,可以按照下图的方法打开wireshark IP包的checksum检查结果:

 不过话说回来,我们应该搞清楚checksum的计算原理,参考:

IP数据报首部checksum的计算_ipchecksum计算_Allen_Kao的博客-CSDN博客

另外注意的是由于我们暴力构造数据已经是网络字节序了,所以计算的时候有必要用htons和ntohs转换一下,参考代码中的calc_checksum()函数。

5. 实验结果

本机X86 ubuntu的ip是192.168.0.104,mac是00:0c:29:7e:2f:86

PC的ip是192.168.0.103,mac是f8:94:c2:db:72:43

因为原始套接字权限要求,必须给与权限运行:

wireshark抓包:

 

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多