基於 tcpdump 原理手寫抓包程序
本公衆號作者爲 Linux 內核之旅社區成員 - 董旭
基於 tcpdump 原理手動實現抓包程序
前面兩篇文章分析 tcpdump 實現抓包原理:
1、文章 1 主要從 Linux 內核角度分析 tcpdump 旁路嗅探數據包的過程
2、文章 2 主要從 Linux 內核角度分析 tcpdump 利用 BPF 機制實現數據包捕獲前的過濾過程
本次將根據前面的分析,手動寫一個基於 tcpdump 工具原理的簡易抓包程序,實現從鏈路層的抓包,加深一下關於 tcpdump 原理的印象。
流程分析
1、PF_PACKET 協議族的 socket
正如在文章 1 中分析時,以socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
爲着手點。我們通常都是通過創建 PF_PACKET 協議族的 socket, 用來抓包、分析數據的。
如下,創建一個 PF_PACKET 類型的 socket,type 指定爲 SOCK_RAW, 當指定 SOCK_RAW 時,獲取的數據包是一個完整的數據鏈路層數據包。proticol 字段設置爲 htons(ETH_P_ALL),表示接收端數據鏈路層所有協議幀。
/*socket函數原型:
#include <sys/socket.h>
sockfd = socket(int socket_family, int socket_type, int protocol);
*/
sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
return 1;
}
2、設置鏈路層屬性
核心結構體:struct sockaddr_ll
struct sockaddr_ll
{
unsigned short int sll_family; /* 一般爲AF_PACKET */
unsigned short int sll_protocol; /* 上層協議類型 */
int sll_ifindex; /* 網卡接口索引號,0 匹配所有的網絡接口卡 */
unsigned short int sll_hatype; /* 報頭類型 */
unsigned char sll_pkttype; /* 包類型 */
unsigned char sll_halen; /* 地址長度 */
unsigned char sll_addr[8]; /* MAC地址 */
};
該結構體爲設備無關的物理層地址結構,數據鏈路層的頭信息通常定義在 sockaddr_all 的結構體中,當發送數據包時,指定 sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol 就足夠了。其它字段設置爲 0;sll_hatype 和 sll_pkttype 是在接收數據包時使用的;如果要 bind, 只需要使用 sll_protocol 和 sll_ifindex 就足夠。
struct sockaddr_ll addr;
memset(&addr, 0, sizeof(addr));
addr.sll_ifindex = if_nametoindex(name);//name爲當前要抓包的網卡接口名稱
addr.sll_family = AF_PACKET;
addr.sll_protocol = htons(ETH_P_ALL);
3、將創建的 soccket 與地址綁定
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr))) {
perror("bind");
return 1;
}
4、抓包的過濾條件
在文章 2 中,介紹了 BPF 過濾機制,在 tcpdump 的過濾機制中有一個重要的結構體: struct sock_filter,同時也是 cBPF 彙編的一個框架
struct sock_filter {
__u16 code; /*指令 32位*/
__u8 jt; /* jt是指令結果爲true的跳轉 */
__u8 jf; /* jf是爲false的跳轉 */
__u32 k; /* 指令參數*/
};
該結構體一般是封裝在 struct sock_fprog 中使用:
struct sock_fprog
{
unsigned short len;
struct sock_filter *filter;
};
在文章 1 中分析過,使用 tcpdump -d 參數可以生成 BPF 彙編僞代碼:
dx@ubuntu:~$ sudo tcpdump -d 'ip and tcp port 80'
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 12
(002) ldb [23]
(003) jeq #0x6 jt 4 jf 12
(004) ldh [20]
(005) jset #0x1fff jt 12 jf 6
(006) ldxb 4*([14]&0xf)
(007) ldh [x + 14]
(008) jeq #0x50 jt 11 jf 9
(009) ldh [x + 16]
(010) jeq #0x50 jt 11 jf 12
(011) ret #262144
(012) ret #0
這種僞代碼在 C 程序中是無法使用的,需要借用 tcpdump -dd 參數生成等效的 c 代碼:
dx@ubuntu:~$ sudo tcpdump -dd 'ip and tcp port 80'
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
這段代碼就是 struct sock_filter:
static struct sock_filter bpfcode[13] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};
struct sock_fprog bpf = { 13, bpfcode };
5、設置 BPF 過濾器
Linux 在安裝和卸載過濾器時都使用了函數 setsockopt(), 其中標誌 SOL_SOCKET 代表對 socket 進行設置,SO_ATTACH_FILTER 表示安裝過濾器動作,setsockopt 在內核中的調用可以看文章 2。
setsockopt(sd, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
perror("setsockopt ATTACH_FILTER");
return 1;
}
6、設置網卡的混雜模式
關鍵結構體:struct packet_mreq
struct packet_mreq
{
intmr_ifindex; /* 接口索引 */
unsigned shortmr_type; /* mreq 類型 */
unsigned shortmr_alen; /* 地址長度 */
unsigned charmr_address[8]; /* 物理地址 */
};
混雜模式:
混雜模式就是接收所有經過網卡的數據包,包括不是發給本機的包,默認情況下網卡只把發給本機的包(包括廣播包)傳遞給上層程序,其它的包一律丟棄;簡單的講,混雜模式就是指網卡能接受所有通過它的數據流,不管是什麼格式,什麼地址,當網卡處於混雜模式時,該網卡就具有 “廣播地址”,它對所有遇到的每一個數據幀都產生一個硬件中斷,以便提醒操作系統處理流經過該物理媒體上的每一個報文包。
通過shortmr_type
字段可以設置混雜模式
struct packet_mreq mreq;
memset(&mreq, 0, sizeof(mreq));
mreq.mr_type = PACKET_MR_PROMISC;//設置混雜模式
mreq.mr_ifindex = if_nametoindex(name);
將設置的混雜模式設置到 socket:
if (setsockopt(sock, SOL_PACKET,
PACKET_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq))) {
perror("setsockopt MR_PROMISC");//ACKET_ADD_MEMBERSHIP 用於增加一個綁定
return 1;
}
7、定義要獲得的數據報文信息
源和目的 MAC 地址
關鍵結構體:
struct ether_header
{
uint8_t ether_dhost[ETH_ALEN]; /* 目的MAC地址 */
uint8_t ether_shost[ETH_ALEN]; /* 源MAC地址 */
uint16_t ether_type; /* packet type ID field */
};
IP 信息 (源、目的 IP,IP 版本)、上層協議類型
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
uint8_t tos;
uint16_t tot_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol; //上層協議類型
uint16_t check;
uint32_t saddr; //源地址
uint32_t daddr; //目的地址
/*The options start here. */
};
8、循環接收捕獲的數據包
在文章 1 中,分析了數據包是怎樣捕獲的:
回顧:
tcpdump 進行抓包的內核流程梳理
應用層通過 libpcap 庫:調用系統調用創建 socket,
sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
tcpdump 在 socket 創建過程中創建 packet_type(struct packet_type), 並掛載到全局的 ptype_all 鏈表上。(同時在 packet_type 設置回調函數 packet_rcv網絡收包 / 發包時,會在各自的處理函數 (收包時:
__netif_receive_skb_core
,發包時:dev_queue_xmit_nit
) 中遍歷 ptype_all 鏈表,並同時執行其回調函數,這裏 tcpdump 的註冊的回調函數就是 packet_rcvpacket_rcv 函數中會將用戶設置的過濾條件,通過 BPF 進行過濾,並將過濾的數據包添加到接收隊列中
應用層調用 recvfrom 。PF_PACKET 協議簇模塊調用 packet_recvmsg 將接收隊列中的數據 copy 應用層,到此將數據包捕獲到。
所以上面創建好的 PF_PACKET 類型 socket,並設置好過濾器後,當網卡有數據進出時,就已經將數據報文添加到了接收隊列上了,下面只需要我們進行 recv 獲取數據報文即可。
for (;;) {
n = recv(sock, buf, sizeof(buf), 0);
if (n < 1) {
perror("recv");
return 0;
}
//獲取鏈路層的源和目的地址
mac_hdr=(struct ether_header *)buf;
ip = (struct iphdr *)(buf + sizeof(struct ether_header));
inet_ntop(AF_INET, &ip->saddr, saddr_str, sizeof(saddr_str));
inet_ntop(AF_INET, &ip->daddr, daddr_str, sizeof(daddr_str));
switch (ip->protocol) {
#define PTOSTR(_p,_str) \
case _p: proto_str = _str; break
PTOSTR(IPPROTO_ICMP, "icmp");
PTOSTR(IPPROTO_TCP, "tcp");
PTOSTR(IPPROTO_UDP, "udp");
default:
proto_str = "";
break;
}
printf(" SMAC:%X:%X:%X:%X:%X:%X",
(u_char)mac_hdr->ether_shost[0],
(u_char)mac_hdr->ether_shost[1],
(u_char)mac_hdr->ether_shost[2],
(u_char)mac_hdr->ether_shost[3],
(u_char)mac_hdr->ether_shost[4],
(u_char)mac_hdr->ether_shost[5]
);
printf(" ==> DMAC:%X:%X:%X:%X:%X:%X ",
(u_char)mac_hdr->ether_dhost[0],
(u_char)mac_hdr->ether_dhost[1],
(u_char)mac_hdr->ether_dhost[2],
(u_char)mac_hdr->ether_dhost[3],
(u_char)mac_hdr->ether_dhost[4],
(u_char)mac_hdr->ether_dhost[5]
);
printf("IPv%d proto=%d(%s) src=%s dst=%s\n",
ip->version, ip->protocol, proto_str, saddr_str, daddr_str);
}
至此,從創建 socket,到設置 socket 過濾器、網卡工作模式,到接收捕獲的數據包就結束了。
附:源代碼
實現捕獲數據包的過濾條件:ip and tcp port 80
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <linux/filter.h>
static struct sock_filter bpfcode[13] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};
int main(int argc, char **argv)
{
int sock;
int n;
char buf[2000];
struct sockaddr_ll addr;
struct packet_mreq mreq;
struct iphdr *ip;
struct ether_header *mac_hdr;
char saddr_str[INET_ADDRSTRLEN], daddr_str[INET_ADDRSTRLEN];
char *proto_str;
char *name;
struct sock_fprog bpf = { 13, bpfcode };
if (argc != 2) {
printf("Usage: %s ifname\n", argv[0]);
return 1;
}
name = argv[1];
sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
return 1;
}
memset(&addr, 0, sizeof(addr));
addr.sll_ifindex = if_nametoindex(name);
addr.sll_family = AF_PACKET;
addr.sll_protocol = htons(ETH_P_ALL);
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr))) {
perror("bind");
return 1;
}
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
perror("setsockopt ATTACH_FILTER");
return 1;
}
memset(&mreq, 0, sizeof(mreq));
mreq.mr_type = PACKET_MR_PROMISC;
mreq.mr_ifindex = if_nametoindex(name);
if (setsockopt(sock, SOL_PACKET,
PACKET_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq))) {
perror("setsockopt MR_PROMISC");
return 1;
}
for (;;) {
n = recv(sock, buf, sizeof(buf), 0);
if (n < 1) {
perror("recv");
return 0;
}
//獲取鏈路層的源和目的地址
mac_hdr=(struct ether_header *)buf;
ip = (struct iphdr *)(buf + sizeof(struct ether_header));
inet_ntop(AF_INET, &ip->saddr, saddr_str, sizeof(saddr_str));
inet_ntop(AF_INET, &ip->daddr, daddr_str, sizeof(daddr_str));
switch (ip->protocol) {
#define PTOSTR(_p,_str) \
case _p: proto_str = _str; break
PTOSTR(IPPROTO_ICMP, "icmp");
PTOSTR(IPPROTO_TCP, "tcp");
PTOSTR(IPPROTO_UDP, "udp");
default:
proto_str = "";
break;
}
printf(" SMAC:%X:%X:%X:%X:%X:%X",
(u_char)mac_hdr->ether_shost[0],
(u_char)mac_hdr->ether_shost[1],
(u_char)mac_hdr->ether_shost[2],
(u_char)mac_hdr->ether_shost[3],
(u_char)mac_hdr->ether_shost[4],
(u_char)mac_hdr->ether_shost[5]
);
printf(" ==> DMAC:%X:%X:%X:%X:%X:%X ",
(u_char)mac_hdr->ether_dhost[0],
(u_char)mac_hdr->ether_dhost[1],
(u_char)mac_hdr->ether_dhost[2],
(u_char)mac_hdr->ether_dhost[3],
(u_char)mac_hdr->ether_dhost[4],
(u_char)mac_hdr->ether_dhost[5]
);
printf("IPv%d proto=%d(%s) src=%s dst=%s\n",
ip->version, ip->protocol, proto_str, saddr_str, daddr_str);
}
return 0;
}
運行:
注意:程序指定的參數:ens33 爲自己的網卡接口名稱,可以通過 ip addr 進行查看。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cUodZmJZ5sJTcULTvWSLkg