从Linux协议栈代码和RFC看西厢计划原理

晨曦之光 发布于 2012/04/10 14:56
阅读 408
收藏 1

终于搞定了西厢计划的方案,由于一直无法下载那个内核模块,于是也就只能自己写了,在理解了西厢计划的原理之后,写这个模块并不很费事(其实为了简单不是写模块,而是直接修改内核协议栈代码),下面先说一下原理,然后再说一下关于内核修改的建议。

序.西厢计划

所谓西厢计划是一个借用历史小说而命名的欺骗GfW的方案,有很多的实现。说实话我真的不知道《西厢记》中的那家伙翻墙到底有何与众不同,有时间一定看一下。而西厢计划无非就是利用了防火墙的一些弱点而瞒天过海的一个方案。本质上作者是利用对TCP协议规范以及防火墙本身的深入理解来制定这个方案的,正所谓知己知彼。
    具体的技术细节,那就是在TCP的三次握手上大做文章,借用服务器对syn-received状态处理的特殊性来执行计划。在TCP的syn-received状态中,服务器本想接收的是客户端对其syn-ack的ack,然而此时客户端并没有如期发送该ack,而是施行了一个两阶段的“自定义报文”发送,其中第一阶段就是发送一个带有fin标志且不带ack标志的报文;第二阶段就是发送一个ack号错误的报文。这样两个阶段就成功欺骗了防火墙,同时又诱使服务器端做了客户端想让它做的事情。

一.从Linux的源码来看如何做

除了RFC,最容易得手的就是Linux的源代码了,其中tcp_rcv_state_process函数可以看出西厢计划为何会得手,这里暂且不谈防火墙做了什么,其实我也不知道。所谓的得手,含义是如此折腾服务器的TCP连接,为何连接没有断掉。
    在Linux的协议栈实现中,tcp_rcv_state_process函数负责处理了TCP连接/释放的状态机,由于在连接开始时(三次握手)状态转换的特殊性,使得我们最好关注三次握手而不是establish状态,在establish状态中,大部分的看似异常报文都是可以通过TCP本身的机制得到修复的,而三次握手过程中则不然,因为TCP的控制块TCB设施正在构建中,以至于很多机制我们还不能用,因此要折腾就折腾这种状态的TCP吧,我们先看下tcp_rcv_state_process:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
              struct tcphdr *th, unsigned len)
{
    struct tcp_opt *tp = tcp_sk(sk);
    int queued = 0;

    tp->saw_tstamp = 0;

    switch (sk->sk_state) {
    case TCP_CLOSE:
    ...
    case TCP_LISTEN:
    ...
    case TCP_SYN_SENT:
    ...
    }
    ...
    /* step 1: check sequence number */
    if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
        if (!th->rst)
            tcp_send_dupack(sk, skb);
        goto discard;
    }

    /* step 2: check RST bit */
    if(th->rst) {
        tcp_reset(sk);
        goto discard;
    }
    ...
    /* step 5: check the ACK field */
    if (th->ack) { //这是关键点
        int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);
        switch(sk->sk_state) {
        case TCP_SYN_RECV:
            if (acceptable) {
            ...
            } else {
                //如果ack序号错误,返回1,则要发送auto-reset
                return 1;
            }
            break;
        case TCP_FIN_WAIT1:
        ...
        case TCP_CLOSING:
        ...
        case TCP_LAST_ACK:
        ...
        }
    } else //如果没有ack标志,则丢弃该报文,报文中含有fin与否对于服务器无关紧要,只是为了欺骗防火墙。
        goto discard;

    /* step 6: check the URG bit */
    tcp_urg(sk, skb, th);

    /* step 7: process the segment text */
    switch (sk->sk_state) {
    ...
    }

    /* tcp_data could move socket to TIME-WAIT */
    if (sk->sk_state != TCP_CLOSE) {
        tcp_data_snd_check(sk);
        tcp_ack_snd_check(sk);
    }

    if (!queued) {
discard:
        __kfree_skb(skb);
    }
    //如果返回0,则正常,如果返回1,则会发送auto-reset,该reset并不会操作本地连接,只是在坏报文到来的反方向默默发送
    return 0;  
}

对以上代码不必做更多的说明了。从该函数的代码逻辑,可以清晰看出客户端需要怎么做,那就是在三次握手的最后一次前,发送两个坏报文。

二.从TCP状态机理解西厢计划第一阶段

我们先看一下TCP的状态机,此图来自RFC,其它的来源都是浮云:

可见在syn-received状态下,没有明确的“收fin”动作(只有应用程序调用close然后发fin的动作),虽然这涉及到TCP的细节,但是还是可以利用的。大多数的实现中,没有规定的行为就是简单的“将包丢弃”。但是要真的想让服务器丢掉这个fin包,还必须使其不包含ack,这是因为RFC的TCP状态机说了,只要收到ack,那么服务器就会进入establish状态,而这会引起西厢计划第二阶段的失败(第二阶段是引发服务器发送reset,而在establish状态下,这种reset实在不易引发)。因此我们只需要在客户端发完syn且收到服务器的syn-ack后,再发送一个不含ack的fin报文即可,该报文过墙时,墙会认为这是个客户端到服务器方向的终止包,直接放过。

三.从RFC理解西厢计划第二阶段

