从如何获取可信赖的IP地址聊起

作者: joyqi 时间: October 11, 2014 分类: 编程开发,PHP
起因

写这篇文章缘起SF的一个问题 http://segmentfault.com/q/1010000000686700/a-1020000000687155。由此我想到了很多,就和大家随便聊聊吧
在PHP中获取ip地址有一段网上流传甚广的代码,还有它的各种变种

function real_ip()
{
    static $realip = NULL;

    if ($realip !== NULL)
    {
        return $realip;
    }

    if (isset($_SERVER))
    {
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);

            /* 取X-Forwarded-For中第一个非unknown的有效IP字符串 */
            foreach ($arr AS $ip)
            {
                $ip = trim($ip);

                if ($ip != 'unknown')
                {
                    $realip = $ip;

                    break;
                }
            }
        }
        elseif (isset($_SERVER['HTTP_CLIENT_IP']))
        {
            $realip = $_SERVER['HTTP_CLIENT_IP'];
        }
        else
        {
            if (isset($_SERVER['REMOTE_ADDR']))
            {
                $realip = $_SERVER['REMOTE_ADDR'];
            }
            else
            {
                $realip = '0.0.0.0';
            }
        }
    }
    else
    {
        if (getenv('HTTP_X_FORWARDED_FOR'))
        {
            $realip = getenv('HTTP_X_FORWARDED_FOR');
        }
        elseif (getenv('HTTP_CLIENT_IP'))
        {
            $realip = getenv('HTTP_CLIENT_IP');
        }
        else
        {
            $realip = getenv('REMOTE_ADDR');
        }
    }

    preg_match("/[\d\.]{7,15}/", $realip, $onlineip);
    $realip = !empty($onlineip[0]) ? $onlineip[0] : '0.0.0.0';

    return $realip;
}

这段代码的原理是从HTTP_X_FORWARDED_FOR以及HTTP_CLIENT_IP还有REMOTE_ADDR中获取ip地址。

这段代码有问题吗?如果从功能角度来看,它是没有问题的,因为它保证了你可以尽可能地获取到一个ip地址,如果你地程序不需要验证这个ip地址是否正确,只是为了显示它的来源,那么你可以这样做没问题。

但是很多情况下我们需要确切地知道当前跟服务器连接的是哪个ip,那这段代码就不妥了。

在PHP的$_SERVER变量中,凡是以HTTP_开头的键值都是直接从客户端的HTTP请求头中直接解析出来的。

这句话啥意思,意思就是客户端告诉你ip是多少就是多少,伪造它的成本可以说非常低。除了这两个以HTTP_开头的地址外,还有一个REMOTE_ADDR地址,这个地址是与你的服务器进行实际tcp连接的地址,它是一个可信赖的地址。要伪造这个地址成本很高,这是由于TCP三次握手协议的原理决定的,除非你能骗过服务器端的路由,那这样的成本就太高了,还不如直接黑进你的服务器

衍生

上图是一个经过简化的TCP握手协议,握手就是指在你正式传输信息之前,客户端和服务端确认双方是否可信的过程,这三步可以通俗化成下面的过程

小C (Client) 向小S (Server) 发了一封信(SYN),告诉它我要寄一个包裹 (数据) 给你,但是为了保证这个包裹你能收到,我先发一封信确认下你的地址 (Server IP) 是不是正确的,怎么确认呢?收到这封信后请按信上的地址 (Client IP) 回信就行了。
小S收到了这封信以后一想,我不仅要回信确认我的地址是存在的(ACK),还要问问你的地址是不是正确的,要不然我咋知道你是不是靠谱的人。于是小S给小C发了一封回信,不仅在信里确认自己的地址是正确的(ACK),也要求(SYN)小C在收到信件后给自己发一封回信,以确认他的地址也是真实的。要不然我会拒绝你的连接
小C在收到小S的确认后,马上发了一封回信(ACK)告诉我已经收到信了
从此它们就可以愉快的通信了。

那么问题来了

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?

这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。这样我们收到ACK的时候只要确认这个值是否正确就行了。

