[转]教大家如何打造使用Tcpview(tcp查看器

  一玩 VS 对战平台的同学有一次发现了一个可以踢人的方法,就是用 TcpView 把那个连
接关掉。后来VS 平台封掉了这个程序,只要一打开TcpView就会被 VS 关掉。于是我萌生了
自己做个 TcpView的想法。
 Tcpview是Winternals公司 Sysinternals
系列工具之一,尽管大部分这些工具网上都
有源代码,唯独没有找到TcpView的源代码。能找到的其实只是个命令行下的一个很简易的
Tcpview。现在的目标就是尽量模拟一个界面版的Tcpview。
  这个程序主要的功能就是显示系统中当前的TCP和UDP 连接信息,包括本地地址、端口,
远程地址、端口,连接状态,所属进程等,以及这些连接的上传、下载的数据量,并且可以
断开 TCP 连接。我还添加了一个功能,显示远程地址对应的物理地址。程序使用VC 6.0 MFC
的单文档开发,主界面是个ListView。

[转]教大家如何打造使用Tcpview(tcp查看器

在该程序中获取 TCP 连接用的是 GetExtendedTcpTable 函数,获取 UDP 连接用的是
GetExtendedUdpTable函数,断开一个TCP 连接用的是 SetTcpEntry。其实这些信息通过调
试 Tcpview都能发现,不过关于如何获取上传下载流量,我真的没弄出来它到底是怎么实现
的(OD 水平太菜了)。但是经过测试,Tcpview 中包含的一个驱动并没有用到,因为任何工
具都没有检测到有驱动加载。希望有高手可以指点一下!

于是,我只好自己用原始套接字实现了,可惜唯一的缺陷是不能获取回环地址通信时的
流量。比如,我本地开了FTP 服务器,然后用127.0.0.1连接上去传输文件,此时是无法截
获到数据包的。关于这一点,我在网上查到的资料说是在Windows下原始套接字是无法办到
的,甚至 Winpcap包也不可以,要捕获的话可以用SPI或是API Hook。

具体实现
 
下面介绍实现的细节。由于GetExtendedUdpTable与GetExtendedTcpTable的用法非常
相似,故这里只介绍GetExtendedTcpTable的用法。

GetExtendedTcpTable函数在 SDK 中没有,所以要自己定义。
typedef DWORD (WINAPI *PFNGetExtendedTcpTable)(
    
__out        
PVOID pTcpTable, //返回查询结构体指针
    
__in_out     
PDWORD pdwSize, //第一次调用该参数会返回所需要的
缓冲区大小
    
__in         
BOOL bOrder, //是否排序
    
__in         
ULONG ulAf, //是 AF_INET还是AF_INET6
    
__in         
TCP_TABLE_CLASS TableClass, // 表示结构体的种类,
此处设为 TCP_TABLE_OWNER_PID_ALL
    
__in         
ULONG Reserved //保留不用,设为 0
);
 pTcpTable 其实是一个指向 MIB_TCPTABLE_OWNER_PID
类型的指针。
MIB_TCPTABLE_OWNER_PID结构定义如下:
typedef struct _MIB_TCPTABLE_OWNER_PID
{
   
DWORD               
dwNumEntries;
   
MIB_TCPROW_OWNER_PID table[ANY_SIZE];
} MIBTCPTABLEOWNERPID, *PMIBTCPTABLEOWNERPID;

dwNumEntries表示
MIB_TCPROW_OWNER_PID结构的数目,每个该结构指定一个TCP 连接
的信息。ANY_SIZE 的值被定义为1,可以理解为 table 是 MIB_TCPROW_OWNER_PID 结构体数
组的首地址,这样我们可以任意地访问每个数组的成员。这种定义方式就好比一列火车,告
诉你车厢数以及火车头的地址,我们就可以得到每节车厢的地址。再来看
MIB_TCPROW_OWNER_PID的定义:

介绍完相关的数据结构就可以来使用该函数了。这里是我程序
typedef struct _MIB_TCPROW_OWNER_PID
{
   
DWORD      
dwState;//连接状态
   
DWORD      
dwLocalAddr;//本地 IP地址
   
DWORD      
dwLocalPort;//本地端口
   
DWORD      
dwRemoteAddr;//远程 IP 地址
   
DWORD      
dwRemotePort;//远程端口

DWORD      
dwOwningPid;//关联的进程ID
} MIB_TCPROW_OWNER_PID, *PMIB_TCPROW_OWNER_PID;

int GetTcpConnect()
{
       
HMODULE hMod = LoadLibrary("Iphlpapi.dll");
 if(!hMod)
 {
  AfxMessageBox("加载Iphlpapi.dll出错");
  return 0;
 }
PFNGetExtendedTcpTable pfnGetTcpTable =
(PFNGetExtendedTcpTable)::GetProcAddress(hMod,"GetExtendedTcpTable");//获取

函数地址
 PMIB_TCPTABLE_OWNER_PID pTcpTable = new
MIB_TCPTABLE_OWNER_PID;
 DWORD dwSize =
sizeof(MIB_TCPTABLE_OWNER_PID);
  if (pfnGetTcpTable(pTcpTable,
&dwSize,
TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) ==
ERROR_INSUFFICIENT_BUFFER)
 {//第一次调用时不知道要传入的缓冲区大小,所以要试探一下,参数dwSize会返
回真正需要的大小
  delete pTcpTable;

pTcpTable =
(MIB_TCPTABLE_OWNER_PID *)new char[dwSize];//重新分配缓
冲区
 }
 if(pfnGetTcpTable(pTcpTable,&dwSize,TRUE,AF_INET,TCP_TABLE_OWNER_PID_AL

L,0) != NO_ERROR)
 {
  AfxMessageBox("获取TCP连接出错");
  delete pTcpTable;
  return 0;
 }
 int nNum = (int)
pTcpTable->dwNumEntries; //TCP连接的数目
 for(int i=0;i<nNum;i++)
 {
  printf(“本地地址:%s:%d 
远程地址:%s:%d  状态:%d 
进程ID:%d”, 
inet_ntoa(*(in_addr*)&
pTcpTable->table[i].dwLocalAddr), //本地IP 地址
htons(pTcpTable->table[i].dwLocalPort), //本地端口
inet_ntoa(*(in_addr*)&
pTcpTable->table[i].dwRemoteAddr), //远程IP地址
htons(pTcpTable->table[i].dwRemotePort), //远程端口

pTcpTable->table[i].dwState, //状态
pTcpTable->table[i].dwOwningPid); //所属进程PID
 }
 delete pTcpTable;
}

获取进程 ID 之后就可以获取进程名和路径了。我使用的是
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0)方法,在ROCESSENTRY32中没有进程的

路径信息,可以用 GetModuleFileNameEx 函数获得,或是通过
CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,nPId)
查找进程模块,出现的第一个模块
就是程序模块,路径存储在me32.szExePath中。这部分代码请参看我的源程序。
  既然要模仿TcpView,当然要有进程图标了。一开始我选择去读取图标资源,但是这种
