2009年3月12日木曜日

FreeBSD:sshサーバーに対する辞書攻撃の防御

FreeBSDでsshをあげているが,かなりの頻度で辞書攻撃されている。webサーバーをあげていて,知り合い(同窓会)を相手にしているので,誰かのマシンがウィルスに感染でもしているのだろうか?基本的にこのサイトは誰にも言ってないから,どっかから情報が漏れているに違いない。ま,でも知られた以上は仕方ないので,攻撃を遮断しないといけない。一応passswordログインはできないようにしているが,攻撃されている間は何度もアクセスしてくるので,回線に負荷をかけるし,auth.logは増えるし,とにかく気に入らない。ということで辞書攻撃の対策を探ってみた。検索語は「FreeBSD 辞書攻撃」としてGoogleにお世話になった。幾つか出てきたが,挑戦してみたのは,ktj Dragonさんのサイト。maxlogins.plというPerlスクリプトを使うパターンらしい。やり方は,何度もsshでアクセスしようとするIPをブラックリストとして,ログファイル(/var/log/maxlogins)に書き込むらしい。で,それを使って /etc/hosts.allow で /var/log/maxlogins にあるIPからの接続を拒否してしまうらしい。

と,いうことで導入してみる。ktj Dragonさんの記述ドキュメント通り(というか,maxlogins.plのサイトの記述通り)に、maxlogins.plを/usr/local/bin にコピーして、ownerとmodeを変更。
# chown root:wheel /usr/local/bin/maxlogins.pl
# chmod 750 /usr/local/bin/maxlogins.pl
さらに/etc/syslog.confの中の
auth.info;authpriv.info        /var/log/auth.log 
という行の下に
auth.info;authpriv.info        |exec /usr/local/bin/maxlogins.pl
を追加した。最後に,/etc/hosts.allow の ALL : ALL : allow という行の前に、
sshd : /var/log/maxlogins : deny
を追加しておいた。

また,auth.logの中に出てくるDid not receive identification string from xxx.xxx.xxx.xxx などの他のメッセージにも対処すべく,「学生と職員の狭間に」にあるmaxloginsのパッチを適用しておいた
 具体的には,maxlogins.pl の88行目付近にある(行を加えたので,ちょっとずれてるかも)
