应对CC攻击的自动防御系统——原理与实现

0x00 系统效果


此DDOS应用层防御系统已经部署在了http://www.yfdc.org网站上(如果访问失败,请直接访问位于国内的服务器http://121.42.45.55进行在线测试)。

此防御系统位于应用层,可以有效防止非法用户对服务器资源的滥用:

只要是发送高频率地、应用层请求以实现大量消耗系统资源的攻击方式,皆可有效防御。

其实现的基本思想是:

定期分析所有访问用户在过去各个时间段内的请求频率,将频率高于指定阈值的用户判定为资源滥用者,将其封杀一段时间,时效过后,防御系统自动将其解封。

在线效果测试:

进入http://www.yfdc.org -> 点击右上侧在线查询,此时将会进入/shell/yf域内,/shell/yf是一个用bash scripts写的动态web-cgi程序,用户每一次提交信息,此程序将会执行一些服务器端的查询操作,然后将数据处理后返回到客户端。

为了防止非法用户高频率地访问这个程序,而影响到其他正常用户的访问,故需要进行一些保护措施。

最终效果:

enter image description here

被封信息页面

在/shell/yf域内,按住F5不放,一直刷新,几秒后松开,就能看到被封信息和解封时间。
只要某个用户对/shell/yf的访问超过了正常的频率,服务将会对这个用户关闭一段时间,期满后自动解封。

0x01 系统原理


操作系统: CentOS 6.5 x86_64 开发语言: Bash Shell Scripts Web服务器: Apache Httpd

enter image description here

(此图为系统结构的鸟瞰图 可存至本地后放大查看)

2.1 自定义日志:/etc/httpd/logs/yfddos_log


enter image description here

(自定义日志文件的格式)

在httpd.conf的日志参数中,加入如下两行:

LogFormat "%a \"%U\" %{local}p %D %{%s}t " yfddos
CustomLog logs/yfddos_log yfddos

我们接下来重点分析日志文件/etc/httpd/logs/yfddos_log.

LogFormat "%a \"%U\" %{local}p %D %{%s}t " yfddos

解释:

%a -> 用户的IP
%U -> 请求的URL地址,但并不包含query string(The URL path requested, not including any query string.)
%{local}p -> 用户请求的服务器端口(一般为80)
%D -> 这个请求共消耗了服务器多少微秒(The time taken to serve the request, in microseconds.)
%{%s}t -> 服务器收到这个请求时,时间戳的值(seconds since 1970-01-01 00:00:00 UTC)

例子:

192.168.31.1 "/shell/yf" 80 118231 1417164313

译为:IP为192.168.31.1的主机,在时间戳为1417164313的时候,访问了/shell/yf,并由服务器的80端口向其提供服务,共耗时118231微秒

或为:IP为192.168.31.1的主机,在2014-11-28 16:45:13的时候,访问了/shell/yf,并由服务器的80端口向其提供服务,共耗时0.118231秒

至于为什么不使用httpd.conf中官方定义的日志,原因如下:

- 用户访问日志的一条记录可大约控制在60Bytes以内,数据量小,便于后期分析,官方定义的日志太过臃肿,影响分析速度
- 使用时间戳标志时间,便于后期分析,官方定义的日志时间参数为常规的表达方式,不便于直接进行处理
- httpd的日志系统本身就是从旧到新进行排序记录的,所以/etc/httpd/logs/yfddos_log日志条目的时间戳,亦为从小到大进行排序的,数据记录更加鲜明

2.2 yfddosd黑名单文件格式


enter image description here

黑名单文件格式

yfddosd黑名单文件/etc/yfddos/web-yf-search.b格式如下:

# ip    add-stamp  rmv-stamp
1.2.3.4 1416046335 1416046395
1.2.3.5 1416046336 1416046396
1.2.3.6 1416046339 1416046399

每一行为一个黑名单条目,上面第一个条目的意义为:

IP地址  :1.2.3.4
开始时间:时间戳1416046335,即 2014-11-15 18:12:15
终止时间:时间戳1416046395,即 2014-11-15 18:13:15

