Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9601033
  • 博文数量: 1227
  • 博客积分: 10026
  • 博客等级: 上将
  • 技术积分: 20273
  • 用 户 组: 普通用户
  • 注册时间: 2008-01-16 12:40
文章分类

全部博文(1227)

文章存档

2010年(1)

2008年(1226)

我的朋友

分类: C/C++

2008-03-14 09:03:15

一、个人防火墙技术概述

    随着网络安全问题日益严重,广大用户对网络安全产品也越来越关注。 防火墙作为一种网络安全工具,早已受到大家的青睐。在PC机上使用的个人防火墙,很大程度上成为广大网民的安全保护者。Windows下的个人防火墙都是基于对数据报的拦截技术之上。当然在具体的实现方式上它们却有很大的不同。总的来说可分为用户级和内核级数据报拦截两类。其中内核级主要是TDI过滤驱动程序,NDIS中间层过滤驱动程序,NDIS过滤钩子驱动程序等,它们都是利用网络驱动来实现的;而用户级的过滤包括SPI接口,Windows2000包过滤接口等。本文主要讲述基于SPI的包过滤实现,它是Winsock 2的一个新特性。

二、Winsock 2 SPI介绍

    Winsock 2 是一个接口,而不是协议,所以它可以用于发现和使用任意数量的底层传输协议所提供的通信能力。起初的Winsock是围绕着TCP/IP协议运行的,但是在Winsock 2中却增加了对更多传输协议的支持。Winsock 2不仅提供了一个供应用程序访问网络服务的Windows socket应用程序编程接口(API),还包含了由传输服务提供者和名字解析服务提供者实现的Winsock服务提供者接口(SPI)和ws2_32.dll。本文仅讨论传输服务提供者及其应用,暂不对名字解析服务提供者进行分析。
    Winsock 2的传输服务提供者是以动态链接库的形式(DLL)存在的,它是通过WSPStartup函数为上层函数提供接口,而其他的传输服务提供者函数则是通过分配表的方式来访问WS2_32.DLL。传输服务提供者的动态链接库只有在应用程序需要时才由Ws2_32.dll来装入内存中的,在不需要时则会被自动卸载。以下是winsock 2在传输服务提供者上的WOSA(Windows开放服务结构):

----------------------------
│Windows socket 2 应用程序│
----------------------------Windows socket 2 API
│       WS2_32.DLL        │
----------------------------Windows socket 2 传输SPI
│   传输服务提供者(DLL)  │  
----------------------------

    Windows socket SPI在服务提供者中使用了以下的函数前缀命名方式:WSP(Windows socket服务提供者),WPU(Windows socket提供者向上调用),WSC(Windows socket配置)。每一个传输服务提供者都有它自己所支持的传输协议,它是使用WSAPROTCOL_INFOW结构来实现的。传输服务提供者把所有的相关信息都存放在这个结构中,而应用程序就是通过这个结构的内容来将自己和相应的传输服务提供者相关联。
    Windows socket SPI提供三种协议:分层协议,基础协议和协议链。分层协议是在基础协议的上层,依靠底层基础协议实现更高级的通信服务。基础协议是能够独立,安全地和远程端点实现数据通信的协议,它是相对与分层协议而言的。协议链是将一系列的基础协议和分层协议按特点的顺序连接在一起的链状结构,请参见下图:

API------------------------
   │      WS2_32.DLL     │
SPI------------------------
   │ 分层协议 │
SPI-------------
   │ 分层协议 │
