sniffer的C编写

网络安全大作业之一,乘机好好学一波网络相关知识(由于这篇文章本来是针对python 写的,但是后来得知要求是用C来完成sniffer。。。。。于是这里先从简单的scanner进行编写:)


C编写scanner

1.socket了解

首先,socket即为所谓的套接字,套接字将底层的网络通信的工程封装成一个个函数,我们通过使用套接字提供的接口,完成网络通信。
网络通信过程中,客户端通过以下几个步骤完成数据通信:

  • 与地址结构进行绑定(指定通信机制和地址)
  • 读写数据
  • 关闭套接字

2.具体通信相关函数

int socket(int domain, int type, int protocol)

  • domain:域,也就是创建的套接字使用的协议簇,比如说ipv4(AF_INET),ipv6(AF_INET6)
  • type: SOCKET的种类,比如SOCK_STREAM(流,tcp),SOCK_DGRAW(数据报, udp),SOCK_RAW(原始socket)
  • protocol: SOCKET使用的协议,比如IPPROTO_TCP, IPPROTO_UDP, IPPROTO_IP
    通过这个函数,能够设置套接字的基本属性,并且返回一个套接字句柄。我们之后可以通过操作此句柄来完成对socket的具体操作。

int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen)

  • sockfd:sock的句柄
  • my_addr:这里是一个结构体,通用结构体如下:
    1
    2
    3
    4
    struct sockaddr {
    sa_family_t sa_family; // 之前的地址协议
    char sa_data[14]; // 存储具体的协议地址
    }
    TCP/IP协议使用的协议地址格式为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
      struct sockaddr_in{
    unsigned short sin_family;/*地址类型*/
    unsigned short sin_port;/*端口号*/
    struct in_addr sin_addr;/*IP地址*/
    unsigned char sin_zero[8];/*填充字节,一般赋值为0*/
    }
    struct in_addr{
    unsignedlong s_addr;
    }

这里注意地方:

  1. inet_addr这个函数能够将一个ip地址穿换成unsigned long的形式,也就是说:
1
addr_serv.sin_addr.s_addr = inet_addr(DEST_IP_ADDRESS);

通常按照如上赋值。
2.htons可以将整型变量从主机字节顺序转变成网络字节顺序。就是所谓的高字节在低位,低数字在高位。

  • addrlen:地址长度
    当使用socket函数后,会创建一个命名空间(就是地址簇),但是我们此时还没有指定当前的地址(也就是当前的socket的名字)。所以我们使用bind将对应的地址进行绑定。
    使用前通常要进行清空处理:
1
2
struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/
memset(&serv_addr,0,sizeof(struct sockaddr_in));// 将当前的内容清空

此函数为服务器端调用

int cnnect(int sockfd, struck sockaddr* servaddr, int addrlen)

  • sockfd: 进行了绑定的sockfd
  • servaddr: 服务方的地址
  • addrlen: 地址的长度
    通常为客户端使用,使用的时候我们 通常使用tcp/udp协议,从而sockaddr通常写作 sockaddr_in结构体。

ssize_t send(int s,const void *msg,size_t len,int flags);
ssize_t recv(int s,const void *msg,size_t len,int flags);

  • s:套接字的描述符fd
  • msg:所发送的缓冲区
  • len:待发送数据的长度
  • flags:数据类型:
  • MSG_OOB:发送或接收加急数据;
  • MSG_PEEK:观察输入报文;
  • MSG_DONTROUTE:旁路路由选择;

4. 设计思路

最初的设计思路比较简单,就是简单额度使用了connect函数进行连接。调用了connect之后,客户端便会向指定服务器发送一个带有SYN的数据包,然后此时如果此端口没有开放的话,则会返回RST数据包。但是这个过程比较慢,因为connect这个过程在失败的时候,计算机会进行等待。显然这么做是非常低效的。于是这里打算采用发送FIN数据包的方式进行端口扫描

socket raw

比起之前提到的两种协议,socket还支持第三种协议:SOCK_RAW,这个协议可以保证让我们自行的构造tcp头部和ip头部。
我们这里只选择创建tcp头部,于是我们的socket要改成如下的形式:

1
socket(AF_INET, SOCK_RAW, IPPROTO_TCP); 

上述的说法能够让我们自己定义tcp头部(如果要定义ip头部的话,我们需要增加setsockopt这个函数)
同时,只有超级用户能够使用socket raw,我们这里调用setuid来提升我们的权限。并且,经测试,在编译期间也必须给予超级用户的权限,否则的话依然会报错