直观意义为:

IP地址:1.2.3.4,从2014-11-15 18:12:15开始,被封杀1分钟,在2014-11-15 18:13:15时自动解封。

这个文件将由驻留在系统中的daemon守护进程yfddosd进行维护更新。

2.3 守护进程yfddosd:防御系统的逻辑核心


enter image description here

守护进程的原理图

守护进程yfddosd是整个CC防御系统的核心,而function analyze_and_insert_black()则是yfddosd的核心。

yfddosd的配置参数:

yfddos_blackfilePath='/etc/yfddos/web-yf-search.b'
yfddos_accesslogPath='/etc/httpd/logs/yfddos_log'

function analyze_and_insert_black() { 
  # analyze_and_insert_black() :
  #   $1:max frequency(seems as abuse if above that) $2:blackip-ttl,time to live,unit is seconds (s) 
  #   $3:the access log ${3} seconds before will be analyzed to generate the abuse ip lists that we will block
  # example : analyze_and_insert_black "limit" "ttl" "time"
  # example : analyze_and_insert_black "4" "10" "5"
  # 分析在过去5s内的用户访问日志 如果有人在这5s内访问量>=4 系统将视其为资源滥用者 将其加入服务黑名单
  # 一条黑名单的作用时间为10s 即在10s之后 系统自动删除此黑名单条目 服务则继续向其开放
  # global vars:
  # stamp logtmpfile yfddos_blackfilePath
  # ......
}

函数analyze_and_insert_black有三个输入参数:

例子: analyze_and_insert_black "4" "10" "5"

解释: 分析日志文件/etc/httpd/logs/yfddos_log中,在过去5s内的用户访问日志,如果有IP在这5s内访问量>=4,守护进程yfddosd将视其为资源滥用者,
    然后将这个IP加入到黑名单文件/etc/yfddos/web-yf-search.b中,此条黑名单的作用时间为10s,在10s之后,守护进程yfddosd将删除此黑名单条目。

例子: analyze_and_insert_black "150" "2700" "905"

解释: 分析日志文件/etc/httpd/logs/yfddos_log中,在过去905s内的用户访问日志,如果有IP在这905s内访问量>=150,守护进程yfddosd将视其为资源滥用者,
    然后将这个IP加入到黑名单文件/etc/yfddos/web-yf-search.b中,此条黑名单的作用时间为2700s,在2700s之后,守护进程yfddosd将删除此黑名单条目。

简记为: analyze_and_insert_black "limit" "ttl" "time"

解释: 分析日志文件/etc/httpd/logs/yfddos_log中,在过去(time)s内的用户访问日志,如果有IP在这(time)s内访问量>=limit,守护进程yfddosd将视其为资源滥用者,
    然后此IP将会被加入到黑名单文件/etc/yfddos/web-yf-search.b中,作用时间为(ttl)s,在(ttl)s之后,守护进程yfddosd将自动删除此条目。

从上述中可看出,守护进程yfddosd至少需要完成如下三个任务:

  • 分析日志文件/etc/httpd/logs/yfddos_log中指定时间内的用户访问记录
  • 将资源滥用者的IP加入文件/etc/yfddos/web-yf-search.b,并设置封杀TTL参数值
  • 将/etc/yfddos/web-yf-search.b中已经过期的条目全部及时删除