SPI------------------------
   │       基础协议       │
   ------------------------

    Ws2_32.dll数据传输部分的主要功能是在服务提供者和应用程序之间提供流量管理的功能。每个应用程序通过Ws2_32.dll和相应的服务提供者进行严格的数据交换。Ws2_32.dll根据应用程序在创建套接字时所提供的参数来选择特定的服务提供者,然后把应用程序的实现过程转发由所选创建套接字的服务提供者来管理。也就是说,Ws2_32.dll只是一个中间过程,而应用程序只是一个接口,数据通信的实现却是有服务提供者来完成的。我们说过,Ws2_32.dll是通过创建套接字的API函数WSASocket或socket的参数来确定使用哪一个服务提供者。而WSASocket/socket的参数中包括了地址族,套接字类型和协议类型,这三个因素共同决定了创建套接字的服务提供者。Ws2_32.dll在服务提供者中寻找第一个和前面三因素相匹配的WSAPROTOCOL_INFOW结构,然后就调用这个WSAPROTOCOL_INFOW结构相应的WSPStartup函数,(所有的数据传输服务提供者以DLL的形式,它们对外的接口就只有WSPStartup,其他的服务提供者函数都是通过WSPStartup来调用的),进而调用如WSPSocket的函数来创建套接字,WSPConnect的函数来建立连接等等。除了流量管理功能外,Ws2_32.dll还提供了其他的服务,比如协议枚举,基于线程的阻塞钩子管理和在Ws2_32.dll和服务提供者之间进行版本协商。

    传输服务提供者实现的功能包括建立连接,传输数据,实现流控制和差错控制等函数。其实Ws2_32.dll并不知道服务提供者的请求等活动是如何实现了,Ws2_32.dll在应用程序和服务提供者之间实现了媒介的功能。传输服务提供者可分为两类:套接字描述符是可安装的文件系统(IFS)句柄的提供者;剩下的是非IFS的提供者。在我们的程序中选用了非IFS提供者。可见,服务提供者实现了底层的与网络相关的协议。Ws2_32.dll提供了介质级别的流量管理,应用程序则提供了有关如何实现网络相关的操作,它实现了用户所希望的功能。

    在传输服务提供者的实现过程中,安装顺序是非常重要的。我们不仅要正确的安装服务提供者,而且还必须在Windows socket中注册,将相关的系统信息保存在数据库中,这样Ws2_32.dll才能够方便的获得下层服务提供者的相关信息。在Ws2_32.dll中提供了用来安装服务提供者的函数WSCInstallProvider,它需要服务提供者的有关数据,比如DLL的名称和路径。同时Ws2_32.dll还提供了卸载服务提供者的函数WSCDeinstallProvider,在不需要时通过它将特定的服务提供者从系统中删除。为什么说传输服务者的安装顺序很重要呢?在服务提供者配置函数中的WSCEnumProtocols是用来枚举系统中所有已安装的服务提供者,它按照服务提供者的安装顺序相应的列出他们。在前面我们也提到过,Ws2_32.dll在服务提供者中按安装顺序搜寻和WSASocket/socket提供的三个参数相匹配的服务提供者,所以安装顺序在一定程度上是决定了服务提供者是否被正确调用的关键。Windows socket 2还提供了一个动态链接库Sporder.dll,它提供了对已安装的所有服务提供者顺序的重新排列(此DLL系统没有自带,common目录中已提供)。在附录中的T-Sporder.exe是一个查询当前已安装所有数据传输服务提供者属性的工具。
  
  服务提供者系统中区分基础协议,分层协议和协议链是通过结构WSAPROTOCOL_INFOW中的Protocolchain结构的ChainLen值来实现的。分层协议的ChainLen值为0,基础协议的值为1,而协议链的值是大于1。在数据传输服务提供者的实现方式中分层协议和基础协议几乎是相同的,它们的不同之处在安装上。Windows中,现有的系统服务提供者(系统自带)几乎已提供了所有基本的服务,因此我们所写的服务提供者程序,都可以对数据报进行适当“修饰”后调用系统服务提供者来完成绝大部分剩下的功能,无论是基础服务提供者还是分层服务提供者都可以使用这种技术,免去不必要的劳动。基础服务提供者的实现过程主要是替换当前系统服务提供者的安装路径为自己的服务提供者的安装路径即可,当然我们必须保存所以系统服务者的相关数据,在我们卸载自己的服务提供者还原系统服务提供者时要用到这些信息,如系统服务者DLL的名称和路径。而协议链就不同了,首先我们必须安装好所有的基础协议和分层协议后,再构造协议链的WSAPROTOCOL_INFOW结构链,组成协议链的每个协议都会在协议链的ProtocolChain.ChainEntries数组中被定义,协议链数组中的第一个协议应该是第一个分层服务提供者。当然在安装分层协议及协议链时我们不会改变系统服务提供者,最多只是改变系统服务提供者的安装顺序罢了。在此,我们以分层服务提供者为例来说明数据传输服务提供者的安装过程。
 
   Ws2_32.dll是使用标准的动态链接库来加载服务提供者接口的DLL到系统中去的,并调用WSPStartup来初始化。WSPStartup是Windows Socket 2应用程序调用SPI程序的初始化函数,也就是入口函数。WSPStartup的参数LPWSAPROTOCOL_INFOW指针提供应用程序所期望的协议信息,然后通过这个结构指针我们可以获得所保存的系统服务提供者的DLL名称和路径,加载系统服务提供者后查找到系统SPI程序的WSPStartup函数的指针,通过这个指针我们就可以将自己服务提供者的WSPStartup函数和系统SPI程序的WSPStartup函数相关联,进而调用系统的各个服务提供者函数。在数据传输服务提供者的实现中,我们需要两个程序,一个是可执行文件用来安装传输服务提供者;另一个就是DLL形式的数据传输服务提供者。下面我们就对安装程序(instif.exe)和实现程序(ipfilter.dll)所使用的主要函数进行简要分析。

