xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 English Version

再谈突破TCP-IP过滤/防火墙进入内网(icmp篇)


创建时间:2002-08-25
文章属性:原创
文章提交:TOo2y (too2y_at_safechina.net)

再谈突破TCP-IP过滤/防火墙进入内网(icmp篇)

Author:T0o2y [原创]
E-mail: nightcolor@hotmail.com
HomePage: www.safechina.net
Date: 8-25-2002

    随着Internet网络的普及,各个中大型公司均建立了自己的局域网络。而公司内部人员上网的限制也逐渐成为一个大家关心的话题。目前最为流行的网络工具大多是基于TCP/IP协议的,而其中最主要的两个协议就是TCP和UDP协议。HTTP,FTP等上层协议均是建立在TCP协议之上了,而DNS,ICQ,TFTP等则是建立在UDP协议之上的。往往我们会遇到这样情况:公司禁止了UDP协议,因为很大一部分的网络通讯软件都是建立在UDP协议之上的,而开通了TCP协议。这样,我们就可以通过TCP协议来为我们转发UDP数据报,具体实现原理可以参看eyas的《突破TCP-IP过滤/防火墙进入内网》,里面详细讨论了如何实现TCP与UDP数据报之间的相互转发,也可以参看本文相关软件T-QQ的源代码,里面也包含了TCP与UDP相互转发的功能,在此就不多说了。现在进入正题,如何实现用ICMP数据报来突破网关的限制?

    ICMP协议(Internet Control Messages Protocol, 网际控制报文协议)是一种多功能的协议,在网络上有很多用处,比如ICMP扫描,拒绝服务(DOS)攻击,隧道攻击,以及我们最常用到的PING程序。而我们就是利用ICMP协议来为我们传送(TCP/UDP)数据。大家知道一般的防火墙都是过滤了来自外部主机的回送请求(echo Request)报文,但为了是自己能够探测外部主机的当前状态,防火墙都不会过滤掉回送应答(echo Reply)数据报,而且ICMP报文可以在广域网上传送,这样我们就可以利用它来突破网关的种种限制。本文主要针对使用ICMP协议来转发UDP数据报的功能,并以OICQ为背景,至于突破TCP的限制,也大同小异。
    以下是QQicmp的工作原理:

                            ----->-----                    ----->-----                         ----->-----
        QQ客户端 <     UDP     > QQicmp(l) <    ICMP     > QQicmp(g) <     UDP     >Tencent服务器
                            -----<-----                     -----<-----                         -----<-----  

    其中QQ客户端和QQicmp(l)都运行在本机上,而QQicmp(g)则是运行在网关上(QQicmp(l) 与 QQicmp(g)均是同一程序,只是运行模式不同:-l 运行于本地主机, -g 运行于网关上),Tencent服务器我想大家都清楚吧。QQ客户端与QQicmp(l),QQicmp(g)与Tencent服务器之间以UDP通信,QQicmp(l)与QQicmp(g)之间则是以ICMP通信。  Win2000/xp都提供了自己构造数据报的功能,也就是我们可以自己定义发送数据报的各项内容,当然也可以监听通过主机的基于IP协议的各种数据报。为了发送ICMP数据报及接收所有的IP数据报,我们必须自定义数据报的格式及校验和的求解:
    typedef struct ipheader
    {
    unsigned char  h_lenver;          //头部长度及版本
    unsigned char  tos;               //服务类型
    unsigned short total_len;         //报文总长度
    unsigned short ident;             //信息包标志
    unsigned short frag_and_flags;    //标志及分段偏移量
    unsigned char  ttl;               //生命周期
    unsigned char  proto;             //协议类型
    unsigned short checksum;          //IP校验和  
    unsigned int   sourceip;          //源IP地址  
    unsigned int   destip;            //目的IP地址
    }ipheader;

    typedef struct icmpheader
    {
    unsigned char   type;             //ICMP类型 0->回送应答 8->回送请求
    unsigned char   code;             //代码
    unsigned short  checksum;         //校验和
    unsigned short  seq;              //序号
    unsigned short  id;               //标识符  
    }icmpheader;

    unsigned short checksum(unsigned short *buffer,int size)
    {
    unsigned long cksum=0;
    while(size>0)                         //各位求和
    {
        cksum+=*buffer++;
        size-=sizeof(unsigned short);
    }
    if(size)
        cksum+=*(unsigned char *)buffer;
    cksum=(cksum>>16)+(cksum & 0xffff);
    cksum+=(cksum>>16);
    return (unsigned short)(~cksum);      //再求补
    }

    首先,我们更改QQ客户端里的服务器地址为127.0.0.1,端口改为QQicmp(l)的监听QQ客户端端口,当然你也可以保持默认的8000,这样QQicmp(l)就应该在8000端口监听QQ客户端的数据。同时,QQ客户端也在端口4000(假设为非内网主机上的第一个QQ)监听来自QQicmp(l)的数据报。
    我们可以看到,QQicmp(l)的主要作用就是将接收到了来自QQ客户端的UPD数据报,
       sock[0][0]=socket(AF_INET,SOCK_DGRAM,0);              //创建基于UDP协议的套接字
       bind(sock[0][0],(struct sockaddr *)&sin[0][1],addrlen);           //绑定到指定地址,指定端口上
       iret=recvfrom(sock[0][0],msgrecv,sizeof(msgrecv),0,(struct sockaddr *)&tempr,&addrlen);   //接收来自QQ客户端的UDP数据

    然后以ICMP数据报的形式发送到QQicmp(g),在此需要自己构造ICMP echo Reply数据报,并将接收到的UDP数据报填充到ICMP报文的数据段,
    sock[0][1]=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);           //创建ICMP协议的原始套接字,用来发送自定义数据报
    bind(sock[0][1],(struct sockaddr *)&sin[0][2],addrlen);     //并捆绑到指定地址,指定端口上

       icmphdr.type=0;    //类型:echo reply
    icmphdr.code=0;    //代码
    icmphdr.id=htons(65456);   //序号
    icmphdr.seq=htons(65456);  //标志符,用以过滤数据报
    icmphdr.checksum=0;

    if(istbcs==0)     //填充ICMP数据报头部
    {
          memset(msgsend,0,sizeof(msgsend));
             memcpy(msgsend,&icmphdr,sizeof(icmphdr));
          istbcs+=sizeof(icmphdr);
    }
    memcpy(msgsend+istbcs,msgrecv,iret);  //将接收到的UDP数据报的内容提取,准备以ICMP的形式发送

    iret=sendto(sock[0][1],msgsend,istbcs,0,(struct sockaddr *)&sin[0][3],addrlen); //发送到网关
    同时,QQicmp(l)监听通过本机的IP数据报,筛选出来自QQicmp(g)及网关的数据报,
    sock[1][0]=socket(AF_INET,SOCK_RAW,IPPROTO_IP);            //创建原始套接字,接收所有的IP数据报
    bind(sock[1][0],(struct sockaddr *)&sin[1][1],addrlen);    //绑定到指定地址,端口

    DWORD   dwbufferlen[10];
    DWORD   dwbufferinlen=1;
    DWORD   dwbytesreturned=0;
    WSAIoctl(sock[1][0],SIO_RCVALL,&dwbufferinlen,sizeof(dwbufferinlen),&dwbufferlen,sizeof(dwbufferlen),&dwbytesreturned,NULL,NULL);
       //设置为接收所有的数据报,需要mstcpip.h头文件,T-QQ相关文件里就有,或安装SDK

    iret=recvfrom(sock[1][0],msgrecv,sizeof(msgrecv),0,(struct sockaddr *)&temps,&addrlen);   //接收所有数据报
    if(iret<=28)     //文件过小
    {
        break;
    }
    if((icmphdr->type!=0) || (icmphdr->code!=0) || ((icmphdr->id)!=htons(65456)) || ((icmphdr->seq)!=htons(65456)))  //不符合接收条件
    {
        break;
    }

    memcpy(msgsend+istbcs,msgrecv,iret);    //将接收到的ICMP数据报的内容提取,准备以UDP的形式发送
     解包后,用UDP数据报将接收到的来自网关的数据发送到QQ客户端,
    idx=28;    //ICMP数据报的前20字节是IP头部,接着的8字节是ICMP头部,
    iret=sendto(sock[1][1],&msgsend[idx],ileft,0,(struct sockaddr *)&sin[1][3],addrlen);    //发送到QQ客户端
      
     我们创建了两个线程在两个方向(udp-->icmp,icmp-->udp)上接收并传送数据,如果某个线程出错,就重新创建该线程,而未出错的线程则保持不变,
    hthreads[0]=CreateThread(NULL,0,u2i,(LPVOID)0,NULL,&hthreadid[0]);     //创建udp接收数据,icmp发送数据的线程0
    hthreads[1]=CreateThread(NULL,0,i2u,(LPVOID)1,NULL,&hthreadid[1]);     //创建icmp接收数据,udp发送数据的线程1

    while(1)
    {
    dwret=WaitForMultipleObjects(2,hthreads,false,INFINITE);                //等待某个线程的结束
    if(dwret==WAIT_FAILED)                                                  //等待出错
    {
        cout<<"WaitForMultipleObjects Error: "<<GetLastError()<<endl;
        return -1;
    }
    log=dwret-WAIT_OBJECT_0;
    if(log==0)                                                              //线程0结束
    {
        CloseHandle(hthreads[0]);                                        //关闭线程handle
           closesocket(sock[0][1]);                                                //关闭套接字
           hthreads[0]=CreateThread(NULL,0,u2i,(LPVOID)0,NULL,&hthreadid[0]);      //重新创建线程0
    }
    else if(log==1)                                                         //线程1结束
    {
        CloseHandle(hthreads[1]);
           closesocket(sock[1][0]);
           hthreads[1]=CreateThread(NULL,0,i2u,(LPVOID)1,NULL,&hthreadid[1]);
    }
    
     以上就是QQicmp(l)的工作原理,QQicmp(g)运行在网关上,虽然模式不同,但工作原理是一样的,只是数据报的流动方向有点差异。
     QQicmp之源代码如下:

#include <iostream.h>
#include <winsock2.h>
#include <windows.h>
#include <mstcpip.h>

#pragma comment (lib,"ws2_32")
#define maxsize 64*1024

typedef struct ipheader
{
    unsigned char  h_lenver;
    unsigned char  tos;
    unsigned short total_len;
    unsigned short ident;
    unsigned short frag_and_flags;
    unsigned char  ttl;
    unsigned char  proto;
    unsigned short checksum;
    unsigned int   sourceip;
    unsigned int   destip;
}ipheader;

typedef struct icmpheader
{
    unsigned char   type;
    unsigned char   code;
    unsigned short  checksum;
    unsigned short  seq;
    unsigned short  id;
}icmpheader;

unsigned short checksum(unsigned short *buffer,int size)
{
    unsigned long cksum=0;
    while(size>0)
    {
        cksum+=*buffer++;
        size-=sizeof(unsigned short);
    }
    if(size)
        cksum+=*(unsigned char *)buffer;
    cksum=(cksum>>16)+(cksum & 0xffff);
    cksum+=(cksum>>16);
    return (unsigned short)(~cksum);
}

void start()
{
    cout<<"            ---------------------------------------------------\n";
    cout<<"              ||                                                  || \n";
    cout<<"              ||             QQicmp  (ICMP转发)             || \n";
    cout<<"              ||                                                  || \n";
    cout<<"              ||         Author:TOo2y     SafeChina        || \n";
    cout<<"              ||                                                  || \n";
    cout<<"            ---------------------------------------------------"<<endl;
}

void usage()
{
       cout<<"\nUsage:\r\n\tQQicmp  -l[-g]  ip  port"<<endl;
    cout<<"\tQQicmp  -h"<<endl;
    cout<<"Example:\r\n";
    cout<<"\tQQicmp  -l  192.168.0.1     8000"<<endl;
    cout<<"\tQQicmp  -g  61.144.238.156  11282"<<endl;
    cout<<"Attention:"<<endl;
    cout<<"\t选项 -l :  运行于本机上,ip填网关地址,port为本地监听客户端端口;"<<endl;
         cout<<"\t选项 -g :  运行于网关上,ip填腾讯服务器地址,port为自定义端口;"<<endl;
    cout<<"\t选项 -h :  查看相关帮助文件。"<<endl;
}

int    addrlen=sizeof(struct sockaddr_in);
SOCKET sock[2][2];
struct sockaddr_in sin[2][4],sag,sal,tempr,temps;

DWORD WINAPI u2i(LPVOID num)
{
    UNREFERENCED_PARAMETER(num);
       char   msgrecv[maxsize]={0},msgsend[maxsize]={0};
    fd_set fdread,fdwrite;
    int    iret,ret,istbcs=0;
       struct icmpheader icmphdr;

    memset(&icmphdr,0,sizeof(icmphdr));
    icmphdr.code=0;
    icmphdr.id=htons(65456);
    icmphdr.seq=htons(65456);
       icmphdr.type=0;
    icmphdr.checksum=checksum((unsigned short *)&icmphdr,sizeof(icmphdr));      

    if((sock[0][1]=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))==INVALID_SOCKET)
    {
        cout<<"Socket sock[0][1] Error: "<<GetLastError()<<endl;
        return -1;
    }
    if(bind(sock[0][1],(struct sockaddr *)&sin[0][2],addrlen)==SOCKET_ERROR)
    {
        cout<<"Bind sock[0][1] Error: "<<GetLastError()<<endl;
        return -1;
    }

    while(1)
    {
        FD_ZERO(&fdread);
        FD_ZERO(&fdwrite);
        FD_SET(sock[0][0],&fdread);
        FD_SET(sock[0][1],&fdwrite);

        if((ret=select(0,&fdread,&fdwrite,NULL,NULL))==SOCKET_ERROR)
        {
            cout<<"Select in thread 0 Error: "<<GetLastError()<<endl;
            break;
        }
        if(ret>0)
        {
            if(FD_ISSET(sock[0][0],&fdread))
            {
                iret=recvfrom(sock[0][0],msgrecv,sizeof(msgrecv),0,(struct sockaddr *)&tempr,&addrlen);
                if(iret==SOCKET_ERROR)
                {
                    cout<<"\nRecvfrom sock[0][0] Error: "<<GetLastError()<<endl;
                    break;
                }
                else if(iret==0)
                {
                    cout<<"Iret==0"<<endl;
                    break;
                }
                cout<<"\nThread 0 Recv "<<iret<<" bytes from\t"<<inet_ntoa(tempr.sin_addr)<<endl;
                if(istbcs==0)
                {
                                  memset(msgsend,0,sizeof(msgsend));
                                   memcpy(msgsend,&icmphdr,sizeof(icmphdr));
                                  istbcs+=sizeof(icmphdr);
                }
                memcpy(msgsend+istbcs,msgrecv,iret);
                istbcs+=iret;
                memset(msgrecv,0,sizeof(msgrecv));
            }
            else if(FD_ISSET(sock[0][1],&fdwrite))
            {
            
                     while(istbcs>0)  
                {

                    if(sin[0][3].sin_addr.s_addr==htonl(0))            
                {
                    cout<<"sin[0][3].sin_addr.s_addr==htonl(0)"<<endl;
                    istbcs=0;                                      
                    memset(msgsend,0,sizeof(msgsend));
                    break;
                }

                iret=sendto(sock[0][1],msgsend,istbcs,0,(struct sockaddr *)&sin[0][3],addrlen);
                if(iret==SOCKET_ERROR)
                {
                    cout<<"Sendto sock[0][1] Error: "<<GetLastError()<<endl;
                    break;
                }
                cout<<"Thread 0 send "<<iret<<" bytes  to \t"<<inet_ntoa(sin[0][3].sin_addr)<<endl;
                istbcs-=iret;
                }
                memset(msgsend,0,sizeof(msgsend));
                istbcs=0;
                }
            Sleep(20);
        }
    }
    return 0;
}