守护进程yfddosd是如何实现上面三个逻辑的:

  • 分析日志文件/etc/httpd/logs/yfddos_log中指定时间内的用户访问记录:

    (1) 取出/etc/httpd/logs/yfddos_log中过去time秒的访问日志数据,使用二分法将这一操作的时间复杂度压缩到K*log2(N)以内,
        其中N为/etc/httpd/logs/yfddos_log中日志总行数,K为一次测试的耗时量,一般为1ms以内,即如有1048576条访问记录,这一操作将仅需要20*1ms
    (2) 使用正则RE对这些数据进行二次处理,过滤出所有访问指定URL的用户IP(这个URL为想要防御的http服务url,例如在http://www.yfdc.org系统中,
        所防御的就是/shell/yf,这个服务向访问者提供信息的search与get服务),再次使用sort与uniq对这些IP进行处理,以统计出每个IP的访问次数并进行高低排序
    
  • 将资源滥用者的IP加入文件/etc/yfddos/web-yf-search.b,并设置封杀TTL参数值

    将所有访问次数超过阈值limit的IP更新到黑名单文件/etc/yfddos/web-yf-search.b中,每个黑名单条目的封杀时间为ttl秒
    
  • 将/etc/yfddos/web-yf-search.b中已经过期的条目全部及时删除

    遍历/etc/yfddos/web-yf-search.b中所有黑名单条目,结合当前时间戳,将所有已经过期的条目一一删去    
    

下面是守护进程yfddosd状态机的伪代码:(略去了一些处理细节)

#init and FSM start work...
counter=0
while true
do
  sleep 5
  counter=counter+1
  delete obsolete items #将/etc/yfddos/web-yf-search.b中已经过期的条目全部删除
  if # every 5 seconds : 5s
  then
    analyze_and_insert_black "6" "10" "5"
    # 分析在过去5s内访问的用户 如果有人其访问量大于等于6 系统将视其为资源滥用者 
    # 遂将其加入服务黑名单 其作用时间为10s 在10s之后 daemon进程自动删除这个ip黑名单条目
  fi
  if #every 5*3 seconds : 15s 
  then
    analyze_and_insert_black "14" "45" "15"
  fi
  if #every 5*3*4+5 seconds : 65s
  then
    analyze_and_insert_black "40" "840" "65"
  fi
  if #every 5*3*4*3*5+5 seconds : 905s : 15min
  then
    analyze_and_insert_black "150" "2700" "905"
  fi
  if #every 5*3*4*3*5*4+5 seconds : 3605s : 1h
  then
    analyze_and_insert_black "300" "7200" "3605"
  fi
  if #every 5*3*4*3*5*4*3+5 seconds : 10805s : 3h
  then
    analyze_and_insert_black "400" "21600" "10805"
    if #在每天的00:01-04:59时间区间 一天仅执行一次
    then
        #备份日志 
    fi
  fi
done

防御者应斟酌调整每个检测时间点的参数值(封杀时间ttl与判定阈值limit),以调节系统应对CC攻击到来时的反应时间。

0x02 源代码


#!bash
##################################### vim /usr/local/bin/yfddosd.sh :
##################################### nohup bash /usr/local/bin/yfddosd.sh &>"/etc/yfddos/""yfddosd-log-`date +%Y-%m-%d`" & 
##################################### yfddos daemon
mkdir /etc/yfddos
yfddos_blackfilePath='/etc/yfddos/web-yf-search.b'
yfddos_accesslogPath='/etc/httpd/logs/yfddos_log'

### refresh tll
logtmpfile=`mktemp`
stamp=`date +%s`
touch "$yfddos_blackfilePath"
if grep -Po '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$yfddos_blackfilePath" &>/dev/null
then
  cat "$yfddos_blackfilePath" | while read i
  do
    deadstamp=`echo "$i" | grep -Po '[0-9]+$'` 
    if [ "$stamp" -le "$deadstamp" ]
    then
      echo "$i" >>"$logtmpfile"
    fi
  done
fi
chmod o+r "$logtmpfile"
mv -f "$logtmpfile" "$yfddos_blackfilePath"
if ! grep -Po '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$yfddos_blackfilePath" &>/dev/null
then
  echo '255.255.255.255 0 0' >> "$yfddos_blackfilePath"
fi

