关闭

网络嗅探器

212人阅读 评论(0) 收藏 举报
分类:

一、网络嗅探器原理

1、首先回顾一下网卡的四种接收模式:

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

2、原始套接字工作原理

首先介绍下套接字 ,套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。常用的TCP/IP协议的3种套接字类型是:流套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)、原始套接字(SOCK_RAW)

3、嗅探器原理:

通常的套接字只响应广播模式和直接模式发出的数据帧,对于其他形式的数据帧网络接口会直接丢弃。用户模式下,对网卡混杂模式的设置是通过原始套接字实现的、创建原始套接字之后,将其绑定到一个明确的本地地址,然后向套接字发送SIO_RCVALL控制命令,让它接收所有的IP包,这样网卡就进入了混杂模式。

二、代码实现

首先创建原始套接字,将原始套接字绑定到一个明确的本地地址(不能为ADDR_ANY),然后通过ioctlsocket()函数(是控制套接口的模式),向套接字发送SIO_RCVALL控制命令,将网卡设置为混杂模式,这样就能接收到所有经过网卡的封包;然后已连接的数据报或流式套接口通过不断调用recv()函数来接收IP数据包,将接收到的数据存入缓冲数组;随后再通过自定义的IP数据包解析函数对接收到的数据进行解析,通过定义的IP地址结构体的指针对象从接收到的数据中取得数据包的IP头;然后从IP头中取出源IP地址和目的IP地址。

sniffer.cpp:

#include "../common/initsock.h"
#include "../common/protoinfo.h" 

#include <stdio.h>
#include <mstcpip.h>    //用于处理TCP/IP协议的一个头文件,这里面包含了WinSockets一系列相关函数.

#pragma comment(lib, "Advapi32.lib")    //链接Advapi32.lib库文件

CInitSock theSock;      //创建CInitSock类的对象

//自定义的函数,用于解析收到的TCP数据封包,解析出封包中的TCP头
void DecodeTCPPacket(char *pData){

    TCPHeader *pTCPHdr = (TCPHeader *)pData;
    /*ntohs()是一个函数名,作用是将一个16位数由网络字节顺序转换为主机字节顺序*/
    printf(" Port: %d -> %d \n", ntohs(pTCPHdr->sourcePort), ntohs(pTCPHdr->destinationPort));

    // 下面还可以根据目的端口号进一步解析应用层协议
    switch(::ntohs(pTCPHdr->destinationPort)){
    case 21:
        break;
    case 80:
    case 8080:
        break;
    }
}

/*
自定义的函数,用于解析收到的IP数据封包,解析出封包中的IP头
*/
void DecodeIPPacket(char *pData){
    IPHeader *pIPHdr = (IPHeader*)pData;    
    in_addr source, dest;       //in_addr是一个结构体,可以用来表示一个32位的IPv4地址
    char szSourceIp[32], szDestIp[32];      // 定义两个数组用于存储源IP地址和目的IP地址
    printf("\n\n*************************************************************************");    
    // 从IP头中取出源IP地址和目的IP地址
    source.S_un.S_addr = pIPHdr->ipSource;      // 从IP头中取出源IP地址
    dest.S_un.S_addr = pIPHdr->ipDestination;       // 从IP头中取出目的IP地址
    strcpy(szSourceIp, ::inet_ntoa(source));
    strcpy(szDestIp, ::inet_ntoa(dest));

    printf("    %s ---> %s \n", szSourceIp, szDestIp);      //打印出源IP地址和目的IP地址
    // IP头长度
    int nHeaderLen = (pIPHdr->iphVerLen & 0xf) * sizeof(ULONG);

    switch(pIPHdr->ipProtocol)
    {
    case IPPROTO_TCP: // TCP协议
        DecodeTCPPacket(pData + nHeaderLen);
        break;
    case IPPROTO_UDP:
        break;
    case IPPROTO_ICMP:
        break; 
    }
}

void main(){
    // 创建原始套节字sRaw
    SOCKET sRaw = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
    // 获取本地IP地址
    char szHostName[56];
    SOCKADDR_IN addr_in;
    struct  hostent *pHost; //hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表
    gethostname(szHostName, 56);
    if((pHost = gethostbyname((char*)szHostName)) == NULL)  
        return ;
    // 在调用ioctl之前,套节字必须绑定
    addr_in.sin_family  = AF_INET;
    addr_in.sin_port    = htons(0);
    memcpy(&addr_in.sin_addr.S_un.S_addr, pHost->h_addr_list[0], pHost->h_length);
    printf(" Binding to interface : %s \n", ::inet_ntoa(addr_in.sin_addr));
    if(bind(sRaw, (PSOCKADDR)&addr_in, sizeof(addr_in)) == SOCKET_ERROR)        //将原始套接字绑定到一个明确的本机地址
        return;

    // 设置SIO_RCVALL控制代码,以便接收所有的IP包  
    DWORD dwValue = 1;
    /*
    ioctlsocket()是控制套接口的模式,向套接字发送SIO_RCVALL控制命令,将网卡设置为混杂模式,这样就能接收到所有经过网卡的封包
    回顾一下网卡的四种接收模式:
    ①广播模式:该模式下的网卡能够接收网络中的广播信息;
    ②组播模式:设置在该模式下的网卡能够接收组播数据;
    ③直接模式:在这种模式下,只有目的网卡才能接收该数据;
    ④混杂模式:在这种模式下的网卡能够接收一切通过它的数据,而不管该数据是否是传给它的
    */
    if(ioctlsocket(sRaw, SIO_RCVALL, &dwValue) != 0)    
        return ;

    // 开始接收封包
    char buff[1024];        //用于接受数据的数组
    int nRet;
    while(true){
        /*
    recv函数用于已连接的数据报或流式套接口进行数据的接收
        该函数的第一个参数指定接收端套接字描述符;
        第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
        第三个参数指明buf的长度;
        第四个参数一般置0
        */
        nRet = recv(sRaw, buff, 1024, 0);   //recv函数接收消息出错的话,会返回 0
        if(nRet > 0){
            DecodeIPPacket(buff);       //调用该函数对IP封包进行解析
        }
    }
    closesocket(sRaw);          //关闭原始套接字
}

