自从学会原始套接字之后,我感觉掌握了整个世界

3,964 阅读8分钟

前言

有些知识即使我们用不到,但也不能不知道。


就像原始套接字,其实他有自己的领域,通常用他来开发安全相关的应用程序,如nmap,网络性能监控、网络探测、网络攻击等工具,在我们普通应用程序中一般是用不到的,那么,但是就像前面那句话,了解原始套接字还是有必要的。

我们套接字有两种主要类型,流式套接字和数据报套接字,流式套接字使用TCP,数据报套接字使用UDP,这些都是遵循IP传输级协议。但是在普通流式套接字和数据报套接字应用程序中,数据传输时,操作系统的内核会向其中添加一些标头,例如IP头和TCP头。因此,应用程序只需要关心它正在发送的数据和的回复的数据即可,另外我们也没法直接对TCP或IP头部字段进行的修改,对它们头部操作的非常受限,只能控制数据包的数据部分,也就是除了传输层首部和网络层首部以外的。而传输层首部和网络层首部则由协议栈根据创建套接字时指定的参数负责。

但是使用原始套接字就不一样了,原始套接字允许直接发送或者接收IP协议数据包,不需要任何传输层协议格式,就是它能绕过常规的TCP/IP处理,把数据包发送到其他用户的应用程序中。

因为网络级IP数据包没有”端口“的概念,所以可以读取网络设备传入的所有数据包,这意味着什么?意味着安全性,使用了原始套接字的应用程序可以读取所有进入系统的网络数据包,也就是我们可以捕获其他应用程序的数据包,所以为了防止这种情况的发生,Linux要求所有访问原始套接字的程序都必须以root身份运行。

如果想写出自己的一套协议,则需要使用原始套接字,他不会自动编码/解码TCP或UDP头。

另外在Windows上有很多局限性,就因为原始套接字提供了普通套接字不具备的功能,能够对网络数据包进行控制,也给攻击者带来了很多便利,所以不同的Windows版本环境对原始套接字也是有区别的。如下面:

  1. 无法通过原始套接字发送TCP数据。

  2. 不允许使用带有原始套接字的IPPROTO_TCP协议调用bind函数。

  3. 源地址无效的UDP数据报不能通过原始套接字发送。网络接口上必须存在任何传出UDP数据报的IP源地址,否则该数据报将被丢弃。进行此更改是为了限制恶意代码创建分布式拒绝服务攻击的能力,并限制发送欺骗数据包(具有伪造源IP地址的TCP/IP数据包)的能力。

那么这就导致原本原始套接字提供的功能无法使用,但是我们可以将编程层次在下降一层,可以使用WinPcap直接操作帧,WinPcap是Windows平台下访问数据链路层的开源库,能够应用于网络数据帧的构造、捕获、分析。

创建原始套接字

创建原始套接字同样使用socket函数,只是参数有所不同。需要将type置为SOCK_RAW,第三个协议类型需要更具需求来选择,比如有IPPROTO_ICMP、 IPPROTO_TCP、 IPPROTO_UDP。

int socket(int domain, int type, int protocol);

例子

捕获所有ICMP数据包

大多数人印象最深的应该就是ping命令了,ping程序的实质就是利用了ICMP请求回显和回显应答报文,ICMP全称是 Internet Control Message Protocol,即互联网控制报文协议,用于网际协议(IP)中发送控制消息,返回发生在通信环境中的各种问题,通过这些信息,我们就可以对所发生的问题作出诊断。

下面的这个程序会捕获进入系统的所有ICMP包,故创建socket时,protocol需要选择IPPROTO_ICMP,创建完毕后就可以通过recvfrom来接收数据包。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

#include <arpa/inet.h>  