function analyze_and_insert_black() { 
  # analyze_and_insert_black() :
  #   $1:max frequency(seems as abuse if above that) $2:blackip-ttl,time to live,unit is seconds (s) 
  #   $3:the access log ${3} seconds before will be analyzed to generate the abuse ip lists that we will block
  # example : analyze_and_insert_black "limit" "ttl" "time"
  # example : analyze_and_insert_black "4" "10" "5"
  # 分析在过去5s内的用户访问日志 如果有人在这5s内访问量>=4 系统将视其为资源滥用者 将其加入服务黑名单
  # 一条黑名单的作用时间为10s 即在10s之后 系统自动删除此黑名单条目 服务则继续向其开放
  # global vars:
  # stamp logtmpfile yfddos_blackfilePath
  local threshold="$1"
  local ttl="$2"
  local stamp_pre="$3"
  local i=0
  local num=""
  local fre=0
  local ip=0
  local localbuf=0
  local linenum=0
  local deadstamp=0
  stamp_pre="$((stamp-stamp_pre))"

  #二分查找初始化
  local temp=0
  local yf_x='1'
  local yf_y=`cat "$logtmpfile" | wc -l`
  if [ "$yf_y" -le "1" ]
  then 
    yf_y=1
  fi
  local yf_I=$(((yf_x+yf_y)/2))

  temp=`cat "$logtmpfile" | wc -l`
  if [ "$temp" -gt "0" ]
  then
    temp=`sed -n '$p' "$logtmpfile" | grep -Po '[0-9]+ $'`
    if [ "$temp" -lt "$stamp_pre" ]
    then
      num=""
    else  
      while true #使用二分查找的方法 快速地分析访问日志
      do
        temp=`sed -n "${yf_x}p" "$logtmpfile" | grep -Po '[0-9]+ $'`
        if [ "$temp" -ge "$stamp_pre" ]
        then
          break
        fi
        if [ "$((yf_y-yf_x))" -le "1" ]
        then
          yf_x="$yf_y"
          break
        fi
        temp=`sed -n "${yf_I}p" "$logtmpfile" | grep -Po '[0-9]+ $'`
        if [ "$temp" -lt "$stamp_pre" ]
        then
          yf_x="$yf_I"
          yf_y="$yf_y"
          yf_I="$(((yf_x+yf_y)/2))"
          continue
        fi
        yf_x="$yf_x"
        yf_y="$yf_I"
        yf_I="$(((yf_x+yf_y)/2))"  
        continue
      done
      temp=`sed -n "${yf_x}p" "$logtmpfile" | grep -Po '[0-9]+ $'`
      if [ "$temp" -ge "$stamp_pre" ]
      then
        num="$yf_x"
      else
        num=""
      fi
    fi  

    if [ -n "$num" ]
    then
      sed -n "${num},\$p" "$logtmpfile" | grep -Po '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -n | uniq -c | sort -rn | while read i
      do
        fre=`echo "$i" | grep -Po '[0-9]+' | head -1`
        ip=`echo "$i" | grep -Po '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' `
        if [ "$fre" -ge "$threshold" ]
        then #insert illegal ips : cat "$yfddos_blackfilePath"
          # ip    add-stamp  rmv-stamp
          #1.2.3.4 1416046335 1416046395
          temp=`grep -Pn "${ip//./\\.} " "$yfddos_blackfilePath"`
          if [ -n "$temp" ]
          then
            linenum=`echo "$temp" | grep -Po '^[0-9]+' | head -1`
            deadstamp=`echo "$temp" | grep -Po '[0-9]+$' | sort -rn | head -1 `  
            if [ "$((stamp+ttl))" -gt "$deadstamp" ]
            then
              sed -i "${linenum}s/.*/${ip} ${stamp} $((stamp+ttl))/g" "$yfddos_blackfilePath"
            fi
          else
            sed -i "\$a ${ip} ${stamp} $((stamp+ttl))" "$yfddos_blackfilePath"
          fi 
        else
          break
        fi
      done
    fi

  fi
}

#init and yfddosd's FSM start work...
counter=0