if ($logline =~ /sshd\[\d*\]: Failed password/){
というのを
if ($logline =~ /sshd\[\d*\]: Failed password/ | 
     $logline =~ /sshd\[\d*\]: Invalid user/ | 
     $logline =~ /sshd\[\d*\]: Did not receive identification string/){
に換え,2行下の
($sshdpid, $ip) = $logline =~ (/sshd\[(\d*)\].*from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port/);
というのを
($sshdpid, $ip) = $logline =~ (/sshd\[(\d*)\].*from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/);
に換える,というもの。

さらに110行目付近にある
push @blacklist, $ip." ##expires: ".($time + expiration())."\n";
のexpirationの時刻の後ろに,人が見てわかるような形式の日付+時刻を加えておいた。具体的には
push @blacklist, $ip." ##expires: ".($expirationtime)." ".
     &GetDateStr(localtime($expirationtime))." ".
     &GetTimeStr(localtime($expirationtime))."\n";
としている。(この投稿の後半にあるように,##expires:の前にスペースを挿入した)

ここで,GetDateStrは
sub GetDateStr {
     my(@GTS_date_array) = @_;
     if ($GTS_date_array[1] eq '') {
         @GTS_date_array = localtime(time);
     }
     my($dum);
     $dum=($GTS_date_array[5]+1900)."/".&ZeroPadding($GTS_date_array[4]+1)."/".
          &ZeroPadding($GTS_date_array[3]);
     return $dum;
}
であり,GetTimeStrは
sub GetTimeStr {
     my(@GTS_date_array) = @_;
     if ($GTS_date_array[1] eq '') {
         @GTS_date_array = localtime(time);
     }
     my($dum);
     $dum=&ZeroPadding($GTS_date_array[2]).":".&ZeroPadding($GTS_date_array[1]).":".
          &ZeroPadding($GTS_date_array[0]);
     return $dum;
}
である。またZeroPaddingは一桁の数字にゼロを加えて二桁にするサブルーチンであり,具体的には以下のようになる。
sub ZeroPadding {
     my($dum)=sprintf("%d",@_);
     $dum="00".$dum;
     $dum=substr($dum,(length($dum)-2));
     return $dum;
}

maxlogins.plの最初の方にデフォルトのoptionの設定をする箇所がある。maxlogins.plのサイトを見ると,それぞれのオプションは以下のような意味らしい。
・badipfile (-b)
  ブロックしたいIPアドレスを格納するためのファイル名。デフォルトは /var/log/maxlogins。
  今回はデフォルトのままにしてみた。

・expire (-e)
  ブロックしたIPのブロックを解除するまでの時間。
  大抵の辞書攻撃は一度失敗すると後でもう一回というのはあまりないみたい。
  なので,デフォルトでは12時間になってる。とりあえずはデフォルトのまま。
  12時間を過ぎてからmaxlogins.plが起動すれば解除される。
  まじめに12時間で解除したい時には,cronでmaxlogins.plを動かした方がよいらしい。
  ま,ある程度の頻度でログインしておけば,その度にmaxlogins.plは起動するが。

・kill (-k)
  killを1にしていると,怪しいアクセスのプロセスを止める。
  0なら止めない。とりあえずデフォルトの1にしておこう。

・loglevel (-l)
  maxlogins.plはauth.logに書かれるのと同じものを読み込んで処理している。
  その結果をauth.logに書けるのだが,どこまで書くかのレベルを決める。
  デフォルトは1。うーん,どうしようかなぁ?これは全部試してみようかな。
   1: informational (e.g., new blocks) --- これは不正アクセスとアクセスブロックの記録
   2: above, plus IP expirations --- ブロック解除の情報も記録する。
   9: verbose logging --- 冗長なログ。どこまで書くのかわからないや(以下の追記を参照してくださいな)
 (追記:デフォルトの1か,せいぜい2で十分だと思う)

・maxattempts (-a)
  何度目のアタックでIPアドレスをブロックするかの回数。
  デフォルトは3回。最大50回。
  少なすぎると自分がはじかれてしまうので,3回のままにした。

・maxsuspects (-s)
  これはブロックする「候補」として追跡するIPアドレスの個数。
  すでにブロック対象となったものは数に入らない。
  対象となるアドレスは,解除時間がくるか,ここで指定した個数を越えて
  新たな対象アドレスが現れると,自動的に消える。
  何箇所から攻撃を受けるかによるが,普通はあまり大きくしなくてもいいみたい。
  デフォルトは3個。最大50個。これは5にしてみた。

結果はどうかってのは,追記します。
======================================================================
(追記)
当初、loglevel=9でログを取ってみたが、どうもうまくいっていなかった。どううまくいっていなかったというと、auth.logを見ると、あるIPからの攻撃の3回目で該当するIPがブラックリストに登録されるのに、同じIPからすぐ攻撃をくらって、また3回目でブラックリストに登録される、というのが延々と続いていた。それってブロックできてへんってことやん。おかしいなぁ、と思って/var/log/maxloginsというファイルを見てみた。すると、以下のようになっていた。
##aa.bb.cc.dd-1-1237004521:bb.cc.dd.ee-2-tttttttt

200.40.xx.yy##expires: 1237026130
200.40.xx.yy##expires: 1237026130
200.40.xx.yy##expires: 1237026130
....
最初の##で始まる行は、ブロックするIPの候補。怪しいIPごとに「IP-回数-時刻」という形式となっていて、それが「:」でつないである。それ以下の行が実際にブロックするIPとそのブロックの解除時刻。ただし時刻はunixのフォーマットなので、そのままではなんのことだかわからないけど。で、問題はこの200.40.xx.yy##expires: tttttみないなのが延々と続いている点。なんかおかしい。このファイルをhosts.allowで読み込ませているが、うまくブロックしてくれてないようが気がする。どうもIPに続けて(スペースが間にない)##...とコメントになっているのがまずいような気がした。そこでIPアドレスと##の間にスペースを入れようと思って,実行ファイルである/usr/local/bin/maxloings.plの中身を見てみた。どうやら
push @blacklist, $ip."##expires: ".($time + expiration())."¥n";
という行でmaxloginsファイルに書く行を指定しているみたい。ここで@blacklistという配列に情報をいれているようだ。そこで
push @blacklist, $ip." ##expires: ".($time + expiration())."¥n";
と変えてみた(##expiresの前にスペースを入れた)
するとうまくいったみたい。auth.logの中に
Mar 14 18:08:16 jail sshd[51816]: Did not receive identification string from 72.xx.yy.zz (註1)
Mar 14 18:08:16 jail xxxx: maxlogins - IPs being blocked=3 (註2)
Mar 14 18:08:16 jail xxxx: maxlogins - Bad login attempt from: 72.xx.yy.zz (PID 51816) (註3)
Mar 14 18:08:16 jail xxxx: maxlogins - Suspect IPs= (註4)
Mar 14 18:08:16 jail xxxx: maxlogins - 72.xx.yy.zz failed login 1 time(s) (註5)
Mar 14 18:12:01 jail sshd[51859]: Invalid user apple from 72.xx.yy.zz (註6)
Mar 14 18:12:02 jail xxxx: maxlogins - # suspects=1 (註7)
Mar 14 18:12:02 jail xxxx: maxlogins - IPs being blocked=3 (註8)
Mar 14 18:12:02 jail xxxx: maxlogins - Bad login attempt from: 72.xx.yy.zz (PID 51859) (註9)
Mar 14 18:12:02 jail xxxx: maxlogins - Suspect IPs=72.xx.yy.zz (註10)
Mar 14 18:12:02 jail xxxx: maxlogins - 72.xx.yy.zz failed login 2 time(s) (註11)
Mar 14 18:12:05 jail sshd[51869]: Invalid user brian from 72.xx.yy.zz (註12)
Mar 14 18:12:05 jail xxxx: maxlogins - # suspects=1 (註13)
Mar 14 18:12:05 jail xxxx: maxlogins - IPs being blocked=3 (註14)
Mar 14 18:12:05 jail xxxx: maxlogins - Bad login attempt from: 72.xx.yy.zz (PID 51869) (註15)
Mar 14 18:12:05 jail xxxx: maxlogins - Suspect IPs=72.xx.yy.zz (註16)
Mar 14 18:12:05 jail xxxx: maxlogins - Blocking 72.xx.yy.zz (註17)
Mar 14 18:12:05 jail xxxx: maxlogins - Killing process 51869 (註18)
Mar 14 18:12:05 jail xxxx: maxlogins - 72.xx.yy.zz failed login 3 time(s) (註19)
Mar 14 18:12:07 jail sshd[51881]: refused connect from 72-xx-yy-zz.ip.grandenetworks.net (72.xx.yy.zz) (註20)

(註1:攻撃の第1波)
(註2:その時点ですでに3つのIPをブロックしていた)
(註3:攻撃を検知したメッセージ)
(註4:今回以前ではまだ怪しいIPの候補はなかった)
(註5:攻撃を検知して無事候補に登録)
(註6:攻撃の第2波。appleなんてアカウントはないぞ)
(註7:問題のIPがブロックすべきIPの候補に入ったので、1個となっている)
(註8:やっぱりすでにブロックされているのは3個のまま)
(註9:第2波を検知したメッセージ)
(註10:今回の攻撃のIPがすでに候補に入っている)
(註11:攻撃が2回あったとのメッセージ)
(註12:攻撃の第3波。今度はbrianか。)
(註13:候補の数は1個のまま)
(註14:ブロックされているのも3個のまま)
(註15:3度目を検知したメッセージ)
(註16:問題のIPが候補にはいっていた)
(註17:3回攻撃がきたので、72.xx.yy.zzをブロックすることに決定)
(註18:プロセスの停止)
(註19:3回不正ログインを試みたというメッセージ)
(註20:ブロックされたというメッセージ)

みたいなのがあって、3回目でブロックされているようだった。辞書攻撃にも対処できて、これで一安心。
ちなみに,loglevelを1にしておくと,
Mar 16 13:03:41 jail sshd[63757]: Invalid user james from 58.64.xx.yy
Mar 16 13:03:42 jail sshd[63760]: Invalid user cvs from 58.64.xx.yy
Mar 16 13:03:43 jail sshd[63762]: Invalid user tony from 58.64.xx.yy
Mar 16 13:03:43 jail xxxx: maxlogins - Blocking 58.64.xx.yy
Mar 16 13:03:44 jail sshd[63765]: refused connect from 58.64.xx.yy (58.64.xx.yy)
のようなメッセージのみがauth.logに残っている。これで十分のような気がする。もし解除の情報も記録したければloglevel = 2にするのがいいと思う。さすがにlevel 9はやりすぎだと思う。
======================================================================
(追記の追記)
なかなかmaxloginsはいい感じ。auth.logの量がぐっと減った。これでかなり安心度がアップしました。

0 件のコメント: