追記 2022/04/06
この内容についてスライドも作りました。
TL;DR
- TL;DRとは
- この記事は #kosen10s Advent Calendar 2018 - Adventar の 17日目の記事です。遅刻して申し訳ございません。
- 昨日の記事は、 id:satuma77 さんで、「掴んで見せます!自分星!」とのことでした。comming soon ってやつですね。
- ちなみにこの方は昨年度、「札幌より「ラブライブ!サンシャイン!!Aqours クラブ活動 LIVE & FAN MEETING 〜 Landing action Yeah!! 〜」の様子をお送りします」という題目で登録した挙句、
共分散行列適応進化戦略(CMA-ES)と自然勾配法 - satuma-portfolio という記事をぶっこんで来た強者です。今年も先日まで仙台にいたみたいですし、何が投稿されるのかは皆目見当がつきません。
- ちなみにこの方は昨年度、「札幌より「ラブライブ!サンシャイン!!Aqours クラブ活動 LIVE & FAN MEETING 〜 Landing action Yeah!! 〜」の様子をお送りします」という題目で登録した挙句、
- 明日の記事は id:denari01 さんです。今のところすべての advent calendar に未投稿の様子です。お忙しいんでしょうね…。
- 毎年、技術っぽい記事とそうでない生活の知恵的な記事を書いているので、これは前者側の記事です。後者側については 9日目の 転職活動まとめ - do_su_0805's blog でした。
- 昨日の記事は、 id:satuma77 さんで、「掴んで見せます!自分星!」とのことでした。comming soon ってやつですね。
- 昔、ちょっと調べた 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 っぽい条件を追加することで、該当ルールとして作動させることができます。
実例
今回は、二つのケースを用意しました。
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 からほしい真ん中だけを抜き出すことができます。
解説にここの処理についてはイメージを作りました。
こうすることで、実質 && 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
- ルール自体は先程とだいたい類似するので、違うポイントを中心に紹介します。
実施方法
ルール考察
6&0xFF=0x6
先ほどと同じで、プロトコルを抜き出しています。これが ICMP では 1 でしたが、 TCP だと 6 になります。
0>>22&0x3C@4&0x01=0x01
先ほどと同じで、まずは IP ヘッダ長を取得し、ジャンプしています。
次に @4
しているのは、 TCP ヘッダの 5byte - 8byte が「シーケンス番号」のためです。
あとは &0x01
することで奇数偶数判定をしています。
結果
- curl を 10発打って、ステータスコードで確認
- 7 なら成功。28 なら失敗
for i in $(seq 1 10); do curl -s (target IP) --connect-timeout 1 || echo $?; done
しました。
INPUT / OUTPUT 側で tcpdump で確認する。
- なお、tcpdump のほうが先にパケットが通るため、後ほど iptables で弾こうが受け手側サーバでも確認はできる
- 送信元の場合、
sudo tcpdump -v -vv tcp and dst (target IP) 2>&1 >> log &
してから、ログをゴネゴネ - 受信元の場合、
sudo tcpdump tcp and src (source IP) 2>&1 >> log &
してから、ログをゴネゴネ
番目 curl ステータス seq 番号 1 7 228203506 2 7 191816870 3 28 1557318979 4 28 3760951153 5 28 3916607363 6 28 2103122571 7 7 3380433540 8 28 4142152097 9 7 2372256056 10 7 1925275216 ちゃんと偶数のときだけ curl が成功していることがわかります。(ステータスが 7 なのはそもそも http サーバ立ててないからです・・・)
終わりに
いかがでしたでしょうか。そもそもが複雑というのと、うまい感じに表などが使えず分かりづらかったかもしれませんが、こういう機能もあるよ、と知っていただければ幸いです。