while true
do
  sleep 5
  counter=$((counter+1))
  echo -n `date +%Y-%m-%d\ %H:%M:%S`" ""counter ${counter}:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  echo -n "refresh tll:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  ### refresh tll
    #refresh ttl: analyze file: "$yfddos_blackfilePath" if some items'ttl has been reach the date , we will remove it and open service to the ip had been banned before.
      #insert illegal ips : cat "$yfddos_blackfilePath"
      # ip    add-stamp  rmv-stamp
      #1.2.3.4 1416046335 1416046395
    #sed -i "/^.* $((stamp-5))$/d;/^.* $((stamp-4))$/d;/^.* $((stamp-3))$/d;/^.* $((stamp-2))$/d;/^.* $((stamp-1))$/d;/^.* $((stamp))$/d;/^$/d" "$yfddos_blackfilePath"  
  logtmpfile=`mktemp`
  stamp=`date +%s`
  touch "$yfddos_blackfilePath"
  if grep -Po '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$yfddos_blackfilePath" &>/dev/null
  then
    cat "$yfddos_blackfilePath" | while read i
    do
      deadstamp=`echo "$i" | grep -Po '[0-9]+$'` 
      if [ "$stamp" -le "$deadstamp" ]
      then
        echo "$i" >>"$logtmpfile"
      fi
    done
  fi
  chmod o+r "$logtmpfile"
  mv -f "$logtmpfile" "$yfddos_blackfilePath"
  if ! grep -Po '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$yfddos_blackfilePath" &>/dev/null
  then
    echo '255.255.255.255 0 0' >> "$yfddos_blackfilePath"
  fi

  logtmpfile=`mktemp`
  stamp=`date +%s`
  cat "$yfddos_accesslogPath" | grep -P ' "/shell/yf" ' >"$logtmpfile"
  if true # every 5 seconds : 5s
  then
    echo -n "analyze_and_insert_black 6 10 5:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
    #analyze yfddos log : analyze_and_insert_black() $1:max frequency(seems as abuse if above that) $2:blackip-ttl $3:the access log ${3} seconds before will be analyzed to generate the abuse ips that we will block
    analyze_and_insert_black "6" "10" "5"
    # 分析在过去5s内访问的用户 如果有人其访问量大于等于6 系统将视其为资源滥用者 遂将其加入服务黑名单 其作用时间为10s 在10s之后 daemon进程自动删除这个ip黑名单条目
  fi
  if [ "$((counter%(3)))" -eq "0" ] #every 5*3 seconds : 15s 
  then
    echo -n "analyze_and_insert_black 14 45 15:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "    
  # example : analyze_and_insert_black "limit" "ttl" "time"
    analyze_and_insert_black "10" "45" "15"
  fi
  if [ "$((counter%(3*4+1)))" -eq "0" ] #every 5*3*4+5 seconds : 65s
  then
    echo -n "analyze_and_insert_black 40 840 65:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  # example : analyze_and_insert_black "limit" "ttl" "time"
    analyze_and_insert_black "25" "840" "65"
  fi
  if [ "$((counter%(3*4*3*5+1)))" -eq "0" ] #every 5*3*4*3*5+5 seconds : 905s : 15min
  then
    echo -n "analyze_and_insert_black 150 2700 905:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  # example : analyze_and_insert_black "limit" "ttl" "time"
    analyze_and_insert_black "150" "2700" "905"
  fi
  if [ "$((counter%(3*4*3*5*4+1)))" -eq "0" ] #every 5*3*4*3*5*4+5 seconds : 3605s : 1h
  then
    echo -n "analyze_and_insert_black 300 7200 3605:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  # example : analyze_and_insert_black "limit" "ttl" "time"
    analyze_and_insert_black "300" "7200" "3605"
  fi
  if [ "$((counter%(3*4*3*5*4*3+1)))" -eq "0" ] #every 5*3*4*3*5*4*3+5 seconds : 10805s : 3h
  then
    echo -n "analyze_and_insert_black 400 21600 10805:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
  # example : analyze_and_insert_black "limit" "ttl" "time"
    analyze_and_insert_black "400" "21600" "10805"
    #### "${yfddos_accesslogPath}" backup : 在每天的00:01-04:59时间区间内 备份日志一次
    if [ "`date +%H`" -le "5" ] && ! [ -f "${yfddos_accesslogPath}-`date +%Y-%m-%d`" ]
    then
      service httpd stop
      mv "${yfddos_accesslogPath}" "${yfddos_accesslogPath}-`date +%Y-%m-%d`"
      service httpd start
    fi
  fi
  rm -fr "$logtmpfile"
  echo "sleep:"`cat /proc/uptime | grep -Po '[0-9\.]+' | head -1`"  "