三、相关程序代码分析

1.instif.exe

    可执行程序instif.exe的主要功能是安装我们自己的分层传输服务提供者,并重新排列所有传输服务提供者的顺序,使我们的服务提供者位于协议链的顶端,这样相应类型的应用程序就会首先进入我们的传输服务提供者接口。本程序只有一个参数,就是安装(-install)或卸载(-remove)。作为演示,本程序只安装了IP分层协议及与UDP相关的协议链。(在ipfilter.dll中,我们只过滤目标端口为8000的UDP数据报)
    自定义函数:
    BOOL  getfilter();     //获得所有已经安装的传输服务提供者
    void  freefilter();    //释放存储空间
    void  installfilter(); //安装分层协议,协议链及排序
    void  removefilter();  //卸载分层协议和协议链

    代码分析:
    protoinfo=(LPWSAPROTOCOL_INFOW)GlobalAlloc(GPTR,protoinfosize);
    //分配WSAPROTOCOL_INFOW结构的存储空间
    totalprotos=WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode);
    //获得系统中已安装的所有服务提供者
    GetCurrentDirectory(MAX_PATH,filter_path);  
    //得到当前的路径
    _tcscpy(filter_name,_T("\\ipfilter.dll"));  
    //构造服务提供者文件ipfilter.dll的路径全名
    WSCInstallProvider(&filterguid,filter_path,&iplayerinfo,1,&errorcode);
    //安装自定义的IP分层协议
    iplayercataid=protoinfo[i].dwCatalogEntryId;
    //获得已安装自定义IP分层协议的由Ws2_32.dll分配的唯一标志
    udpchaininfo.ProtocolChain.ChainEntries[0]=iplayercataid;
    //将自定义的IP分层协议作为自定义UDP协议链的根分层服务提供者安装在协议链的顶端
    WSCInstallProvider(&filterchainguid,filter_path,chainarray,provcnt,&errorcode);
    //安装协议链
    WSCWriteProviderOrder(cataentries,totalprotos);
    //更新所有服务提供者的安装顺序,把自定义的服务提供者排在所有协议的最前列
    WSCDeinstallProvider(&filterguid,&errorcode);
    //卸载IP分层协议
    WSCDeinstallProvider(&filterchainguid,&errorcode);
    //卸载协议链

  2.ipfilter.dll

    传输服务提供者都是以动态链接库的形式存在的,在应用程序需要时由Ws2_32.dll加载,在用完之后就被卸载。本文的ipfilter.dll提供了对发送的UDP数据报进行过滤的功能。也就是自定义WSPSendTo函数,在调用系统服务提供者之前进行过滤,判断是否继续向下调用,而其他的函数都是直接调用下层的系统服务提供者由它们直接处理。传输服务提供者只有一个入口函数就是WSPStartup,它是Windows Socket 应用程序调用SPI的初始化函数,其他SPI函数的调用都是通过WSPStartup的参数WSPUPCALLTABLE来实现的。
    自定义函数:
    int  WSPAPI WSPSendTo(SOCKET s,LPWSABUF lpbuffer,DWORD dwbuffercount,LPDWORD lpnumberofbytessent,
  DWORD dwflags,const struct sockaddr FAR *lpto,int itolen,LPWSAOVERLAPPED lpoverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE  lpcompletionroutine,LPWSATHREADID lpthreadid,LPINT lperrno);
    //SPI函数WSPSendTo和Windows Socket 2的API函数WSASendTo相对应
    int  WSPAPI WSPStartup(WORD wversionrequested,LPWSPDATA lpwspdata,LPWSAPROTOCOL_INFOW lpprotoinfo,
         WSPUPCALLTABLE upcalltable,LPWSPPROC_TABLE lpproctable);
    //SPI函数WSPStartup和Windows Socket 2的API函数WSAStartup相对应,WSPStartup是唯一的入口函数,剩下的30个SPI函数则是通过参数upcalltable来实现的,它们只能在内部调用,不向外提供入口
    
    代码分析:
    GetModuleFileName(NULL,processname,MAX_PATH);
    //获得调用本服务提供者动态链接库的可执行文件的全名
    OutputDebugString(_T("WSPSendTo Tencent Filtered"));
    //输出调试信息
    nextproctable.lpWSPSendTo(s,lpbuffer,dwbuffercount,lpnumberofbytessent,dwflags,lpto,
                              itolen,lpoverlapped,lpcompletionroutine,lpthreadid,lperrno);
    //如果数据报满足发送条件,调用下层系统服务提供者发送数据
    layerid=protoinfo[i].dwCatalogEntryId;
    //获得已安装自定义IP分层协议的由Ws2_32.dll分配的唯一标志
    nextlayerid=lpprotoinfo->ProtocolChain.ChainEntries[i+1];
    //获得下一层传输服务提供者的标志信息
    WSCGetProviderPath(&protoinfo[i].ProviderId,filterpath,&filterpathlen,&errorcode);
    //获得下一层传输服务提供者的安装路径
    ExpandEnvironmentStrings(filterpath,filterpath,MAX_PATH);
    //扩展环境变量
    hfilter=LoadLibrary(filterpath));
    //装载下一层传输服务提供者
    wspstartupfunc=(LPWSPSTARTUP)GetProcAddress(hfilter,"WSPStartup"));
    //获得下一层传输服务提供者的入口函数WSPStartup,以便调用
    wspstartupfunc(wversionrequested,lpwspdata,lpprotoinfo,upcalltable,lpproctable);
    //调用下一层传输服务提供者的WSPStartup函数,实现钩子功能
    nextproctable=*lpproctable;
    //保存下一层服务提供者的30个服务函数指针
    lpproctable->lpWSPSendTo=WSPSendTo;
    //调用自定义函数WSPSendTo

    由于以动态链接库形式的服务提供者要向外提供一个入口函数,因此还须一个配置文件ipfilter.def:
    EXPORTS    WSPStartup
    //向外提供入口函数WSPStartup
  
    3.T-Sporder.exe
    T-Sporder.exe是一个辅助工具,用来查看当前系统中所有已经安装的传输服务提供者的属性。
    totalprotocols=WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode);
    //获得系统中的所有传输服务提供者,然后根据参数输出它们的各项属性。