int main(){
     int sockfd,retval,n;
     socklen_t clilen;
     struct sockaddr_in cliaddr, servaddr;
     char buf[10000]; 
     int i;
     
     sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); 
     if (sockfd < 0){
          exit(1);
     }
     clilen = sizeof(struct sockaddr_in);    
     while(1){
          n = recvfrom(sockfd, buf, 10000, 0, (struct sockaddr *)&cliaddr, &clilen);
          struct ip *ip_hdr = (struct ip *)buf;
          printf("IP 头部大小 %d bytes.\n", ip_hdr->ip_hl*4);
          
          for (i = 0; i < n; i++) {
               printf("%02X%s", (uint8_t)buf[i], (i + 1)%16 ? " " : "\n");
          }
          printf("\n");
          
          struct icmp *icmp_hdr = (struct icmp *)((char *)ip_hdr + (4 * ip_hdr->ip_hl));

          printf("ICMP msgtype=%d, code=%d ", icmp_hdr->icmp_type, icmp_hdr->icmp_code);
          printf("%s -> ", inet_ntoa(ip_hdr->ip_src));  
          printf("%s\n",   inet_ntoa(ip_hdr->ip_dst)); 
     }
}

ICMP报文结构中有个称为报文类型,取值有很多,使用ping程序的一端会把这个类型置为8,被ping的一端响应时会把类型置为0,运行后通过命令ping 127.0.0.1 发出icmp包,这个程序即可捕获到,会输出如下信息。从中可以看出请求的类型为8。

IP 头部大小 20 bytes.
45 00 00 3C 7E 8B 00 00 80 01 3A 14 C0 A8 00 67
C0 A8 00 6A 08 00 4C 21 00 01 01 3A 61 62 63 64
65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74
75 76 77 61 62 63 64 65 66 67 68 69 
ICMP msgtype=8, code=0 192.168.0.103 -> 192.168.0.106
IP 头部大小 20 bytes.
45 00 00 3C 7E 8D 00 00 80 01 3A 12 C0 A8 00 67
C0 A8 00 6A 08 00 4C 20 00 01 01 3B 61 62 63 64
65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74
75 76 77 61 62 63 64 65 66 67 68 69 
ICMP msgtype=8, code=0 192.168.0.103 -> 192.168.0.106
IP 头部大小 20 bytes.
45 00 00 3C 7E 8F 00 00 80 01 3A 10 C0 A8 00 67
C0 A8 00 6A 08 00 4C 1F 00 01 01 3C 61 62 63 64
65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74
75 76 77 61 62 63 64 65 66 67 68 69 
ICMP msgtype=8, code=0 192.168.0.103 -> 192.168.0.106

发送ICMP数据包

能捕获就能发送,发送同样需要创建原始套接字,发送ICMP就需要我们自己构建ICMP数据包,其中最麻烦的一步是计算校验和,下图是从网络上找的一张,需要注意的是,ICMP是封装在IP数据包中的。

下图是Windows向Linux发出的ICMP数据包,我们知道Windows中的ping程序默认会发出4次ICMP包,那么同样Linux也要回复4次,所以总共有8个数据包,两个操作系统的IP分别是:

Windows:192.168.0.103

Linux:192.168.0.106

下图是ICMP的数据包,type为8,和我们上面的一样。

ICMP校验和计算方式如下:

  1. 将icmp包(包括header和data)以16bit(2个字节)为一组,并将所有组相加(二进制求和)
  2. 若高16bit不为0,则将高16bit与低16bit反复相加,直到高16bit的值为0,从而获得一个只有16bit长度的值
  3. 将此16bit值进行按位求反操作。

其他并没有什么难度。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <netdb.h>

u_int16_t checksum(unsigned short *buf, int size)
{
	unsigned long sum = 0;
	while (size > 1) {
		sum += *buf;
		buf++;
		size -= 2;
	}
	if (size == 1)
		sum += *(unsigned char *)buf;
	sum = (sum & 0xffff) + (sum >> 16);
	sum = (sum & 0xffff) + (sum >> 16);
	return ~sum;
}