done

©乌云知识库版权所有 未经许可 禁止转载


30
燕云 2015-04-27 11:08:18

@han 你好!
黑名单存放在/etc/yfddos/web-yf-search.b 文中有介绍此文件的格式 web应用程序只需要在每一次接入新的连接时判定一下即可

30
han 2015-02-05 15:05:51

没看到怎么封锁客户端ip的代码

30
燕云 2014-12-17 17:06:47

我之前一直用C开发工具,后来经历了Python->shell scripts
一个人和一个团队是不一样的,时间成本问题

我测过 效率真的没有你想的那样糟 :)

30
燕云 2014-12-17 17:02:44

你好!

因为系统有几个监测时间点,如果你在第一个时间段已经访问了很多次,当到达第二个时间段时,依旧被判定为了 abuse ,所以出现了上面的现象 :)

30
Sd_red 2014-12-16 09:57:25

已加入收藏夹,还没仔细看。我有时间测试下效率问题。因为应对CC如果用shell的话,计算效率是很低的。。。。总之,很感谢作者分享的文章。让我受益很多。

30
cc 2014-12-11 10:05:54

解封后 点一次页面就被封

30
燕云 2014-12-04 00:18:38

ps: 这里的env指的是cgi环境的env

30
燕云 2014-12-04 00:16:48

Apache Module mod_log_config:
%{FOOBAR}e : The contents of the environment variable FOOBAR
使用这个选项所有http连接参数都能写到日志中 cookie信息自然也可以了 :)

30
DBA 2014-12-03 22:01:01

未考虑NAT网络的情况。token+ip 这应该是程序里才能实现的,仅仅利用Apache的日志怕是不行了。

30
燕云 2014-12-03 20:59:39

笔者是14本科毕业生,现正找一份关于Linux系统维护的工作,[email protected]

30
燕云 2014-12-03 20:57:58

使用IP进行封杀确实存在一定误杀几率,现在有如下若干解决方案:
1.验证码:这种方法在面对用户需要经常访问的服务面前,只能作为辅助手段,否则影响用户体验
2.Cookie:因为Cookie非常容易伪造,这种方法只能阻碍住一般水平的攻击
3.usr-token+ip:采用用户登录后为其发放令牌,令其与用户IP进行绑定的方法,使用(usr-token,ip)二元组,这种方法最为理想,但需要用户先进行登录系统
因为目前yfdc网站上并无用户系统,所有并未实行第三种方案。
要实行第三种方法,只需要在yfddos_log里增加usr-token数据项,在后续的处理中对(usr-token,ip)一同处理即可。

另外,mod_security确实很强大

30
燕云 2014-12-03 20:55:37

这个系统只是快速建立的“第一个系统”原型,第一个系统顺利运行成功后,以后的优化和改进方向便更加清晰明了(惭愧 自己动手之前确实不知道nginx/tengine的limit_req_zone功能)
日志文件很小,对于每5s一次的分析任务,这点消耗服务器还是顶得住吧 :)

30
燕云 2014-12-03 20:51:17

代码好用,应经部署在了yfdc.org上,可自行测试

30
Forever80s 2014-12-03 16:44:43

100人用一个外网ip会不会false positive?
记得mod_security很强大

30
missdiog 2014-12-03 15:47:20

shell实现的
nginx /tengine 的limit_req_zone 功能

通过日志获取频率,于磁盘IO,CPU都不好,性能太低了,高并发死得快,还是nginx好点,漏桶原理,动作很快,姿势很帅

30
wangy3e 2014-12-03 13:18:54

代码好用么???~`

30
[email protected] 2014-12-03 12:37:40

Mark!

感谢知乎授权页面模版