Intro

毕业回家的前一天,把手机卡和宽带一并注销了,晚上收拾好行李之后,面对着没法上网的电脑开始发呆。什么事情都干不了,那就打开 Wireshark 抓包玩玩吧,看看在未进行上网认证的情况下还有哪些请求能够被正常送出。

发现,即使在未进行上网认证的状态下,依旧可以 Ping 得通内网实验室的服务器,但是 Ping 不通外网的服务器。说明在用户未进行上网认证的情况下,学校并没有禁止内网的 ICMP 协议通信。

那么,能否通过 ICMP 协议来传输数据呢?

抓包看看

不知道你是否曾抓过 Ping 命令的包,Ping 命令依赖于 ICMP协议,在执行 Ping 命令的时候,计算机会向对方发送一个 ICMP 的请求,类型为 Echo Request,对方收到这个 ICMP 请求之后,同样会返回一个 ICMP 请求,类型为 Echo Reply。通过这一来一回两个请求就可以测定两台计算机之间网络的延迟。

而 Echo 类型的 ICMP 请求中有一个叫 Data 的字段,并且 Windows 和 Linux 的 Ping 命令发出的 ICMP 请求的 Data 字段是不同的:

LinuxWindows

可以看到 Windows 上 Ping 命令所发出的 ICMP 请求,Data 字段是abcdefghijklmnopqrstuvwabcdefghi,而 Linux 则是以 !"#$%&'()*+,-./01234567 结尾的字符串,并且对方返回的 Echo Reply 包中的 Data 字段和发送的 Data 是完全一致的,你发送什么给我,我就返回什么给你。

那么,假设我们自己构建一个 ICMP 请求,并且将其中的 Data 字段替换为想要传输的数据,并发送出去,那不就可以利用 ICMP 来传输数据了嘛!

先来读读 ICMP 协议的 RFC 吧

ICMP 协议全称"Internet Control Message Protocol",即"互联网控制消息协议",通常用于返回的错误信息或是分析路由。ICMP 和 UDP 一样,是不可靠的传输协议。

ICMP 是在 RFC 792 中定义的,日常使用的 Ping 命令是通过 ICMP 协议中定义的 EchoEcho Reply 这两种消息类型来实现的,在 RFC 的第 14~15 页规定了这两种消息类型的格式:

Echo or Echo Reply Message  
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-  
   IP Fields:  
   Addresses  
      The address of the source in an echo message will be the
      destination of the echo reply message.  To form an echo reply
      message, the source and destination addresses are simply reversed,
      the type code changed to 0, and the checksum recomputed.  
   IP Fields:  
   Type  
      8 for echo message;  
      0 for echo reply message.  
   Code  
      0  
   Checksum  
      The checksum is the 16-bit ones's complement of the one's
      complement sum of the ICMP message starting with the ICMP Type.
      For computing the checksum , the checksum field should be zero.
      If the total length is odd, the received data is padded with one
      octet of zeros for computing the checksum.  This checksum may be
      replaced in the future.  
   Identifier  
      If code = 0, an identifier to aid in matching echos and replies,
      may be zero.  
   Sequence Number
      If code = 0, a sequence number to aid in matching echos and
      replies, may be zero.  
   Description  
      The data received in the echo message must be returned in the echo
      reply message.  
      The identifier and sequence number may be used by the echo sender
      to aid in matching the replies with the echo requests.  For
      example, the identifier might be used like a port in TCP or UDP to
      identify a session, and the sequence number might be incremented
      on each echo request sent.  The echoer returns these same values
      in the echo reply.  
      Code 0 may be received from a gateway or a host.

可以看到其中有一个 data 字段,并且 RFC 中有提到:

The data received in the echo message must be returned in the echo reply message.

收到的 Echo 消息的 data 必须在 Echo Reply 消息中返回,这跟我们抓包观察到的现象一致。

那么,通过代码构建一个 ICMP 请求,并将其中的 Data 字段填充为自定义数据,不就可以通过 ICMP 协议来传输数据了🤔。

通过 ICMP 传输数据

使用 Golang 构建并发送 ICMP 请求

Golang 官方提供了一个 ICMP 包,可以构建并发送 ICMP 请求,只需要几行代码就能实现发送自定义的 data:

func Send(packetConn *icmp.PacketConn, destAddress string, seq int, data []byte) error {
	dest, err := net.ResolveIPAddr("ip4", destAddress)
	if err != nil {
		return err
	}

	icmpMessage := icmp.Message{
		Type: ipv4.ICMPTypeEcho,
		Code: 0,
		Body: &icmp.Echo{
			ID:   os.Getpid() & 0xffff,
			Seq:  seq,
			Data: data,
		},
	}
	icmpMessageBytes, err := icmpMessage.Marshal(nil)
	if err != nil {
		return err
	}
	succeedBytesCount, err := packetConn.WriteTo(icmpMessageBytes, dest)
	if err != nil {
		return err
	}
	if succeedBytesCount != len(icmpMessageBytes) {
		return FailedToSendICMPMessage
	}
	return nil
}

其中,我们可以将想要发送的数据放到 icmp.Echo 结构体中的 Data 字段中,这样就构建了一个带有自定义数据的 ICMP 消息,然后使用 icmp.PacketConn.WriteTo 方法,发出 ICMP 请求。

然后再糊一些接收 ICMP 请求的代码就大功告成啦,完整的代码在这里:https://github.com/LGiki/icmp_message

禁止 ICMP 自动回复

因为 Linux 内核会自动回应 ICMP Echo 请求,所以在发送自定义 ICMP 消息之前,需要关闭这个功能,关闭的方法很简单:

# echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all

以上关闭 ICMP 自动回复为临时关闭,下次重启就会恢复,如果想手动开启 ICMP 自动回复则可以执行以下命令:

 # echo "0" > /proc/sys/net/ipv4/icmp_echo_ignore_all

验证

可以看到成功让两台计算机使用 ICMP 协议进行通讯,双方都可以发送并接收到对方的消息,Wireshark 抓包结果中的 Data 字段也被替换为了自定义的数据。

后记

之前一致想验证一下能否通过 ICMP 协议来传输数据,这次总算是验证了该方案的可行性,这在一些受限的网络环境中可能非常有用。

当然,通过 ICMP 来传输数据也存在着一些问题:

  • ICMP 协议是明文传输数据,在网络中的任意一层都可以很轻松抓到 ICMP 的明文请求,如果想要传输一些敏感数据则可能还需要往上叠一层 TLS 层。
  • ICMP 是不可靠的传输协议,如果在连接不稳定的网络环境中使用 ICMP 来传输数据,还需要自行实现 ACK、重传等机制来确保稳定传输数据。

References