void setup_icmphdr(u_int8_t type, u_int8_t code, u_int16_t id, u_int16_t seq, struct icmphdr *icmphdr)
{
	memset(icmphdr, 0, sizeof(struct icmphdr));
	icmphdr->type = type;
	icmphdr->code = code;
	icmphdr->checksum = 0;
	icmphdr->un.echo.id = id;
	icmphdr->un.echo.sequence = seq;
	icmphdr->checksum = checksum((unsigned short *)icmphdr, sizeof(struct icmphdr));
}

int main(int argc, char **argv)
{
	int n, soc;
	char buf[1500];
	struct sockaddr_in addr;
	struct in_addr insaddr;
	struct icmphdr icmphdr;
	struct iphdr *recv_iphdr;
	struct icmphdr *recv_icmphdr;

	if (argc < 2) {
		printf("请传入参数 : %s IP地址\n", argv[0]);
		return 1;
	}

	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(argv[1]);
	soc = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
	setup_icmphdr(ICMP_ECHO, 0, 0, 0, &icmphdr);

	n = sendto(soc, (char *)&icmphdr, sizeof(icmphdr), 0, (struct sockaddr *)&addr, sizeof(addr));
	if (n < 1) {

		return 1;
	}

	n = recv(soc, buf, sizeof(buf), 0);
	if (n < 1) {

		return 1;
	}

	recv_iphdr = (struct iphdr *)buf;

	recv_icmphdr = (struct icmphdr *)(buf + (recv_iphdr->ihl << 2));
    	printf("ICMP msgtype=%d, code=%d\n", recv_icmphdr->type, recv_icmphdr->code);
	insaddr.s_addr = recv_iphdr->saddr;
	close(soc);
	return 0;
}

捕获所有包

下面这个例子会捕获进入系统的所有数据包并打印出这个数据包的一些信息,但是做了判断,只有当源地址是192.168.0.101的才会打印,这个ip是我的手机地址,由系统启动一个Tomcat后,再由手机去向这个Tomcat发起HTTP请求,此时这个终端才会打印。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/ip.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main() {

    struct sockaddr_in source_socket_address, dest_socket_address;

    int packet_size;


    unsigned char *buffer = (unsigned char *)malloc(65536);

    int sock = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
    if(sock == -1)
    {

        perror("无法创建Socket");
        exit(1);
    }
    while(1) {

      packet_size = recvfrom(sock , buffer , 65536 , 0 , NULL, NULL);
      if (packet_size == -1) {
        return 1;
      }

      struct iphdr *ip_packet = (struct iphdr *)buffer;

      memset(&source_socket_address, 0, sizeof(source_socket_address));
      source_socket_address.sin_addr.s_addr = ip_packet->saddr;
      memset(&dest_socket_address, 0, sizeof(dest_socket_address));
      dest_socket_address.sin_addr.s_addr = ip_packet->daddr;

      char *sourceAddress=inet_ntoa(source_socket_address.sin_addr);
      if(strcmp(sourceAddress ,"192.168.0.101")==0){
          printf("数据包大小 (bytes): %d\n",ntohs(ip_packet->tot_len));
          printf("原地址: %s\n",sourceAddress );
          printf("目的地址: %s\n", (char *)inet_ntoa(dest_socket_address.sin_addr));
          printf("Identification: %d\n\n", ntohs(ip_packet->id));
       }

    }

    return 0;
}

输出如下,这个捕获是和wireshark捕获是一样的。

数据包大小 (bytes): 52
原地址: 192.168.0.101
目的地址: 192.168.0.106
Identification: 846

数据包大小 (bytes): 52
原地址: 192.168.0.101
目的地址: 192.168.0.106
Identification: 847

数据包大小 (bytes): 52
原地址: 192.168.0.101
目的地址: 192.168.0.106
Identification: 848

数据包大小 (bytes): 52
原地址: 192.168.0.101
目的地址: 192.168.0.106
Identification: 849

.....

关于原始套接字的例子写就这么多,从中会发现,真的是打开了新大陆。