四、小结与后记

    本文向大家介绍了Windows Socket 2的一个新特性,那就是服务提供者接口SPI(Service Provider Interface)。它不仅包括我们主要讲解的传输服务提供者接口,还包括名字空间服务提供者接口。当然,基于SPI的包过滤安全措施并不是特别的好,因为很多建立在TDI上面的数据传输并不会受到SPI的影响,所以当前流行的防火墙大都是建立在NDIS之上的。
    传输服务提供者是以DLL的形式存在于系统之中的,在基于IP协议的网络程序运行时,如果参数相匹配就会加载我们的传输服务提供者程序。而且在Windows下有很多系统网络服务,它们都是在系统启动时自动加载的,这就为我们隐藏木马的进程提供了有利的条件。也就是说在传输服务提供者程序里嵌入木马程序,很多基于IP协议的网络系统程序在开机时运行,这样我们嵌入的木马程序就会在系统启动时自动加载,在系统关闭时才会卸载。它的特点是只要安装一次后每每系统启动就会加载我们的传输服务提供者(里面包含木马程序),而不必像远程注入线程那样在系统每次启动时执行安装程序,并且它可同时被多个系统网络程序加载。
    已编译好的可执行文件(过滤QQ数据报),您可以在我们的网站()下载。

五、附录之源程序

 1.instif.exe
