iptables-extentions に含まれる u32 についてのご紹介

TL;DR

  • TL;DRとは
  • この記事は #kosen10s Advent Calendar 2018 - Adventar の 17日目の記事です。遅刻して申し訳ございません。
    • 昨日の記事は、 id:satuma77 さんで、「掴んで見せます!自分星!」とのことでした。comming soon ってやつですね。
    • 明日の記事は id:denari01 さんです。今のところすべての advent calendar に未投稿の様子です。お忙しいんでしょうね…。
    • 毎年、技術っぽい記事とそうでない生活の知恵的な記事を書いているので、これは前者側の記事です。後者側については 9日目の 転職活動まとめ - do_su_0805's blog でした。
  • 昔、ちょっと調べた 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 からほしい真ん中だけを抜き出すことができます。

解説にここの処理についてはイメージを作りました。

f:id:do_su_0805:20181218072607p:plain
IPヘッダの先頭4byteイメージ

f:id:do_su_0805:20181218072612p:plain
>>22 したあとのイメージ

f:id:do_su_0805:20181218072754p:plain
&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発行い、実行ステータスを確認する。
    • この時、ホスト A で、tcpdump を取る

ルール考察

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 & してから、ログをゴネゴネ
      • (冗談みたいな話ですが、これやるとき tcpdump してればええやろーとルール投下後に ssh したら通らなくてそりゃ偶数弾くんだから確率で ssh も通らないわな・・・という自体が起きました。)
    番目 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 サーバ立ててないからです・・・)

終わりに

いかがでしたでしょうか。そもそもが複雑というのと、うまい感じに表などが使えず分かりづらかったかもしれませんが、こういう機能もあるよ、と知っていただければ幸いです。