技術討論區 > PHP程式設計討論區

防止機器人灌水

頁: (1/6) > >>

ricky:

目前為了避免自動灌水機器人
各個網站的防治方法不外乎使用圖片文字去防堵
不過這種方式除了傷眼睛,對系統而言還得耗費額外的資源去產生動態的圖片
並且紀錄session
相對而言,想要搞破壞的人只要拼命的對你的sever發出request
Server勢必會被這些session以及圖形產生器給搞死
對使用者而言,除了麻煩還是麻煩
那是否有更好的解決方法呢
這幾天上下班的途中突然有了靈感 :D
於是就做出了這個東西

首先我們說明一下原理
目前灌水機器人或是想寫個灌水機器人的"人"
幾乎都是分析HTML的內容去找出特定的input表單
然後在針對這些tag去做post塞資料的動作
所以我們在server端產生一個驗證的字串
透過javascript指定給一個hidden欄位
我們再用這個hidden欄位去做檢查的動作

可是有人會開始質疑,去分析javascript的內容把特定欄位的值抓出來
不就形同破解了
所以為了避免這種情況,我們動用了一個暴力的手法
(當初這種手段是用在病毒上將自己加密來躲過使用特徵碼偵測方式的防毒軟體)
所以嘍,在這裡做了兩個動作

1.將填入驗證值的那段javascript給加密
2.將解密段的程式變數採用不故定長度,而且隨機的名稱,避免被人直接分析文字內容

透過上面這兩個步驟,想單純使用文字分析的方式就破功了
想破解這段,勢必得弄個javascript的直譯器才行
這工程太浩大了,應該沒人這麼閒吧

下一篇就是程式碼實做的部份嘍

待續...

ricky:

這邊我寫好了一個php的class
robotaway.inc

