この記事ではIPv4ヘッダーに興味がある方に向けてヘッダーの構造やIPパケットの解析方法、IPv4ヘッダーを自らプログラミングして送信する方法を解説します。
そのため次のスキルセットを持っていることを前提にしています。
- IP、TCP、UDPの違いを知っている
- C言語でのプログラミング経験がある
目次
IPv4ヘッダーフォーマット
インターネットで送受信されるパケットの先頭にはIPv4ヘッダーがあります。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- Version(4bit):IPバージョン(バージョンは4です)
- IHL(4bit):4オクテット単位でIPヘッダーサイズ表す(IPヘッダーは最低20バイトなので、ここの値は最低でも5が格納される)
- Type of Service(8bit):TOS値
- Total Length(16bit):パケット全体のサイズ
- Identification(16bit):ID番号(パケットがフラグメント化された際はこのID番号を基に再構築される)
- Flags(3bit):フラグメント状態
- Fragment Offset(13bit):ペイロードの先頭パケットからのオフセット値を8オクテット単位で表す
- Time to Live(8bit):TTL値
- Protocol(8bit):上位プロトコルのプロトコル番号
- Heaer Checksum(16bit):IPヘッダーのチェックサム
- Source Address(32bit):送信元IPアドレス
- Destination Address(32bit):送信先IPアドレス
- Options(可変):タイムスタンプやソールルーティングなどオプションがあればここに格納される
- Padding(可変):IPヘッダーのオプションは可変長のため4の倍数に収まるようにパディングする
重要な箇所を説明すると、IHL
はIPヘッダーサイズを4オクテット単位で表します。この値は4ビットなので最大値は60となります。IPヘッダーの最小値は20バイトですから、IPオプションは40バイトまで追加できる事が分かります。IPオプションはソースルーティングなどで使用されますが、実際にはIPオプションはほとんど使われていません。
Identification
はID番号と呼ばれるものでIPパケットの識別に使われます。パケットをフラグメントした際はこの値を基に再構築されます。
Fragment Offset
はパケットをフラグメントした際にペイロード部分の先頭からのオフセットを8オクテット単位で表します。そのためフラグメント化した先頭のパケットはFragment Offset
の値が「0」になります。
パケットを解析してIPv4ヘッダーを表示する
この章ではパケットを解析してIPv4ヘッダーをコンソール画面に表示させる、簡易的なtcpdumpのようなプログラムを作成します。
プログラムを作成する前にC言語のヘッダーファイルでIPv4ヘッダーの構造を確認しておくと理解が深まります。Linuxの場合/usr/include/netinet/ip.h
にIPv4ヘッダーの構造体が定義されています。また、macOSや*BSD(FreeBSDやOpenBSD、NetBSDなど)も同様の場所にヘッダーファイルがあるはずです。
Linuxの場合、2種類の構造体が用意されています。最初に掲載するのはLinux固有の構造体です。Linuxだけ扱う場合はこの構造体でも悪くないのですが、macOSや*BSDなどマルチプラットフォームでコンパイルできるようにするためにはもうひとつの構造体を使った方が良いでしょう。
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. */
};
以下はBSD由来の構造体です。本記事ではこちらの構造体を使います。
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ip_hl:4; /* header length */
unsigned int ip_v:4; /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
unsigned int ip_v:4; /* version */
unsigned int ip_hl:4; /* header length */
#endif
uint8_t ip_tos; /* type of service */
unsigned short ip_len; /* total length */
unsigned short ip_id; /* identification */
unsigned short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
uint8_t ip_ttl; /* time to live */
uint8_t ip_p; /* protocol */
unsigned short ip_sum; /* checksum */
struct in_addr ip_src, ip_dst; /* source and dest address */
};
ip_hl:4
とip_v:4
の:4
の箇所は4ビットという意味です。また、#if __BYTE_ORDER == __LITTLE_ENDIAN
でバイトオーダーによってip_hl
とip_v
を入れ替えています。後で改めて解説しますが、IP通信はネットワークバイトオーダーで通信が流れています。ネットワークバイトオーダーはビックエンディアンと同じです。そのためx86などリトルエンディアンのCPUを使う場合は常にバイトオーダーに注意する必要があります。
通信を受信する方法はOSによって異なる
HTTP通信などを受信する場合とは異なり、IPv4ヘッダーを含めて通信を受信する方法はOSによって異なります。
Linuxの場合はroot権限でsocketを開き通信を受信します。macOSや*BSDは/dev/bpfというデバイスファイルをroot権限で開いてデバイスから通信を読み出します。
このようにOSによって差異があるため、その差異を吸収するためにtcpdumpで使われるlibpcap
という便利なライブラリが存在します。本来はlibpcapを使った方がプログラムの作成が楽なのですが今回はライブラリに頼らず自力で通信を受信する方法を解説します。
LinuxではIPヘッダーを含めて受信する場合はsocket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
のように記述してソケットを作成します。見慣れないhtons(ETH_P_ALL)
というものがありますが、これはすべての通信を対象にするという意味です。
そのためIPv4以外の通信(たとえばIPv6やARP)も受信しますからif (ip->ip_v != 0x4)
のようにしてIPv4でない場合は無視しています。
パケットを扱う際はバイトオーダーに注意する
また、パケットを扱う際に注意しなければいけないのはバイトオーダーです。さきほど、インターネットはネットワークバイトオーダーで流れていると解説しました。ネットワークバイトオーダーはビッグエンディアンと同じであるためリトルエンディアンの環境ではバイトの並び順が一致しません。
たとえばビッグエンディアンの「0x1234」という16bitのデータはリトルエンディアンの環境では「0x3412」と表現されます。また、ビッグエンディアンの「0x12345678」という32ビットのデータはリトルエンディアンの環境では「0x78563412」となります。
- ビッグエンディアン:
12
34
56
78
- リトルエンディアン:
78
56
34
12
そのためネットワークバイトオーダーで受信したデータをリトルエンディアンの環境で正しく扱うためにntohs()
を使う必要があります。
ntohs()
はネットワークバイトオーダーのデータをホストバイトオーダーに変換します。ntohsのn
は「ネットワークバイトオーダー(Network Byte Order)」の事でto
は「To」です。そしてh
は「ホストバイトオーダー(Host Byte Order)」で最後のs
は「Short」となります。つまり、Short(16bit)のデータをネットワークバイトオーダーからホストバイトオーダーに変換する、という意味です。
ちなみに32bitのデータをネットワークバイトオーダーからホストバイトオーダーに変換する場合はntohl()
のようにLongを意味する「l」が付きます。
IPv4パケットを受信してヘッダー値を表示するサンプルプログラム
次のCコードは非常にシンプルですが、IPv4通信を送受信するとコンソール画面にIPv4ヘッダーの値を表示します。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <netinet/ip.h>
#include <net/ethernet.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int
main(void)
{
int sd;
int read_len;
char buf[256];
struct ip *ip;
if ((sd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) < 0) {
perror("socket");
exit(-1);
}
while (1)
{
if ((read_len = read(sd, buf, sizeof(buf))) < 0) {
perror("read");
exit(-1);
}
ip = (struct ip *)buf;
if (ip->ip_v != 0x4)
continue;
printf("======== read_len: %d bytes ========\n", read_len);
printf("ip_v: %d\n", ip->ip_v);
printf("ip_hl: %d\n", ip->ip_hl);
printf("ip_tos: %d\n", ip->ip_tos);
printf("ip_len: %d\n", ntohs(ip->ip_len));
printf("ip_id: %d\n", ntohs(ip->ip_id));
printf("ip_off: %d\n", ntohs(ip->ip_off));
printf("ip_ttl: %d\n", ip->ip_ttl);
printf("ip_p: %d\n", ip->ip_p);
printf("ip_sum: %d\n", ntohs(ip->ip_sum));
printf("ip_src: %s\n", inet_ntoa(ip->ip_src));
printf("ip_dst: %s\n", inet_ntoa(ip->ip_dst));
}
}
このCコードを「linux-read_ipv4_packet.c」として保存しました。コンパイルする際にオプションは不要です。
cc linux-read_ipv4_packet.c
実行する際はroot権限が必要になります。無限ループするので終了する際はCtrl-Cで終了してください。
$ sudo ./a.out
======== read_len: 84 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 84
ip_id: 23691
ip_off: 16384
ip_ttl: 64
ip_p: 1
ip_sum: 6778
ip_src: 192.168.0.250
ip_dst: 1.1.1.1
======== read_len: 84 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 84
ip_id: 27958
ip_off: 0
ip_ttl: 55
ip_p: 1
ip_sum: 21199
ip_src: 1.1.1.1
ip_dst: 192.168.0.250
^C
$
この表示は1.1.1.1へpingを実行した際のものです。そのためip_p
の値はICMPを意味する「1」となっています。
IPv4ヘッダーを自作して送信する
IPv4パケットのフォーマットについて理解を深めることができました。それでは次にIPv4パケットを送信します。
IPv4パケットの送信は応用がきくので、このスキルをマスターするとネットワークハッキングが楽しくなるはずです。それでは始めましょう。
IPv4パケットの送信はsocketを使う
IPv4パケットを送信する際はLinuxでもmacOSでも*BSDでもsocketを使います。この部分に関してはUnix系OSであれば差異がありません。
ただし、LinuxとBSD系のOSではIPヘッダーの値の設定方法が異なるため注意が必要となります。本来はライブラリを使うべきところですが、この章でも自力で実装していきます。
OSごとに異なるドキュメント化されていない仕様
LinuxとmacOSや*BSDでは微妙に仕様が異なります。これを知らないと正しく実装できないので、それらの違いについて解説します。
主な違いは以下のとおりです。
- macOS、*BSDは
IP_HDRINCL
オプションが必須 - Linuxは
IP_HDRINCL
オプションが任意 - BSDは
ip_len
の値とsendto()で渡す送信サイズが一致している必要がある - Linuxは
ip_len
の値をOSが設定する - macOS、*BSDは
ip_off
とip_len
はホストバイトオーダー、それ以外はネットワークバイトオーダーで送信する - LinuxはすべてのIPヘッダー値をネットワークバイトオーダーで送信する
C言語でIPヘッダーを含めて送信する際はソケットオプションでIP_HDRINCL
を追加する必要がありますが、Linuxはこのオプションは任意です。
ip_len
はIPパケット全体のサイズを指定するものですが、macOSや*BSDはsendto()の引数で指定する送信サイズと一致しなければなりません。Linuxの場合は間違った値を設定してもsendo()の引数で指定したサイズに変更されます。
ip_off
とip_len
については特に重要です。Linuxの場合は何も考えずネットワークバイトオーダーで送信すれば良いのですが、macOSや*BSDの場合はip_off
とip_len
をホストバイトオーダーで設定してip_id
はネットワークバイトオーダーで設定する必要があります。
バイトオーダーの変更はhtons()
を使います。パケットを受信するときに使ったntohs()
とは逆ですね。Short(16bit)のデータをホストバイトオーダーからネットワークバイトオーダーに変換します。
また、LinuxとmacOS、*BSDはどれもIPヘッダーのチェックサムをOSが設定するので自ら計算する必要がありません。
IPv4パケットを作成して送信するサンプルプログラム
次のコードは非常にシンプルですが、IPヘッダーを自在に変更してパケットを送信することができます。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
int
main(int argc, char *argv[])
{
int sd;
struct ip *ip;
struct sockaddr_in src_addr;
struct sockaddr_in dst_addr;
char buf[256];
if (argc != 3) {
fprintf(stderr, "Usage: %s <src ip address> <dst ip address>\n", argv[0]);
exit(-1);
}
/*
* 送信元IPアドレスと宛先IPアドレスを設定する
*/
if (inet_pton(AF_INET, argv[1], &src_addr.sin_addr) < 0) {
perror("inet_pton");
exit(-1);
}
if (inet_pton(AF_INET, argv[2], &dst_addr.sin_addr) < 0) {
perror("inet_pton");
exit(-1);
}
if ((sd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {
perror("socket");
exit(-1);
}
/*
* IPヘッダを作成する
*/
ip = (struct ip *)buf;
ip->ip_v = 4; /* IPv4 */
ip->ip_hl = 5; /* オプションがないのでヘッダーサイズは20バイト */
ip->ip_tos = 0; /* TOS */
ip->ip_len = htons(0); /* Linuxはこの値を設定しなくてもOK */
ip->ip_id = htons(1234); /* IPID */
ip->ip_off = htons(0); /* フラグメントオフセット */
ip->ip_ttl = 64; /* TTL */
ip->ip_p = IPPROTO_RAW; /* プロトコル番号 */
ip->ip_sum = 0; /* チェックサムはOSが計算して設定する */
ip->ip_src = src_addr.sin_addr; /* 送信元IPアドレス */
ip->ip_dst = dst_addr.sin_addr; /* 宛先IPアドレス */
if (sendto(sd, buf, sizeof(struct ip), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr)) < 0) {
perror("sendto");
exit(-1);
}
close(sd);
return 0;
}
このCコードを「linux-send_ipv4_packet.c」として保存しました。コンパイルする際にオプションは不要です。
cc linux-send_ipv4_packet.c
実行する際はroot権限が必要になります。LAN内であれば送信元IPアドレスを偽装することもできます。試しに送信元IPアドレスを「1.2.3.4」に設定して送信してみます。1番目の引数が送信元IPアドレス、2番目の引数が送信先IPアドレスです。
sudo ./a.out 1.2.3.4 192.168.0.250
前半に作成したIPv4パケットを受信してコンソール画面に表示するプログラムで正常に送信できているのか確認します。
======== read_len: 20 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 20
ip_id: 4660
ip_off: 0
ip_ttl: 255
ip_p: 255
ip_sum: 58126
ip_src: 1.2.3.4
ip_dst: 192.168.0.250
成功です!送信元IPアドレスを偽装できていますね。IPヘッダーの値を書き換えて、どのように送信されるのか色々と試してみてください。
補足事項
さきほど「LAN内であれば送信元IPアドレスを偽装することもできます」と書きました。その理由は、 インターネット経由で送信する場合、ISP(インターネットサービスプロバイダー)の網内で異常検知され遮断されるためです。
たとえばわたしが利用しているISPの網内からみると1.2.3.4は外部ネットワークのIPアドレスです。本来、ISP網の外から発信される通信が網の中から発信されるというは異常でありIPスプーフィング攻撃(あるいはDoSアタック)と判定され網の中で破棄されます。そのため、インターネット上でIPアドレスを偽装して送信するのは難しいケースが多いです。
まとめ
今回は基礎中の基礎であるIPヘッダーを取り上げました。非常にシンプルなサンプルプログラムを掲載しましたが、応用させると高度なパケットアナライザーやパケットジェネレーターを作成できます。
まずはIPヘッダーを自在に操れるようにしておき、それからICMPやUDP、TCPへと発展させていきましょう。低レベルレイヤーのネットワークプログラミングに興味がある方はUNIXネットワークプログラミングが大変おすすめです。