Home » FreeBSD » FreeBSD Brute Force Attack Counter Tool (No. 1) (Tag: )

sshd や popd に対する brute force attack (総当たり攻撃) が原因で、サーバ側の負荷が急増し、ホームページ参照やメール受信に支障を来すケースが散見されています。

sshd は数年前からすでに対策ツールを作成して運用済みでしたが、ここ 1 年程は popd に対する攻撃が急増、しかも容赦のないセッション数を張って load averages が 500 台まで上がることがあります。

攻撃の特徴として、ある IP アドレスから複数のサーバに総当たり攻撃を行うので、負荷の急増に伴いゲートウェイとなる L3 スイッチ自体で遮断していましたが、数があまりにも増えて来ました。

sshd では、認証失敗時に /var/log/auth.log に Falied password … と出力されます。一定回数出力した場合は、ipfw で該当する IP アドレスをすべてのプロトコルで遮断し、一定時間経過後解放しています。

この仕組みを popd (/usr/ports/mail/popd/) にも適用しました。まずは、チェックツール全体を添付、概要は 次回 に説明しましょう。

#
# bfcheck.pl
#

#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#// use Module
#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

use strict;
use vars qw( $opt_p );

use Getopt::Long;

#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#// Controller
#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

    #// ----------------------------------------------------------
    #// Option Parse
    #// ----------------------------------------------------------

    $opt_p = 0;

    my @basename = split(/\//, $0);
    my $basename = pop(@basename);
    my $result   = GetOptions('p');

    #// ----------------------------------------------------------
    #// Item Set
    #// ----------------------------------------------------------

    my $item = {
        mkdir    => '/bin/mkdir',
        cat      => '/bin/cat',
        chmod    => '/bin/chmod',
        ps       => '/bin/ps',
        hostname => '/bin/hostname',
        mail     => '/usr/bin/mail',
        awk      => '/usr/bin/awk',
        grep     => '/usr/bin/grep',
        chown    => '/usr/sbin/chown',

        cp       => '/bin/cp',
        wc       => '/usr/bin/wc',
        touch    => '/usr/bin/touch',
        head     => '/usr/bin/head',
        tail     => '/usr/bin/tail',
        host     => '/usr/bin/host',
        ipfw     => '/sbin/ipfw',

        target0  => '/var/log/auth.log',
        target1  => '/var/log/poplog',
        dir0     => '/home/tools/bfcheck.s/',
        dir1     => '/home/tools/bfcheck.p/',

        mistime  => 10,
        expire   => 1800,
        delim    => 'last_line',
        good     => 'pass',
        stat0    => '-',
        stat1    => 'add',
        stat2    => 'expire',

        maddr    => 'trouble@example.jp',
        report   => 0,

        base     => $basename,
        file     => '/tmp/.' . $basename
    };

    if ($opt_p == 0) {
        $item->{target} = $item->{target0};
        $item->{dir}    = $item->{dir0};
        $item->{file}  .= '.s';
    }

    if ($opt_p == 1) {
        $item->{target} = $item->{target1};
        $item->{dir}    = $item->{dir1};
        $item->{file}  .= '.p';
    }

    if ( !-d $item->{dir} ) {
        `$item->{mkdir} $item->{dir}`;
        `$item->{chmod} 750 $item->{dir}`;
    }

    #// ----------------------------------------------------------
    #// Start
    #// ----------------------------------------------------------

    #// list

    my $check = {};

    ($item, $check) = mklog($item);

    my $last0 = $item->{last_line};
    my $last  = count($item);
    my $tnum  = $last;

    #// read log

    if ($last >= $last0) { $tnum = $last - $last0; }
    # $last < $last0 --> log rotated

    my $diff = `$item->{tail} -n $tnum $item->{target};`;

    foreach (split(/\n/, $diff)) {
        my $ipaddr = replace($item, check($_));

        $check->{$ipaddr}->{num} += 1;
    }

    #// data check

    my @line   = ();
    my $deny   = '';
    my $expire = {};
    my $log    = '';

    foreach (%$check) {
        next unless ($check->{$_});
        next if ($_ eq $item->{good});

        if ($check->{$_}->{add} && $check->{$_}->{add} == 1) {
            my $time = date2utime($check->{$_}->{time});

            if (($item->{utime} - $time) >= $item->{expire}) {
                $expire->{$_} = 1;
                $log    .= $item->{time} . "\t" . $_ . "\t" .
                           0 . "\t" . $item->{stat2} . "\n";
            }

            if ($check->{$_}->{mode}) {
                $log    .= $item->{time} . "\t" . $_ . "\t" .
                           0 . "\t" . $item->{stat1} . "\n";
            }

        } else {
            next if ($check->{$_}->{num} == 0);

            if ($check->{$_}->{num0} &&
                $check->{$_}->{num0} == $check->{$_}->{num}) {

            } elsif ($check->{$_}->{num} >= $item->{mistime}) {
                my $line = "00101 deny ip from $_ to any";

                push(@line, $line);
                $deny .= "$line\n";

                $log  .= $item->{time} . "\t" . $_ . "\t" .
                         $check->{$_}->{num} . "\t" . $item->{stat1} . "\n";

            } else {
                $log  .= $item->{time} . "\t" . $_ . "\t" .
                         $check->{$_}->{num} . "\t" . $item->{stat0} . "\n";
            }
        }
    }

    if ($deny ne '' && $item->{report} == 1) {
        report($item, $deny);
    }

    #// ipfw list 101

    my @list = `$item->{ipfw} list | $item->{grep} 00101`;

    foreach (@list) {
        chomp;

        my @line0 = split(/ /);
        my $check = $line0[4];

        next if ($expire->{$check} && $expire->{$check} == 1);

        push(@line, $_);
    }

    if (@list) {
        `$item->{ipfw} delete 101`;
    }

    foreach (@line) {
        my $line = "$item->{ipfw} add " . $_;

        `$line`;
    }

    #// write log

    open READ, "<$item->{log}";
    my @log = <READ>;
    close READ;

    open WRITE, "+>$item->{log}";

    unless (@log) {
        print WRITE $item->{delim} . "\t\t" . $last . "\n";

    } else {
        foreach (@log) {
            if ($_ =~ /^$item->{delim}/) {
                print WRITE $item->{delim} . "\t\t" . $last . "\n";
            } else {
                print WRITE $_;
            }
        }
    }

    print WRITE $log;
    close WRITE;

#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#// Model
#// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

sub date2utime {
    my $date = shift;

    $date =~ s/[\/:]/ /g;
    $date =~ s/^(\s+)//g; $date =~ s/(\s+)$//g;
    $date =~ s/(\s+)/ /g;

    my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $date);

    if (!defined $sec) { $sec = 0; }
    $mon -= 1;

    eval { timelocal($sec, $min, $hour, $mday, $mon, $year); };
    if ($@) { return 0; }

    my $utime = timelocal($sec, $min, $hour, $mday, $mon, $year);

    return $utime;
}

