0%

网络相关知识:实现 Linux Sniffer

Sniffer 总述

Sniffer,中文可以翻译为嗅探器,也叫抓数据包软件,是一种基于被动侦听原理的网络分析方式

  • 使用这种技术方式,可以监视网络的状态、数据流动情况以及网络上传输的信息

与 Sniffer 相关的知识

网络信息通过 TCP/IP 来定位网络中计算机的位置:

  • 应用程序的 IP 报文被封装成以太网帧(底层链路层报文上面的一层报文,包含有源地址,报文和一些需要用来传送至目标主机的信息)
  • 目的 IP 地址对应着一个6字节的目的以太网址(MAC 地址),它们之间通过 ARP/RARP 协议进行映射
    • ARP(Address Resolution Protocol)地址解析协议:IP地址转换为MAC物理地址
    • RARP(Reverse Address Resolution Protocol)反向地址转换协议:将MAC物理地址转换为IP地址
  • 包含着以太网帧的报文从源主机传输到目的主机,中间经过一些网络设备,如交换机,路由器等

源主机发出的太网帧基于广播方式传播(网络中的所有网卡都能看到它的传输)

  • 每个网卡会检查帧开始的6个字节(目的主机的 MAC 地址),但是只有一个网卡会发现自己的地址和其相符合,然后它接收这个帧
  • 读取该太网帧中的 IP 报文
  • 网络驱动程序会检查帧中报文头部的协议标识,以确定接收数据的上层协议
  • 然后通过对应的网络协议栈传送至接收的应用程序
    • 大多数情况下,上层就是 IP 协议,所以接收机制将去掉 IP 报文头部,然后把剩下的传送至 UDP 或者 TCP 接收机制,这些协议将把报文送到 socket-handling 机制
    • 然后把报文数据变成应用程序可接收的方式发送出去,在这个过程中,报文将失去所有的和其有关的网络信息(比如:IP,MAC,端口号,IP 选择,TCP 参数 ……),所以如果目的主机没有一个包含正确参数的打开端口,那么这个报文将被丢弃而且永远不会被送到应用层去

数据传输经过的各层协议过程

  • ARP/RARP 用于实现 MAC/IP 之间的切换

太网帧大致结构

1
2
3
4
[MAC头,---------------{数据}----------------,MAC尾]		>>>> 以太网驱动
-------[IP头,----------{数据}--------------]------- >>>>> IP
-------------[TCP/UDP头,----{应用数据}-----]------- >>> TCP/UDP
------------------------[应用软件头,用户数据]------- >>>> 应用程序

通信过程中,每层协议都要加上一个数据首部(用于存储信息),称为封装 Encapsulation,常见头部如下:

  • 以太网帧头部 - 14字节(也叫 MAC 头部):
1
2
3
4
5
6
7
8
9
10
11
#ifndef __UAPI_DEF_ETHHDR
#define __UAPI_DEF_ETHHDR 1
#endif

#if __UAPI_DEF_ETHHDR
struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /* 目标以太网地址 */
unsigned char h_source[ETH_ALEN]; /* 源以太网地址 */
__be16 h_proto; /* 数据包类型ID字段 */
} __attribute__((packed));
#endif
  • IP 头部 - 20~60字节(取决于有没有 options):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, /* 首部长度 */
version:4; /* 版本(IPv4/IPv6) */
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, /* 版本(IPv4/IPv6) */
ihl:4; /* 首部长度 */
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; /* 服务类型 */
__be16 tot_len; /* 总长度(指首部加上数据的总长度) */
__be16 id; /* 标识(标识主机发送的每一份数据报) */
__be16 frag_off; /* 标志-偏移量 */
__u8 ttl; /* 生存时间(数据报在网络中的寿命) */
__u8 protocol; /* 协议(数据报携带的数据时使用何种协议) */
__sum16 check; /* 首部校验和(只校验数据报的首部) */
__be32 saddr; /* 源地址(发送方IP地址) */
__be32 daddr; /* 目标地址(接收方IP地址) */
/*The options start here. */
};
  • UDP 头部 - 20字节(8字节真头部,12字节假头部):