方法兼容性不好,比较麻烦,其实只要用ExtractIcon函数就好了。以下是提取代码:

遇到没有图标的程序可以使用系统默认的程序图标,只要调用
HICON GetExeIcon(CString strExe,int nIdx = 0)//获取第 nIdx+1
个图标,一般程序
将 nIdx 设为 0就可以了
{
 if(strExe == "") return NULL;
 HMODULE hExe = LoadLibrary(strExe);//把EXE
当二进制资源加载
 if (hExe == NULL) 
 {
  return NULL;
 }
 int nNum =
(int)::ExtractIcon(hExe,strExe,-1);//获取图标数
 if(nNum == 0) return NULL;
  HICON hIcon =
::ExtractIcon(hExe,strExe,nIdx);//提取图标
 FreeLibrary(hExe);//释放资源
 return hIcon;
}

遇到没有图标的程序可以使用系统默认的程序图标,只要调用
GetExeIcon("shell32.dll",2)就好了。要在每一项前面显示图标,需要在视图类的定义中
添加图像列表指针 CImageList *m_pImgList,并在构造函数中初始化 m_pImgList = new
CImageList; m_pImgList->Create(16, 16,
ILC_COLORDDB|ILC_COLOR32, 8, 8),然后在
List 添加列的同时将图像列表加入 List : m_list.SetImageList(m_pImgList,
LVSIL_SMALL)。获取图标句柄后调用m_pImgList->Add(hIcon),返回值为图标在
ImageList
中的索引,该索引即是InsertItem时设置的图标索引。
关闭 TCP 连接的方法是用 SetTcpEntry 函数设置连接的状态为:
MIB_TCP_STATE_DELETE_TCB,下面是具体代码:

