Linux socket プログラミング/パケットモニタリングプログラム

目次

概要

NIC(ネットワークインターフェースカード)が受け取ったネットワーク上のパケットを抜き出しヘッダー情報を表示するプログラムです。表示されるパケットは TCP パケットに限定しています。

プログラムのイメージ

このプログラムで使われる関数のイメージを以下に示します。

流れ解説
socket(PF_PACKET,SOCK_DGRAM, )PF_PACKET ドメインの DGRAM ソケットを作成
ioctl(,SIOCGIFINDEX,)インターフェースのインデックスを得る
setsockopt(,,PACKET_ADD_MEMBERSHIP,,)無差別受信モードに設定
recvfrom()パケットを受信(繰り返す)
printf()パケットのヘッダーを表示(繰り返す)

無差別受信モードへの設定の方法は、かつては、

INET ドメインの SOCK_PACKET ソケットを作成し、ifr_flags に IFF_PROMISC をセットする

というやり方が主流だったのですが、現在は上記のように

PF_PACKET ドメインの SOCK_RAW ソケットもしくは SOCK_DGRAM ソケットを作り、setsockopt()にて無差別受信モードをセットする

というやり方が主流のようです。(man packet(7) 参照)

ソースコード

    1  /*
    2   *  packet monitor program
    3   *  pckmon2.c
    4   *  cc pckmon2.c -lnsl -o pckmon2
    5   */
    6  #include <stdio.h>
    7  #include <stdlib.h>
    8  #include <netinet/in.h>
    9  #include <errno.h>
   10  #include <netdb.h>
   11  #include <netinet/tcp.h>
   12  #include <netinet/ip.h>
   13  #include <sys/socket.h>
   14  #include <arpa/inet.h>
   15  #include <sys/ioctl.h>
   16  #include <net/if.h>
   17  #include <net/ethernet.h>
   18  #include <netpacket/packet.h>
   19
   20  void recv_pkt();
   21  int sock ;
   22
   23  int
   24  main(int argc, char *argv[])
   25  {
   26      struct ifreq ifr;
   27      struct packet_mreq mreq;
   28
   29      if(argc != 2) {
   30          printf("Usage: %s interface\n", argv[0]);
   31          exit(1);
   32      }
   33
   34      if ((sock = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) < 0 ){
   35          perror("socket");
   36          exit(1);
   37      }
   38
   39      strcpy(ifr.ifr_name, argv[1]);
   40
   41      if(ioctl(sock, SIOCGIFINDEX, &ifr) < 0 ){
   42          perror("ioctl SIOCGIFINDEX");
   43          exit(1);
   44      }
   45
   46      memset(&mreq,0,sizeof(mreq));
   47      mreq.mr_type = PACKET_MR_PROMISC;
   48      mreq.mr_ifindex = ifr.ifr_ifindex;
   49
   50      if((setsockopt(sock,SOL_PACKET,PACKET_ADD_MEMBERSHIP,(void *)&mreq,sizeof(mreq))) < 0){
   51          perror("setsockopt");
   52          exit(1);
   53      }
   54
   55      recv_pkt();
   56  }
   57
   58  /***********************************************
   59   * recv_pkt()
   60   * Receive and display packets
   61   ***********************************************/
   62  void
   63  recv_pkt()
   64  {
   65      int rsin_size, count;
   66      struct sockaddr_in rsin;
   67      struct in_addr insaddr,indaddr;
   68      fd_set fds;
   69
   70      struct buf  {
   71          struct iphdr ip;
   72          struct tcphdr tcp;
   73          unsigned char blah[65535];
   74      } buf;
   75
   76      rsin_size = sizeof(rsin);
   77
   78      FD_ZERO(&fds);
   79      FD_SET(sock, &fds);
   80
   81      for ( count = 0 ;; count++){
   82          if( select(sock + 1, &fds , NULL, NULL, NULL) < 0 ){
   83              perror("select");
   84              exit(1);
   85          }
   86
   87          if ( FD_ISSET(sock, &fds)){
   88              if(recvfrom(sock,&buf,sizeof(buf),0,(struct sockaddr *)&rsin,&rsin_size) < 0 ){
   89                  perror("recvfrom");
   90              }
   91              /*
   92               * Ignore the packets other than TCP
   93               */
   94              if ( buf.ip.protocol != IPPROTO_TCP)
   95                  continue;
   96              insaddr.s_addr = buf.ip.saddr;
   97              indaddr.s_addr = buf.ip.daddr;
   98
   99              printf("Packet number : %d\n", count);
  100              printf("----IP Header--------------------\n");
  101              printf("version     : %u\n",buf.ip.version);
  102              printf("ihl         : %u\n",buf.ip.ihl);
  103              printf("tos         : %u\n",buf.ip.tos);
  104              printf("tot length  : %u\n",ntohs(buf.ip.tot_len));
  105              printf("id          : %u\n",ntohs(buf.ip.id));
  106              printf("frag_off    : %u\n",ntohs(buf.ip.frag_off) & 8191);
  107              printf("ttl         : %u\n",buf.ip.ttl);
  108              printf("protocol    : %u\n",buf.ip.protocol);
  109              printf("check       : 0x%x\n",ntohs(buf.ip.check));
  110              printf("saddr       : %s\n",inet_ntoa(insaddr));
  111              printf("daddr       : %s\n",inet_ntoa(indaddr));
  112
  113              printf("----TCP Header-------------------\n");
  114              printf("source port : %u\n",ntohs(buf.tcp.source));
  115              printf("dest port   : %u\n",ntohs(buf.tcp.dest));
  116              printf("sequence    : %u\n",ntohl(buf.tcp.seq));
  117              printf("ack seq     : %u\n",ntohl(buf.tcp.ack_seq));
  118              printf("frags       :");
  119              buf.tcp.fin ? printf(" FIN") : 0 ;
  120              buf.tcp.syn ? printf(" SYN") : 0 ;
  121              buf.tcp.rst ? printf(" RST") : 0 ;
  122              buf.tcp.psh ? printf(" PSH") : 0 ;
  123              buf.tcp.ack ? printf(" ACK") : 0 ;
  124              buf.tcp.urg ? printf(" URG") : 0 ;
  125              printf("\n");
  126              printf("window      : %u\n",ntohs(buf.tcp.window));
  127              printf("check       : 0x%x\n",ntohs(buf.tcp.check));
  128              printf("urt_ptr     : %u\n\n\n",buf.tcp.urg_ptr);
  129          }
  130      } /* for() loop */
  131  }