1
2
3
4
5
6
struct udphdr {
__be16 source; /* 源主机端口 */
__be16 dest; /* 目标主机端口 */
__be16 len; /* UDP长度 */
__sum16 check; /* UDP效验和 */
};
  • TCP 头部 - 20~60字节(取决于有没有 options):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct tcphdr {
__be16 source; /* 源主机端口 */
__be16 dest; /* 目标主机端口 */
__be32 seq; /* 队列数目 */
__be32 ack_seq; /* 确认编号 */
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check; /* 效验和 */
__be16 urg_ptr; /* 紧急指针 */
};

网络工具 netwox 简述

netwox 是由 lauconstantin 开发的一款网络工具集,适用群体为网络管理员和网络黑客,它可以创造任意的 TCP、UDP 和 IP 数据报文,以实现网络欺骗,并且可以在 Linux 和 Windows 系统中运行

1
2
3
4
➜  Sniffer sudo netwox 32
Ethernet________________________________________________________.
| 00:0C:29:BF:5F:3F->00:08:09:0A:0B:0C type:0x0000 |
|_______________________________________________________________|
  • 00:0C:29:BF:5F:3F:源 MAC 地址,是当前主机的 MAC 地址
  • 00:08:09:0A:0B:0C:目标 MAC 地址
  • 0x0000:以太网类型

启动 netwox 显示它提供的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➜  Sniffer netwox                                           
Netwox toolbox version 5.39.0. Netwib library version 5.39.0.

######################## MAIN MENU #########################
0 - leave netwox
3 - search tools
4 - display help of one tool
5 - run a tool selecting parameters on command line
6 - run a tool selecting parameters from keyboard
a + information
b + network protocol
c + application protocol
d + sniff (capture network packets)
e + spoof (create and send packets)
f + record (file containing captured packets)
g + client
h + server
i + ping (check if a computer if reachable)
j + traceroute (obtain list of gateways)
k + scan (computer and port discovery)
l + network audit
m + brute force (check if passwords are weak)
n + remote administration
o + tools not related to network
Select a node (key in 03456abcdefghijklmno):
  • 这不是本篇博客的重点,以后有机会慢慢看

网络编程基础

Linux 实现网络通信的核心函数 socket 如下:

1
int socket(int domain, int type, int protocol);
  • domain:即协议域,又称为协议族(family)
    • 常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE
    • 协议族决定了 socket 的地址类型,在通信中必须采用对应的地址:
      • AF_INET:用 ipv4 地址(32位的)与端口号(16位的)的组合
      • AF_UNIX:用一个绝对路径名作为地址
  • type:指定 socket 类型
    • 常用的 socket 类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET
    • 不同 socket 类型有不同的功能:
      • SOCK_STREAM:提供面向连接的稳定数据传输,即TCP协议 
      • OOB:在所有数据传送前必须使用 connect() 来建立连接状态 
      • SOCK_SEQPACKET:提供连续可靠的数据包连接 
      • SOCK_RDM:提供可靠的数据包连接  
      • SOCK_PACKET:与网络驱动程序直接通信
      • SOCK_DGRAM:使用不连续不可靠的数据包连接(去除以太网报文头部)
      • SOCK_RAW:提供原始网络协议存取(让应用程序对以太网报文头部有完全的控制)
  • protocol:
    • 指定协议
    • 常用的协议有,IPPROTO_TCP(TCP传输协议)、IPPTOTO_UDP(UDP传输协议)、IPPROTO_SCTP(STCP传输协议)、IPPROTO_TIPC(TIPC传输协议)

服务端需要 bind 函数来“绑定”一个端口