MIB_TCPROW tcprow;
tcprow.dwLocalAddr = dwLocalIp;//本地 IP地址
tcprow.dwRemoteAddr = dwRemoteIp;//远程IP 地址
tcprow.dwLocalPort = ntohs(nLocalPort);//本地端口
tcprow.dwRemotePort = ntohs(nRemotePort);//远程端口
tcprow.dwState = MIB_TCP_STATE_DELETE_TCB;//删除连接
SetTcpEntry(&tcprow);

使用原始套接字可以监听到接收到的所有IP 数据包,只要分析TCP 包和 UDP 包的相关
信息就可以得到每个连接的流量信息。每个连接在内存中是以 UpDownInfo 结构体的形式存

储的。网上讲述原始套接字的文章有很多,黑防也有不少这种文章,这里只给出关键代码:

//由于代码很长,部分地方会省略冗长的代码,详细请见源程序
vector<DWORD> dwMyIp; //本机IP列表,存放所有IP
地址,以便嗅探所有数据包  
char szHost[256];
// 取得本地主机名称
::gethostname(szHost, 256);
// 通过主机名得到地址信息
hostent *pHost = ::gethostbyname(szHost);
in_addr addr;
for(int i = 0; ; i++)
{
 char *p =
pHost->h_addr_list[i];
 if(p == NULL) break;
 dwMyIp.push_back(inet_addr(inet_ntoa(*(in_addr

*)pHost->h_addr_list[i]))); //保存IP地址
}
nNetNum = i;
for(i = 0;i<nNetNum;i++)

{
 ::CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)WorkThread,(LPVOID)this,0,