ソースファイル pckmon2.c

実行例

プログラム名の後に引数としてパケットをキャプチャする NIC を指定します。 なお、このプログラムは root ユーザで実行する必要があります。

# ./pckmon2 eth0				  
Packet number : 0				  
----IP Header--------------------		  
version     : 4					  
ihl         : 5					  
tos         : 0					  
tot length  : 40				  
id          : 14879				  
frag_off    : 0					  
ttl         : 128				  
protocol    : 6					  
check       : 0xd575				  
saddr       : 172.29.73.2			  
daddr       : 172.29.73.254			  
----TCP Header-------------------		  
source port : 1270				  
dest port   : 22				  
sequence    : 1671008060			  
ack seq     : 1093140794			  
frags       : ACK				  
window      : 17340				  
check       : 0x4699				  
urt_ptr     : 0					  
						  
						  
Packet number : 1				  
----IP Header--------------------		  
version     : 4					  
ihl         : 5					  
tos         : 16				  
tot length  : 540				  
id          : 11933				  
frag_off    : 0					  
ttl         : 64				  
protocol    : 6					  
check       : 0x1ef4				  
saddr       : 172.29.73.254			  
daddr       : 172.29.73.2			  
----TCP Header-------------------		  
source port : 22				  
dest port   : 1270				  
sequence    : 1093140794			  
ack seq     : 1671008060			  
frags       : PSH ACK				  
window      : 6432				  
check       : 0xbbdc				  
urt_ptr     : 0                                   

NIC に到着した TCP のパケットを書き出しています。 内容をみると、SSH のセッションのパケットであることが分かります。

解説