struct tcphdr :结构体,可以用来 构成tcp头部。注意当我们手动构造tcp的头部的时候,此时目标地址的port也要提前放入dest_addr(?)

这是头文件中的结构体(看到网上很多地方都没有提到怎么写啊…),几乎就是直接往这些结构体元素中填入我们指定的值,就能够构成tcp头部。

我们这里只需要随机构造我们的random,完成我们的32bit随机数。
并且由于不适用connect,我们需要改成如下函数:
int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);

  • s:socket fd
  • msg:传输的信息
  • len:传输信息的长度,就是整个报文的长度
  • flags:附加说明,没有填0
  • to :地址
  • tolne:地址的长度(直接sizeof(struct sockaddr_in即可))

在实验了两天后,发现首先第一个问题是checksum不能错哇。。。不然的话tcp那边不认。。会丢掉这个包的。。。
然后,发现发送一个fin包的时候,是不会有任何的回应的(?),于是靠着syn包的回复来进行辨认。。。。

C编写sniffer

1. 什么是sniffer

sniffer 在wiki上的解释就是packet analyzer,也就是【包分析】。sniffer可以拦截记录经过的数据包。

换句话说,就是要有

  • 拦截
  • 记录
  • 分析
    三个功能。
    换句话说,这个程序的功能就是记录数据包并且分析其内容,需要的话可以拦截数据包传输

2. 系统设计

大致的过程了解清楚以后,就可以大概设计一下框架了:

然后参考了他人读的sniffer,加上大致的思考,getPacket,decodePacket应该都会与socket有关系,应该将这两个方法抽象在同一个类里面。

sniffer必定会处理多个数据包(数据包已经被封装了),所以在Sniffer中必定要有一个记录每一个经过数包的属性。

3. 具体设计

指明了不给用python 啊好气,看来只能是用C的Winpcap来实现这个过程了。

获得所有设备

首先我们需要获得所有的ethernet adapter(以太网适配器),这个过程可以使用函数pcap_findalldevs_ex()来实现:

1
2
3
4
5
int pcap_findalldevs_ex ( char *  source,
struct pcap_rmtauth * auth,
pcap_if_t ** alldevs,
char * errbuf
)

source: 源地址,我们通过设定源地址来指定我们需要监听的地址,比如监听本地的话就是:‘rpcap://’ 监听远程的话就是’rpcap://host:port’ 。其中,宏 PCAP_SRC_IF_STRING 就是’rpcap://’。
auth: 指定了当前监听的对象权限的路径。如果是本地的话,可以直接设置为空
alldevs: 关键参数,如果成功获得设备的话,会给当前指针一个pcap_if_t的链表的头指针。
errbuf: 出错参数。这个char*[PCAP_ERRBUF_SIZE]中会包含当前的出错信息。

关键参数就是这个alldevs,其结构体为pcap_if

1
2
3
4
5
6
7
8
9
10
11
struct pcap_if *  next
指向下一个pcap_if,如果是最会后一个则返回NULL
char * name
获得当前设备的名字
char * description
如果不为空的话,返回当前设备的基本信息(比如VMWARE ADAPTER那种)
struct pcap_addr * addresses
指向地址链表接口中的第一个地址
u_int flags
符号位,唯一可能的值是PCAP_IF_LOOPBACK, 只有在当前的设备是回环设备的时候设置

释放设备

void pcap_freealldevs(pcap_if_t** alldevs);
用于释放当前获得的设备

获得指定设备

1
2
3
4
5
6
7
pcap_t* pcap_open ( const char *  source,
int snaplen,
int flags,
int read_timeout,
struct pcap_rmtauth * auth,
char * errbuf
)

snaplen: 制定要捕获数据包中的哪些部分。 在一些操作系统中 (比如 xBSD 和 Win32), 驱动可以被配置成只捕获数据包的初始化部分: 这样可以减少应用程序间复制数据的量,从而提高捕获效率。如果将值定为65535,它比我们能遇到的最大的MTU还要大。因此总能收到完整的数据包。