#define  UNICODE     
#define  _UNICODE         

#include 
#include 
#include 
#include 
#include 
                           
GUID  filterguid={0x4d1e91fd,0x116a,0x44aa,{0x8f,0xd4,0x1d,0x2c,0xf2,0x7b,0xd9,0xa9}};

GUID  filterchainguid={0xd3c21121,0x85e1,0x48f3,{0x9a,0xb6,0x23,0xd9,0x0c,0x73,0x07,0xef}};

BOOL  getfilter();
void  freefilter();
void  installfilter();
void  removefilter();
void  start();
void  usage();

int                   totalprotos=0;
DWORD                 protoinfosize=0;
LPWSAPROTOCOL_INFOW   protoinfo=NULL;

int main(int argc,char *argv[])           
{
 start();

 if(argc==2)
 {
  if(strcmp(argv[1],"-install")==0)   
  {
   installfilter();
   return 0;
  }
  else if(strcmp(argv[1],"-remove")==0)  
  {
   removefilter();
   return 0;
  }
 }
 usage();
 return 0;
}

BOOL getfilter()
{
 int  errorcode;

 protoinfo=NULL;
 totalprotos=0;
 protoinfosize=0;

 if(WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode)==SOCKET_ERROR)
 {
  if(errorcode!=WSAENOBUFS)
  {
   _tprintf(_T("First WSCEnumProtocols Error: %d\n"),errorcode);
   return FALSE;
  }
 }

 if((protoinfo=(LPWSAPROTOCOL_INFOW)GlobalAlloc(GPTR,protoinfosize))==NULL)
 {
  _tprintf(_T("GlobalAlloc in getfilter Error: %d\n"),GetLastError());
  return FALSE;
 }

 if((totalprotos=WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode))==SOCKET_ERROR)
 {
  _tprintf(_T("Second WSCEnumProtocols Error: %d\n"),GetLastError());
  return FALSE;
 }

 _tprintf(_T("Found %d protocols!\n"),totalprotos); 
 return TRUE;
}

void freefilter()
{
 GlobalFree(protoinfo);
}

void installfilter()
{
 int                i;
 int                provcnt;
 int                cataindex;
 int                errorcode;
 BOOL               rawip=FALSE;
 BOOL               udpip=FALSE;
 DWORD              iplayercataid=0,udporigcataid=0;
 TCHAR              filter_path[MAX_PATH];            
 TCHAR              filter_name[MAX_PATH];
 TCHAR              chainname[WSAPROTOCOL_LEN+1];      
 LPDWORD            cataentries;
 WSAPROTOCOL_INFOW  iplayerinfo,udpchaininfo,chainarray[1];

 getfilter();
    
 for(i=0;i0;i--)
   {
    udpchaininfo.ProtocolChain.ChainEntries[i+1]=udpchaininfo.ProtocolChain.ChainEntries[i];
   }
  }

  udpchaininfo.ProtocolChain.ChainLen++;
  udpchaininfo.ProtocolChain.ChainEntries[0]=iplayercataid;

  memcpy(&chainarray[provcnt++],&udpchaininfo,sizeof(WSAPROTOCOL_INFOW));
 }

 if(WSCInstallProvider(&filterchainguid,filter_path,chainarray,provcnt,&errorcode)==SOCKET_ERROR)
 {
  _tprintf(_T("WSCInstallProvider for chain Error: %d\n"),errorcode);
  return ;
 }

 freefilter();

 getfilter();

 if((cataentries=(LPDWORD)GlobalAlloc(GPTR,totalprotos*sizeof(WSAPROTOCOL_INFOW)))==NULL)
 {
  _tprintf(_T("GlobalAlloc int installfilter Error: %d\n"),errorcode);
  return ;
 }

 cataindex=0;
 for(i=0;i
 2.ipfilter.dll

#define  UNICODE
#define  _UNICODE

#include 
#include 

GUID  filterguid={0x4d1e91fd,0x116a,0x44aa,{0x8f,0xd4,0x1d,0x2c,0xf2,0x7b,0xd9,0xa9}};

LPWSAPROTOCOL_INFOW  protoinfo=NULL;
WSPPROC_TABLE        nextproctable;
DWORD                protoinfosize=0;
int                  totalprotos=0;

BOOL getfilter()
{
 int    errorcode;

 protoinfo=NULL;
 protoinfosize=0;
 totalprotos=0;

 if(WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode)==SOCKET_ERROR)
 {
  if(errorcode!=WSAENOBUFS)
  {
   OutputDebugString(_T("First WSCEnumProtocols Error!")); 
   return FALSE;
  }
 }

 if((protoinfo=(LPWSAPROTOCOL_INFOW)GlobalAlloc(GPTR,protoinfosize))==NULL)
 {
  OutputDebugString(_T("GlobalAlloc Error!"));                
  return FALSE;
 }

 if((totalprotos=WSCEnumProtocols(NULL,protoinfo,&protoinfosize,&errorcode))==SOCKET_ERROR)
 {
  OutputDebugString(_T("Second WSCEnumProtocols Error!"));   
  return FALSE;
 }

 return TRUE;
}

