2007年5月10日木曜日

iptables起動設定

 Debianにてiptablesを起動時に復元するための設定方法を調査した。どうやら、きちんと決められた方法はなく、自分でシェルスクリプトを書く必要があるらしい。 /etc/network/interfacesのpre-upに書くという方法が一番簡単そうだったが、現在のiptablesの設定はインターフェース単位ではないのでその方法はとらず、/etc/init.dにスクリプトを置くという方法をとることにした。

 /etc/init.d配下のシェルスクリプトには決められた様式があり、ひな型としては、/etc/init.d/skeletonがあるが、これはデーモン用のものであるため、とりあえず、/etc/init.d/ifupdownを参考にして、前に作成したSSHのフィルタリングを行うルール(この記事を参照)を設定するように以下のスクリプトmyfirewallを作成した。

#!/bin/sh
### BEGIN INIT INFO
# Provides:          myfirewall
# Required-Start:    
# Required-Stop:     $local_fs
# Default-Start:     S
# Default-Stop:      0 6
# Short-Description: Set firewall.
### END INIT INFO

[ -x /sbin/iptables ] || exit 0

. /lib/lsb/init-functions

MYNAME="${0##*/}"
report() { echo "${MYNAME}: $*" ; }
report_err() { log_failure_msg "$*" ; }
[ -r /etc/default/$MYNAME ] && . /etc/default/$MYNAME

start_firewall () {
  iptables -N SSHEvil
  iptables -N SSH
  iptables -A INPUT -j SSH -p tcp --dport 22

  iptables -A SSHEvil -m recent --name badSSH --set
  iptables -A SSHEvil -j LOG --log-level DEBUG --log-prefix "evil SSH user:"
  iptables -A SSHEvil -j DROP

  iptables -A SSH -j ACCEPT -s 192.168.0.0/24
  iptables -A SSH -p tcp ! --syn -m state --state ESTABLISHED,RELATED -j ACCEPT
  iptables -A SSH -p tcp --syn -m recent --name badSSH --update --seconds 600 -j REJECT
  iptables -A SSH -p tcp --syn -m recent --name conSSH --rcheck --seconds 60 --hitcount 5 -j SSHEvil
  iptables -A SSH -p tcp --syn -m recent --name conSSH --set
  iptables -A SSH -p tcp --syn -j ACCEPT
}

stop_firewall () {
  iptables -F SSHEvil
  iptables -F SSH
  iptables -F INPUT
  iptables -X
}

case "$1" in
  start)
    log_begin_msg "Setting up firewall..."
    start_firewall
    log_end_msg 0
    exit 0
    ;;

  stop)
    log_begin_msg "Stopping firewall..."
    stop_firewall
    log_end_msg 0
    exit 0
    ;;

  restart|force-reload)
    log_begin_msg "Restarting firewall..."
    stop_firewall
    start_firewall
    log_end_msg 0
    exit 0
    ;;

  *)
    echo "Usage: $0 {start|stop|restart|force-reload}" >&2
    exit 3
    ;;
esac

exit 0

 次に実際に起動時にinitに起動してもらうため、必要なランレベルのディレクトリィにリンクを張る必要がある。Debianの/etc/inittabは以下のようになっている。

# The default runlevel.
id:2:initdefault:

# Boot-time system configuration/initialization script.
# This is run first except when booting in emergency (-b) mode.
si::sysinit:/etc/init.d/rcS

# What to do in single-user mode.
~~:S:wait:/sbin/sulogin

# /etc/init.d executes the S and K scripts upon change
# of runlevel.
#
# Runlevel 0 is halt.
# Runlevel 1 is single-user.
# Runlevels 2-5 are multi-user.
# Runlevel 6 is reboot.

l0:0:wait:/etc/init.d/rc 0
l1:1:wait:/etc/init.d/rc 1
l2:2:wait:/etc/init.d/rc 2
l3:3:wait:/etc/init.d/rc 3
l4:4:wait:/etc/init.d/rc 4
l5:5:wait:/etc/init.d/rc 5
l6:6:wait:/etc/init.d/rc 6
Debianではランレベル0がhalt, 6がrebootとなりそれぞれ/etc/rc{0,6}.d/の下のスクリプトが実行される。そして、/etc/rcS.dの下はsysinit、つまりランレベルに関わらず最初に実行されるディレクトリィとなっている。

 基本的にネットワーク起動前にiptablesを設定する必要があるのだが、Debianの場合は起動は以前に調査した結果、/etc/init.d/udev startでeth0が起動し、/etc/init.d/networking stopでeth0が停止することがわかっている(詳細はこの記事参照)。/etc/init.d/udevは/etc/rcS.d/S03udevで開始され、/etc/init.d/networkingは/etc/rc{0,6}.d/S35networkingで停止される。続くS36ifupdownもネットワークに関連するスクリプトであり、状態ファイルの削除を行っているものである。

 通常Sから始まるスクリプトは起動スクリプトであるが、initよりランレベルを引数として実際に起動されている/etc/rcスクリプトを見てみると、以下のようになっている。