flags: 最最重要的flag是用来指示适配器是否要被设置成混杂模式(promiscuous,宏为PCAP_OPENFLAG_PROMISCUOUS )。 一般情况下,适配器只接收发给它自己的数据包, 而那些在其他机器之间通讯的数据包,将会被丢弃。 相反,如果适配器是混杂模式,那么不管这个数据包是不是发给机器,机器都会去捕获。这意味着在一个共享媒介(比如总线型以太网),WinPcap能捕获其他主机的所有的数据包。 大多数用于数据捕获的应用程序都会将适配器设置成混杂模式。

to_ms:指定读取数据的超时时间,以毫秒计(1s=1000ms)。在适配器上进行读取操作(比如用 pcap_dispatch() 或 pcap_next_ex()) 都会在to_ms 毫秒时间内响应,即使在网络上没有可用的数据包。 在统计模式下,to_ms 还可以用来定义统计的时间间隔。 将to_ms 设置为0意味着没有超时,那么如果没有数据包到达的话,读操作将永远不会返回。 如果设置成-1,则情况恰好相反,无论有没有数据包到达,读操作都会立即返回。

pcap_open()函数的返回类型为 pcap_t * ,在引用时我们需要先声明一个pcap_t 类型的指针。这个函数将会返回当前的设备。并且在获得当前设备后,我们就能够释放掉其他的设备。

监听是否得到数据包(回掉函数)

1
2
3
4
5
pcap_loop(pcap_t *  p,
int cnt,
pcap_handler callback,
u_char * user
)

p: 当前设备的句柄
cnt: 指定收到多少的数据包就停止检测。如果我们设置为0,那么将会一直接受数据包
user: 指定的操作者(?)
callback: 关键参数,用于存放回调函数,回调函数的格式为:

1
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)

param: 传入的各类参数(?)
header: 捕捉的数据包的头部
pkt_data: 数据包的内容
数据包头中会记录相关的时间信息

1
2
3
4
5
struct pcap_pkthdr {
struct timeval ts; /* 时间戳 */
bpf_u_int32 caplen; /* 部分长度 */
bpf_u_int32 len; /* 数据包总长度 */
};

为了获得可读取的时间,我们需要对header中的时间参数进行修改:

1
2
3
time_t local_tv_sec = header->ts.tv_sec;
localtime_s(&ltime, &local_tv_sec); // 将时间转换成一个结构 体,其中存放了时间的参数
strftime(timestr, sizeof timestr, "%H:%M:%S", &ltime); // 将时间转换成固定格式

监听是否得到数据包

1
2
3
4
int pcap_next_ex  ( pcap_t *  p,
struct pcap_pkthdr ** pkt_header,
const u_char ** pkt_data
)

p: 设备句柄
pkt_header: 包的时间信息与长度的指针
pkt_data: 包收到的数据的指针

返回值:
1 成功读取数据
0 经过了在pcap_open_live() 设置的存活时间. 此时 pkt_header 和 pkt_data 将不会指向一个有效的包
-1 发生了错误
-2 此时读到了捕捉器的末尾(?)

在能够抓取到数据包后,我们应该尝试的解析数据包中内容。winpcap能够抓到的最上层的报头为链路层报头,于是乎我们还得复习一下链路层的帧的结构:

实现里面遇到的那些坑

1. ssh数据包?

一开始做实验的时候,发现无论怎么发送请求,都会返回数据包,仔细研究了NNNN久之后,意识到我是用ssh连接上去的,然后这个ssh呢,自然也是有数据包的。。。。

2. 防火墙的安全起见

这个也是一个大坑。当我快要完成的时候,我发现无论怎么发送数据包到主机,都不能成功。后来在大佬同学的指点下,才知道windows的防火墙会过滤掉这种数据包,只有在关闭了防火墙我才能够继续发送请求。。

3. bind?std::bind?

这个是帮同学调试数据传输的时候遇到的问题。C++的std下有一个函数也叫bind,而且作用是绑定一个函数形成函数指针,而winsock2下的bind是用来绑定端口的地址的,然后,在漫长的调试之后,终于发现了这个事情。。。所以当我们使用winsock2的bind时,应该写作:

1
::bind();

4. EX的Winpcap

由于windows下不能使用socket而准备使用的winpcap,如果要使用官方的例子的代码的话,就必须要在引入pcap.h之前,加入***#define HAVE_REMOTE**的宏定义,否则很多函数就会出现未定义的提示

参考博客:
http://blog.csdn.net/tigerjibo/article/details/6764613
http://blog.csdn.net/cqcre/article/details/39924789
http://blog.csdn.net/u010487568/article/details/39329791