客户端需要 connect 函数来连接指定的服务端(通过 IP:port

实现 Sniffer

Sniffer 需要一个 socket 去监听每个端口,得到那些没有被丢弃的报文,使用 PF_PACKET 的协议簇,应用程序可以直接利用网络驱动程序发送和接收报文,避免了原来的协议栈处理过程

Sniffer 案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/in.h>
#include <linux/if_ether.h>

int main(int argc, char **argv) {
int sock, n;
char buffer[2048];
unsigned char *iphead, *ethhead;

if ((sock=socket(PF_PACKET, SOCK_RAW,htons(ETH_P_IP)))<0) {
/* 使用PF_PACKET协议族
socket类型为SOCK_RAW
使用ETH_P_IP来处理IP的一组协议 */
perror("socket");
exit(1);
}

while (1) {
printf("----------\n");
n = recvfrom(sock,buffer,2048,0,NULL,NULL); /* 接收数据报并存储源地址 */
printf("%d bytes read\n",n);

/* 检查数据包是否至少包含完整的MAC标头(14),IP标头(20)和TCP/UDP标头(8) */
if (n<42) {
perror("recvfrom():");
printf("Incomplete packet (errno is %d)\n",errno);
close(sock);
exit(0);
}

ethhead = buffer;
printf("Source MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[0],ethhead[1],ethhead[2],
ethhead[3],ethhead[4],ethhead[5]);
printf("Destination MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[6],ethhead[7],ethhead[8],
ethhead[9],ethhead[10],ethhead[11]);

iphead = buffer+14; /* 跳过MAC头 */
if (*iphead==0x45) { /* 再次检查IPv4且不存在任何options */
printf("Source host %d.%d.%d.%d\n",
iphead[12],iphead[13],
iphead[14],iphead[15]);
printf("Dest host %d.%d.%d.%d\n",
iphead[16],iphead[17],
iphead[18],iphead[19]);
printf("Source,Dest ports %d,%d\n",
(iphead[20]<<8)+iphead[21],
(iphead[22]<<8)+iphead[23]);
printf("Layer-4 protocol %d\n",iphead[9]);
}
}
}
  • 使用 PF_PACKET 协议族嗅探所有发往自己主机的报文
  • 通过简单的格式化输出 MAC 头与 IP 头的信息

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
➜  Sniffer sudo ./test1  
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 54530,34372
Layer-4 protocol 6
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 34372,54530
Layer-4 protocol 6
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 54530,34372
Layer-4 protocol 6
----------
  • 本地进程通信的太网帧

Sniffer 案例二

在上述案例中(非混杂模式),网卡丢弃所有不含有主机 MAC 地址的数据包,只有三个例外:

  • 如果一个帧的目的 MAC 地址是一个受限的广播地址 255.255.255.255 那么它将被所有的网卡接收
  • 如果一个帧的目的地址是组播地址,那么它将被那些打开组播接收功能的网卡所接收
  • 网卡如被设置成混杂模式,那么它将接收所有流经它的数据包

在 Linux 中可以使用如下代码 开启/关闭 混杂模式:

1
2
sudo ifconfig ens33 promisc
sudo ifconfig ens33 -promisc
  • 我的 VMware 上使用 ens33 虚拟网卡

使用如下代码来查看目标网卡是否处于混杂模式:

1
cat /sys/class/net/ens33/flags
  • 0x1103:开启
  • 0x1003:关闭

在C语言中使用如下代码也可以开启混杂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ifreq ethreq;

strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
if (ioctl(sock,SIOCGIFFLAGS,&ethreq) == -1) {
perror("ioctl");
close(sock);
}

ethreq.ifr_flags|=IFF_PROMISC;
if (ioctl(sock,SIOCSIFFLAGS,&ethreq) == -1) {
perror("ioctl");
close(sock);
}

Sniffer 案例三

接下来要实现数据包的过滤,Linux 内核允许我们把一个名为 LPF 的过滤器直接放到 PF_PACKET 协议处理例程中(在网卡接收中断执行后立即执行)

  • 该过滤程序可以根据使用者的定义来运行
  • 由一种名为 BPF(Berkely Packet Filter 伯克利数据包过滤器)的伪机器码写成的

使用 tcpdump 可以快速生成 BPF 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  Sniffer sudo tcpdump -d host 192.168.157.136 
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 6
(002) ld [26]
(003) jeq #0xc0a89d88 jt 12 jf 4
(004) ld [30]
(005) jeq #0xc0a89d88 jt 12 jf 13
(006) jeq #0x806 jt 8 jf 7
(007) jeq #0x8035 jt 8 jf 13
(008) ld [28]
(009) jeq #0xc0a89d88 jt 12 jf 10
(010) ld [38]
(011) jeq #0xc0a89d88 jt 12 jf 13
(012) ret #262144
(013) ret #0
  • 第 0-1,6-7 行:确定被抓到的帧是否在传输着 IP,ARP 或者 RARP 协议
  • 第 2-5,8-11 行:比较源地址或者目的地址是否与 192.168.157.1 相同
  • 将第12格的值与协议的特征值比较,如果比较失败,那么丢弃这个包

在C语言中,建立一个 sock_filter 并为它绑定一个打开的端口,就可以使用该 BPF

使用 tcpdump 可以生成 BPF 对应的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  Sniffer sudo tcpdump -dd host 192.168.157.136             
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a89d88 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a89d88 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

最后得到最终的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
➜  Sniffer sudo ./test2
----------
199 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,36313
Layer-4 protocol 17
----------
102 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,46817
Layer-4 protocol 17
----------
213 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,59949
Layer-4 protocol 17
----------

Sniffer 代码

下面给出我写的 Sniffer 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <netinet/if_ether.h>
#include <netinet/tcp.h>
#include <net/if.h>
#include <linux/ip.h>
#include <linux/filter.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <time.h>

int backupInit(FILE **file){
*file = fopen("log","w+");

if(file == NULL){
return -1;
}
return 0;
}

int printLog(char *str,FILE* file){
fprintf(stdout,str);
fprintf(file,str);
}

int getNowtime(FILE* file)
{
static char str[60]={0};
char YMD[15] = {0};
char HMS[10] = {0};
time_t current_time;
struct tm* now_time;

char *cur_time = (char *)malloc(21*sizeof(char));
time(&current_time);
now_time = localtime(&current_time);

strftime(YMD, sizeof(YMD), "%F ", now_time);
strftime(HMS, sizeof(HMS), "%T", now_time);

strncat(cur_time, YMD, 11);
strncat(cur_time, HMS, 8);

sprintf(str,"==========\nCurrent time: %s\n==========\n", cur_time);
printLog(str,file);
free(cur_time);

cur_time = NULL;
return 0;
}

int change(size_t num,int key){
return (num >> (key*8))&0x000000ff;
}

int main(int argc, char **argv) {
int sock, n;
FILE* file;
char buffer[2048];
char str[512];
unsigned char *ethhead;
struct iphdr *iphead;
struct tcphdr *tcphead;

struct sock_filter BPF_code[]= {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a89d88 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a89d88 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};

struct sock_fprog Filter;

Filter.len = 14;
Filter.filter = BPF_code;

if ((sock=socket(PF_PACKET, SOCK_RAW,htons(ETH_P_IP)))<0){
perror("socket");
exit(1);
}

if(backupInit(&file)<0){
perror("backup");
exit(1);
}

if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER,&Filter, sizeof(Filter))<0){
perror("setsockopt");
close(sock);
exit(1);
}