このプログラムではいままでのプログラムと同様に socket() を呼びソケットを作成しています。しかし、いままでのプログラムの違うのはソケットを作成するときドメインとして PF_PACKET をセットしているところです。

   34      if ((sock = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) < 0 ){

これにより、 TCP のデータ部だけでなく、よりデバイスに近い情報である、IP や TCP のヘッダー情報までも参照できるようになります。上記プログラムでは行っていませんが、ソケットタイプとして SOCK_RAW を指定すれば Ethernet ヘッダーも見ることもできます。

  • 通常はデータしか見れない
    Ethernet ヘッダー
    IP ヘッダー
    TCP ヘッダー
    データ

  • PF_PACKET ドメインの SOCK_DGRAM を指定した場合、IP、TCP ヘッダーもみれる。本プログラムはここまで
    Ethernet ヘッダー
    IP ヘッダー
    TCP ヘッダー
    データ

  • PF_PACKET ドメインの SOCK_RAW を指定した場合、さらに Ethernet ヘッダーもみれる
    Ethernet ヘッダー
    IP ヘッダー
    TCP ヘッダー
    データ

また、データを受け取るときに先の POPクライアントプログラムでは recv() システムコールを使っていましたが、今回は recvfrom() システムコールを使っています。 recv() システムコールはコネクション型(=SOCK_STREAM)接続にて、コネクションの確立が終わった後に、そのソケット記述子を使ってデータを受信していましたが、recvfrom() システムコールの場合はコネクションレス型(=SOCK_DGRAM)で使われ、ソケット記述子 と socket address を使って packet を識別し、データを受信します。ようするに、コネクション型の場合は最初に相手先との専用の道を作ってしまうので、到着するパケットがだれからきたものだか分かりますが、コネクションレス型の場合は 相手を確認するためにのアドレスやポート番号を確認する必要があるという事です。

recvfrom()
概要メッセージの受信
インクルードファイル#include <sys/types.h>
#include <sys/socket.h>
形式int recvfrom(int s, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);
引数int s : socket 記述子
void *buf: 受信メッセージのバッファアドレス
int len: 受信メッセージのサイズ
unsigned int flags: 受信メッセージのフラグ
struct sockaddr *from: socket address のポインタ
int *fromlen: sockaddr のサイズ
戻り値成功時: 受信したバイト数
エラー: -1

後は recvfrom で受け取ったデータ(=buf)を以下の構造体に当てはめてやります。

   70      struct buf  {
   71          struct iphdr ip;           --->  IP ヘッダの構造体
   72          struct tcphdr tcp;         --->  TCP ヘッダの構造体
   73          unsigned char blah[65535]; --->  データ
   74      } buf;

IP ヘッダ、TCPヘッダは /use/include/netinet/ip.h および tcp.h でそれぞれ iphdr 、 tcphdr 構造体として宣言されています。最後のデータは 65535 byte の キャラクターとして宣言していますが、これは TCP の最大長が 65535 byte であるためで、全データがこの構造体の範囲を越える事はありません。たぶん。
これで受け取ったデータの最初の部分が、ip ヘッダ と tcp ヘッダ として割り振られた事になるので、次は printf() にてこの構造体の各メンバを 出力しているという事です。
IP 、TCP ヘッダ構造体の各メンバの出力時には メンバごとに少々アレンジしていて、port 番号、window size については ntoh() 関数を用いてバイト序列をネットワーク型からホスト型へ変換してから表示しています。また、IP アドレスについては 一度 in_addr 構造体に当てはめてから(96 〜97行目) inet_ntoa() 関数を使って、おなじみの数字+ドット表記のアドレスに変換しています。inet_ntoa() を使わないと 2 進数や 16 進数での表示になってしまい、直感的にアドレスが分かりません。しかしながら、一度 in_addr に当てはめるという行為がはたして正しいのかは不明です。(そうしないとできなかったもので、、)なにか他にいい方法があるような気がしますが、分かりませんでした。

最後に・・・

相変わらず自分でも不明な部分が多く、 汚いソースですが、色々応用して使えるのではないかと思います。今回は for 文にて次々にパケットを表示しておりますが、IP addres や port 番号でフィルタをかけるようにすれば必要なパケットだけをフィルタリングするようにも出来ますし、今回は利用しておりませんが,tcp のデータ部(=blah)を出力してやれば、telnet や pop などの 通信内容もモニターできるようになるはずです。


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-01-03 (月) 20:17:35