DWORD WINAPI i2u(LPVOID num)
{
       UNREFERENCED_PARAMETER(num);
       fd_set  fdread,fdwrite;
    char    msgrecv[maxsize]={0},msgsend[maxsize]={0};
    int     ret,iret,idx,istbcs=0,ileft;
    DWORD   dwbufferlen[10];
    DWORD   dwbufferinlen=1;
    DWORD   dwbytesreturned=0;
       struct  ipheader   *iphdr;
    struct  icmpheader *icmphdr;

    if((sock[1][0]=socket(AF_INET,SOCK_RAW,IPPROTO_IP))==INVALID_SOCKET)
    {
        cout<<"Socket sock[1][0] Error: "<<GetLastError()<<endl;
        return -1;
    }
    if(bind(sock[1][0],(struct sockaddr *)&sin[1][1],addrlen)==SOCKET_ERROR)
    {
        cout<<"Bind sock[1][0] Error: "<<GetLastError()<<endl;
        return -1;
    }

    WSAIoctl(sock[1][0],SIO_RCVALL,&dwbufferinlen,sizeof(dwbufferinlen),&dwbufferlen,sizeof(dwbufferlen),&dwbytesreturned,NULL,NULL);
       iphdr=(struct ipheader *)msgrecv;
    icmphdr=(struct icmpheader *)(msgrecv+sizeof(struct ipheader));

    while(1)
    {
        FD_ZERO(&fdread);
        FD_ZERO(&fdwrite);
        FD_SET(sock[1][0],&fdread);
        FD_SET(sock[1][1],&fdwrite);

        if((ret=select(0,&fdread,&fdwrite,NULL,NULL))==SOCKET_ERROR)
        {
            cout<<"Select in thread 1 Error: "<<GetLastError()<<endl;
            break;
        }
        if(ret>0)
        {
            if(FD_ISSET(sock[1][0],&fdread))
            {
                {
                iret=recvfrom(sock[1][0],msgrecv,sizeof(msgrecv),0,(struct sockaddr *)&temps,&addrlen);
                if(iret==SOCKET_ERROR)
                {
                    cout<<"Recvfrom sock[1][0] Error: "<<GetLastError()<<endl;
                    break;
                }

                if(iret<=28)
                {
                    break;
                }
                if((icmphdr->type!=0) || (icmphdr->code!=0) || ((icmphdr->id)!=htons(65456)) || ((icmphdr->seq)!=htons(65456)))
                {
                    break;
                }
                if((sin[1][0].sin_addr.s_addr!=htonl(0)) && (sin[1][0].sin_addr.s_addr!=temps.sin_addr.s_addr))
                    break;
                }
                else if(sin[1][0].sin_addr.s_addr==htonl(0))          
                {
                    sin[1][0].sin_addr.s_addr=temps.sin_addr.s_addr;
                    sin[0][3].sin_addr.s_addr=temps.sin_addr.s_addr;
                    cout<<"sin[0][3] ==> "<<inet_ntoa(sin[0][3].sin_addr)<<endl;
                }

                cout<<"\nThread 1 Recv "<<iret<<" bytes from \t"<<inet_ntoa(temps.sin_addr)<<endl;

                memcpy(msgsend+istbcs,msgrecv,iret);
                istbcs+=iret;
                memset(msgrecv,0,sizeof(msgrecv));
                }
            }
            else if(FD_ISSET(sock[1][1],&fdwrite))
            {
                ileft=istbcs-28;
                idx=28;
                while(ileft>0)
                {
                    iret=sendto(sock[1][1],&msgsend[idx],ileft,0,(struct sockaddr *)&sin[1][3],addrlen);
                    if(iret==SOCKET_ERROR)
                    {
                        cout<<"Sendto sock[1][1] Error: "<<GetLastError()<<endl;
                        break;
                    }
                    cout<<"Thread 1 send "<<iret<<" bytes  to  \t"<<inet_ntoa(sin[1][3].sin_addr)<<endl;
                    ileft-=iret;
                    idx+=iret;
                }
                istbcs=0;
                memset(msgsend,0,sizeof(msgsend));
            }
            Sleep(20);
        }
    }
    return 0;
}


