下面介绍实现的细节。由于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()
{
PMIB_TCPTABLE_OWNER_PID pTcpTable(NULL);
DWORD dwSize(0);
GetExtendedTcpTable(pTcpTable, &dwSize, TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) == ERROR_INSUFFICIENT_BUFFER);
pTcpTable = (MIB_TCPTABLE_OWNER_PID *)new char[dwSize];//重新分配缓冲区
if(GetExtendedTcpTable(pTcpTable,&dwSize,TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) != NO_ERROR)
{
delete pTcpTable;
return 0;
}
int nNum = (int) pTcpTable->dwNumEntries; //TCP连接的数目
for(int i=0;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 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
{
::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;im_connList.size();i++)
{// m_connList为 vector类型,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也是可以实现的。