(省略)
case "$runlevel" in
  0|6)
    ACTION=stop
    # Count down from 0 to -100 and use the entire bar
    first_step=0
    progress_size=100
    step_change=-1
    ;;
(省略)
# First, run the KILL scripts.
if [ "$previous" != N ]
then
  # Run all scripts with the same level in parallel
  CURLEVEL=""
  for s in /etc/rc$runlevel.d/K*
  do
    level=$(echo $s | sed 's/.*\/K\([0-9][0-9]\).*/\1/')
    if [ "$level" = "$CURLEVEL" ]
    then
      continue
    fi
    CURLEVEL=$level
    SCRIPTS=""
    for i in /etc/rc$runlevel.d/K$level*
    do
      (省略)
      SCRIPTS="$SCRIPTS $i"
    done
    startup stop $SCRIPTS
  done
fi
# Now run the START scripts for this runlevel.
# Run all scripts with the same level in parallel
CURLEVEL=""
for s in /etc/rc$runlevel.d/S*
  do
    level=$(echo $s | sed 's/.*\/S\([0-9][0-9]\).*/\1/')
    if [ "$level" = "$CURLEVEL" ]
    then
      continue
    fi
    CURLEVEL=$level
    SCRIPTS=""
    for i in /etc/rc$runlevel.d/S$level*
    do
      [ ! -f $i ] && continue
      (省略)
      SCRIPTS="$SCRIPTS $i"
    done
    startup $ACTION $SCRIPTS
  done
fi
ランレベル0,6のときは、先頭文字がS,Kに関わらずstopが引数として渡されている。そして、スクリプトの実行順番は、K,Sの順序である。これらの結果より、/etc/rcS.dにS03udevよりも早く/etc/rc{0,6}.dにS36ifupdownより遅くなるように、/etc/rcS.d/S03myfirewallと/etc/rc{0,6}.d/S37myfirewallというリンクを作成すればよいことがわかる。

 Debianではinitスクリプトのリンクを作成するコマンドupdate-rc.dがある。事前に/etc/init.d/配下にスクリプトが存在する必要があるので、上記のmyfirewallを/etc/init.d/へコピーする。update-rc.dコマンドの書式は以下となる。

update-rc.d {スクリプト名} {start | stop} {2桁連番} {runlevel} {.} ...

{start | stop}から{.}までは複数個記述することができる。よって、以下のコマンドを実行すればよい。
$ update-rc.d -n myfirewall start 03 S . start 37 0 6 .
 Adding system startup for /etc/init.d/myfirewall ...
  /etc/rc0.d/S37myfirewall -> ../init.d/myfirewall
  /etc/rc6.d/S37myfirewall -> ../init.d/myfirewall
  /etc/rcS.d/S03myfirewall -> ../init.d/myfirewall

■ 参考資料

Software/iptables - Debian GNU/Linux スレッドテンプレ
Manpage of IPTABLES
Manpage of BASH

2007年5月7日月曜日

getopt

概要

 getoptは主にシェルスクリプトでコマンドオプションを使いやすい形に変形するために使う。いくつか書式があるが、伝統的なgetoptでは以下のように使う。

$getopt optstring parameters
引数はoptstringとparamtersに分かれる。optstringにはオプションと認識する一文字を連続して書く。その文字の最後に:をつけた場合は、そのオプションはパラメータをとるという意味になる。parametersは、解析する引数を書く。

伝統的なコマンド使用例

 例えば ab:cとした場合は、-a -b -cをオプションとして認識し、-bオプションはパラメータをとることになる。出力の順番はオプションと必要があればそのパラメータがすべて出力され、'--'を出力後にどれでもないものが出力される。