int main(int argc,char *argv[])
{
    WSADATA      wsa;
       BOOL         gl;
    HANDLE       hthreads[2];
    DWORD        hthreadid[2];
       struct       hostent *hp;
    char         cname[100];
    int          dwret,log;

    system("cls.exe");
    start();

    if(argc==2)
    {
        if(strcmp(argv[1],"-h")==0)
        {
            ShellExecute(NULL,"open","help.txt",NULL,NULL,SW_SHOWMAXIMIZED);
            return 0;
        }
        else
        {
            usage();
            return -1;
        }
    }
    else if(argc!=4)
    {
        usage();
        return -1;
    }
    if(!strcmp(argv[1],"-g"))
        gl=true;
    else if(!strcmp(argv[1],"-l"))
        gl=false;
    else
    {
        usage();
        return -1;
    }

    if(WSAStartup(MAKEWORD(2,2),&wsa)!=0)
    {
        cout<<"WSAStartup Error: "<<GetLastError()<<endl;
        return -1;
    }

    gethostname(cname,sizeof(cname));
       hp=gethostbyname(cname);
    for(int ipnum=0;hp->h_addr_list[ipnum]!=NULL;ipnum++)
        sag.sin_addr=*(in_addr *)hp->h_addr_list[ipnum];
    sag.sin_family=AF_INET;
    sag.sin_port=htons(65456);

    sal=sag;
       if(ipnum>1)
        sal.sin_addr=*(in_addr *)hp->h_addr_list[ipnum-2];

    if(gl)
    {
        sin[0][0].sin_addr.s_addr=inet_addr(argv[2]);
        sin[0][0].sin_family=AF_INET;
        sin[0][0].sin_port=htons(8000);

        sin[0][1].sin_addr.s_addr=htonl(INADDR_ANY);      
        sin[0][1].sin_family=AF_INET;
        sin[0][1].sin_port=htons(atoi(argv[3]));

           sin[0][2]=sal;                                    

        memset(&sin[0][3],0,sizeof(sin[0][3]));            
        sin[0][3].sin_family=AF_INET;
    }
    else
    {
        sin[0][0].sin_addr.s_addr=inet_addr("127.0.0.1");
        sin[0][0].sin_family=AF_INET;
        sin[0][0].sin_port=htons(4000);                

        sin[0][1].sin_addr.s_addr=htonl(INADDR_ANY);
        sin[0][1].sin_family=AF_INET;
        sin[0][1].sin_port=htons(atoi(argv[3]));

        sin[0][2]=sal;                                  

        sin[0][3].sin_addr.s_addr=inet_addr(argv[2]);
        sin[0][3].sin_family=AF_INET;
    }
       sin[1][0]=sin[0][3];      
       sin[1][1]=sin[0][2];  
       sin[1][2]=sin[0][1];  
       sin[1][3]=sin[0][0];      

       if((sock[0][0]=socket(AF_INET,SOCK_DGRAM,0))==INVALID_SOCKET)
    {
        cout<<"Socket sock[0][0] Error: "<<GetLastError()<<endl;
        return -1;
    }
    if(bind(sock[0][0],(struct sockaddr *)&sin[0][1],addrlen)==SOCKET_ERROR)
    {
        cout<<"Bind sock[0][0] Error: "<<GetLastError()<<endl;
        return -1;
    }
       sock[1][1]=sock[0][0];

    cout<<"\n正常工作中..."<<endl;

    hthreads[0]=CreateThread(NULL,0,u2i,(LPVOID)0,NULL,&hthreadid[0]);
    hthreads[1]=CreateThread(NULL,0,i2u,(LPVOID)1,NULL,&hthreadid[1]);
    while(1)
    {
    dwret=WaitForMultipleObjects(2,hthreads,false,INFINITE);        
    if(dwret==WAIT_FAILED)
    {
        cout<<"WaitForMultipleObjects Error: "<<GetLastError()<<endl;
        return -1;
    }
    log=dwret-WAIT_OBJECT_0;
    if(log==0)
    {
        CloseHandle(hthreads[0]);
           closesocket(sock[0][1]);
           hthreads[0]=CreateThread(NULL,0,u2i,(LPVOID)0,NULL,&hthreadid[0]);
    }
    else if(log==1)
    {
        CloseHandle(hthreads[1]);
           closesocket(sock[1][0]);
           hthreads[1]=CreateThread(NULL,0,i2u,(LPVOID)1,NULL,&hthreadid[1]);
    }
    else
    {
           for(int no1=0;no1<2;no1++)
           {
               CloseHandle(hthreads[no1]);
               for(int no2=0;no2<2;no2++)
                       closesocket(sock[no1][no2]);
        }
    }
    }
    WSACleanup();
    return 0;
}

    本文相关软件T-QQ主要针对禁止使用QQ的网关,提供UDP,TCP及ICMP数据报转发功能,本软件同样适用于各种基于UDP协议的通信软件。其中的TCP数据报转发功能,也可以使用UDP数据报来转发TCP数据。相关软件及源代码下载地址:  http://www.cshu.net/down/t-qq.rar