getNowtime(file);

while (1) {
n = recvfrom(sock,buffer,2048,0,NULL,NULL);
sprintf(str,"%d bytes read\n",n);
printLog(str,file);

if (n<42) {
perror("recvfrom():");
printf("Incomplete packet (errno is %d)\n",errno);
close(sock);
exit(0);
}

ethhead = buffer;
sprintf(str,"Source MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[0],ethhead[1],ethhead[2],
ethhead[3],ethhead[4],ethhead[5]);
printLog(str,file);

sprintf(str,"Destination MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[6],ethhead[7],ethhead[8],
ethhead[9],ethhead[10],ethhead[11]);
printLog(str,file);

iphead = (struct iphdr*)(buffer+14);
tcphead = (struct tcphdr*)(buffer+14+20);

if (iphead->version > 0) {
sprintf(str,"Source host %d.%d.%d.%d\n",
change(iphead->saddr,0),change(iphead->saddr,1),
change(iphead->saddr,2),change(iphead->saddr,3));
printLog(str,file);

sprintf(str,"Dest host %d.%d.%d.%d\n",
change(iphead->daddr,0),change(iphead->daddr,1),
change(iphead->daddr,2),change(iphead->daddr,3));
printLog(str,file);

sprintf(str,"Source ports:%d\nDest ports:%d\n",
tcphead->source,tcphead->dest);
printLog(str,file);

sprintf(str,"Layer-4 protocol %d\n",iphead->protocol);
printLog(str,file);
}

sprintf(str,"----------\n");
printLog(str,file);
}
}

参考:NSFOCUS绿盟科技