sub utime2date {
    my ($sec, $min, $hours, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($_[0]);

    my $years    = $year + 1900;
    my $this_mon = sprintf("%02d",$mon + 1);

    $mday  = sprintf("%02d", "$mday");
    $hours = sprintf("%02d", "$hours");
    $min   = sprintf("%02d", "$min");
    $sec   = sprintf("%02d", "$sec");

    my $date = "$years/$this_mon/$mday $hours:$min:$sec";

    return $date;
}

sub mklog {
    my ($item) = @_;

    my $check = {};

    my $utime = time();
    my $time  = utime2date($utime);
    my $time0 = substr(utime2date($utime), 0, 10);
    my $file  = $time0; $file =~ s/\///g;

    $item->{log}   = $item->{dir} . $file;
    $item->{utime} = $utime;
    $item->{time}  = $time;
    $item->{time0} = $time0;

    ($item, $check) = last_line($item);

    return ($item, $check);
}

sub last_line {
    my ($item) = @_;

    my $check = {};
    my $num   = 0;

    if ( -e $item->{log} ) {
        my $str = `$item->{head} -1 $item->{log}`;

        chomp $str;
        $str =~ s/([\s\t]+)/ /g;

        if ($str =~ /$item->{delim} (.+)/) {
            $num = $1;
        }

        ($item, $check) = fetch_log($item, $item->{log});

    } else {
        `$item->{touch} $item->{log}`;

        my $time  = date2utime("$item->{time0} 00:00:00");
        my $time0 = substr(utime2date($time - 1), 0, 10);
        my $file0 = $time0; $file0 =~ s/\///g;

        my $log = $item->{dir} . $file0;

        if ( -e $log ) {
            my $str = `$item->{head} -1 $log`;

            chomp $str;
            $str =~ s/([\s\t]+)/ /g;

            if ($str =~ /$item->{delim} (.+)/) {
                $num = $1;
            }

            ($item, $check) = fetch_log($item, $log, 1);
        }
    }

    $item->{last_line} = $num;

    return ($item, $check);
}

sub fetch_log {
    my ($item, $log, $mode) = @_;

    unless ($mode) { $mode = 0; }

    open READ, "<$log";
    my @log = <READ>;
    close READ;

    my $check = {};

    foreach (@log) {
       chomp;
       next if ($_ =~ /^$item->{delim}/);

       my ($time, $ipaddr, $num, $stat) = split(/\t/);

       if ($stat eq $item->{stat0}) {   #// '-'
           $check->{$ipaddr}->{num}  = $num;
           $check->{$ipaddr}->{num0} = $num;
           $check->{$ipaddr}->{add}  = 0;
       }

       if ($stat eq $item->{stat1}) {   #// 'add'
           $check->{$ipaddr}->{num}  = $num;
           $check->{$ipaddr}->{add}  = 1;
           $check->{$ipaddr}->{time} = $time;
           $check->{$ipaddr}->{mode} = $mode;
       }

       if ($stat eq $item->{stat2}) {   #// 'expire'
           $check->{$ipaddr}->{num}  = $num;
           $check->{$ipaddr}->{num0} = 0;
           $check->{$ipaddr}->{add}  = 0;
       }
    }

    return ($item, $check)
}

sub count {
    my ($item) = @_;

    my $num = `$item->{wc} -l $item->{target} | $item->{awk} '{print \$1}'`;
    chomp $num;

    return $num;
}

sub check {
    my ($line) = @_;

    my $mode = 0;

    # sample
    # Oct  2 12:34:00 hsXX sshd[17625]: Failed password for root from 202.152.209.136

    if ($line =~ /Failed /) {
        if ($line =~ /(.+)Failed password for invalid user (.+)/) {
            $mode = 1;

        } elsif ($line =~ /(.+)Failed publickey for (.+)/) {
            $mode = 0;

        } elsif ($line =~ /(.+)Failed password for (.+)/) {
            $mode = 3;

            # Oct  2 12:34:00 hsXX sshd[17625]: root from 202.152.209.136
        }

        if ($mode != 0) { $line = $1 . $2; }
    }

    return ($mode, $line);
}

sub replace {
    my ($item, $mode, $line) = @_;

    chomp $line;
    return $item->{good} if ($mode == 0);

    $line =~ s/(\s+)/ /g;

    my @str = split(/ /, $line);
    my $ipaddr = $str[7];

    return $ipaddr;
}

sub report {
    my ($item, $msg) = @_;

    my $host = `$item->{hostname}`; chomp $host;
    my $time = utime2date(time());

    my $file = $item->{file};
    my $subj = "Info: ipfw ($host)";

    my $body = <<"BODY";
$time => ipfw add

$msg
BODY

    open  FILE, "+>$file";
    print FILE $body;
    close FILE;

    `$item->{mail} -s "$subj" $item->{maddr} < $file`;

    unlink($file);
}

Random Select

おかずセレクト (2013/03/19)
ミニストップ (ベルギーチョコソフト) の続きです。自宅の近辺には見掛けませんが、オフィス周辺には歩いて 1 分程度の距離に 2 件 もあります。神田錦町 1 丁目店は少なくとも 10 年以上はあり、
よなよなエール
最寄の綾瀬駅前にはイトーヨーカドーと 東急ストア があり、普段生鮮食品は地下に降りなくて済む東急ストアで済ませます。ここの惣菜が美味しいのはまた別の機会として、ある日ふとあまり見掛けないビールが置いて
Heart Shaped Dish
48 時間ファスティングダイエット (No. 3) の続きです。4 日目 : お腹ぺったん3 日目の朝よりもはっきり感じます。ドローイン vs ロングプレス で意図的に引っ込めていた時と、普通にしてい
三鷹の森ジブリ美術館 (4)
三鷹の森ジブリ美術館 は 10 年程前に一度訪れていますが、今回連休中に再び訪れる機会がありました。まずは JR 三鷹駅まで電車の旅で、南口に降りるとコミュニティバスがあると聞いていました。てっきり無
花人逢 (瀬底島)
沖縄出張 (No. 30 – 花人逢) の続きです。07/14 (日) に訪れたばかりでしたが久々な好天気と景色にピザを味わい、どうしてももう一度行きたくなり、1 週間後に別のメンバーと訪
行き止まり ?
沖縄本島 (No. 24 – 瀬底大橋 工事中) の続きです。国道 449 号線 (本部循環線) から瀬底大橋向けに左折して渡り切ると 沖縄県道 172 号瀬底健堅線 に切り替わります。島
ラジオ体操中
普段は私が沖縄に出張しますが、今回は沖縄から Drive Network のスタッフが勢ぞろいで東京へ出張に来ました。今まで Rack Map や写真でしか見たことのないデータセンター内の見学が一番の
若鶏もも肉のロティ・フレッシュトマトのソース
沖縄出張 (No. 22 ? ビストロ Veggie 〜野菜の王子さま〜) ではまだ話足りない気がしたので、再び別のお店にチャレンジしつつ繰り出しました。見繕ってくれたお店のうち、La Vita (ラ
辛つけ麺 (大盛 500g)
東京オフィスから神保町方面に 4, 5 分靖国通り沿いを歩くと、つけ麺さとう があります。ランチ時に限りませんが、入口手前の慣れないと見落とすところにトッピング無料券があります。味玉温野菜ネギ中に入っ
ハッシュ・ド・ビーフ 材料
ここ最近は年末年始の長期休暇の度に作っています。2012 – 2013 年にかけても作って味わいました。facebook では是非食べたいと好評でしたし、いつか本当に食べていただきたいです
Valid HTML5 Valid CSS3 Another HTML Lint