$ getopt ab:c -a
-a --
上の例では、parametersが-aオプションのみなので-aと'--'が出力される。
$getopt ab:c -b
getopt: オプションには引数が必要です -- b
--
今度は-bオプションを指定したが、-bオプションには引数が必要となるためエラーとなる。
$ getopt ab:c -b aaa
-b aaa --
上記のように書くと、aaaが-bオプションの引数になるので正常に出力される。
$ getopt ab:c -b aaa -a bbb -c
-b aaa -a -c -- bbb
-aの後にbbbを書くと、-aは引数をとらないのでbbbはオプションでもオプションの引数でもないことになり、--の後ろに出力され、順番が入れ替わる。
$ getopt ab:c -b aaa -a -c -d
getopt: オプションが違います -- d
-b aaa -a -c --
-dはoptstringに定義されていないため、エラーとなる。

 このようにgetoptでオプション文字並びにオプションの引数チェックとオプション文字でもオプションの引数でもないものを後ろに並べかえてくれる。

拡張コマンド使用例

 GNUのgetoptの場合は以下のような書き方ができる。(他にもいくつか書き方があるが詳細はgetop(1)を参照のこと)

$getopt -o optstring -l longoptstring -- parameters
-oは短いオプションを上記と同じ形式で記述する。-lには、長いオプション名を','で区切りながら記述できる。また、短いオプションと同様に最後に':'をつけるとそのオプションは引数を取ることになる。paramtersで長いオプションは'--'で始まる。
$getopt -o ab:c -l dfg,hij: -- -a --dfg abc --hij aaa
-a --dfg --hij 'aaa' -- 'abc'
--dfgと--hijがオプションとして認識され、aaaは--hijの引数となる。また、この書式を使うとシェルでのメタ文字の展開を避けるため、オプションの引数とオプション文字でもオプションの引数でもないものはシングルクォートされて出力される。

シェルスクリプト利用例

 シェルスクリプトで使う場合は以下のようにして使う。

OPTIONS=`getopt -o ab:c --long dfg,hij: -- "$@"`
if [ $? != 0 ] ; then
 exit 1
fi
eval set -- "$OPTIONS"
while true; do
    case "$1" in
        -a)
        # -aのときの処理
        shift
        ;;
    -b)
        # -bのときの処理
        shift 2
        ;;
    -c)
        # -cのときの処理
        shift
        ;;
    --dfg)
        # --dfgのときの処理
        shift
        ;;
    --hij)
        # --hijのときの処理
        shift 2
        ;;
    --)
        shift
        break
        ;;
    *)
        echo "Internal error!" >&2
        exit 1
        ;;
    esac
done
# オプションでもオプションの引数でもないものの処理
$@はシェルスクリプトの位置パラメータ($1,$2,...)に展開されるので、それをgetoptにかけ、オプションとそのパラメータが先になるように並べ変える。その際、認識できないオプションがあったり、オプションのパラメータが存在しない場合はエラーとなる。その出力結果をもう一度「set -- 」で位置パラメータに入れ直す。ただし、シェルの特別な文字を展開しないように、$OPTIONSをダブルクォート("$OPTIONS")としてパラメータ展開を行っているため、evalとして実行する。(そうしないと、setのパラメータとして展開されるので、ひとつの単語とみなされ$1にまとめて設定されてしまう。evalをつけるとevalのパラメータとして展開された後、「set --」が実行されるため、スペース区切りでそれぞれ位置パラメータに設定される。)

■ 参考資料

Manpage of GETOPT
Manpage of BASH

2007年5月5日土曜日

SSHのbrute forceアタック対応

 自宅サーバーのログをチェックしていると、/var/log/auth.logにSSHに対してユーザを変えながら何度もアクセスしているログが大量に残っていた。どうもbrute forceアタックをされているようだ。brute forceアタックというのはユーザ、パスワードを総当りで破ろうとする力ずくの攻撃のこと。

● 主な対策方法

 SSHのbruteforceアタック対しての対策方法は主に以下があるようだった。
1.ポート番号を1024番以降(well-knownポート以外)に変える。
2.TCPWrapperやiptablesで特定のIPアドレスのみ接続を受け付ける。
3.iptablesのipt_recentを使って一定時間にアクセスがあったホストをはじく。
2は、今後自分が外からSSH経由でログインする場合は、IPアドレスを固定できないので使えない。1が、もっとも簡単なのだが、勉強がてら3の方法に挑戦してみた。

● iptables概要

