一般来说,ip包的抓取和转发可以用赫赫有名的libpcap来做,发包时不仅仅需要构造ip包,也需要构造以太网帧的头部,并指定链路层的device。但有的同学会觉得libpcap太重了,如果只是用于学习和简单调试,在这里我来简单的介绍另外一种ip包嗅探和转发的方法: 使用raw socket,并提供一个简单对http请求进行ip包转发demo。

在阅读这篇文章及案例前,需要对OSI网络模型有些基本的了解,tcp,ip,http头部信息结构可以看看对应的维基,在此我就不再赘述了。

raw socket

那么, 什么是raw-socket呢?他跟普通的socket有什么不同?

详细介绍可以看这里:https://linux.die.net/man/7/raw

简单的说,raw socket和其他socket操作是类似的,只是他支持发送和接受不包含链路层头部的原生ip报文。

就tcp来说,经过系统级三次握手从而建立连接之后,tcp socket接受到的是stream,而如果使用raw socket拿到则是从握手开始的最基本的IP包。

使用步骤

  • 创建raw socket(如果不想规定类型是TCP,将IPPROTO_TCP换成IPPROTO_IP)
   int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
   if(s == -1)
    {
        perror("Failed to create socket");
    }
  • 将socket option中的IP_HDRINCL设置为1,这样才能发包(如果只需要抓包不需要这一步)
  int one = 1;
  const int *val = &one;
  if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
    {
        perror("Error setting IP_HDRINCL");
    }
  • 将socket的recvfrom置于无限循环中,这样便能无限抓包 (如果只需要发包不需要这一步)
  while(1){
    struct sockaddr_in serverProxy;
    u_char raw_Buffer[1024];
    int saddr_size = sizeof serverProxy;
    int data_size = recvfrom(s , raw_Buffer , 1024, 0 , (struct sockaddr *)&serverProxy , &saddr_size);

    if(data_size < 0)
    {
       printf("Recvfrom error , failed to get packets\n");
       return 1;
    }
    //Now process the packet
    ProcessPacket(raw_Buffer , data_size, s);
  }
  • 构造ip包后即可发包(IP包篡改可具体看后面案例,现在假设IP包已经构造好了,buffer是报文,iph是ip头部信息,tcph是tcp的头部信息)
  struct sockaddr_in dest_addr;
  dest_addr.sin_family = AF_INET;
  dest_addr.sin_port = tcph -> dest;
  dest_addr.sin_addr.s_addr = iph -> daddr;

  if (sendto (s, buffer, ntohs(iph -> tot_len),  0, (struct sockaddr *) &dest_addr, sizeof (dest_addr)) < 0)
  {
      perror("sendto failed");
  }
  else
  {
      printf ("Packet Send. Length : %d \n" , ntohs(iph -> tot_len));
  }

使用案例

demo必须功能简单,实现的场景如下:

A为client, B为代理机器,C为真实http server。

  1. B的raw socket服务启动,监听所有发至端口55555和55556的类型为tcp的IP包

  2. A向B的端口55555发出http请求(curl)

  3. B通过raw socket嗅探到发至端口55555包,提取ip头部、tcp头部内容。

  4. 将目标IP修改成C的IP,源IP修改成B的IP,目标端口修改成C的服务端口,储存A的端口号,将源端口修改成55556

  5. 若该IP包含http请求,将Host: B的IP:55555 修改成Host: C的IP:C的端口

  6. 重新生成IP头部校验和、TCP头部校验和,生成最终的IP包。

  7. 使用raw socket将篡改后IP报文发送给真实的http服务机器C。

  8. B通过raw socket嗅探到发至端口55556包,提取ip头部、tcp头部内容。

  9. 将目标IP修改成A的IP,源IP修改成B的IP,目标端口修改成A的端口,源端口修改成55555,重复6操作,将IP包发至A。

  10. 重复以上步骤,经过几次IP包的转发后,A可以拿到C的返回内容。

实际代码见:

https://github.com/hongxuanlee/simple_raw_socket

(该案例由于使用了linux系统内置的ip、tcp库,所以不支持mac系统)

如何调试

可能的坑

  • 修改iptables

如果你的raw socket未绑定端口。linux 内置的TCP端口握手协议可能会优先你的嗅探器返回RST,解决方案是修改iptables,把RST的请求给禁掉。

sudo iptables -A OUTPUT -p tcp -m tcp --tcp-flags RST RST -j DROP

结束后想移除可以使用以下命令

sudo iptables -D OUTPUT -p tcp -m tcp --tcp-flags RST RST -j DROP