|
~ To be, or not to be, or to ask someone to be. ~ null-i.net |
| Linux/Synフラッドの防ぎ方 | ||||
|
Synフラッドをサブネット単位で阻止する(2026-04-12)
はじめに夜間に大量のアクセスが集中してWebサーバにアクセスしづらくなるとか、そんな経験ありませんか? */5 * * * * \netstat -tan > /var/log/netstat/log`date +\%H%M`.txt
Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State (中略) tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.92:58987 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.96:49559 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.8.197:58838 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.8.248:19745 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.208:40599 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.11.212:28493 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.20:59951 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.158:45795 SYN_RECV tcp 0 0 aa.bb.cc.dd:443 xxx.189.10.59:26735 SYN_RECV (以下省略) 攻撃者が xxx.189.9.0/28 〜 xxx.189.11.0/28のIPアドレスからSynフラッドを仕掛けてきています。 wget https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest wget https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest wget https://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest wget https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest wget https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest
IPアドレスの集約方法今回の例は作者が理解しやすいように「二進数文字列に変換」なんて手法を取っていますが、ぜひ、もっと冴えたやり方を考えてみてください。 x.189.9.92 = xxxxxxxx.10111101.00001001.01011100 x.189.9.96 = xxxxxxxx.10111101.00001001.01100000 x.189.8.197 = xxxxxxxx.10111101.00001000.11000101 見やすさのためにドット(.)を入れましたが、つまり二進数32文字に変換しています。 実際のコード後述するperl スクリプトを netstater.pl として、netstat -tan の出力を食わせます # RIRの5ファイルを rir_list/ 配下に準備し # mkdir log/ した状態で # netstat -tan > netstat.log した状態で実行 # (netstat.log ファイルは複数指定可能) $ ./netstater.pl -yes netstat.log (中略) xxx.189.8.248 XX 1 xxx.189.9.208 XX 1 xxx.189.8.192/28 (2) xxx.189.8.224/28 (2) xxx.189.8.224/28 (2) xxx.189.9.64/28 (4) (以下省略。 XXには国コード、 カッコ内の数値はサブネット内にいくつのIPが含まれるか、 そして -yes 指定で log/netstat_ban.txt が生成される) 実行するとSYN_RECV しているIPアドレスをサブネットで抽出します。
#!/usr/bin/perl -w
# 前半は国情報の付与のための準備、
# 後半で、netstat -tan のログからサブネット集約を試みます
#
# あらかじめ、このスクリプトと同じ場所に
# mkdir log/
# (変数 ban_file 、info_file を参照。ログ出力先)
# あと rir_list/ 配下に取得済みの RIR ファイルを設置すること
#
# 国別の判定を追加・削除する場合は JP 部分を変更するか、
# 前半の RIR 読み込み処理をごっそり削除してください
#
# 引数で -yes を指定して実行することで
# log/ 配下に netstat_ban.txt が出力されるので
# これを firewall-cmd などで指定して一時的にアクセス拒否する
#
# @2026.04.12 null-i.net
#
sub dbg_printf{
# デバッグ文、作成過程では入れまくってます。
if(! defined $DEBUG ){
return; # ここをコメントアウト
}
#printf STDERR "dbg:";
printf STDERR @_ ;
$|=1;
}
#--------------------
# ここからメイン処理
#--------------------
if(! defined($ARGV[0])){
printf("no args\n");
exit;
}
# まず、サブネット 国名 一覧を読み込む
use File::Basename;
my $base_dir = dirname(__FILE__) . "/rir_list/";
@rir_lists = (
$base_dir . "delegated-afrinic-extended-latest",
$base_dir . "delegated-apnic-extended-latest",
$base_dir . "delegated-arin-extended-latest",
$base_dir . "delegated-lacnic-extended-latest",
$base_dir . "delegated-ripencc-extended-latest"
);
foreach my $r_list (@rir_lists){
open($list, "< $r_list") or die "$! [r_list]\n";
while(<$list>){
# 上記の、こんな感じのファイルを読みたい
# apnic|JP|ipv4|1.0.16.0|4096|20110412|allocated|A92D9378
if(/^[^\|]+\|(\w+)\|ipv4\|([\d\.]+)\|(\d+)\|/){
$ip=$2; $num=$3; $cnt=$1;
if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
$bin=$1*256*256*256
+$2*256*256
+$3*256
+$4; # IP表記から数値に戻す
$ip_range{$bin} = $num;
$ip_cntry{$bin} = $cnt;
}
}
}
}
my %ip_n;
my %ip_text;
my %score;
my %flag;
my %access;
my %f_ip;
my %f_c_code;
dbg_printf("make country table\n");
my $yn_output = "no";
foreach my $e_file (@ARGV){
if($e_file =~ /^-/){
if($e_file =~ /^-yes/){
$yn_output = "yes";
}
if($e_file =~ /^-dbg/){
$DEBUG = "yes";
}
next;
}
dbg_printf("file:%s\n", $e_file);
open($old_file, "< $e_file") or die "$! [$e_file]\n";
while(<$old_file>){
if(/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/){
# Proto Recv-Q Send-Q Local Address Foreign Address State
# tcp 0 0 aa.bb.cc.dd:443 xxx.189.9.92:58987 SYN_RECV
my ($o_proto, $o_rq, $o_sq, $o_local, $o_foreign, $o_state) = ($1, $2, $3, $4, $5, $6);
if($o_proto ne "tcp"){ next; }
if($o_state ne "SYN_RECV"){ next; } # まずは syn flood のみで
if($o_local =~ /(\d+\.\d+\.\d+\.\d+):(\d+)/){ # IPv6未対応
my ($l_ip, $l_port) = ($1, $2);
#if($l_port == 25 or $l_port == 443 or $l_port == 80){
if($l_port == 25){
next; # 無視するポートがあれば指定する(例としてSMTP)
}
}
my $o_ip;
if($o_foreign =~ /(\d+\.\d+\.\d+\.\d+):(\d+)/){
my ($o2_ip, $o2_port) = ($1, $2);
if(defined $f_ip{$o2_ip}){ # 既にカウント済みの場合
$f_ip{$o2_ip} = $f_ip{$o2_ip} + 1; # そのままカウントアップ
next; # 次へすすむ
}else{
$f_ip{$o2_ip} = 1;
$o_ip = $o2_ip; # 後続の処理へ続く
dbg_printf("netstat:%s", $_);
}
}
if(defined $f_c_code{$o_ip}){ next; } # 国コードが無い場合は後続へ
my $o_bin;
if($o_ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
$o_bin=$1*256*256*256
+$2*256*256
+$3*256
+$4; # IP表記から数値に変換
}
my $where = "??";
foreach my $start (keys(%ip_range)){
if($start <= $o_bin && $o_bin <= ($start + $ip_range{$start})){
$where = $ip_cntry{$start};
last;
}
}
$f_c_code{$o_ip} = $where;
}else{
printf(STDERR "ERR: %s\n", $_);
}
}
}
dbg_printf("--------\n");
$base_dir = dirname(__FILE__);
my $ban_file = $base_dir . "/log/netstat_ban.txt";
my $info_file = $base_dir . "/log/netstat_info.txt";
if($yn_output eq "yes"){
open($f_info, "> $info_file") or die "$! [$info_file]\n";
open($f_ban, "> $ban_file") or die "$! [$ban_file]\n";
}
foreach my $iip (keys %f_ip){
printf(STDOUT "%-16s %s %d\n", $iip, $f_c_code{$iip}, $f_ip{$iip});
if(! defined $f_info){ next; }
printf($f_info "%-16s %s %d\n", $iip, $f_c_code{$iip}, $f_ip{$iip});
if($f_ip{$iip} > 11 && $f_c_code{$iip} ne "JP"){
# n 以上かつ国外は当選、一時間?はbanする
if($iip !~ /\/32/){ # 32 はサブネットではなく普通にアドレス
printf($f_ban "%s\n", $iip);
}
}
}
my $SUBNET_BAN=26; #--- これより大きいサブネットマスクで集約する ----
dbg_printf("make binary table-----\n");
sub number2ip {
# 数値変換したIPアドレスを文字列へ戻す
my $b = shift;
my $ip = sprintf("%d.%d.%d.%d",
unpack("C", pack("B8", substr($b, 0, 8))),
unpack("C", pack("B8", substr($b, 8,16))),
unpack("C", pack("B8", substr($b,16,24))),
unpack("C", pack("B8", substr($b,24,32)))
);
return $ip;
}
my %f_bin2n; # 二進数と数字
my %f_bin2ip;# 二進数とIPv4アドレス表記
foreach my $iip (keys %f_ip){
if($iip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
$o_bin=$1*256*256*256
+$2*256*256
+$3*256
+$4; # IP表記から数値に変換
}else{
next;
}
$bin32 = sprintf("%032b", $o_bin); # 二進数表記へ
$f_bin2n{$bin32} = $o_bin;
$f_bin2ip{$bin32} = $iip;
}
$base_dir = dirname(__FILE__);
my $subinfo_file = $base_dir . "/log/netstat_subinfo.txt";
if($yn_output eq "yes"){
open($f_sbinfo, "> $subinfo_file") or die "$! [$info_file]\n";
}
dbg_printf("sort and dump netmask----\n");
my %subnet_mask;
my %subnet_32bit;
my %subnet_range;
my $last_ip = sprintf("%032b", -1);
for my $ip (sort keys %f_bin2n) {
my ($pre, $suf, $mask) = ("", $ip, "");
my $i;
for($i=$SUBNET_BAN; $i<=32; $i++){ #---- ここでサブネット集約する範囲を指定
#--- ソート済みの状態から、
#--- 一つ前のIPアドレスと二進数で文字列比較する
if(substr($last_ip, 0, $i) eq substr($ip, 0, $i)){ #---- 一致するなら、集約
$pre = substr($ip, 0, $i); #--- 一致部分
$suf = substr($ip, $i, 32); #--- 不一致部分
$mask = "0" x (32 - $i); #--- 不一致部分をゼロでマスクする
}
else {
last;
}
}
my $subnet;
if(length($pre) > 0){
$subnet = number2ip($pre . $mask) . "/" . $i;
}else{
$subnet = number2ip($suf);
}
if(length($pre) > $SUBNET_BAN){
# 赤で表示(\e[31m)サブネットを添えて
if(defined $f_sbinfo){ printf($f_sbinfo "\e[31m%s\e[0m%s %19s\n", $pre, $suf, $subnet );}
}else{
# 緑で表示(\e[32m)IPアドレスを添えて
if(defined $f_sbinfo){ printf($f_sbinfo "\e[32m%s\e[0m%s %19s\n", $pre, $suf, $f_bin2ip{$ip});}
}
if(length($pre) > $SUBNET_BAN){
if($subnet =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/.+/){
my $sub_n=$1*256*256*256
+$2*256*256
+$3*256
+$4; # サブネットを数値にして、ハッシュのキーとして使用
$subnet_mask{$sub_n} = $subnet;
$subnet_32bit{$sub_n} = $pre . $suf;
$subnet_range{$sub_n} = length($pre);
}
}
$last_ip = $ip;
}
# サブネット同士で集約する----------------------
my %s_mask;
my %s_32bit;
my %s_range;
foreach my $sa (keys %subnet_mask){
my $hit=0;
foreach my $sb (keys %subnet_mask){
if($sa == $sb){
next; # 自分自身と比較はしない
}
my $len=$subnet_range{$sb};
if($subnet_range{$sa} <= $len){
next; # 自分の方が短い場合、比較はしない
}
if(substr($subnet_32bit{$sa}, 0, $len) eq substr($subnet_32bit{$sb}, 0, $len)){
#if(defined $f_sbinfo){ printf($f_sbinfo "# %s -> %s\n", $subnet_mask{$sa}, $subnet_mask{$sb});} # ここはあとで削除。確認用。
$hit++; # 他のサブネットで集約される
last;
}
}
if($hit==0){
$s_mask{$sa} = $subnet_mask{$sa};
$s_32bit{$sa} = $subnet_32bit{$sa};
$s_range{$sa} = $subnet_range{$sa};
}
}
my %s_include; # 集約範囲に含まれるIPアドレスのカウント
for my $ip (sort keys %f_bin2ip) { # ここはsortで無くても可。処理速度に応じて。
my $hit=0;
foreach my $mask (keys %s_mask){
my $len=$s_range{$mask};
if(substr($ip, 0, $len) eq substr($s_32bit{$mask}, 0, $len)){
$s_include{$mask} = (defined($s_include{$mask}) ? $s_include{$mask} +1 : 1);
#dbg_printf("# %s -> %s[%d]\n", $f_bin2ip{$ip}, $s_mask{$mask}, $s_include{$mask}); # ここはあとで削除。確認用。
$hit++;
last;
}
}
if($hit>0){ # 既に集約済みならば、出力せずにスキップ
next;
}
}
my $is_sub=0;
for my $n (sort keys %s_mask) { # ここはsortで無くても可。処理速度に応じて。
printf(STDOUT "%s\t(%d)\n", $s_mask{$n}, $s_include{$n});
if(defined $f_ban){ printf($f_ban "%s\n", $s_mask{$n}); }
$is_sub++;
}
if($is_sub <= 0){
printf(STDOUT "no subnet mask\n");
}
dbg_printf("did it!\n");
参考:netstat -tan のログの例ひとまず xxx でマスクをかけてますが、適当な数値で変換してから使ってください。
使い方の例まず、事前にファイアウォールのルールを作っておきます。 # firewall-cmd --permanent --new-ipset=ban_netstat --type=hash:ip # firewall-cmd --reload # firewall-cmd --add-rich-rule='rule source ipset=ban_netstat drop' --permanent # firewall-cmd --reload
# crontab -e で以下を登録 # 先に mkdir /var/log/netstat/ してください # 5分おきに、過去3時間分のログが残るように上書きしています */5 0,3,6,9,12,15,18,21 * * * \netstat -tan > /var/log/netstat/log`date +1\%M` */5 1,4,7,10,13,16,19,22 * * * \netstat -tan > /var/log/netstat/log`date +2\%M` */5 2,5,8,11,14,17,20,23 * * * \netstat -tan > /var/log/netstat/log`date +3\%M`
./netstater.pl -yes /var/log/netstat/log*
#/bin/sh
BASE_DIR="/ツールを置いた場所"
LOG_DIR="/var/log/netstat"
STATER_BAN_LIST="$BASE_DIR/log/netstat_ban.txt"
cat /dev/null > ${STATER_BAN_LIST}.old
\cp -f $STATER_BAN_LIST ${STATER_BAN_LIST}.old
${BASE_DIR}/netstater.pl -yes ${LOG_DIR}/log* > /dev/null
if [ -s ${STATER_BAN_LIST}.old -a -s ${STATER_BAN_LIST} ]; then
# 前回分のリストを一旦、削除
# 範囲重複によるINVALID_ENTRY を消したいので quiet指定
firewall-cmd --quiet --ipset=ban_netstat --remove-entries-from-file=${STATER_BAN_LIST}.old --permanent > /dev/null
fi
if [ -s ${STATER_BAN_LIST} ]; then
# 今回分のリストを追加
firewall-cmd --quiet --ipset=ban_netstat --add-entries-from-file=${STATER_BAN_LIST} --permanent > /dev/null
fi
firewall-cmd --reload > /dev/null
*/5 0,3,6,9,12,15,18,21 * * * \netstat -tan > /var/log/netstat/log`date +1\%M` && /ツールを置いた場所/cron_netstat.sh (同様に残りの二行も追記・変更)
|
|
|||