iptablesの最小単位はパケットをどう扱うかというルールで、それをチェインというルールをひとまとめにしたものに追加していく。チェインは組み込み済みのもの以外にユーザが自分で定義することができる。そして、チェインはテーブルに属しており、テーブルには、用途に応じてfilter,nat,mangleという3種類がある。filterは通常のパケットの送受信に使うもので、INPUT,OUTPUT,FORWARDの組み込みチェインがある。他にNAT変換時に使うnatと、何に使うのかよくわからないがmangleというテーブルがある。今回はfilterテーブルに設定を行う。

● iptablesの設定

 以下のようにiptablesの設定を行う。

iptables -N SSHEvil
iptables -N SSH
iptables -A INPUT -j SSH -p tcp --dport 22

iptables -F SSHEvil
iptables -A SSHEvil -m recent --name badSSH --set
iptables -A SSHEvil -j LOG --log-level DEBUG --log-prefix "evil SSH user:"
iptables -A SSHEvil -j DROP

iptables -F SSH
iptables -A SSH -j ACCEPT -s 192.168.0.0/24
iptables -A SSH -p tcp ! --syn -m state --state ESTABLISHED,RELATED -j ACCEPT
# SSHルール1
iptables -A SSH -p tcp --syn -m recent --name badSSH --update --seconds 600 -j REJECT
# SSHルール2
iptables -A SSH -p tcp --syn -m recent --name conSSH --rcheck --seconds 60 --hitcount 5 -j SSHEvil
# SSHルール3
iptables -A SSH -p tcp --syn -m recent --name conSSH --set
iptables -A SSH -p tcp --syn -j ACCEPT

 まず、filterテーブルに新規にSSHEvilとSSHというチェインを作成。3行目でINPUTチェインに対して22番ポート(SSHのポート)を受信した場合は、SSHチェインへ渡すように設定。filterテーブルには組み込みでINPUT,OUTPUT,FORWARDの3つのチェインが組み込まれている。INPUTは受信したパケットにFORWARDは他のインターフェースへ流すパケットに、OUTPUTは送信するパケットに対して適用される。

 4行目からはSSHEvliチェインに対してルールを設定する。 まず、-Fで全てのルールを削除。次にipt_recentモジュールのルール設定でbadSSHというリスト(-name)に送信元IPアドレスを記憶し(--set)、ログを出力してパケットを捨てるというルールを記述して、SSHEvliチェインは終了。 このように、SSHEvilチェインは一本道の単純なものでパケットは最終的に全て捨てられることになる。

 続いてSSHチェインのルール設定を行う。8行目で-Fで全てのルールを削除。ローカルアドレスは無条件で受信を許可する。続いてSYNフラグなし(!--syn)、TCP状態がESTABLISHEDならびにRELATED(--state)を指定することでTCPでの接続確立後のデータパケットは許可する。以降のルールは接続確立要求のパケット(--syn)を対象に設定する。送信元IPアドレスがbadSSHリストで(-name)600秒以内にアクセスがあるか(--seconds)チェックし、あった場合は受信時間を更新し(--updates)パケットを拒否する。  次のルールでは、送信元IPアドレスがconSSHリスト(-name)で60秒以内のアクセスが(--seconds)、5回以上ある(--hitcount)かチェックしあった場合は(--rcheck)SSHEvilチェインへ渡す設定をし、さらにconSSHというリスト(-name)に送信元IPアドレスを記憶し(--set)、最後にパケットを受け入れる。

● パケットフィルタの動作

SSH接続を要求してきたホストは、SSHルール3によりconSSHリストに登録された後要求を受け付ける。しかし、60秒に5回を超えてSSH接続をする場合は、その前にあるSSHルール2に該当することになりSSHEvilチェインへと移動し、badSSHリストに登録されてパケットは捨てられることになる。さらに接続しようとすると今度はSSHルール1に引っかかり、パケットは拒否されることになる。SSHルール1に引っかかった場合は、その都度時間が更新されるため、このルールから抜け出すためには600秒間アクセスを行わない必要がある。

● 問題点

 ただし、ここでいう接続はTCPレベルのものであり、SSHの接続が成功したかどうかを見ているわけではない。SSH接続が成功したとしても1回にカウントされてしまうので、正しいホストも1分間に5接続以上はできないことになる。

 また、IPアドレスで見ているため、NAT下からの複数ユーザの接続の場合は同じIPアドレスとみなされてしまうため接続できなくなる可能性がある。

とりあえず、自分の運用ではどららも問題になりそうもないので、この設定を使うことにする。

● 参考資料

iptables の ipt_recent で ssh の brute force attack 対策