既然已经通过自行构造的fin在客户端到服务器方向骗过了防火墙,那么下一步就是第二阶段的任务了,在服务器到客户端的方向欺骗防火墙。期望服务器采用正常且优雅的fin方式是不可行的,因为那样连接真的就断了,因此就要采用异常的方式,那就是引发服务器发送一个reset报文,而RFC中规定了多种引发reset的方式,西厢计划明显采用了下面的方式(要知道为何发送一个reset不会导致自己这边的连接释放,请接着往下看):
RFC 793 [Page 35]
Reset Generation
    2.  If the connection is in any non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED), and the incoming segment acknowledges something not yet sent (the segment carries an unacceptable ACK), or if an incoming segment has a security level or compartment which     does not exactly match the level and compartment requested for the connection, a reset is sent.

    那么如何得知服务器端发送的reset报文就一定能顺利到达客户端并且服务器端还不释放连接呢?如果嫌RFC实在不好啃,作为程序员,看代码一定会舒服很多,我们知道Linux协议栈源码是一个不错的选择,它实现了绝大多数的RFC建议。Linux的实现中,reset报文分为auto-reset和active-reset,其中auto-reset仅仅根据引发reset的报文构造一个附带RST位的TCP回复报文,它并不和任何的socket相关联,发送了reset报文之后也不会针对本地的连接进行任何操作,这种方式reset报文有一个假设,那就是它将引发auto-reset的“坏报文”的产生归结为两点:

1.远端主机的“异常行为”或者是有人没有按照TCP规范而有意为之,比如establish状态时收到一个syn;

2.本端实在没有可以和该坏报文相关联的TCP连接,比如连接了一个不存在或未开启的端口。

对于active-reset,则是在可以将坏报文和既有连接联系的可预知事件发生时发送的,比如重传定时器连续超时超过了一定的次数等,当这种active-reset发送之后,本端的连接也随之烟消云散。在Linux中,auto-reset是由tcp_v4_send_reset来执行的,我想其注释已经阐述的很清晰了:
/*
 *    This routine will send an RST to the other tcp.
 *
 *    Someone asks: why I NEVER use socket parameters (TOS, TTL etc.)
 *              for reset.
 *    Answer: if a packet caused RST, it is not for a socket
 *        existing in our system, if it is matched to a socket,
 *        it is just duplicate segment or bug in other side's TCP.
 *        So that we build reply only basing on parameters
 *        arrived with segment.
 *    Exception: precedence violation. We do not implement it in any case.
 */
static void tcp_v4_send_reset(struct sk_buff *skb)
{
    ...
}

正是由于这个auto-reset机制(这也是RFC的意思),西厢计划才得以成功,西厢计划正是使用一个坏的报文来使服务器生成一个auto-reset报文,该reset报文过防火墙时,会被认为是由服务器发起到客户端方向的“终止”报文,然而客户端是可以忽略该报文的...西厢计划这样就成功了另一半。
    我们不可能利用active-reset报文,因为该reset报文和一个连接相关联,一旦发送,将同时释放本端的TCP连接记录(TCB)。最终,两个阶段全部完成,一个交互图如下:

四.一点扩展

综上,我们能否使用一个fin包同时引发服务器发送一个reset呢?答案是肯定的,那就是在收到服务器的syn-ack后,发送一个带有fin且ack序号错误的报文,这样由于:1.syn-received状态不检查收到的fin标志,因此它只是毫无代价欺骗了墙;2.由于ack序号错误,因此服务器端会发送一个auto-reset从而在反方向欺骗墙。

五.两本书

最后,如果你觉得RFC不好啃,Linux源码有很繁杂,那么推荐一个简单的协议栈实现,那就是Xinu系统的协议栈实现,代码很少很清晰。它也是著名的《用TCP/IP进行网际互连(第二卷)》的讲解所采用的,看完《用TCP/IP进行网际互连(第二卷)》比看完《TCP/IP祥解(第二卷)》会让你对协议方面理解更多而不会迷失在茫茫的BSD代码之中,其实它们说的是一回事。
    看完《用TCP/IP进行网际互连(第二卷)》,你的思路会焕然一新的,Xinu内核完全是微内核设计,几乎所有的模块都是一个独立的进程,靠IPC进行通信,当然协议栈也不例外。Xinu的IP层由一个IP进程来完成,该IP进程设计的非常统一,使你很容易就能理解IP层的处理,特别是它抽象出了LOCAL接口,这样本地发到IP层的IP数据报和从物理网卡接收的报文就能使用一种更加统一的处理方式,如果在Xinu上实现Netfilter的话,对于filter表,Xinu一下子就砍掉了INPUT和OUTPUT两条链,因为Xinu并不区分数据报是从哪里来的,对于Xinu的IP进程,它们都来自于某一个“接口”,如下:

    另外一个亮点,那就是其timer-list的设计,以相对时间作为填充,且按照距离表头的相对时间排序,管理起来很高效,只需要修改表头即可,也大大增加了cache的命中率。再者,Xinu的TCP状态机的实现采用了和Linux完全不同的方式。

六.内核协议栈的修改

修改tcp_rcv_state_process的以下代码段:
case TCP_SYN_SENT:
...
在tcp_rcv_synsent_state_process函数中的tcp_send_ack之前发送两个坏报文即可,至于如何构造这两个坏报文,前面分析过了。

原文链接:http://blog.csdn.net/dog250/article/details/7246895
加载中
返回顶部
顶部