http://blog.csdn.net/wangxg_7520/article/details/2795229
看了太多的“自己动手”,这次咱也“自己动手”一下,写个简单的网络抓包工具吧。要写出像tcpdump和wireshark(ethereal)这样的大牛程序来,咱也没那能耐,呵呵。所以这个工具只能抓取本地IP数据报,同时它还使用了BPF,目的是了解如何进行简单有效的网络抓包。
当打开一个标准SOCKET套接口时,我们比较熟悉的协议往往是用AF_INET来建立基于TCP(SOCK_STREAM)或UDP(SOCK_DGRAM)的链接。但是这些只用于IP层以上,要想从更底层抓包,我们需要使用AF_PACKET来建立套接字,它支持SOCK_RAW和SOCK_DGRAM,它们都能从底层抓包,不同的是后者得到的数据不包括以太网帧头(最开始的14个字节)。好了,现在我们就知道该怎样建立SOCKET套接口了:
- sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
最后一个参数 ETH_P_IP 指出,我们只对IP包感兴趣,而不是ARP,RARP等。之后就可以用recvfrom从套接口读取数据了。
现在我们可以抓到发往本地的所有IP数据报了,那么有没有办法抓到那些“流经”本地的数据呢?呵呵,当然可以了,这种技术叫网络嗅探(sniff),它很能威胁网络安全,也非常有用,尤其是当你对网内其他用户的隐私感兴趣时:( 由于以太网数据包是对整个网段广播的,所以网内所有用户都能收到其他用户发出的数据,只是默认的,网卡只接收目的地址是自己或广播地址的数据,而把不是发往自己的数据包丢弃。但是多数网卡驱动会提供一种混杂模式(promiscous mode),工作在这种模式下的网卡会接收网络内的所有数据,不管它是发给谁的。下面的方法可以把网卡设成混杂模式:
// set NIC to promiscous mode, so we can recieve all packets of the network
strncpy
(ethreq.ifr_name,
"eth0"
, IFNAMSIZ);
ioctl(sock, SIOCGIFFLAGS, ðreq);
ethreq.ifr_flags |= IFF_PROMISC;
ioctl(sock, SIOCSIFFLAGS, ðreq);
|
通过ifconfig可以很容易的查看当前网卡是否工作在混杂模式(PROMISC)。但是请注意,程序退出后,网卡的工作模式不会改变,所以别忘了关闭网卡的混杂模式:
// turn off promiscous mode
ethreq.ifr_flags &= ~IFF_PROMISC;
ioctl(sock, SIOCSIFFLAGS, ðreq);
|
现在我们可以抓到本网段的所有IP数据包了,但是问题也来了:那么多的数据,怎么处理?CPU可能会被严重占用,而且绝大多数的数据我们可能根本就不敢兴趣!那怎么办呢?用if语句?可能要n多个,而且丝毫不会降低内核的繁忙程度。最好的办法就是告诉内核,把不感兴趣的数据过滤掉,不要往应用层送。BPF就为此而生。
BPF(Berkeley Packet Filter)是一种类是汇编的伪代码语言,它也有命令代码和操作数。例如,如果我们只对用户192.168.1.4的数据感兴趣,可以用tcpdump的-d选项生成BPF代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
$tcpdump -d host 192.168.1.4
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 6
(002) ld [26]
(003) jeq #0xc0a80104 jt 12 jf 4
(004) ld [30]
(005) jeq #0xc0a80104 jt 12 jf 13
(006) jeq #0x806 jt 8 jf 7
(007) jeq #0x8035 jt 8 jf 13
(008) ld [28]
(009) jeq #0xc0a80104 jt 12 jf 10
(010) ld [38]
(011) jeq #0xc0a80104 jt 12 jf 13
(012) ret #96
(013) ret #0
|
其中第一列代表行号,第二列是命令代码,后面是操作数。下面我们采用汇编注释的方式简单的解释一下:
(000) ldh [12] ;load h?? (2 bytes) from ABS offset 12 (the TYPE of ethernet header)
(001) jeq #0x800 jt 2 jf 6 ;compare and jump, jump to line 2 if true; else jump to line 6
(002) ld [26] ;load word (4 bytes) from ABS offset 26 (src IP address of IP header)
(003) jeq #0xc0a80104 jt 12 jf 4 ;compare and jump, jump to line 12 if true, else jump to line 4
(004) ld [30] ; load word (4 bytes) from ABS offset 30 (dst IP address of IP header)
(005) jeq #0xc0a80104 jt 12 jf 13 ;see line 3
(006) jeq #0x806 jt 8 jf 7 ;compare with ARP, see line 1
(007) jeq #0x8035 jt 8 jf 13 ;compare with RARP, see line 1
(008) ld [28] ;src IP address for other protocols
(009) jeq #0xc0a80104 jt 12 jf 10
(010) ld [38] ;dst IP address for other protocols
(011) jeq #0xc0a80104 jt 12 jf 13
(012) ret #96 ;return 96 bytes to user application
(013) ret #0 ;drop the packet
但是这样的伪代码我们是无法在应用程序里使用的,所以tcpdum提供了一个-dd选项来输出一段等效的C代码:
- $tcpdump -dd host 192.168.1.4
- { 0x28, 0, 0, 0x0000000c },
- { 0x15, 0, 4, 0x00000800 },
- { 0x20, 0, 0, 0x0000001a },
- { 0x15, 8, 0, 0xc0a80104 },
- { 0x20, 0, 0, 0x0000001e },
- { 0x15, 6, 7, 0xc0a80104 },
- { 0x15, 1, 0, 0x00000806 },
- { 0x15, 0, 5, 0x00008035 },
- { 0x20, 0, 0, 0x0000001c },
- { 0x15, 2, 0, 0xc0a80104 },
- { 0x20, 0, 0, 0x00000026 },
- { 0x15, 0, 1, 0xc0a80104 },
- { 0x6, 0, 0, 0x00000060 },
- { 0x6, 0, 0, 0x00000000 },
该代码对应的数据结构是struct sock_filter,该结构在linux/filter.h中定义如下:
1
2
3
4
5
6
7
|
struct
sock_filter
// Filter block
{
__u16 code;
// Actual filter code
__u8 jt;
// Jump true
__u8 jf;
// Jump false
__u32 k;
// Generic multiuse field
};
|
code对应命令代码;jt是jump if true后面的操作数,注意这里用的是相对行偏移,如2就表示向前跳转2行,而不像伪代码中使用绝对行号;jf为jump if false后面的操作数;k对应伪代码中第3列的操作数。
了解了BPF伪代码和结构,我们就可以自己定制更加简单有效的BPF filter了,如上例中的6-11行不是针对IP协议的,而我们的套接字已经指定只读取IP数据了,所以就可以把他们删除,不过要注意,行偏移也要做相应的修改。
另外,tcpdump默认只返回96字节的数据,但对大部分应用来说,96字节是远远不够的,所以tcpdump提供了-s选项用于指定返回的数据长度。
OK,下面我们就来看看怎样把过滤器安装到套接口上吧:
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
|
$tcpdump ip -d -s 2048 host 192.168.1.2
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 7
(002) ld [26]
(003) jeq #0xc0a80102 jt 6 jf 4
(004) ld [30]
(005) jeq #0xc0a80102 jt 6 jf 7
(006) ret #2048
(007) ret #0
struct
sock_filter bpf_code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 5, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 2, 0, 0xc0a80102 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 0, 1, 0xc0a80102 },
{ 0x6, 0, 0, 0x00000800 },
{ 0x6, 0, 0, 0x00000000 }
};
struct
sock_fprog filter;
filter.len =
sizeof
(bpf_code)/
sizeof
(bpf_code[0]);
filter.filter = bpf_code;
setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter,
sizeof
(filter));
|
最后加上信号处理器,以便能在程序退出前恢复网卡的工作模式。到现在我们已经可以看到一个小聚规模抓包小工具了,呵呵,麻雀虽小,但也五脏俱全啊!下面给出完整的代码。
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
162
163
164
165
|
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ETH_HDR_LEN 14
#define IP_HDR_LEN 20
#define UDP_HDR_LEN 8
#define TCP_HDR_LEN 20
static
int
sock;
void
sig_handler(
int
sig)
{
struct
ifreq ethreq;
if
(sig == SIGTERM)
printf
(
"SIGTERM recieved, exiting.../n"
);
else
if
(sig == SIGINT)
printf
(
"SIGINT recieved, exiting.../n"
);
else
if
(sig == SIGQUIT)
printf
(
"SIGQUIT recieved, exiting.../n"
);
// turn off the PROMISCOUS mode
strncpy
(ethreq.ifr_name,
"eth0"
, IFNAMSIZ);
if
(ioctl(sock, SIOCGIFFLAGS, ðreq) != -1) {
ethreq.ifr_flags &= ~IFF_PROMISC;
ioctl(sock, SIOCSIFFLAGS, ðreq);
}
close(sock);
exit
(0);
}
int
main(
int
argc,
char
** argv) {
int
n;
char
buf[2048];
unsigned
char
*ethhead;
unsigned
char
*iphead;
struct
ifreq ethreq;
struct
sigaction sighandle;
#if 0
$tcpdump ip -s 2048 -d host 192.168.1.2
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 7
(002) ld [26]
(003) jeq #0xc0a80102 jt 6 jf 4
(004) ld [30]
(005) jeq #0xc0a80102 jt 6 jf 7
(006) ret #2048
(007) ret #0
#endif
struct
sock_filter bpf_code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 5, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 2, 0, 0xc0a80102 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 0, 1, 0xc0a80102 },
{ 0x6, 0, 0, 0x00000800 },
{ 0x6, 0, 0, 0x00000000 }
};
struct
sock_fprog filter;
filter.len =
sizeof
(bpf_code)/
sizeof
(bpf_code[0]);
filter.filter = bpf_code;
sighandle.sa_flags = 0;
sighandle.sa_handler = sig_handler;
sigemptyset(&sighandle.sa_mask);
//sigaddset(&sighandle.sa_mask, SIGTERM);
//sigaddset(&sighandle.sa_mask, SIGINT);
//sigaddset(&sighandle.sa_mask, SIGQUIT);
sigaction(SIGTERM, &sighandle, NULL);
sigaction(SIGINT, &sighandle, NULL);
sigaction(SIGQUIT, &sighandle, NULL);
// AF_PACKET allows application to read pecket from and write packet to network device
// SOCK_DGRAM the packet exclude ethernet header
// SOCK_RAW raw data from the device including ethernet header
// ETH_P_IP all IP packets
if
((sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))) == -1) {
perror
(
"socket"
);
exit
(1);
}
// set NIC to promiscous mode, so we can recieve all packets of the network
strncpy
(ethreq.ifr_name,
"eth0"
, IFNAMSIZ);
if
(ioctl(sock, SIOCGIFFLAGS, ðreq) == -1) {
perror
(
"ioctl"
);
close(sock);
exit
(1);
}
ethreq.ifr_flags |= IFF_PROMISC;
if
(ioctl(sock, SIOCSIFFLAGS, ðreq) == -1) {
perror
(
"ioctl"
);
close(sock);
exit
(1);
}
// attach the bpf filter
if
(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter,
sizeof
(filter)) == -1) {
perror
(
"setsockopt"
);
close(sock);
exit
(1);
}
while
(1) {
n = recvfrom(sock, buf,
sizeof
(buf), 0, NULL, NULL);
if
(n < (ETH_HDR_LEN+IP_HDR_LEN+UDP_HDR_LEN)) {
printf
(
"invalid packet/n"
);
continue
;
}
printf
(
"%d bytes recieved/n"
, n);
ethhead = buf;
printf
(
"Ethernet: MAC[%02X:%02X:%02X:%02X:%02X:%02X]"
, ethhead[0], ethhead[1], ethhead[2],
ethhead[3], ethhead[4], ethhead[5]);
printf
(
"->[%02X:%02X:%02X:%02X:%02X:%02X]"
, ethhead[6], ethhead[7], ethhead[8],
ethhead[9], ethhead[10], ethhead[11]);
printf
(
" type[%04x]/n"
, (ntohs(ethhead[12]|ethhead[13]<<8)));
iphead = ethhead + ETH_HDR_LEN;
// header length as 32-bit
printf
(
"IP: Version: %d HeaderLen: %d[%d]"
, (*iphead>>4), (*iphead & 0x0f), (*iphead & 0x0f)*4);
printf
(
" TotalLen %d"
, (iphead[2]<<8|iphead[3]));
printf
(
" IP [%d.%d.%d.%d]"
, iphead[12], iphead[13], iphead[14], iphead[15]);
printf
(
"->[%d.%d.%d.%d]"
, iphead[16], iphead[17], iphead[18], iphead[19]);
printf
(
" %d"
, iphead[9]);
if
(iphead[9] == IPPROTO_TCP)
printf
(
"[TCP]"
);
else
if
(iphead[9] == IPPROTO_UDP)
printf
(
"[UDP]"
);
else
if
(iphead[9] == IPPROTO_ICMP)
printf
(
"[ICMP]"
);
else
if
(iphead[9] == IPPROTO_IGMP)
printf
(
"[IGMP]"
);
else
if
(iphead[9] == IPPROTO_IGMP)
printf
(
"[IGMP]"
);
else
printf
(
"[OTHERS]"
);
printf
(
" PORT [%d]->[%d]/n"
, (iphead[20]<<8|iphead[21]), (iphead[22]<<8|iphead[23]));
}
close(sock);
exit
(0);
}
|
参考资料:
[1] Linux下Sniffer程序的实现
[2] 使用socket BPF