不知有多少人了解IP地址127.0.0.1
一定程度上是完全等价于127.1
和0x7f.1
的,不过我从上回看到ping 127.1
能正常工作开始,就一直很好奇背后的原因,最近又在 一个CTF题目 用到基于IP表示法的技巧,于是决定稍微探索一下。
我发现一个IPv4地址可能拥有上百个不同的表示形式,而由于一些历史原因,在这方面的标准尚未完全统一,因此这些形式在大部分情况下都是可被正常解析的(举个例子,URL http://000000300.0x000000000a8.00102.00000000351 会解析成http://192.168.66.233/ ),这就在绕过限制和其他一些安全问题上提供了想象的空间。
本文主要介绍这些混淆背后的历史原因和原理,至于如何生成适用形式的IP也不复杂,并且常常需要考虑具体场景。不过我写了个小脚本,放在了GitHub 上,有需要可作为参考。
关于IP和IPv4地址
IPv4是应用于分组交换网络的无状态协议,是网际协议(Internet Protocol , IP)的第四个版本,也是第一个投入生产的版本,1983年开始首先应用在ARPANET项目中。
IP地址用以标记使用IP接入网络的设备。IPv4把IP地址定义为32位二进制数,可表示 2**32
约42亿个网络设备接口,早期使用分类网络(Classful Addressing)的方法划分为五类,随着IP地址需求的增长,这种分类法被无类别域间路由(Classless Inter-Domain Routing , CIDR)取代。【参见RFC 1517-1519】
IPv4 地址句法的历史与现状
一个IPv4地址除了被机器解析外,还会用在很多需要人类阅读理解的地方,而一个32位二进制数(如11000000101010000100001011101001
)对人类是很不友好的,因此人们必然会需要某种文本描述(textual representation) 。我们现在最常见到的点分十进制表示法(dotted-decimal notation) 就是其一。什么是点分十进制呢?就是由点号分隔开的四个十进制数(如192.168.66.233
) ,其中每个十进制数表示一个字节(octets , 八位二进制数),较高有效位在左,较低有效位在右。
尽管从上面的描述我们可以了解到IPv4地址的常见形式,但是关于IP地址的文本描述具体应该如何,似乎从来没有严谨全面的定义。另一方面,IP作为互联网中较为基础的设施之一,常常不可避免地出现在各种协议的描述里,这些描述有时顺带也会提及IP地址的写法,但提法不尽相同,也并不足够强硬和严谨。这篇 文章 细数了一些RFC文档里出现过的描述 ,可以看到不同场景下出现过#127.0.0.1
、[127.0.0.1]
、127.000.000.001
等形式的写法。
当IETF版本的句法处于无意识发展时,BSD版本的句法悄然登场。一个权威的解释大概也不是那么重要,尤其是当一项技术的某种实现已经被广泛使用。对于IPv4地址而言,这个实现就是4.2BSD
。 4.2BSD
引入了名为inet_aton()
的用于将字符串解释为IP地址的函数,这个函数被广泛地复制和演绎,从而使得BSD版本的关于IP地址文本描述的句法成为了事实上的标准——能够被inet_aton()
解释即合标准。至于inet_aton()
接受哪些形式的IP地址,将在下文给出。
这里先简要谈谈这两种句法的异同。
相同点
对于最大多数情况——不带前导0的点分十进制( dotted decimal octets with leading zeroes suppressed
) ,两者都是支持的。
不同点
- BSD版本的许多句法IETF版本都不支持
- 最重要的。IETF版本的句法在所有表述中始终如一地暗示要将带有前导0的数字解释为十进制,而BSD版本的句法在实现中将带有前导0的数字解释为八进制。举个例子,前者认为
192.168.1.011
等价于192.168.1.11
,而后者认为等价于192.168.1.9
。
值得一提的是IPv6 的发展也对此产生了一定的影响。IPv6中的函数inet_pton()
在处理IPv4地址时只接受点分十进制,并且明确地拒绝了一些能够被inet_aton()
接受的句法。然而,对于是否接受前导0语焉不详。
此外,2005年的RFC 3986 提出取两者安全的公共子集作为严格的IP地址句法定义,形成倾向于IETF的标准,但同时保持对BSD实现的后向兼容。这个子集的定义如下,简单说就是用点号分隔的四个十进制数,禁止使用前导0。
1 | A 32-bit IPv4 address is divided into four octets. Each octet is |
inet_aton()允许哪些形式的IP地址
- a single number giving the entire 32-bit address.
- dot-separated octet values.
- It also interpreted two intermediate syntaxes:
- octet-dot-octet-dot-16bits, intended for class B addresses
- octet-dot-24bits, intended for class A addresses.
- It also allowed some flexibility in how the individual numeric parts were specified. it allowed octal and hexadecimal in addition to decimal, distinguishing these radices by using the C language syntax involving a prefix “0” or “0x”, and allowed the numbers to be arbitrarily long.
归纳起来有这么几种情况
- IP地址只有一个部分,表示为
a
,每部分表示32位二进制数 - IP地址有两个部分,表示为
a.b
,a
表示8位二进制数,b
表示24位二进制数 - IP地址有三部分,表示为
a.b.c
,a
和b
各表示8位二进制数,c
表示16位二进制数 - IP地址有四个部分,表示为
a.b.c.d
,每部分表示8位二进制数
以及这么两个重点
- 每一个部分可以都有三种表示法,十进制、十六进制和八进制,用前缀表明进制。
- 每部分的数字可以是任意长度。(这意味着可以把
077
和0xff
表示成000000077
和0x00000ff
等)
到此为止,可以看到127.1
属于上述第二种情况,最开始的疑惑也就不复存在。
这应该算是一个历史遗留问题,不过在未来一段时间内,在广泛涉及URL和IP地址的浏览器和许多应用层程序(如Ping、telnet、wget、curl、GET、HEAD等)中,符合BSD版本句法的IPv4地址表示形式仍然是可接受的,而这些表示可以多达上百种,就可能在一些安全问题上发挥出人意料的作用。
生成一个IP地址的上百种形式
1 | # coding:utf8 |
结果列表
1 | [0] 0x000c0.0xa8.0x0000042.0x0e9 |