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

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

tcpview的使用界面

在该程序中获取 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也是可以实现的。

转载于:https://www.cnblogs.com/vc60er/p/tcpview.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值