--- 代碼: ---
 class RobotAway
 {
/*
  $VerifyKey 指定給Client端的驗證碼
  $EncodeKey 加密用的key
  $ElementID input表單中hidden欄位的id值
  $Prefix        驗證函數開頭名稱
  $TimeOut    驗證碼時效(分鐘)
  $CheckFunctionName  檢驗函數的名稱,這邊也導入了隨機命名,增加混亂度
*/
  var $VerifyKey;
  var $EncodeKey;
  var $ElementID;
  var $Prefix;
  var $TimeOut;
  var $CheckFunctionName;

/*用來產生隨機名稱的函數*/
  private function GenerateRandomFunctionName()
 {
   return '_'.substr(md5(time().microtime().rand()),0,rand(10,32));
  }

/*
用來將Javascript var變數定義存在一個Array裡
將順序打亂在輸出成一個String
*/
  private function GenerateVarDefineString($VarArray)
  {
   $VarStrings='';
   $VarArrayCount=count($VarArray);
   for($i=0;$i<=$VarArrayCount;$i++)
   {
    $RandSwap1=rand(0,$VarArrayCount-1);
    $RandSwap2=rand(0,$VarArrayCount-1);
    $Tmp=$VarArray[$RandSwap1];
    $VarArray[$RandSwap1]=$VarArray[$RandSwap2];
    $VarArray[$RandSwap2]=$Tmp;
   }
   foreach($VarArray as $Index => $VarString)
    $VarStrings.=$VarString;
   return $VarStrings;
  }

/*
 在Javascript中加入混淆用註解的函數
*/
  private function ConfuseComment()
  {
   $ConfuseStringPatten="0123456789abcdef_+-/,'\";= ";
   $ConfuseString='';
   $Size=rand(5,10);
   for($i=0;$i<$Size;$i++)    $ConfuseString.=$ConfuseStringPatten{rand(0,strlen($ConfuseStringPatten)-1)};
   return "/*$ConfuseString*/";
  }
/*
用來將變數打亂然後加上混淆註解的函數
例如
$this->('1234567');
程式輸出結果會變成
[垃圾註解]'12'+'345'+'67'[垃圾註解]
*/
  private function Confuse($String)
  {
   if(strlen($String)<=5) return $this->ConfuseComment()."'$String'".$this->ConfuseComment();
   $ConfusedString='';
   $StringLen=strlen($String);
   for($i=0;;)
   {
    $RndStrLen=rand(5,10);
    if($RndStrLen>=strlen($String)-$i-1) $RndStrLen=strlen($String)-$i;
    $ConfusedString.=$this-> ConfuseComment()."'".substr($String,$i,$RndStrLen)."'".$this-> ConfuseComment()."+";
    $i+=$RndStrLen;
    if($i>=strlen($String)-1) break;
   }
   return $this->ConfuseComment()."$ConfusedString''".$this->ConfuseComment();
  }

/*建構函數*/
  function __construct($EncodeKey,$ElementID='RACheck',$Timeout=10,$Prefix='RA')
  {
   $this->ElementID=$ElementID;
   $this->Prefix=$Prefix;
   $this->GenerateVerifyKey();
   $this->TimeOut=$TimeOut;;
  }
/*
 用來檢驗Client Post送過來的hidden欄位的驗證碼
 驗證碼的格式為時間戳記加上兩個md5組成的字串
 8byte的時間戳記
 32byte ID值
 32byte 驗證值
 驗證值則是ID跟加密的Key兩個的md5
 所以我們只要檢查這個驗證碼是否合法來判別輸入的驗證碼是否正確
 就不需額外使用session來存驗證碼了
*/
  function Verify($Key)
  {
   $Time=substr($Key,0,8);
   if(($this->TimeOut)&&(hexdec($Time)+$this->TimeOut*60<time())) return false;
   $ID=substr($Key,8,32);
   if($ID!=md5($Time.$this->EncodeKey)) return false;
   $CheckValue=substr($Key,40);
   $Result=md5($ID.$this->EncodeKey);
   return ($Result==$CheckValue);
  }
/*
這邊是用來產生驗證碼
ID就是一個亂數產生的識別碼
*/
  private function GenerateVerifyKey()
  {
   $Time=sprintf("%x",time());
   $ID=md5($Time.$this->EncodeKey);
   $this->VerifyKey=$Time.$ID.md5($ID.$this->EncodeKey);
  }
/*
 這邊是一個XOR的加密函數
 用來將產生的Javascript給加密起來
*/
  private function XorEncode($Str,$Key)
  {
   $KeyLen=strlen($Key);
   $EncStr='';
   for($i=0;$i<strlen($Str);$i++)
   {
    $BinStr=hexdec(bin2hex($Str{$i}));
    $BinKey=hexdec(bin2hex($Key{$i%$KeyLen}));
    $EncStr.=pack('C',$BinStr^$BinKey);
   }
   return urlencode($EncStr);
  }
/*
這裡就是整個程式的核心
用來產生隨機的Javascript
*/
  function GenerateJS()
  {
   $VarName='_'.substr(md5(time().microtime().rand()),0,rand(10,32));
   $FunctionName='_'.substr(md5(time().microtime().rand()),0,rand(10,32));
   $EvalDataEncodeKey=sha1(time().microtime().rand());
   $EncodedFunction='_'.substr(md5(time().microtime().rand()),0,rand(10,32));
   $EvalDataEncodeKeyName='_'.substr(md5(time().microtime().rand()),0,rand(10,32));
/*
 $JS 是用來取得驗證碼的核心Javascript 函數
  由於驗證碼是一個64bytes的字串值
  這邊我們把他打散並且加密起來
  最後產生的Javscript會像這樣
var _df12688fab12='xxx';
_df12688fab12+='xxx';
_df12688fab12+='xxx';
其中變數名稱以及打散的數量是不固定的
*/
   $JS="var $VarName='';";
   for($i=0;;)
   {
    $RndStrLen=rand(1,10);
    if($RndStrLen>=strlen($this->VerifyKey)-$i-1) $RndStrLen=strlen($this->VerifyKey)-$i;
    $JS.="$VarName+='".substr($this->VerifyKey,$i,$RndStrLen)."';";
    $i+=$RndStrLen;
    if($i>=strlen($this->VerifyKey)-1) break;
   }
/*
這邊的EvalData就是經過xor加密過後的Javascript函數嘍
$EvalDataEncodeKey 就是解密的Key
*/
   $EvalData=$this->XorEncode("var $FunctionName".'=function(){'.$JS."return $VarName;};",$EvalDataEncodeKey);

/*
這邊是將Javascript中的var變數宣告加入一個php的array中
透過Confuse這個函數去打亂
最後再組回一段字串成為Javascript輸出
*/
   array_push($RandomJSVarDefineList,"var $EncodedFunction=decodeURIComponent(".$this->Confuse($EvalData).");");
   array_push($RandomJSVarDefineList,"var $EvalDataEncodeKeyName=".$this->Confuse($EvalDataEncodeKey).";");
   array_push($RandomJSVarDefineList,"var $EvalDataName=".$this->Confuse('').";");
//Javascript Start


/*
以下是Javascript的部份
為了方便閱讀所以把特別排版成這樣
實際輸出時把跳行字元給拿掉節省空間
為了讓大家了解他在做什麼
我幫Javascript加上註解
使用時請把註解給拿掉,節省空間
*/
   return str_replace("\n",'',"
function ".$this->CheckFunctionName."()
{
".$this->GenerateVarDefineString($RandomJSVarDefineList)."
for(i=0;i<$EncodedFunction.length;i++)
$EvalDataName+=String.fromCharCode($EvalDataEncodeKeyName.charCodeAt(i%$EvalDataEncodeKeyName.length)^$EncodedFunction.charCodeAt(i));
eval($EvalDataName);
document.getElementById('".$this->ElementID."').value=$AssignFunctionName();
}
");
//Javascript 結束
  }
 }
?>
--- 程式碼結尾 ---

剛剛發覺一個漏洞
如果有人把驗證碼記下來,還是可以拼命灌
所以剛剛修改了一下
加上了一個TimeOut預設是10分鐘
超過10分鐘後這個驗證碼就會失效了

修改版本
加入了註解混淆,以及隨機變數宣告的功能
目的就是讓整個Javascript更亂,更難以閱讀
達到混淆的目的

梁楓:

ricky 好樣的
真是不錯的方案!

ricky:

實際使用範例

--- 代碼: ---
<?php
 require_once&#40;'./robotaway.inc'&#41;;
 $EncodeKey='自己指定一個Key的內容吧隨便取都可以,越長越好';
 $RA=new RobotAway&#40;$EncodeKey,'CheckKey'&#41;;
 if&#40;isset&#40;$_POST['CheckKey'&#93;&#41;&#41;
 &#123;
  //驗證Clinet送過來的這個Checkey值是否合法
  $OK=$RA->Verify&#40;$_POST['CheckKey'&#93;&#41;?'Valid'&#58;'Invalid';
 &#125;
?>
<html>
<head>
 <script>
<?=$RA->GenerateJS()?>
 </script>
 </head>
 <body>
 CheckStatus:<?=$OK?>
 <form action='' method=post>
  <input type=hidden name="CheckKey" id="CheckKey" value=''><br>
<!--
在執行submit之前呼叫RA_Check();將驗證碼指定給CheckKey這個hidden欄位
-->
  <input type=button value=" verify   " onclick="<?=$RA->CheckFunction()?>;submit();">
 </form>
 </body>
</html>

--- 程式碼結尾 ---


如果是一個正常的User在操作,並不會感覺到需要額外輸入任何東西
可是一但灌水機器人介入時
要分析就很麻煩了

ricky:

我把程式打包好了
線上Demo
裡面就有下載點嘍

頁: (1/6) > >>

前往完整版本