void freefilter()
{
 GlobalFree(protoinfo);
}

BOOL WINAPI DllMain(HINSTANCE hmodule,
DWORD     reason,
LPVOID    lpreserved)
{
 TCHAR   processname[MAX_PATH];
 TCHAR   showmessage[MAX_PATH+25];


 if(reason==DLL_PROCESS_ATTACH)
 {
  GetModuleFileName(NULL,processname,MAX_PATH);
  _tcscpy(showmessage,processname);
  _tcscat(showmessage,_T(" Loading IPFilter ..."));
  OutputDebugString(showmessage);  
 }
 return TRUE;
}

int WSPAPI WSPSendTo(SOCKET s,
  LPWSABUF         lpbuffer,
  DWORD            dwbuffercount,
  LPDWORD          lpnumberofbytessent,
  DWORD            dwflags,
  const struct     sockaddr FAR *lpto,
  int              itolen,
  LPWSAOVERLAPPED  lpoverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE  lpcompletionroutine,
  LPWSATHREADID    lpthreadid,
  LPINT            lperrno)
{

 struct sockaddr_in sin;

 sin=*(const struct sockaddr_in *)lpto;
 if(sin.sin_port==htons(8000))        
 {
  OutputDebugString(_T("WSPSendTo Tencent Filtered"));
  return 0;
 }
 else
 {
  return nextproctable.lpWSPSendTo(s,lpbuffer,dwbuffercount,
   lpnumberofbytessent,dwflags,lpto,itolen,
   lpoverlapped,lpcompletionroutine,lpthreadid,lperrno);
 }
}

int WSPAPI WSPStartup(
WORDwversionrequested,
LPWSPDATA         lpwspdata,
LPWSAPROTOCOL_INFOWlpprotoinfo,
WSPUPCALLTABLEupcalltable,
LPWSPPROC_TABLElpproctable
)
{
 OutputDebugString(_T("IPFilter WSPStartup ..."));

 int           i;
 int           errorcode; 
 int           filterpathlen;
 DWORD         layerid=0; 
 DWORD         nextlayerid=0;
 TCHAR         *filterpath;
 HINSTANCE     hfilter;
 LPWSPSTARTUP  wspstartupfunc=NULL;

 if(lpprotoinfo->ProtocolChain.ChainLen<=1)
 {
  OutputDebugString(_T("ChainLen<=1"));  
  return FALSE;
 }

 getfilter();

 for(i=0;iProtocolChain.ChainLen;i++)
 {
  if(lpprotoinfo->ProtocolChain.ChainEntries[i]==layerid)
  {
   nextlayerid=lpprotoinfo->ProtocolChain.ChainEntries[i+1];
   break;
  }
 }

 filterpathlen=MAX_PATH;
 filterpath=(TCHAR*)GlobalAlloc(GPTR,filterpathlen);  
 for(i=0;ilpWSPSendTo=WSPSendTo;

 freefilter();
 return 0;
}
(全文完)
阅读(1589) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~