第四周课上实践验收
任务一:基于winpcap的网络嗅探工具的实现
1 实践原理介绍
Windows用户态中的winpcap与Unix中的libpcap库相兼容,使用winpcap库实现网络嗅探工具的原理与Unix下使用libpcap非常的类似,具体的实现技术如下图:
根据上面的技术图可知嗅探器的实现基本的流程是检测网卡->选择网卡->过滤器的设置(可以设置需要捕捉的协议包)->数据包分析。
2 实践环境搭建
-
Step 1 实践软件的准备
首先需要下载winpcap的两个东西:
(1)winpcap V4.1.2 驱动程序、dll文件:http://www.winpcap.org/install/bin/WinPcap_4_1_2.exe
(2)winpcap V4.1.2 Developr's Pack,库文件、头文件、简单的示例程序代码和帮助文件:http://www.winpcap.org/install/bin/WpdPack_4_1_2.zip
(3)所有的配置文件我分享在百度网盘中:链接:https://pan.baidu.com/s/14mfm1BEr1vdc_kchzlapTw 提取码:jwrc
(4)很奇怪我的代码push不上github???所以还是百度网盘分享吧。链接:https://pan.baidu.com/s/1gVwdBxYQAaSzO4Pizi8HoQ 提取码:hjca
-
Step 2 打开Dev-C++->新建项目(一般选择C++,Dev-C++这个软件也能够很好地兼容C)
-
Step 3 配置项目环境
(1)Dev-C++导航栏工具->编译选项->设定编译器配置->目录->C/C++包含文件->点击右下角文件图标添加文件目录->某盘\WpdPack\Include->确定
(2)Dev-C++导航栏项目->项目属性->参数->链接->加入库或者对象->某盘\WpdPack\Lib\wpcap.lib和某盘\Microsoft SDKs\Windows\v7.0A\Lib\WS2_32.Lib ->确定
(3)代码中加入语句
#define HAVE_REMOTE
、#define WPCAP
、#include "winsock.h"
(说明:have_remote是嗅探器的远程捕获,wpcap是winpcap文件,winsock.h是windows下的sock编程头文件,socket编程可以自行百度)至此整个环境就配置完成了。
3 实现过程
首先给出winpcap中文技术文档,这个文档中有winpcap的模块、数据结构、头文件列表等,里面的代码都是开源的,可以按照模块中的文件步骤一步步下来就能实现简单的嗅探器。
- Step 1 获取已连接的网络适配器列表
libpcap和WinPcap都提供了 pcap_findalldevs_ex() 函数来实现这个功能: 这个函数返回一个 pcap_if 结构的链表, 每个这样的结构都包含了一个适配器的详细信息。值得注意的是,数据域 name 和 description 表示一个适配器名称和一个可以让人们理解的描述。
下面上代码获取适配器列表:
pcap_if_t * alldevs, *device;
int i = 0;
int iNum;
pcap_t * adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
//修改这里可以更改捕获的数据包使用的协议类型
char packet_filter[] = "(ip and udp) or (ip and tcp) or (ip and icmp)";
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{ //获取设备列表
fprintf(stderr,"无法打开网络设备:%s\n", errbuf);
return 1;
}
for (device = alldevs; device != NULL; device = device->next)
{ //打印列表
if (i == 0)
{
printf("请按CTRL + C退出!\n\n");
printf("网络设备如下:\n");
}
printf("%d. %s\n", ++i, device -> name);
if (device->description)
printf(" (%s)\n", device->description);
else
printf("没有设备描述信息!");
}
if (i == 0)
{
printf("\n请先安装WinPcap!");
return -1;
}
说明:
(1) pcap_findalldevs_ex() ,和其他libpcap函数一样,有一个 errbuf 参数。一旦发生错误,这个参数将会被libpcap写入字符串类型的错误信息;
(2)不是所有的操作系统都支持libpcap提供的网络程序接口,因此,如果我们想编写一个可移植的应用程序,我们就必须考虑在什么情况下, description 是 null。本程序中,我们遇到这种情况时,会打印提示语句"没有设备描述信息!";
(3)当我们完成了设备列表的使用,我们要调用 pcap_freealldevs() 函数将其占用的内存资源释放。
(4)pcap_if_t数据结构有五个属性,名字、描述、pcap_addr类型的地址( pacp_addr 同样有五个属性,ip地址、子网掩码、广播地址、目标地址)、flags、与指向下一个元素的指针。
- Step 2 选择网络设备接口
printf("请选择网络设备接口:(1 - %d):", i);
scanf("%d", &iNum);
if (iNum < 1 || iNum > i)
{
printf("设备不存在!\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到已选设备
for (device = alldevs, i = 0;i < iNum -1 ; device = device -> next,i++);
说明:可以打开联网主机的网络适配器查看网卡有哪些,正在联网的网卡是哪个,下面是我的主机的联网情况(这个代码不能识别出无线wifi的网卡,有点疑惑这个地方,想了下可能是因为wifi和以太网是属于两种链路层网络的协议所以代码的实现不同,网上查了下也没找到解决办法)
- Step 3 打开适配器
打开设备的函数是 pcap_open()。下面是参数 snaplen, flags 和 to_ms 的解释说明:
snaplen 用来指定要捕获数据包中的哪些部分。本例中,我们将值定为65535,它比我们能遇到的最大的MTU还要大。因此,能够收到完整的数据包。
flags: 最最重要的flag是用来指示适配器是否要被设置成混杂模式。 一般情况下,适配器只接收发给它自己的数据包, 而那些在其他机器之间通讯的数据包,将会被丢弃。 相反,如果适配器是混杂模式,那么不管这个数据包是不是发给我的,都会去捕获。 这意味着在一个共享媒介(比如总线型以太网),WinPcap能捕获其他主机的所有的数据包。 大多数用于数据捕获的应用程序都会将适配器设置成混杂模式,所以,这个代码中也使用混杂模式。
to_ms 指定读取数据的超时时间,以毫秒计(1s=1000ms)。在适配器上进行读取操作(比如用 pcap_dispatch() 或 pcap_next_ex()) 都会在 to_ms 毫秒时间内响应,即使在网络上没有可用的数据包。 在统计模式下,to_ms 还可以用来定义统计的时间间隔。 将 to_ms 设置为0意味着没有超时,那么如果没有数据包到达的话,读操作将永远不会返回。 如果设置成-1,则情况恰好相反,无论有没有数据包到达,读操作都会立即返回。
上代码:
// 打开适配器
if ( (adhandle= pcap_open(device->name, // 设备名
65536, // 要捕捉的数据包的部分
// 65535保证能捕获到不同数据链路层上的每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\n不能打开适配器!\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}
- Step 4 过滤数据包
WinPcap和Libpcap的最强大的特性之一,是拥有过滤数据包的引擎。 它提供了有效的方法去获取网络中的某些数据包,这也是WinPcap捕获机制中的一个组成部分。 用来过滤数据包的函数是 pcap_compile()pcap_setfilter()
pcap_compile() 它将一个高层的布尔过滤表达式编译成一个能够被过滤引擎所解释的低层的字节码。有关布尔过滤表达式的语法可以参见 Filtering expression syntax 。
pcap_setfilter() 将一个过滤器与内核捕获会话向关联。当 pcap_setfilter() 被调用时,这个过滤器将被应用到来自网络的所有数据包,并且,所有的符合要求的数据包 (即那些经过过滤器以后,布尔表达式为真的包) ,将会立即复制给应用程序。
代码:
if (device->addresses != NULL) //获得接口第一个地址的掩码
netmask = ((struct sockaddr_in *)(device->addresses->netmask))->sin_addr.S_un.S_addr;
else //如果接口没有地址,那么我们假设一个C类的掩码
netmask = 0xffff00;
if (pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) < 0)
{ //编译过滤器 packet_filter为过滤器需要过滤的协议数据类型
fprintf(stderr, "不能监听过滤该数据报!\n");
pcap_freealldevs(alldevs);
return -1;
}
if (pcap_setfilter(adhandle, &fcode) < 0)
{ //设置过滤器
fprintf(stderr, "过滤设置错误!\n");
pcap_freealldevs(alldevs);
return -1;
}
说明:
(1)代码展示了如何编译并设置过滤器。 请注意,我们必须从 pcap_if 结构体中获得掩码,因为一些使用 pcap_compile() 创建的过滤器需要它。
(2)在这段代码片断中,传递给 pcap_compile() 的过滤器是char packet_filter[] = "(ip and udp) or (ip and tcp) or (ip and icmp)";
,这说明我们可以获取IP、TCP、ICMP和UDP的数据包,并把他们发送给应用程序。
(3)struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
sin_family:指代协议族,可取值如下:
AF_INET 2 IPv4
AF_INET6 23 IPv6
AF_UNSPEC 0 协议无关
sin_port:存储端口号(使用网络字节顺序)
sin_addr:存储IP地址,使用in_addr这个数据结构
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
IP地址的定义:
typedef struct in_addr {
union {
struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{ unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR;
in_addr:它是一个存储ip地址的共用体,有三种表达方式:第一种用四个字节来表示IP地址的四个数字;
第二种用两个双字节来表示IP地址;第三种用一个长整型来表示IP地址。
- Step 5 分析数据
(1)首先看下TCP、IP、UDP等协议的数据结构定义
typedef struct ip_address
{ //ip地址
u_char b1;
u_char b2;
u_char b3;
u_char b4;
} ip_address;
typedef struct mac_address
{//mac地址
u_char b1;
u_char b2;
u_char b3;
u_char b4;
u_char b5;
u_char b6;
} mac_address;
typedef struct ethe_header
{ //mac帧首部
mac_address mac_dest_address;//目的地址
mac_address mac_source_address;//源地址
u_short ether_type;//以太网MAC帧的格式分为DIX EthernetII和IEEE的802.3标准
} ethe_header;
typedef struct ip_header
{ //ip地址首部
u_char ver_ihl; // 版本 (4 bits) + 首部长度 (4 bits)
u_char tos; // 服务类型(Type of service)
u_short tlen; // 总长(Total length)
u_short identification; // 标识(Identification)
u_short flags_fo; // 标志位(Flags) (3 bits) + 段偏移量(Fragment offset) (13 bits)
u_char ttl; // 存活时间(Time to live)
u_char proto; // 协议(Protocol)
u_short crc; // 首部校验和(Header checksum)
ip_address saddr; // 源地址(Source address)
ip_address daddr; // 目的地址(Destination address)
u_int op_pad; // 选项与填充(Option + Padding)
} ip_header;
typedef struct udp_header
{ //UPD首部
u_short sport; // 源端口(Source port)
u_short dport; // 目的端口(Destination port)
u_short len; // UDP数据包长度(Datagram length)
u_short crc; // 校验和(Checksum)
} udp_header;
typedef struct tcp_header
{ //TCP首部
u_short sport; // 源端口(16位)
u_short dport; // 目的端口(16位)
u_int num; // 序列号 (32位)
u_int ack; // 确认号(32位)
u_short sum; // 数据偏移(4位),保留(6位),标志位(6位)
u_short windonw; // 窗口 (16位)
u_short crc; // 检验和 (16位)
u_short ugr; // 紧急指针(16位)
} tcp_header;
(2)使用函数pcap_loop(adhandle, 0, packet_handler, NULL);
开始捕捉。其中packet_handler为回调函数,当收到每一个数据包时会被libpcap所调用
void packet_handler(u_char *dumpfile, const struct pcap_pkthdr *header, const u_char *pkt_data)
{ //回调函数,当收到每一个数据包时会被libpcap所调用
if(header->caplen>400) return;
struct tm *ltime;
char timestr[16];
ip_header * ip_hd;
udp_header * udp_hd;
tcp_header * tcp_hd;
ethe_header * ethe_hd;
int ip_len,tcp_len,start;
u_short sport,dport;
printf("\n");
ltime=localtime(&header->ts.tv_sec); //将时间戳转换为可读字符
strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);
printf("时间:%s\n",timestr);
ethe_hd = (ethe_header *)pkt_data;
ip_hd = (ip_header *)(pkt_data + 14);//获得IP数据包头部位置
/*获得UDP首部位置*/
ip_len = (ip_hd ->ver_ihl & 0xf) * 4; //ip首部长度
udp_hd = (udp_header *)((u_char *)ip_hd + ip_len);
/*将网络字节序列转换成主机字节序列*/
sport = ntohs(udp_hd->sport);
dport = ntohs(udp_hd->dport);
if(ip_hd->proto==17)
{
printf("协议:UDP");
start=ip_len+8; //在IP长度基础上加8字节UDP头移动到UDP数据部分
}
else if(ip_hd->proto==6)
{
printf("协议:TCP");
tcp_hd = (tcp_header *)((u_char *)ip_hd + ip_len);
tcp_len=ntohs(tcp_hd->sum)>>12;
start=ip_len+tcp_len*4;
}
else if(ip_hd->proto==1)
{
printf("协议:ICMP");
start=ip_len+23;
}
else printf("协议:其他");
//printf("start=%d\n",start);
printf(" 数据报的长度:%d\n",header->caplen);
//打印IP地址和端口
printf("源IP地址: %d.%d.%d.%d:%d 目的IP地址:%d.%d.%d.%d:%d\n源端口:%d 目的端口:%d\n源物理地址: %x-%x-%x-%x-%x-%x 目的物理地址:%x-%x-%x-%x-%x-%x\n",
ip_hd->saddr.b1, ip_hd->saddr.b2, ip_hd->saddr.b3, ip_hd->saddr.b4,
ip_hd->daddr.b1, ip_hd->daddr.b2, ip_hd->daddr.b3, ip_hd->daddr.b4, sport, dport,
ethe_hd->mac_source_address.b1, ethe_hd->mac_source_address.b2, ethe_hd->mac_source_address.b3,
ethe_hd->mac_source_address.b4, ethe_hd->mac_source_address.b5, ethe_hd->mac_source_address.b6,
ethe_hd->mac_dest_address.b1, ethe_hd->mac_dest_address.b2, ethe_hd->mac_dest_address.b3,
ethe_hd->mac_dest_address.b4, ethe_hd->mac_dest_address.b5, ethe_hd->mac_dest_address.b6);
//输出数据部分
printf("数据部分内容为:\n");
for (int i=start; (i < header->caplen + 1 ) ; i++)
{
printf("%.2x ", pkt_data[i-1]); //也可以改为 %c 以 ascii码形式输出。
if ( (i % LINE_LEN) == 0) printf("\n");
}
printf("\n\n");
}
说明:
(1) ntohs() 简述: 将一个无符号短整形数从网络字节顺序转换为主机字节顺序(关于主机字节顺序和网络字节顺序可详见博客)。
#include <winsock.h>
u_short PASCAL FAR ntohs( u_short netshort);
netshort:一个以网络字节顺序表达的16位数。
返回值:ntohs()返回一个以主机字节顺序表达的数。
(2)注意输出各协议数据的时候需要移动指针到各层对应的应用数据部分,即跳过各层协议的数据头部定义,具体需要移动多少位,需要参看各层的数据包详细结构,这里不做赘述。
4 实验截图
-
(1)检测到的网卡有:
-
(2)捕获的UDP数据包:
-
(3)捕获的TCP数据包:
任务二:嗅探登录网站的账号和密码
Step 1 打开孙启龙同学分享的网站 http://49.233.180.160/jsptest.jsp
说明:现在很多网站都是密文传输,这个网站呢是明文传输并且很好捕获,感谢孙启龙同学的分享让我的第二个任务完成的比较快。
Step 2 打开wireshark开始抓包
Step 3 网站上输入账号密码
Step 4 过滤wireshark中的http数据包查找捕获到的账号和密码
由上图可知我的账号是JodyLZL,密码是123456
任务三:抓取手机App登录过程数据包
Step 1 打开手机热点让电脑和手机处于一个网络中
Step 2 打开wireshark开始抓包
Step 3 电脑上登录百度网盘输入账号密码
Step 4 过滤http数据包查找是否捕获到用户账号和密码
说明:百度网盘这个是密文传输的数据,所以捕获到的是一串乱码的东西,看网上说是MD5加密,又之前上网络信任体系中学到了OpenSSL这个密码库,本来想说用OpenSSL库的MD5加密试试验证下,但是想了下自己没有秘钥...所以最后捕获到的还是一串乱码...
实践总结
感觉实践像买彩票一样,可能运气好的时候呢,一下子就能做完,运气不好的时候呢,可能要做好几天(也是我操作太烂)。总的来说,这次实践收获最大的还是网络嗅探器的实现,最开始也是在网上各种的找资料,后来找到了winpcap的中文技术文档,我彩票中了!这写的也太好了,找了些配置博客下载了一些文件很快就能捕获数据了,但是其中查的资料也很多,协议数据包的格式定义,winpcap数据结构和数据类型等都需要考虑,还调试了我的网卡设置...反正搞了一堆,也学了不少(希望我忘记的也很少...)网络攻防真的是很多门课程的综合,需要学习和实践的东西真的很多。