追記 2022/04/06
この内容についてスライドも作りました。
speakerdeck.com
TL;DR
TL;DRとは
この記事は #kosen10s Advent Calendar 2018 - Adventar の 17日目の記事です。遅刻して申し訳ございません。
昔、ちょっと調べた iptables の u32 というオプションでどんなことができるのか、というのを例を交えてご紹介します。
ちなみに「これ見たことがあるぞ!」という人がいるかもですが、記憶を頼りに調べなおして見た感じです。
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x01" -A INPUT -j DROP
すれば icmp_seq
が偶数のときだけ通って、
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x00" -A INPUT -j DROP
すれば icmp_seq
が奇数のときだけ ping が通るような不思議サーバができます、
という話をパケットの仕組みを交えながら解説します。
前書き
iptables を使うと、いろいろな通信について、カウント(ロギング)や許可、排除などができます。
(実を言うと iptables 自体に慣れ親しんでいるわけでもないので、どこまでできるのか、というのは詳しくはありません。 )
ですが、そんな iptables でも手が届かない場所ってのはあって、そんなケースにおいてもパケット自体を解析してルール化できるのが iptables u32
です。
今回は下記実例をもとに、紹介できればと思います。
なお、軽くは紹介しますが、この記事を読むに当たって、 IPヘッダの図 と ICMPヘッダの図 と、 TCP ヘッダの図 を片手に読まれるとスムーズに読めると思います。
想定できる利用用途
といってもそんなに想定できません。
逆に、変なときにこれを知っていると実現できるかも、みたいなケースがあるかもしれません。
詳しいことは忘れて、存在だけ覚えておくといいかもしれないです。
パケットの構造さえ理解できれば、様々な種類のパケットに対応できるので、使い方は無限大だと思います。
共通お作法
使い方
iptables -m u32 --u32
まではお約束になります。
それ以降は、パケットの気持ちになってスタート位置を変えたり、シフト演算したり、アンド演算したりして、自分のほしい条件を作っていきます。
末尾に iptables っぽい条件を追加することで、該当ルールとして作動させることができます。
実例
今回は、二つのケースを用意しました。
対向から ping を飛ばし、 2 回に 1 回 受ける側で drop する
対向から curl をずっとし続けて、 シーケンス 番号が偶数のものを drop する。
1. 対向から ping を飛ばし、 2 回に 1 回 drop する についての解説
先にルールを書いて、そこから解説していきます。
偶数だけ通る
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x01" -A INPUT -j DROP
奇数だけ通る
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x00" -A INPUT -j DROP
自ホストの応答 output でやる場合
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x00 && 0>>22&0x3C@6>>16&0x01=0x01" -A OUTPUT -j DROP
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x00 && 0>>22&0x3C@6>>16&0x01=0x00" -A OUTPUT -j DROP
確認方法
ホスト A から、 ping を ホスト B に対して打ち続け、意図したとおりの状況になることを確認する
ルール解説
6&0xFF=0x1
について
iptables の u32 が解析するのは IP ヘッダからになります。
まずは、IPヘッダのプロトコル 番号から、ICMP パケットかどうかを調べます。
プロトコル番号一覧 - Wikipedia を参考に、ICMP の場合は、 1 であることがわかりました。
u32
では、0 を IPヘッダの先頭とし、1バイトずつスタート地点をずらし、指定した起点から4バイトを読み込むことができます。
プロトコル 自体は、IPヘッダの先頭から10バイト目にあります。 そのため、 6 を指定し、 7 ~ 10 バイト目を読み込みます。
また、読み込んだ 4byte は、1つの4byte列として処理を行います。(これを1オクテットと呼びます)
例として ping を受ける側で tcpdump -x
してみたところ、4000 4001
(16進数) がターゲットに入りました。
先頭 3bit がフラグにあたり、010
であることがわかります。
そこからの 4bit ~ 16bit までの 13 ビットがフラグメントオフセットで、 0 であることがわかります。
きりが良くなり 3byte 目がTTL で、0x40
であることがわかります。
最後の 1byte がプロトコル になり、0x01
であることがわかります。
ここで、判定したい情報は最後の 1byte の 0x01
であるため、 0xFF
で AND マスクを行います。すると、4byte に格納された情報は、0x00000001
に変わります
これが 6&0xFF
までの処理で、あとはその値が 0x1
と等しいかを比較したいので、6&0xFF=0x1
となりました。
&& 0>>22&0x3C@0>>24=0x08
または 0>>22&0x3C@0>>24=0x00
について
さっきまでの内容で、とりあえず ICMP パケットだけを絞り込むことができました。
と言っても、ICMP は利用範囲が大きいので、 echo-reply のみに絞れる方法を探します。
ここでは、ICMP パケットのタイプを利用します。
ICMP の タイプが 8 なら echo-request となるため、ここで範囲を絞ります。
なお、応用として、 echo-reply で弾く場合は、ここを 0 と考えて、OUTPUT に仕込みます
さっきまでの処理は IP パケットの中身で行っていましたが、今度は IP パケットの先にある、ICMP パケットに踏み込まなければなりません。
しかし、IPパケットには、「オプション」というフィールドが存在し、可変長として定義されています。
そのため、なんとかして、「IPパケットのサイズを知って、その先に踏み込む」処理が必要です。
では、IPパケットの構造に戻ります。実は IP パケットの先頭 1byte の後半 4bit に、「ヘッダ長」というエリアがあります。
例として取得してみると、先頭 4byte が 4500 0054
でした。これだと、どうやらヘッダ長は 5
みたいです。
しかし、ヘッダ長は、「オクテット数」を記録しており、5オクテット、つまり 20byte 目までは IP ヘッダであることを指します。
先程の話で、 6
で 7 byte 目まで移動できると説明しました。つまり次はここに、 20
を投入できれば今回のケースでの 21byte目、つまり ICMP ヘッダに踏み込むことができます。
しかしながら、現在、0x45000054
という列を相手にしているため、現状だとマスキングするだけでは、 0x05000000
という馬鹿でかいものが手に入ってしまい、うまく使えなそうです。
ここで、シフト演算が登場します。 今回の例だと、 0>>24
みたいにすると、0x00000045
にデータ加工することができます。しかしこれではほしい 20
は手にはいりません。
ここで 20 を入手するためのちょっとしたテクニックなんですが、シフト演算の性質を利用して、 0>>24
せずに、 0>>22
することで、最初から ヘッダ長に 4 をかけられた状態を情報を入手しています。
しかし、そのままだと 「サービス種別」の先頭 2bit が紛れ込んでいるため、 &0x3C
することでマスキングしています。
0x3C
は、 0b00111100
となり、中途半端にシフトされた 4bit からほしい真ん中だけを抜き出すことができます。
解説にここの処理についてはイメージを作りました。
IPヘッダの先頭4byteイメージ
>>22 したあとのイメージ
&0x3C したイメージ
こうすることで、実質 && 20
から始まりそうなんですが、これだとまだ 20 という数値が手に入っただけで、移動できていません。
続いて、 @0
というのが出てきました。これは先程触れた、移動をしてくれる記号です。
@
のあとについている 0
は初期条件同様、移動先から何byte 目から読み出すかを示します。
今回はICMPパケットの先頭 4byte を読みたいので、0 から読み出すことにします。
これでやっと ICMP パケットに踏み出すことに成功しました。取得したパケット例では、 0800 eaea
が記録されていました。
先頭 1byte がタイプにあたり、今回は 0x08
であることがわかります。
次の 1byte がコードにあたり、今回は 0x00
であることがわかります。
最後の 2byte がチェックサム にあたり、今回は、 0xeaea
に当たります。
ここから、欲しい情報である先頭 1byte を抜き出すために >>24
します。すると、 0x08
のみがやっと手に入りました。
ここで、ping の受け手の INPUT 時であれば、8 が入っているはずなので、 =0x08
で比較します。
これを、一旦 ping の Echo Request 自体は受けて、返答時に落とすという変わった方法を取る場合は、 ここが 0 になるため、 =0x00
で比較します。
ここまでの条件を使うと、ICMP の Echo Request をすべて落とすことができます。
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08" -A INPUT -j DROP
すると、外からの ping はすべて落ちると思います。
0>>22&0x3C@6>>16&0x01=0x01
ここで混乱を招かないように解説ですが、 &&
で繋いだあとはスタート地点が最初の 0 に戻ります。
続いて、「奇数のときだけ落とす」を実現するためにICMPパケットを更に深堀します。
0>>22&0x3C@
までは先程と同様で、IPヘッダから IPヘッダサイズを取得して移動しています。
しかし、今回は、 @6
になります。これは、Echo Message / Echo Reply Message 時に利用される、「シーケンス番号」が 7byte - 8byte 目に含まれるためです。
@6
することで、 7byte ~ 10byte までの 4byte が手にはいります。これが 0>>22&0x3C@6
までのハイライトです。
ここから、シーケンス番号が先頭2byte にあるため、一気に >>16
で前半 2byte のみにします。
あとは、奇数かどうかを判定するだけなので、末尾 1bit が 1 かどうかを調べるので、 &0x01
でマスキングして、 0x01
かどうかを比較すれば終わります。
あとはお好きな CHAIN に、お好きなルールで投入してみてください。
動作確認結果
対向から ping を 10 発打って結果を確認しました。
下記に2パターンの結果を載せていますが、正常に動作していそうです。
地味に奇数のみ許可時に 50% packet loss
を表示させるのが難しい。素直にカウント指定を入れればよかったけど、勘でいい感じに成功しました。
偶数のみ許可時
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x01" -A INPUT -j DROP
した状態です。
do-su-0805@ubuntu01:~$ ping 192.168.1.7
PING 192.168.1.7 (192.168.1.7) 56(84) bytes of data.
64 bytes from 192.168.1.7: icmp_seq=2 ttl=64 time=0.329 ms
64 bytes from 192.168.1.7: icmp_seq=4 ttl=64 time=0.325 ms
64 bytes from 192.168.1.7: icmp_seq=6 ttl=64 time=0.311 ms
64 bytes from 192.168.1.7: icmp_seq=8 ttl=64 time=0.320 ms
64 bytes from 192.168.1.7: icmp_seq=10 ttl=64 time=0.317 ms
^C
--- 192.168.1.7 ping statistics ---
10 packets transmitted, 5 received, 50% packet loss, time 9210ms
rtt min/avg/max/mdev = 0.311/0.320/0.329/0.017 ms
奇数のみ許可時
iptables -m u32 --u32 "6&0xFF=0x1 && 0>>22&0x3C@0>>24=0x08 && 0>>22&0x3C@6>>16&0x01=0x00" -A INPUT -j DROP
した状態です
do-su-0805@ubuntu01:~$ ping 192.168.1.7
PING 192.168.1.7 (192.168.1.7) 56(84) bytes of data.
64 bytes from 192.168.1.7: icmp_seq=1 ttl=64 time=0.357 ms
64 bytes from 192.168.1.7: icmp_seq=3 ttl=64 time=0.367 ms
64 bytes from 192.168.1.7: icmp_seq=5 ttl=64 time=0.376 ms
64 bytes from 192.168.1.7: icmp_seq=7 ttl=64 time=0.379 ms
64 bytes from 192.168.1.7: icmp_seq=9 ttl=64 time=0.372 ms
--- 192.168.1.7 ping statistics ---
10 packets transmitted, 5 received, 50% packet loss, time 9209ms
rtt min/avg/max/mdev = 0.357/0.370/0.379/0.014 ms
2. 対向から curl をずっとし続けて、 ack 番号が偶数のものを drop する。
iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4&0x01=0x01" -A INPUT -j DROP
ルール自体は先程とだいたい類似するので、違うポイントを中心に紹介します。
実施方法
ホスト A から、 while で ホストB:80 に対して curl を 10発行い、実行ステータスを確認する。
ルール考察
6&0xFF=0x6
先ほどと同じで、プロトコル を抜き出しています。これが ICMP では 1 でしたが、 TCP だと 6 になります。
0>>22&0x3C@4&0x01=0x01
先ほどと同じで、まずは IP ヘッダ長を取得し、ジャンプしています。
次に @4
しているのは、 TCP ヘッダの 5byte - 8byte が「シーケンス番号」のためです。
あとは &0x01
することで奇数偶数判定をしています。
結果
終わりに
いかがでしたでしょうか。そもそもが複雑というのと、うまい感じに表などが使えず分かりづらかったかもしれませんが、こういう機能もあるよ、と知っていただければ幸いです。