sulf8510 | 2008-12-12 10:37 | 手把手教你玩转ARP包
首先要感谢网络安全资深专家卢湖川博士以及VC网络版的limin朋友提供的资料以及帮助^_^ 经常看到论坛有人问起关于数据包的截获、分析等问题,幸好本人也对此略有所知,所以就想写一系列的文章来详细深入的探讨关于数据包的知识,,我希望通过这一系列的文章,能使得关于数据包的知识得以普及,所以这系列的每一篇文章我都会有由浅入深的解释、详细的分析、以及编码步骤,另外附上带有详细注释的源码(为了照顾大多数朋友,我提供的都是MFC的源码)。 不过由于也是初学者,疏漏之处还望不吝指正。 本文凝聚着笔者心血,如要转载,请指明原作者及出处,谢谢!^_^ OK,. Let’s go ! Have fun!! q^_^p 第一篇 手把手教你玩转ARP包 目录: 一. 关于ARP协议的基础知识 1. ARP的工作原理 2.ARP包的格式 3.ARP包的填充 二。发送数据包的编程实现 1.填充数据包 2.发送数据包 三。一些附加步骤及说明 1.如果在VC中使用winpcap 2.获得网卡信息列表 3.获得系统ARP信息列表 四。ARP包的游戏 1.小伎俩 2.ARP欺骗的实现 3.基于ARP欺骗的*****原理 作者: CSDN VC/MFC 网络编程 PiggyXP ^_^ 一. 关于ARP协议的基础知识 1.ARP的工作原理 本来我不想在此重复那些遍地都是的关于ARP的基本常识,但是为了保持文章的完整性以及照顾初学者,我就再啰嗦一些文字吧,资深读者可以直接跳过此节。 我们都知道以太网设备比如网卡都有自己全球唯一的MAC地址,它们是以MAC地址来传输以太网数据包的,但是它们却识别不了我们IP包中的IP地址,所以我们在以太网中进行IP通信的时候就需要一个协议来建立IP地址与MAC地址的对应关系,以使IP数据包能发到一个确定的地方去。这就是ARP(Address Resolution Protocol,地址解析协议)。 讲到此处,我们可以在命令行窗口中,输入 arp –a 来看一下效果,类似于这样的条目 210.118.45.100 00-0b-5f-e6-c5-d7 dynamic 就是我们电脑里存储的关于IP地址与MAC地址的对应关系,dynamic表示是临时存储在ARP缓存中的条目,过一段时间就会超时被删除(xp/2003系统是2分钟)。 这样一来,比如我们的电脑要和一台机器比如210.118.45.1通信的时候,它会首先去检查arp缓存,查找是否有对应的arp条目,如果没有,它就会给这个以太网络发ARP请求包广播询问210.118.45.1的对应MAC地址,当然,网络中每台电脑都会收到这个请求包,但是它们发现210.118.45.1并非自己,就不会做出相应,而210.118.45.1就会给我们的电脑回复一个ARP应答包,告诉我们它的MAC地址是xx-xx-xx-xx-xx-xx,于是我们电脑的ARP缓存就会相应刷新,多了这么一条: 210.118.45.1 xx-xx-xx-xx-xx-xx dynamic 为什么要有这么一个ARP缓存呢,试想一下如果没有缓存,我们每发一个IP包都要发个广播查询地址,岂不是又浪费带宽又浪费资源? 而且我们的网络设备是无法识别ARP包的真伪的,如果我们按照ARP的格式来发送数据包,只要信息有效计算机就会根据包中的内容做相应的反应. 试想一下,如果我们按照ARP响应包的相应的内容来刷新自己的ARP缓存中的列表,嘿嘿,那我们岂不是可以根据这点在没有安全防范的网络中玩些ARP包的小把戏了?在后面的文章里我就手把手来教你们如何填充发送ARP包,不过先别急,我们再继续学点基础知识^_^ 2.ARP包的格式 既然我们要来做一个我们自己的ARP包,当然首先要学习一下ARP包的格式。 从网络底层看来,一个ARP包是分为两个部分的,前面一个是物理帧头,后面一个才是ARP帧。 首先,物理帧头,它将存在于任何一个协议数据包的前面,我们称之为DLC Header,因为这个帧头是在数据链路层构造的,并且其主要内容为收发双方的物理地址,以便硬件设备识别。 DLC Header 字段 长度(Byte) 默认值 备注 接收方MAC 6 广播时,为 ff-ff-ff-ff-ff-ff 发送方MAC 6 Ethertype 2 0x0806 0x0806是ARP帧的类型值 图1 物理帧头格式 图1是需要我们填充的物理帧头的格式,我们可以看到需要我们填充的仅仅是发送端和接收端的物理地址罢了,是不是很简单呢? 接下来我们看一下ARP帧的格式. ARP Frame 字段 长度(Byte) 默认值 备注 硬件类型 2 0x1 以太网类型值 上层协议类型 2 0x0800 上层协议为IP协议 MAC地址长度 1 0x6 以太网MAC地址长度为 6 IP地址长度 1 0x4 IP地址长度为 4 操作码 2 0x1表示ARP请求包,0x2表示应答包 发送方MAC 6 发送方IP 4 接收方MAC 6 接收方IP 4 填充数据 18 因为物理帧最小长度为64字节,前面的42字节再加上4个CRC校验字节,还差18个字节 图2 ARP帧格式 我们可以看到需要我们填充的同样也只是MAC,IP,再加上一个1或2的操作码而已。 3.ARP包的填充 1) 请求包的填充: 比如我们的电脑MAC地址为 aa-aa-aa-aa-aa-aa,IP为 192.168.0.1 我们想要查询 192.168.0.99的MAC地址,应该怎么来做呢? 首先填充DLC Header,通过前面的学习我们知道,想要知道某个计算机对应的MAC地址是要给全网发送广播的,所以接收方MAC肯定是 ffffffffffff,发送方MAC当然是自己啦,于是我们的DLC Header就填充完成了,如图,加粗的是我们要手动输入的值(当然我编的程序比较智能,会根据你选择的ARP包类型帮你自动填入一些字段,你一用便知^_^)。 DLC Header 字段 长度(Byte) 填充值 接收方MAC 6 ffffffffffff 发送方MAC 6 aaaaaaaaaaaa Ethertype 2 0x0806 图3 ARP请求包中 DLC Header内容 接下来是ARP帧,请求包的操作码当然是 1,发送方的MAC以及IP当然填入我们自己的,然后要注意一下,这里的接收方IP填入我们要查询的那个IP地址,就是192.168.0.99了,而接收方MAC填入任意值就行,不起作用,于是,如图, ARP Frame 字段 长度(Byte) 填充值 硬件类型 2 1 上层协议类型 2 0800 MAC地址长度 1 6 IP地址长度 1 4 操作码 2 1 发送方MAC 6 aaaaaaaaaaaa 发送方IP 4 192.168.0.1 接收方MAC 6 任意值 xxxxxxxxxxxx 接收方IP 4 192.168.0.99 填充数据 18 0 图4 ARP请求包中 ARP帧的内容 如果我?****乖煲桓稣庋陌⑺统鋈ィ绻?192.168.0.99存在且是活动的,我们马上就会收到一个192.168.0.99发来的一个响应包,我们可以查看一下我们的ARP缓存列表,是不是多了一项类似这样的条目: 192.168.0.99 bb-bb-bb-bb-bb-bb 是不是很神奇呢? 我们再来看一下ARP响应包的构造 2) 响应包的填充 有了前面详细的解说,你肯定就能自己说出响应包的填充方法来了吧,所以我就不细说了,列两个表就好了 比如说给 192.168.0.99(MAC为 bb-bb-bb-bb-bb-bb)发一个ARP响应包,告诉它我们的MAC地址为 aa-aa-aa-aa-aa-aa,就是如此来填充各个字段 DLC Header 字段 长度(Byte) 填充值 接收方MAC 6 bbbbbbbbbbbb 发送方MAC 6 aaaaaaaaaaaa Ethertype 2 0x0806 图5 ARP响应包中 DLC Header内容 ARP Frame 字段 长度(Byte) 填充值 硬件类型 2 1 上层协议类型 2 0800 MAC地址长度 1 6 IP地址长度 1 4 操作码 2 2 发送方MAC 6 aaaaaaaaaaaa 发送方IP 4 192.168.0.1 接收方MAC 6 bbbbbbbbbbbb 接收方IP 4 192.168.0.99 填充数据 18 0 图6 ARP响应包中 ARP帧的内容 这样192.168.0.99的ARP缓存中就会多了一条关于我们192.168.0.1的地址映射。 好了,终于到了编程实现它的时候了^_^ 二. 发送ARP包的编程实现 1. 填充数据包 上面的那些关于ARP包各个字段的表格,对应在程序里就是结构体,对应于上面的表格,于是我们需要三个下面这样的结构体 // DLC Header typedef struct tagDLCHeader { unsigned char DesMAC[6]; /* destination HW addrress */ unsigned char SrcMAC[6]; /* source HW addresss */ unsigned short Ethertype; /* ethernet type */ } DLCHEADER, *PDLCHEADER; // ARP Frame typedef struct tagARPFrame { unsigned short HW_Type; /* hardware address */ unsigned short Prot_Type; /* protocol address */ unsigned char HW_Addr_Len; /* length of hardware address */ unsigned char Prot_Addr_Len; /* length of protocol address */ unsigned short Opcode; /* ARP/RARP */ unsigned char Send_HW_Addr[6]; /* sender hardware address */ unsigned long Send_Prot_Addr; /* sender protocol address */ unsigned char Targ_HW_Addr[6]; /* target hardware address */ unsigned long Targ_Prot_Addr; /* target protocol address */ unsigned char padding[18]; } ARPFRAME, *PARPFRAME; // ARP Packet = DLC header + ARP Frame typedef struct tagARPPacket { DLCHEADER dlcHeader; ARPFRAME arpFrame; } ARPPACKET, *PARPPACKET; 这些结构体一定能看懂吧,在程序中就是对号入座就好了 1. 填充数据包 下面我举个填充包头的例子,我首先定义个了一个转换字符的函数,如下 /**************************************************************************** * Name & Params:: * formatStrToMAC * ( * c*****t LPSTR lpHWAddrStr : 用户输入的MAC地址字符串 * unsigned char *HWAddr : 返回的MAC地址字符串(赋给数据包结构体) * ) * Purpose: * 将用户输入的MAC地址字符转成数据包结构体需要的格式 ****************************************************************************/ void formatStrToMAC(c*****t LPSTR lpHWAddrStr, unsigned char *HWAddr) { unsigned int i, index = 0, value, temp; unsigned char c; _strlwr(lpHWAddrStr); // 转换成小写 for (i = 0; i { c = *(lpHWAddrStr + i); if (( c>='0' && c='a' && c { if (c>='0' && c if (c>='a' && c if ( (index % 2) == 1 ) { value = value*0x10 + temp; HWAddr[index/2] = value; } else value = temp; index++; } if (index == 12) break; } } // 开始填充各个字段 ARPPACKET ARPPacket; // 定义ARPPACKET结构体变量 memset(&ARPPacket, 0, sizeof(ARPPACKET)); // 数据包初始化 formatStrToMAC(“DLC源MAC字符串”,ARPPacket.dlcHeader.SrcMAC); // DLC帧头 formatStrToMAC(“DLC目的MAC字符串”,ARPPacket.dlcHeader.DesMAC); formatStrToMAC(“ARP源MAC字符串”,ARPPacket.arpFrame.Send_HW_Addr); // 源MAC ARPPacket.arpFrame.Send_Prot_Addr = inet_addr(srcIP); // 源IP formatStrToMAC(“ARP目的MAC字符串”,ARPPacket.arpFrame.Targ_HW_Addr); // 目的MAC ARPPacket.arpFrame.Targ_Prot_Addr = inet_addr(desIP); // 目的IP ARPPacket.arpFrame.Opcode = ht*****((unsigned short)arpType); // arp包类型 // 自动填充的常量 ARPPacket.dlcHeader.Ethertype = ht*****((unsigned short)0x0806); // DLC Header的以太网类型 ARPPacket.arpFrame.HW_Type = ht*****((unsigned short)1); // 硬件类型 ARPPacket.arpFrame.Prot_Type = ht*****((unsigned short)0x0800); // 上层协议类型 ARPPacket.arpFrame.HW_Addr_Len = (unsigned char)6; // MAC地址长度 ARPPacket.arpFrame.Prot_Addr_Len = (unsigned char)4; // IP地址长度 That’s all ! ^_^ 填充完毕之后,我们需要做的就是把我们的ARPPACKET结构体发送出去 2.发送ARP数据包: 我们发送ARP包就要用到winpcap的api了,具体步骤及函数是这样的,为了简单易懂,我把错误处理的地方都去掉了,详见代码 /********************************************************************** * Name & Params:: * SendARPPacket() * Purpose: * 发送ARP数据包 * Remarks: * 用的是winpcap的api函数 ***********************************************************************/ void SendARPPacket() { char *AdapterDeviceName =GetCurAdapterName(); // 首先获得获得网卡名字 lpAdapter = PacketOpenAdapter(AdapterDeviceName); // 根据网卡名字打开网卡 lpPacket = PacketAllocatePacket(); // 给PACKET结构指针分配内存 PacketInitPacket(lpPacket, &ARPPacket, sizeof(ARPPacket)); //初始化PACKET结构指针 // 其中的ARPPacket就是我们先前填充的ARP包 PacketSetNumWrites(lpAdapter, 1); // 每次只发送一个包 PacketSendPacket(lpAdapter, lpPacket, true) // Send !!!!! ^_^ PacketFreePacket(lpPacket); // 释放资源 PacketCloseAdapter(lpAdapter); } 呵呵,至此,关于ARP包最关键的部分就讲完了,你现在就可以来随心所欲的发送自己的ARP包了 既然作为一篇“科普文章”,接下来我再讲一讲与整个项目有关的附加步骤以及说明 三.附加步骤以及说明 1. 如何在VC中使用winpcap驱动 虽然winpcap开发包使用起来非常简便,但是前期准备工作还是要费一番功夫的,缺一不可。^_^ 首先就是要安装它的驱动程序了,可以到它的主页下载,更新很快的 http://winpcap.polito.it/install/default.htm 下载WinPcap auto-installer (driver +DLLs),直接安装就好了,或者我提供的代码包里面也有。 希望以后用winpcap作开发的朋友,还需要下载 Developer's pack,解压即可。 然后,需要设置我?****こ痰母郊影柯嘉颐窍略谼eveloper's pack开发包的Inclulde目录,连接器的附加依赖库设置为Developer's pack的lib目录。 当然,因为我们的工作比较简单,就是借用winpcap发送数据包而已,所以只用从 winpcap开发包的include文件夹中,拷贝Packet32.h,到我们的工程来,并且包含它就可 以,但是要注意,Packet32.h本身还要包含一个Devioctl.h,也要一并拷贝进来,当然还有运 行库Packet.lib,一共就是需要拷贝3个文件了,如果加入库不用我多说了吧,在工程里面设 置,或者是在需要它的地方加入 #pragma comment(lib, "Packet.lib")了。 整个项目其实可以分为四个部分,填充数据包、发送数据包、枚举系统网卡列表和 相关信息以及枚举系统ARP缓存列表,下面我再讲一下如何获得系统的网卡以及ARP列 表,这两个部分都要用到IP Helper的api,所以要包含以及库文件Iphlpapi.lib, 其实都是很简单的,只用寥寥几行就OK了 2. 枚举系统网卡以及信息 最好是先定义关于网卡信息的一个结构体,这样显得结构比较清晰 // 网卡信息 typedef struct tagAdapterInfo { char szDeviceName[128]; // 名字 char szIPAddrStr[16]; // IP char szHWAddrStr[18]; // MAC DWORD dwIndex; // 编号 }INFO_ADAPTER, *PINFO_ADAPTER; /********************************************************************* * Name & Params:: * AddAdapInfoToList * ( * CListCtrl& list : CARPPlayerDlg传入的list句柄 * ) * Purpose: * 获得系统的网卡信息,并将其添加到list控件中 * Remarks: * 获得网卡IP及MAC用到了IpHelper api GetAdaptersInfo ******************************************************************/ void AddAdapInfoToList(CListCtrl& list) { char tempChar; ULONG uListSize=1; PIP_ADAPTER_INFO pAdapter; // 定义PIP_ADAPTER_INFO结构存储网卡信息 int nAdapterIndex = 0; DWORD dwRet = GetAdaptersInfo((PIP_ADAPTER_INFO)&tempChar, &uListSize);//关键函数 if (dwRet == ERROR_BUFFER_OVERFLOW) { PIP_ADAPTER_INFO pAdapterListBuffer = (PIP_ADAPTER_INFO)new(char[uListSize]); dwRet = GetAdaptersInfo(pAdapterListBuffer, &uListSize); if (dwRet == ERROR_SUCCESS) { pAdapter = pAdapterListBuffer; while (pAdapter) // 枚举网卡然后将相关条目添加到List中 { // 网卡名字 CString strTemp = pAdapter->AdapterName; strTemp = "//Device//NPF_" + strTemp; // 加上前缀 list.InsertItem(nAdapterIndex,strTemp); strcpy(AdapterList[nAdapterIndex].szDeviceName,strTemp); // IP strcpy(AdapterList[nAdapterIndex].szIPAddrStr, pAdapter->IpAddressList.IpAddress.String ); list.SetItemText(nAdapterIndex,1,AdapterList[nAdapterIndex].szIPAddrStr); // MAC formatMACToStr( AdapterList[nAdapterIndex].szHWAddrStr, pAdapter->Address ); list.SetItemText(nAdapterIndex,2,AdapterLis[nAdapterIndex].szHWAddrStr); // 网卡编号 AdapterList[nAdapterIndex].dwIndex = pAdapter->Index; pAdapter = pAdapter->Next; nAdapterIndex ++; } delete pAdapterListBuffer; } } } 2)获取ARP条目列表 // ARP条目信息 typedef struct tagARPInfo { char szIPAddrStr[16]; // IP char szHWAddrStr[18]; // MAC DWORD dwType; // 类型 }INFO_ARP, *PINFO_ARP; /********************************************************************** * Name & Params:: * AddARPInfoToList * ( * CListCtrl& list : CARPPlayerDlg传入的list句柄 * c*****t short nAdapterIndex : 用户选中的网卡编号 * ) * Purpose: * 读入系统的ARP缓存列表,.并添加到对话框中 * Remarks: * 用到了IpHelper api GetIpNetTable * 而且用到了WinSock的api,所以要包含 *****************************************************************/ void AddARPInfoToList(CListCtrl& list,c*****t short nAdapterIndex) { char tempChar; DWORD dwListSize = 1; DWORD dwRet; in_addr inaddr; list.DeleteAllItems(); dwRet = GetIpNetTable((PMIB_IPNETTABLE)&tempChar, &dwListSize, TRUE); // 关键函数 if (dwRet == ERROR_INSUFFICIENT_BUFFER) { PMIB_IPNETTABLE pIpNetTable = (PMIB_IPNETTABLE)new(char[dwListSize]); dwRet = GetIpNetTable(pIpNetTable, &dwListSize, TRUE); if (dwRet == ERROR_SUCCESS) { for (int i=0; idwNumEntries; i++) { // IP inaddr.S_un.S_addr = pIpNetTable->table.dwAddr; strcpy( ARPList.szIPAddrStr, inet_ntoa(inaddr) ); // MAC formatMACToStr( ARPList.szHWAddrStr, pIpNetTable->table.bPhysAddr ); // Type ARPList.dwType = pIpNetTable->table.dwType; if (AdapterList[nAdapterIndex].dwIndex != pIpNetTable->table.dwIndex) continue; list.InsertItem(i,ARPList.szIPAddrStr); list.SetItemText(i,1,ARPList.szHWAddrStr); switch(ARPList.dwType) { // 根据type的值来转换成字符显示 case 3: list.SetItemText(i,2,"Dynamic"); break; case 4: list.SetItemText(i,2,"Static"); break; case 1: list.SetItemText(i,2,"Invalid"); default: list.SetItemText(i,2,"Other"); } } } delete pIpNetTable; } } 这样一来,我们基本上大功告成了,其他还有一些东西在这里就不讲了,大家可以下载我的代码看看就好了。 下面我们来用ARP包玩一些小把戏 ^_^。 四.ARP包的游戏 既然我们可以自己来填充数据包,那么来玩些ARP的“小游戏”欺骗就是易如反掌了,当然,是在没有安全防护的网络里 ,比如只有hub或者交换机把你们相连,而没有路由分段……^_^ 下面我就由浅入深的讲一些介绍一些关于ARP的小伎俩。 1. 小伎俩 1) 你可以试着发一个请求包广播,其中的ARP帧里关于你的信息填成这样: (为了节省篇幅,我只写需要特别指出的填充字段) 发送方MAC 6 随便乱填一个错误的 发送方IP 4 填上你的IP 出现什么结果?是不是弹出一个IP地址冲突的提示?呵呵,同样的道理,如果发送方IP填成别人的,然后每隔1秒发一次………..-_-b 2) 比如你们都靠一个网关192.168.0.1 上网 ,如果你想让192.168.0.77 上不了网,就可以伪装成网关给192.168.0.77发一个错误的ARP响应包, like this 发送方MAC 6 随便乱填一个错误的 发送方IP 4 网关IP 192.168.0.1 接收方就填192.168.0.77的相关信息,发送之后,它还能上网不? 这样能折腾他好一阵子了,只要它的系统得不到正确的到网关的ARP映射表它就一直上不了网了 ^_^ 呵呵类似的伎俩还有很多,不过只停留在这点东西上也没什么意思,还是看看稍微高深一点的吧^_^ 2. ARP欺骗 因为在以太网里,网络设备就是靠MAC信息来识别的计算机的,比如A电脑知道MAC地址为22-22-22-22-22-22的电脑是B,而如果我给A发送一个ARP响应包,告诉它我的MAC是22-22-22-22-22-22的话,A同样会认为我的计算机是B了,那么好,我们设想有这么一个环境, A的防火墙只对IP为192.168.0.2 MAC为 22-22-22-22-22-22的B有信任关系,而且A打开了21端口提供FTP服务,正常情况下因为防火墙的缘故我们的计算机是连不到A的, 于是我们想办法让B down掉,或者在它关机的时候,我们把我们的IP改成B的192.168.0.2,然后给A发送一个ARP回应包,告诉A更新一下ARP缓存列表,192.168.0.2的IP映射到我们的 MAC地址上来,于是,奇迹出现了,我们可以连到A的FTP上了,防火墙失效了^_^ 不过这个办法只能在同网段内生效,如果我们和A不在一个网段内,那就要复杂的多了,还要配合ICMP的重定向来控制报文的路由,这个我准备在以后阐述ICMP包的时候, 详细讲解,就不再此多说了。 3. 基于ARP欺骗的*****原理 *****的技术有很多了,不过我们常用的sniffer工具只能在基于hub的网络中起作用,碰到哪怕是交换机都无能为力了,这个时候我们的ARP欺骗技术就派上用场了。 还是假设有三台主机A,B,还有我们的主机,位于同一个交换式局域网中 A与B正在通信,如果我们想要刺探A――>B通信的内容,于是我们就可以给A发送一个伪造的ARP回应包,告诉A,B的IP对应的MAC条目为我们的MAC地址,于是,A也就会相应的刷新自己的ARP缓存,将发给B的数据,源源不断的发送到我们的主机上来,这样我就可以对接收到的数据包进行分析就好了,达到了*****的目的。当然,因为动态ARP缓存是动态的,有超时时间的,所以我们必须每隔一段时间就给A发送一个ARP回应包 虽然我们这样达到了目的, ,但是A到B的通信却被停止了,为了不让B发现,我们还要对每次接收到的数据包进行转发,全部都转发给B,这样就天衣无缝了^_^ 同样的,如果我们还想*****B à A的数据包,一样给B发一个ARP回应包,告诉B,A的IP对应的MAC是我们的主机MAC,于是B到A的数据包也源源不断的发到我们的主机上来了,当然我们也是一样要对这些数据包进行转发,如图: A 我们的主机 B 一切都无误的话,A和B的通信内容就这样不知不觉的被我们*****到了^_^ 具体的代码实现由于篇幅的关系我就不放在这里讲了,如果需要我就专门另写篇文章附上完整代码吧 至此,我们的ARP基础知识就讲完了,但愿您能从中有所收获 后记: 因为本人开发都是使用VC++.net 2003,所以没有安装.net的朋友是打不开工程的,可以试一下vckbase上的工程转换工具,本人没有试过,不保证有效 http://www.vckbase.com/tools/assist/prjconverter.rar 而且本文的代码使用了winpcap开发包,是要另外安装ainpcap驱动。 读者可以安装我代码包里的驱动,不过它更新很快,可以到它主页上去下载最新版本 http://winpcap.polito.it/install/default.htm 不做开发的读者,只用下载并安装这个就可以了WinPcap auto-installer (driver +DLLs) 我的原文及源码下载地址稍后贴出,请关注本帖^_^ 希望对大家有所帮助。我不知道论坛上有没有人发过,有的话就不用下了, |
|