基於 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_rcv

  • packet_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