protoinfo.h:

/*
定义协议格式
定义协议中使用的宏
 */
#ifndef __PROTOINFO_H__
#define __PROTOINFO_H__
#define ETHERTYPE_IP    0x0800
#define ETHERTYPE_ARP   0x0806

typedef struct _ETHeader         // 14字节的以太头
{
    UCHAR   dhost[6];           // 目的MAC地址destination mac address
    UCHAR   shost[6];           // 源MAC地址source mac address
    USHORT  type;               // 下层协议类型,如IP(ETHERTYPE_IP)、ARP(ETHERTYPE_ARP)等
} ETHeader, *PETHeader;
#define ARPHRD_ETHER    1

// ARP协议opcodes
#define ARPOP_REQUEST   1       // ARP 请求   
#define ARPOP_REPLY     2       // ARP 响应


typedef struct _ARPHeader       // 28字节的ARP头
{
    USHORT  hrd;                //  硬件地址空间,以太网中为ARPHRD_ETHER
    USHORT  eth_type;           //  以太网类型,ETHERTYPE_IP ??
    UCHAR   maclen;             //  MAC地址的长度,为6
    UCHAR   iplen;              //  IP地址的长度,为4
    USHORT  opcode;             //  操作代码,ARPOP_REQUEST为请求,ARPOP_REPLY为响应
    UCHAR   smac[6];            //  源MAC地址
    UCHAR   saddr[4];           //  源IP地址
    UCHAR   dmac[6];            //  目的MAC地址
    UCHAR   daddr[4];           //  目的IP地址
} ARPHeader, *PARPHeader;


// 协议
#define PROTO_ICMP    1
#define PROTO_IGMP    2
#define PROTO_TCP     6
#define PROTO_UDP     17

typedef struct _IPHeader        // 20字节的IP头
{
    UCHAR     iphVerLen;      // 版本号和头长度(各占4位)
    UCHAR     ipTOS;          // 服务类型 
    USHORT    ipLength;       // 封包总长度,即整个IP报的长度
    USHORT    ipID;           // 封包标识,惟一标识发送的每一个数据报
    USHORT    ipFlags;        // 标志
    UCHAR     ipTTL;          // 生存时间,就是TTL
    UCHAR     ipProtocol;     // 协议,可能是TCP、UDP、ICMP等
    USHORT    ipChecksum;     // 校验和
    ULONG     ipSource;       // 源IP地址
    ULONG     ipDestination;  // 目标IP地址
} IPHeader, *PIPHeader; 


// 定义TCP标志
#define   TCP_FIN   0x01
#define   TCP_SYN   0x02
#define   TCP_RST   0x04
#define   TCP_PSH   0x08
#define   TCP_ACK   0x10
#define   TCP_URG   0x20
#define   TCP_ACE   0x40
#define   TCP_CWR   0x80

typedef struct _TCPHeader       // 20字节的TCP头
{
    USHORT  sourcePort;         // 16位源端口号
    USHORT  destinationPort;    // 16位目的端口号
    ULONG   sequenceNumber;     // 32位序列号
    ULONG   acknowledgeNumber;  // 32位确认号
    UCHAR   dataoffset;         // 高4位表示数据偏移
    UCHAR   flags;              // 6位标志位
                                //FIN - 0x01
                                //SYN - 0x02
                                //RST - 0x04 
                                //PUSH- 0x08
                                //ACK- 0x10
                                //URG- 0x20
                                //ACE- 0x40
                                //CWR- 0x80

    USHORT  windows;            // 16位窗口大小
    USHORT  checksum;           // 16位校验和
    USHORT  urgentPointer;      // 16位紧急数据偏移量 
} TCPHeader, *PTCPHeader;

typedef struct _UDPHeader
{
    USHORT          sourcePort;     // 源端口号     
    USHORT          destinationPort;// 目的端口号        
    USHORT          len;            // 封包长度
    USHORT          checksum;       // 校验和
} UDPHeader, *PUDPHeader;

#endif // __PROTOINFO_H__

initsock.h:

#include <winsock2.h>
#pragma comment(lib, "WS2_32")  // 链接到WS2_32.lib库文件

class CInitSock{
public:
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2) {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);            //Winsock版本
        /*为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对
        Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
        该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
        操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,
        操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。
        以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。*/
        if(::WSAStartup(sockVersion, &wsaData) != 0){       //(Windows异步套接字的启动命令)加载套接字库
            exit(0);
        }
    }
    ~CInitSock() { //析构函数
        ::WSACleanup();         //关闭加载的套接字库
    }
};

三、总结和拓展

几个重要的函数:

1、WSAStartup函数
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中以后应用程序就可以调用所请求的Socket库中的其它Socket函数了
2、recv函数
recv函数用于已连接的数据报或流式套接口进行数据的接收。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0
3、拓展
在检查IP封包的端口时,我们可以根据端口号知道数据传输所使用的协议。如果是21端口,说明使用的就是FTP协议。然后通过比较字符串找出含有username和password的字段就能找出程序访问这些服务器所使用的用户名和密码。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:9888次
    • 积分:610
    • 等级:
    • 排名:千里之外
    • 原创:50篇
    • 转载:13篇
    • 译文:0篇
    • 评论:0条