0); //建立新线程,WorkThread是嗅探的工作线程
}
int WINAPI WorkThread(LPVOID Param)
{ // 创建原始套接字
 SOCKET sock;
  sock = socket(AF_INET, SOCK_RAW,
IPPROTO_IP);
 // 设置 IP头操作选项,其中 flag 设置为ture,亲自对 IP头进行处理
 BOOL flag=TRUE;
  setsockopt(sock, IPPROTO_IP, IP_HDRINCL,
(char*)&flag, sizeof(flag));
 SOCKADDR_IN addr_in;
  addr_in.sin_addr.S_un.S_addr =
dwMyIp[nIndex++];//用于设置监听的IP
 addr_in.sin_family = AF_INET;
 addr_in.sin_port = htons(10013); //绑定任意端口
  if(bind(sock,
(PSOCKADDR)&addr_in, sizeof(addr_in)) ==
SOCKET_ERROR)
 {
  AfxMessageBox("绑定地址失败");
  return 1;
 }
 // dwValue为输入输出参数,为1 时执行,0时取消
  DWORD dwValue = 1;
 // 设置 SOCK_RAW
为SIO_RCVALL,以便接收所有的IP包。其中SIO_RCVALL
   // 的定义为: #define SIO_RCVALL
_WSAIOW(IOC_VENDOR,1)

ioctlsocket(sock, SIO_RCVALL,
&dwValue); //将网卡设置为混合模式 
 char RecvBuf[BUFFER_SIZE];//接收数据的缓冲区
 while(1) //死循环
 {
int ret = recv(sock, RecvBuf, BUFFER_SIZE, 0);
  if (ret >0)
{
    IpHeader
*iphdr = (IpHeader *)RecvBuf; //此处略去IP头定义,下同,
详见代码
   int nLen =
ntohs(iphdr->TotalLen) - sizeof(IpHeader);
//获取下层
数据包长度
    int bUp =
IsMyIp(iphdr->SrcAddr,iphdr->DstAddr);
//判断是否是发
送数据,返回-1 表示不是本机数据包,因为原始套接字必须要设为混杂模式,否则无
法监听到数据
   if(bUp == -1)
continue;//拒绝接收
DWORD dwLocalIp =
bUp?iphdr->SrcAddr:iphdr->DstAddr;

DWORD
dwRemoteIp =
bUp?iphdr->DstAddr:iphdr->SrcAddr;

int
hdrLen,i; 
  
if(iphdr->Protocol == IPPROTO_TCP) //是TCP 包
  
{  
   TcpHeader *tcphdr = (TcpHeader
*)(RecvBuf + iphdr->HdrLen*4);
   hdrLen
=iphdr->HdrLen*4+sizeof(TcpHeader);
    USHORT
nLocalPort =
bUp?ntohs(tcphdr->SrcPort):ntohs(tcphdr->DstPort);
//获取本地端口
    USHORT
nRemotePort =
bUp?ntohs(tcphdr->DstPort):ntohs(tcphdr->SrcPort);
//获取远程端口
   for(int
i=0;i<pThis->m_connList.size();i++)

{//
m_connList为
vector<UpDownInfo>类型,UpDownInfo是保存连接相

关信息的结构体
   if( 判断 m_connList中是否存在该连接
)
   {
if(bUp)
  {
            
m_connList[i].dwUpData += nLen - sizeof(TcpHeader);
          
m_connList[i].nUpPacket ++;
   }else{
          
m_connList[i].dwDownData += nLen
-sizeof(TcpHeader); 
 
   m_connList[i].nDownPacket
++;
}
  
}   
  
if(i>=m_connList.size())
{
    UpDownInfo
info;
    
省略初始化结构体代码
   
m_connList.push_back(info);
 }
   }
if(iphdr->Protocol == IPPROTO_UDP) //是UDP 包
   {
    
UdpHeader *udphdr = (UdpHeader *)(RecvBuf +
iphdr->HdrLen*4);
//省略
UDP部分的处理代码,与上类似

}
  }else{
   //出现错误
   break;
  }
 }
 return 0;
}

有一点要注意的是, UDP 协议与 TCP 不同,很多基于P2P 的程序只是绑定一个本地的 UDP
端口,然后监听,此时可能所有的数据包都是发送到该端口上来的。原版的Tcpview是不分
析 UDP 地址的,只看本地端口。我在此基础上做了点改进,会显示远程的 IP 地址,不过显
示的只是收到的最近包的地址。其实也是可以做成显示所有远程通信地址的,只不过编写上
会更麻烦一些(有兴趣读者可以自己完成)。
显示物理地址部分我用的是网上的代码,就是查找纯真 IP 数据库中的记录,得到物理
地址。如果当前程序目录下没有“qqwry.dat”文件,List中是不会添加“物理地址”这一
列的。程序启动后连接默认会根据进程名排序,新加入的连接也会自动进行插入排序。

总结
 
在程序制作过程中也花了不少精力,比如刚开始总是有严重的资源泄漏问题,后来用的
是 BoundsChecker不断调试解决的。现在我提供的这个版本还是有点简陋,有些细节也没有
处理好,欢迎大家批评指正。最后谈一谈开头的话题,就是 VS 中的踢人问题,原理很简单,作为主机(服务端)时
所有数据包都会发送到我电脑上来,只要我把要踢的人的TCP 连接关了,那他自然就掉线了,
但前提是你要清楚要踢的是谁。还有种踢人挂用的是API Hook的方式,主要是对 send函数
的挂钩,用SPI也是可以实现的。

上一篇:HDU 5634 Rikka with Phi


下一篇:jQuery的入门与简介《思维导图》