如果邮政局被收买了怎么?

这确实是一个风险,这就是我刚才提到的,路由器被黑掉的可能。这也就是很多人为什么说互联网的基础是非常脆弱的,只要公网上的其中一个路由器说了假话,所有经过这个路由器的数据都会变成不可信的。

但同时这个成本非常高,因为路由器的安全级别通常是很高的,而且一般的路由器管理端口也不会向普通访问者开放。所以能做到这一点的黑客,通常会选择直接去黑你的服务器。

这就好比我为了寄点东西噁心你一下,还要去贿赂一整个邮政局,显然不划算。还不如我直接买张火车票,赶到你的住处,把你门撬了,放你桌上。

再发散下

SYN FLOOD攻击

也就是SYN洪水攻击,看了上面的解释你应该大概知道SYN是啥意思了,就是一个询问包。所谓SYN洪水攻击就是指Client发送第一个SYN包的时候,告诉Server的源地址是一个假的,当Server发送回执时会进入到一个half-open(半开)状态。

为啥叫半开,因为这个端口只开了一半,握手过程已经开始,但还有一半没完成。这个半开连接大家应该都听过,早先玩迅雷下载或者bt的时候都需要打一个windows半开连接数补丁,就是因为桌面操作系统的计算资源是优先倾向于GUI的,当系统处于半开连接时因为处于等待状态,是会消耗内存和计算资源的,所以操作系统会默认把这个值调低,限制你能接受的连接。而我们p2p下载又需要比较高的连接量,所以就有了这个补丁,把它的限制打开。

我花这段篇幅解释半开连接数并不是没事找事,这其实就是SYN FLOOD攻击的原理。因为当系统处于半开状态时要消耗资源,而服务器往往半开连接数限制都比较大(或者干脆没限制),因此接第一段的话,因为服务器得到的源地址是个假的,发送回执后肯定会收不到确认,因此就会进入到漫长的等待过程(相对于响应时间)。攻击者通过伪造大量的这种无效请求,使服务器端等待大量连接,从而耗尽服务器资源,以达到使其瘫痪的目的。

想看看你是不是受到了此种攻击,只需要在netstat时看看是不是有大量的SYN_RCVD连接即可。SYN_RCVD表示服务器处于握手的第二步。

随机数的漏洞

我先将刚刚讲过的一个问题引用下

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?
这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。
这段话段的关键在于“随机数”这三个字,因为我们这个验证过程的一大基础是Server生成的随机数是不可能被Client端提前知道的,这就好比打扑克时你不可能知道其他人的牌是什么,一个道理。

但如果这个Client是个赌神呢?

http://www.securityfocus.com/bid/25348/discuss
根据安全机构的研究,Linux内核中包含了一个可导致Dos和权限提升的漏洞,可以被黑客利用来运行攻击代码。这是一个核心内存的堆栈溢出问题,可以导致系统的崩溃,再特定的环境下,还可以提升权限。这个漏洞影响到了Linux内核2.6.22.3前的版本。
这是一个被爆出来的随机数漏洞,如果我知道了服务端的随机数漏洞,那么完全可以利用这一点来伪造出一个完美的假ip。

最后弱弱地总结下

那我们到底该相信哪个ip呢?

首选REMOTE_ADDR,因为虽然有如此多地伪造方法,但在语言层面你只能选一个最可靠地。
如果你地服务隐藏在负载均衡或者缓存系统后面,它通常会给你发一个Client-Ip或者X-Forwarded-For的HTTP头,告诉你跟它连接的客户端ip,这时的HTTP_CLIENT_IP和HTTP_X_FORWARDED_FOR是可信的,因为它是从前端服务器上的直接传递过来的,当然你必须在程序中指定只信任这一个来源,而不要像最开始的代码那样每个都检测一遍

本文作者: TMs
本文链接: https://blog.tms.im/2014/12/14/how-to-get-real-ip-address.html
版权声明: 本作品采用 CC BY-NC-SA 3.0 CN 进行许可。转载请注明出处!
知识共享许可协议