关闭

关闭

关闭

封号提示

内容

首页 NDIS协议驱动开发.pdf

NDIS协议驱动开发.pdf

NDIS协议驱动开发.pdf

上传者: drui118 2010-12-18 评分 0 0 0 0 0 0 暂无简介 简介 举报

简介:本文档为《NDIS协议驱动开发pdf》,可适用于IT/计算机领域,主题内容包含NDIS协议驱动开发协议驱动开发协议驱动开发协议驱动开发ByboywhpEmail:boywhpcom一一一一、、、、Windows网络网络网络网络符等。

NDIS协议驱动开发协议驱动开发协议驱动开发协议驱动开发ByboywhpEmail:boywhpcom一一一一、、、、Windows网络网络网络网络NDIS接口模型接口模型接口模型接口模型协议驱动直接建立同下层微端口数据接口通信可以直接处理底层网卡数据。下层的中间层驱动主要提供一个数据过滤功能对上层协议驱动提供微端口接口对下层NIC微端口驱动提供协议驱动接口。下层的微端口驱动是网卡厂家提供的网卡驱动(基于NDIS框架非标准Windows驱动)具体的细节参考NDIS框架资料。开发虚拟网卡以及防火墙推荐NDIS中间层只是收发数据推荐在协议层驱动下面是协议层驱动数据收发模型。NDIS协议驱动NDIS中间层驱动NDIS微端口驱动TDI传输驱动接口TCPIPTCPWANmyprotocolNICNICNIC其中的NIC、NIC、NIC就是你机器上的网卡可以是微端口驱动也可能是中间层虚拟的一个NIC通常就是你在IPConfigALL下看到的网络适配器。一个协议驱动可以和几个网卡建立绑定。具体和哪个网卡绑定决定于协议驱动的需要。我们的协议驱动和底层的NIC直接交互通常系统已经自带了TCPIP、TCPWAN等协议驱动我们的驱动和这些是平行的关系。一个NIC接受到数据后会查询其所有绑定的协议驱动链依次调用协议驱动注册的收包函数。因此使用协议驱动来做防火墙并不合适因为你并不能阻止TCPIP的收发数据但是嗅探数据是可以的实际上winpcap就是基于标准的NDIS协议驱动写的。另外还有一点就是尽管上层的socket接口以及应用如何变化协议层看到的只是一个个的NDISPakcet收发请求每个NDISPacket对应一个网络数据包。具体的数据包格式参考TcpIp详解。数据包的组包、校验以及重传等功能在上层处理。协议层驱动只是把收的网络数据包缓冲起来等待上层读取并将上层的数据包写入到下层的网卡因此写协议驱动真是很轻松的事情。二二二二、、、、协议驱动编程协议驱动编程协议驱动编程协议驱动编程在没有熟悉协议驱动编程前可以很多人会很茫然不知道如何下手真正开发一个协议驱动这里我强烈推荐DDK里面的packet实例使用SourceInsight来看(强烈不推荐VC)我就是这样入门的当然我悟性也许比你低那么一点点。另外参考文章就不要找了找也找不到的尤其是中文的我这个就是目前最好的文档了自己看DDK会有点收获的另外我假定你对内核编程有一个基本了解入门请参考楚狂人的文件过滤驱动文档。((((一一一一))))初始化初始化初始化初始化DriverEntryWindowsNDIS内部维护了一个协议数据结构的双向链表我们当然得把我们的协议驱动挂上去先同时该数据结构包含了一系列函数指针需要我们指定代码在packetc里。填写NDISPROTOCOLCHARACTERISTICS数据结构NdisZeroMemory(protocolChar,sizeof(NDISPROTOCOLCHARACTERISTICS))protocolCharMajorNdisVersion=protocolCharMinorNdisVersion=protocolCharName=protoNameprotocolCharOpenAdapterCompleteHandler=PacketOpenAdapterCompleteprotocolCharCloseAdapterCompleteHandler=PacketCloseAdapterCompleteprotocolCharSendCompleteHandler=PacketSendCompleteprotocolCharTransferDataCompleteHandler=PacketTransferDataCompleteprotocolCharResetCompleteHandler=PacketResetCompleteprotocolCharRequestCompleteHandler=PacketRequestCompleteprotocolCharReceiveHandler=PacketReceiveIndicateprotocolCharReceiveCompleteHandler=PacketReceiveCompleteprotocolCharStatusHandler=PacketStatusprotocolCharStatusCompleteHandler=PacketStatusCompleteprotocolCharBindAdapterHandler=PacketBindAdapterprotocolCharUnbindAdapterHandler=PacketUnbindAdapterprotocolCharUnloadHandler=protocolCharReceivePacketHandler=PacketReceivePacketprotocolCharPnPEventHandler=PacketPNPHandler我下面加载次序依次讲解各个函数首先是NdisRegisterProtocolNdisRegisterProtocol(status,GlobalsNdisProtocolHandle,protocolChar,sizeof(NDISPROTOCOLCHARACTERISTICS))如果调用成功会返回一个协议NDISPROTOCOLBLOCK的数据指针(微软称为NdisProtocolHandle)实际上他是一个注册协议驱动单链表首指针。通过该指针即可遍历所以系统安装的协议实现一些邪恶的用途鉴于其邪恶性故不给出代码微软也不推荐该做法。当然你还要规规矩矩的填写DriverObject>MajorFunction等回调函数好在Packet代码帮我们做了。正常情况下如果你使用了标准的官方做法(使用标准的inf安装文件并添加协议驱动到网卡)系统会在你注册协议驱动后调用BindAdapterHandler也就是你的PacketBindAdapter函数但是很多不喜欢驱动签名且不喜欢inf安装并邪恶的人发明了直接在DriverEntry里面使用NdisOpenAdpater直接打开网卡的方法DDK对此的原文描述如下:CommentsAprotocoldrivercallsNdisOpenAdapterNdisOpenAdapterNdisOpenAdapterNdisOpenAdapterfromitsProtocolBindAdapterfunctionNDISnolongersupportscallingNdisOpenAdapterNdisOpenAdapterNdisOpenAdapterNdisOpenAdapterfromtheDriverEntryDriverEntryDriverEntryDriverEntryfunction,whichwasanoptionavailabletolegacy(V)protocolsNDISnolongersupportsVprotocolsNDISfailsanyattempttocallNdisOpenAdapterNdisOpenAdapterNdisOpenAdapterNdisOpenAdapteroutsidethecontextofProtocolBindAdapter下面公布我测试的结果:windows可以正常打开但是好像不怎么稳定有时能够正常截获数据有时不行XP和Windows都不怎么好用。((((二二二二))))绑定网卡绑定网卡绑定网卡绑定网卡PacketBindAdapterPacketBindAdapter(OUTPNDISSTATUSStatus,INNDISHANDLEBindContext,INPNDISSTRINGDeviceName,INPVOIDSystemSpecific,INPVOIDSystemSpecific)由于有多块网卡所以一般情况下在此为每个网卡创建一个Device初始化一些数据并打开下层适配器官方做法如下:首先为每个网卡创建一个设备在该设备扩展DeviceExtension中指明一个OPENINSTANCE自定义数据结构该数据结构标识了一个我们协议驱动打开的下层网卡所需数据当然你也可以不记录。然后就是打开下层适配器了用的参数主要就是一个DeviceName好像是对应注册表里面一项网卡的唯一标识吧。唯一要说明的就是ProtocolBindingContext:VOIDNdisOpenAdapter(OUTPNDISSTATUSStatus,OUTPNDISSTATUSOpenErrorStatus,OUTPNDISHANDLENdisBindingHandle,OUTPUINTSelectedMediumIndex,INPNDISMEDIUMMediumArray,INUINTMediumArraySize,INNDISHANDLENdisProtocolHandle,INNDISHANDLEProtocolBindingContext,INPNDISSTRINGAdapterName,INUINTOpenOptions,INPSTRINGAddressingInformationOPTIONAL)该参数会在以后的收发数据里面经常出现且是用户自定义的只能在打开适配器的时候指定以后在数据操作时候就可以通过该参数来得知是那个网卡来的数据通常情况下我们需要将该参数指定为上述OPENINSTANCE数据结构((((三三三三))))打开网卡完成打开网卡完成打开网卡完成打开网卡完成PacketOpenAdapterComplete这里没有什么好处理的packet代码就是简单是设置了一个事件以便通知打开操作完成。你也可以在此时完成设置网卡过滤模式这一点很重要否则你不会接受到什么数据。但是在设置网卡过滤模式上要注意其操作是异步完成的如下代码是会BSOD。NDISSTATUSstatusULONGMode=NDISPACKETTYPEDIRECTEDINTERNALREQUESTRequestif(Request){RequestIrp=RequestRequestRequestType=NdisRequestSetInformationRequestRequestDATASETINFORMATIONOid=OIDGENCURRENTPACKETFILTERRequestRequestDATASETINFORMATIONInformationBuffer=ModeRequestRequestDATASETINFORMATIONInformationBufferLength=sizeof(ULONG)NdisRequest(status,open>AdapterHandle,RequestRequest)}原因就是因为Request是一个局部变量而NdisRequest是异步操作的不会等待操作完成才返回。因此导致NdisRequest传递的指针会提前被释放。最后PacketRequestComplete得到的是一个实际不存在的指针导致BSOD。解决的办法很简单就是申请一块全局内存。并且记得在PacketRequestComplete中记得释放就行了。NDISSTATUSstatusULONGMode=NDISPACKETTYPEDIRECTEDPINTERNALREQUESTpRequest=注意NdisRequest是异步操作不能使用局部变量!pRequest申请的内存会在RequestComplete中释放pRequest=ExAllocatePool(NonPagedPool,sizeof(INTERNALREQUEST))if(pRequest){pRequest>Irp=pRequest>RequestRequestType=NdisRequestSetInformationpRequest>RequestDATASETINFORMATIONOid=OIDGENCURRENTPACKETFILTERpRequest>RequestDATASETINFORMATIONInformationBuffer=ModepRequest>RequestDATASETINFORMATIONInformationBufferLength=sizeof(ULONG)NdisRequest(status,open>AdapterHandle,pRequest>Request)}PacketRequestComplete函数在NDISPROTOCOLCHARACTERISTICS指定你可以参考Packetc的代码。((((四四四四))))接受到网络数据包接受到网络数据包接受到网络数据包接受到网络数据包主要有两个个函数:ReceiveHandler、ReceivePacketHandler都是在NDISPROTOCOLCHARACTERISTICS时候指定这两个回调函数有一点点不同通常教新的网卡会支持后者因为效率更加高一点前者一般在教老的windows下常见一些参数也复杂一点。但是很不幸的是你要同时支持这两种数据接收函数我们先从简单的ReceivePacket入手。PacketReceivePacket(INNDISHANDLEProtocolBindingContext,INPNDISPACKETPacket)第一个参数就是我们NdisOpenAdapter时候时候指定的自定义数据(一个POPENINSTANCE数据结构)第二个参数就是我们要接受的NDISPacket了里面含有我们需要的网络数据包。现在的问题是我们要怎么把这个包收下来一般情况下这个包是临时的你别指望把这个指针记录下来等过完年再收这个包。我们必须自己创建一块内存区把包给缓冲起来这一点是很重要的DDK的Packet收包比较彪悍。如果你不怕丢包就用他的代码否则老老实实的自己管理收包。我的方案是维护一个NdisRecvPackets队列每接受到一个数据包就是进入队列用户端负责读取数据并清队列中的包。思路比较简单直接show代码。UINTbytesTransfered=UINTsizePNDISPACKETmyPacket=PVOIDvirtualAddress=virtualAddress=SafeCopyNdisPacket(myPacket,Packet,(POPENINSTANCE)ProtocolBindingContext,size)if(virtualAddress){POPENINSTANCEopen=(POPENINSTANCE)ProtocolBindingContextExInterlockedInsertTailList(open>RcvPackets,RESERVED(Packet)>ListElement,open>RcvPacksLock)}其中SafeCopyNdisPacket是我写的一个复制NDIS包的函数。他创建一个NdisPacket并Copy指定的Packet数据到创建好的数据包。下面简单的讲解一个NdisPacket的创建过程:、首先你得通过NdisAllocatePacketPool创建一个包缓冲池通常在OpenAdapterComplete中已经创建好了包缓冲池。、通过NdisAllocatePacket向包缓冲池中申请一个NdisPacket。、通过NdisAllocateMemory向系统申请一块指定大小的内存地址空间。、通过NdisAllocateBuffer创建一个缓冲描述符映射到上述内存地址。、通过NdisChainBufferAtFront将该缓冲描述符添加到NdisPacket链的首部。另外得到一个NdisPacket后通过NdisQueryBuffer以及NdisGetNextBuffer即可得到所有Ndis包中的数据具体细节见代码就是简单的照样画老虎了纯体力活。PVOIDSafeCopyNdisPacket(OUTPNDISPACKET*dstpacket,INPNDISPACKETsrcpacket,INPOPENINSTANCEopen,OUTUINT*pSize)**创建一个线性内存NDISPacket,并复制srcpacket的数据*{UINTbytesTransfered=UINTpacketLengthPNDISPACKETmyPacketNDISSTATUSstatusPVOIDvirtualAddress=NdisQueryPacket(srcpacket,,,,packetLength)virtualAddress=BuildNdisPacket(myPacket,open,packetLength)if(virtualAddress==){DebugPrint(("BuildNdisPacketFaild!n"))return}FollowingblockofcodelocksthedestinationpacketMDLsinasafemannerThisisatemporaryworkaroundforNdisCopyFromPacketToPacketthatcurrentlydoesn'tusesafefunctionstolockpagesofMDLThisisrequiredtopreventsystemfrombugcheckingunderlowmemoryresources备注:后来的DDK版本直接提供了NdisCopyFromPacketToPacketSafe实现{PVOIDvirtualAddressPNDISBUFFERfirstBuffer,nextBufferULONGtotalLengthNdisQueryPacket(srcpacket,,,firstBuffer,totalLength)while(firstBuffer!=){NdisQueryBufferSafe(firstBuffer,virtualAddress,totalLength,NormalPagePriority)if(!virtualAddress){SystemisrunninglowonmemoryresourcesSofailthereadstatus=STATUSINSUFFICIENTRESOURCESgotoCleanExit}NdisGetNextBuffer(firstBuffer,nextBuffer)firstBuffer=nextBuffer}}NdisCopyFromPacketToPacket(myPacket,,packetLength,srcpacket,,bytesTransfered)if(bytesTransfered>){*pSize=bytesTransfered*dstpacket=myPacketreturnvirtualAddress}CleanExit:FreeNdisPacket(myPacket)return}PVOIDBuildNdisPacket(INOUTPNDISPACKET*pPacket,INPOPENINSTANCEopen,INULONGsize){PVOIDvirtualAddressPNDISPACKETmyPacketPNDISBUFFERbufferNDISPHYSICALADDRESShighestAcceptableAddress={,}NDISSTATUSstatus=NDISSTATUSSUCCESSNdisAllocatePacket(status,myPacket,open>PacketPool)if(status!=NDISSTATUSSUCCESS){DebugPrint(("BuildPacketNofreepacketsn"))return}status=NdisAllocateMemory(virtualAddress,size,,highestAcceptableAddress)if(status!=NDISSTATUSSUCCESS){DebugPrint(("BuildPacketAllocateMemoryFailedn"))NdisFreePacket(*pPacket)return}NdisAllocateBuffer(status,buffer,open>BufferPool,virtualAddress,size)if(status!=NDISSTATUSSUCCESS){DebugPrint(("BuildPacketAllocateBufferFailedn"))NdisFreeMemory(virtualAddress,size,)NdisFreePacket(myPacket)return}NdisChainBufferAtFront(myPacket,buffer)*pPacket=myPacketreturnvirtualAddress}下面我们看ReceiveHandler的处理NDISSTATUSPacketReceiveIndicate(INNDISHANDLEProtocolBindingContext,INNDISHANDLEMacReceiveContext,INPVOIDHeaderBuffer,INUINTHeaderBufferSize,INPVOIDLookAheadBuffer,INUINTLookaheadBufferSize,INUINTPacketSize)第一个参数不解释了第二个参数也不解释了因为我不清楚用不上。HeaderBuffer指向一个以太帧格式自己看TcpIp详解HeaderBufferSize一般就是了至少我没有见过其他大小的。LookAHeadBuffer指向剩下的数据LookAHeadBufferSize是其数据大小PacketSize是后面实际的数据大小。一般情况下PacketSize<=LookAHeadBufferSize这是最理想的情况说明我们的数据包完整的存在于LookAHeadBuffer中否则就麻烦一点但是对于我们优秀的程序员来说也不过是小菜了好在我见过的绝大多数情况都是正常情况。PVOIDvirtualAddressNDISSTATUSstatus=NDISSTATUSSUCCESSUINTbytesTransfered=ULONGbufferLength=HeaderBufferSizePacketSizePVOIDpacketVA=if(HeaderBufferSize>ETHERNETHEADERLENGTH)returnvirtualAddress=BuildNdisPacket(pPacket,open,bufferLength)if(!virtualAddress)returnif(PacketSize<=LookaheadBufferSize){NdisMoveMappedMemory((PUCHAR)virtualAddress,HeaderBuffer,ETHERNETHEADERLENGTH)NdisMoveMappedMemory((PUCHAR)virtualAddressETHERNETHEADERLENGTH,LookAheadBuffer,PacketSize)returnvirtualAddress}Else处理BT情况BT情况下需要我们自己调用NdisTransferData通知下层网卡将数据传输上来到VOIDNdisTransferData(OUTPNDISSTATUSStatus,INNDISHANDLENdisBindingHandle,INNDISHANDLEMacReceiveContext,INUINTByteOffset,INUINTBytesToTransfer,INOUTPNDISPACKETPacket,OUTPUINTBytesTransferred)请注意其中的ByteOffset我以为可以指定Copy的首地址先创建一个数据包首先把以太帧填充到前个字节然后指定BytesOffset为调用NdisTransferData天真的认为Ndis复制后面的数据到以后的Packet缓冲中。但是得到的结果是函数失败:)没办法了只好在拷贝的时候先把以太帧复制到包的最后面然后调用当然在PacketTransferDataComplete记得要将以太头移到前面来虽然方法猥琐了点但是凑合着用吧。NdisMoveMappedMemory((PUCHAR)virtualAddressbufferLengthETHERNETHEADERLENGTH,HeaderBuffer,ETHERNETHEADERLENGTH)调用NdisTransferData将剩下的数据传上来IoIncrement(open)NdisTransferData(status,open>AdapterHandle,MacReceiveContext,,PacketSize,*pPacket,bytesTransfered)KdPrint(("NdisTransferDataSTATUSxn",status))if(status==NDISSTATUSPENDING){returnNDISSTATUSSUCCESS}PacketTransferDataComplete(open,*pPacket,status,bytesTransfered)returnNDISSTATUSSUCCESS((((五五五五))))完成数据传输完成数据传输完成数据传输完成数据传输PacketTransferDataComplete该函数同样是在注册协议的时候指定这里我们只需将数据包简单的重组一下就可以了先创建一个NdisPacket然后将数据Copy过来即可记得先拷贝后面字节的以太头然后再拷贝后面的实际数据并释放IO计数器原型如下。PacketTransferDataComplete(INNDISHANDLEProtocolBindingContext,INPNDISPACKETpPacket,INNDISSTATUSStatus,INUINTBytesTransfered)注意其中的pPacket就是我们调用NdisTransferData指定的数据包指针用完后一定要记得释放!另外关于IO计数器我还要讲几句。Packet里面进行一个异步数据操作时候一般会先调用一下IoIncrement函数此函数简单的维护一个计数器你可以把他理解为COM里面的引用计数器。每发出一个异步IO调用时候Packet将计数器加一在数据操作完成后再将计数器减一。感觉很无趣吧:)哈哈!这样做主要是为了在驱动释放资源的时候它会等待计数器清空否则你将有可能在卸载驱动时候BSOD因为你把一些资源Free掉后Ndis可能出现一个完成事件结果调用你的完成函数就容易出现意外总之安全起见还是遵守这个原则吧毕竟人家微软程序员也这么干。((((六六六六))))应用程序读数据应用程序读数据应用程序读数据应用程序读数据PacketRead操作操作操作操作NTSTATUSPacketRead(INPDEVICEOBJECTDeviceObject,INPIRPIrp)注意DeviceObject扩展数据指向的是一个POPENINSTATNCE数据指针Irp里面有用户程序的缓冲区大小以及指针。我们把NDIS接受数据包队列里面的数据包复制到Irp里面的缓冲区并释放队列。remainSize=irpSp>ParametersReadLengthpBuffer=MmGetSystemAddressForMdlSafe(Irp>MdlAddress,NormalPagePriority)do{packListEntry=ExInterlockedRemoveHeadList(open>RcvPackets,open>RcvPacksLock)if(packListEntry!=){PNDISBUFFERfirstBufferPVOIDpPacketVAddressUINTpackSize首先得到当前NDISPACKET指针reserved=CONTAININGRECORD(packListEntry,PACKETRESERVED,ListElement)pPacket=CONTAININGRECORD(reserved,NDISPACKET,ProtocolReserved)注意:、收包的时候一个NdisPacket只创建一个Buffer、用户缓冲至少可以完整收一个NdisPacket包NdisQueryPacket(pPacket,,,firstBuffer,)NdisQueryBuffer(firstBuffer,pPacketVAddress,packSize)if(packSize<=remainSize){NdisMoveMemory((PUCHAR)pBuffer,(PUCHAR)pPacketVAddress,packSize)(PUCHAR)pBuffer=packSizeremainSize=packSizetotalSize=packSizeFreeNdisPacket(pPacket)}else{重新加入链表ExInterlockedInsertHeadList(open>RcvPackets,RESERVED(pPacket)>ListElement,open>RcvPacksLock)break}}}while(packListEntryremainSize>)注意:这里处理数据如果缓冲区不足一个NdisPacket的大小我重新将包放入队列代码很简单别问我为什么因为彪悍的代码不需要注释。((((七七七七))))数据包发送数据包发送数据包发送数据包发送PacketWriteNTSTATUSPacketWrite(INPDEVICEOBJECTDeviceObject,INPIRPIrp)太简单了但是有始有终吧先创建一个NdisPacket然后将Irp数据复制过去呵呵!其实不是啦可以直接用NdisChainBufferAtFront把Irp的MdlAddress链接到Ndispacket上去记得处理SendComplete(该函数也是在注册协议驱动的时候指定)就可以了。不过好在Packet代码貌似处理很好我基本上不用改了自己看看吧无非就是设置Irp完成释放NdisPacketIO计数器减一下面我直接show微软的代码。PacketWrite代码代码代码代码NdisAllocatePacket(Status,pPacket,open>PacketPool)if(Status!=NDISSTATUSSUCCESS){Irp>IoStatusStatus=STATUSINSUFFICIENTRESOURCESIoCompleteRequest(Irp,IONOINCREMENT)IoDecrement(open)returnSTATUSINSUFFICIENTRESOURCES}RESERVED(pPacket)>Irp=IrpNdisChainBufferAtFront(pPacket,Irp>MdlAddress)IoMarkIrpPending(Irp)NdisSend(Status,open>AdapterHandle,pPacket)if(Status!=NDISSTATUSPENDING){PacketSendComplete(open,pPacket,Status)}PacketSendComplete代码代码代码代码irp=RESERVED(pPacket)>Irpif(irp==){该NdisPacket是转发的包FreeNdisPacket(pPacket)IoDecrement((POPENINSTANCE)ProtocolBindingContext)return}irpSp=IoGetCurrentIrpStackLocation(irp)NdisFreePacket(pPacket)if(Status==NDISSTATUSSUCCESS){irp>IoStatusInformation=irpSp>ParametersWriteLengthirp>IoStatusStatus=STATUSSUCCESS}else{irp>IoStatusInformation=irp>IoStatusStatus=STATUSUNSUCCESSFUL}IoCompleteRequest(irp,IONOINCREMENT)IoDecrement((POPENINSTANCE)ProtocolBindingContext)((((八八八八))))结束语结束语结束语结束语很高兴你还能认真的看到这里希望能够对你有点帮助我之所以花一天的时间专门来写一个开发文档就是因为我自己在开发的时候走了太多的弯路了中文资料实在是少了点尤其是涉及到一些细节更是如此。另外我没有附完整的代码是因为我的代码不是正统的有点邪恶:)DDK里面的Packet代码才是而且自己动手才能体会到其中的乐趣如果你不能从中享受到乐趣那还是不要搞开发。另外希望看到我Email:boywhpcom的MM不要太兴奋因为这个Email估计有年之久了昔日的boy早已是人老珠黄了要发manwhpcom:)这个也是我的邮箱希望你能接受。Byboywhp广州

用户评论(0)

0/200

精彩专题

上传我的资料

每篇奖励 +2积分

资料评价:

/18
0下载券 下载 加入VIP, 送下载券

意见
反馈

立即扫码关注

爱问共享资料微信公众号

返回
顶部