Aho-Corasick算法、多模正則匹配、Snort入門學習


希望解決的問題

1. 在一些高流量、高IO的WAF中,是如何對規則庫(POST、GET)中的字符串進行多正則匹配的,是單條輪詢執行,還是多模式並發執行
2. Snort是怎么組織、匹配高達上千條的正則規則庫的,怎樣保證效率和准確性的平衡 
3. 狀態機、Aho-Corasick算法的核心思想
4. 怎么進行多模正則匹配的編程實現

 

相關學習資料

http://zh.wikipedia.org/wiki/%E7%A1%AE%E5%AE%9A%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E8%87%AA%E5%8A%A8%E6%9C%BA
http://zh.wikipedia.org/wiki/%E9%9D%9E%E7%A1%AE%E5%AE%9A%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E8%87%AA%E5%8A%A8%E6%9C%BA
http://stblog.baidu-tech.com/?p=418
http://blog.csdn.net/beta2/article/details/5698171
http://www.cnblogs.com/xudong-bupt/p/3433506.html
http://blog.csdn.net/sealyao/article/details/4560427
http://www.cppblog.com/yefeng/archive/2009/12/06/102671.html
http://www.wanghd.com/blog/2012/09/27/aho-corasicksuan-fa/
http://en.wikipedia.org/wiki/Aho-Corasick
http://yzmduncan.iteye.com/blog/1217014
http://www.openfoundry.org/tw/tech-column/8265--snort-
http://www.freebuf.com/articles/system/37507.htm

 

目錄

1. 狀態機簡介
2. 多模匹配算法簡介
3. Aho-Corasick算法學習
4. 多模正則匹配的編程學習
5. Snort高速規則匹配原理學習

   

1. 狀態機簡介

狀態機的概念非常復雜,我們着重理解其中的一些核心知識概念

0x1: 確定有限狀態自動機

在計算理論中,"確定有限狀態自動機""確定有限自動機"(deterministic finite automaton, DFA)是一個能實現狀態轉移的自動機。
對於一個給定的屬於該自動機的狀態(往往是初始狀態)和一個屬於該自動機字母表的字符(輸入參數),它都能根據事先給定的轉移函數轉移到下一個狀態(這個狀態可以是先前那個狀態,即狀態自循
環)

"確定有限狀態自動機"的數學定義:

組成
1. 非空有限的"狀態集合": Q
2. 輸入字母表: A(非空有限的字符集合)
3. 轉移函數: F
它接收兩個參數:
    1) 當前狀態: q
    2) 輸入字母: a
返回一個結果: 一個新的狀態: p
4. 一個開始狀態(初始狀態): i
5. 一個接收狀態的集合: S
這個所謂的"接收狀態的集合"可以這么理解,當所有的輸入字母表都輸入完畢后,狀態機停留在最后一個狀態的位置,這個狀態位置是否達到了我們的要求(即是否接收)
(結合一下正則匹配的過程來理解,正則匹配最后輸出的匹配位置其實就是一種狀態機的"終點轉移位置")

它們一起組成狀態機的5元組
{Q、A、F、i、S}

"確定有限狀態自動機"的工作方式

確定有限狀態自動機從起始狀態開始,一個字符接一個字符地讀入一個字符串string,並根據給定的轉移函數一步一步地轉移至下一個狀態。在讀完該字符串后,如果該自動機停在一個屬於S的接受
狀態,那么它就接受該字符串,反之則拒絕該字符串 看到這里,我們可以很自然地想到這個理論的利用場景:   
1) 正則的匹配(思考狀態機的逐字符讀取、以及最終狀態的判定)   2) 目標字符串的搜索

0x2: 非確定有限狀態自動機

在計算理論中,非確定有限狀態自動機或非確定有限自動機(NFA)是對每個狀態和輸入符號對可以有多個可能的下一個狀態的有限狀態自動機。這區別於確定有限狀態自動機(DFA),它的下一個可能
狀態是唯一確定的。 非確定有限自動機有時被稱為有限類型的子移位(subshift)。非確定有限狀態自動機可推廣為概率自動機,它為每個狀態轉移指派概率。

"非確定有限狀態自動機"的性質

機器開始於任意初始狀態並"逐個讀取"來自它的符號表的符號的字符串。
自動機使用狀態轉移函數:F來使用當前狀態: q,和剛讀入的符號:a或空串: null來確定下一個狀態: p
如果在自動機完成讀取的時候,它處於接受狀態,則稱 NFA 接受了這個字符串,否則稱為它拒絕了這個字符串

0x3: 有限狀態自動機的原理性偽碼表示

關於確定有限狀態自動機、非確定有限狀態自動機,除了數學上的定義理解,我們還可以從偽代碼的角度去理解

while(讀取輸入字母表的下一個字符: a)
{ 
    curState: q = 狀態轉移函數F(p, a);
    lastState: p = curState: q;
}

function 狀態轉移函數F(p, a)
{
    if(condition_1)
    {}
    else if(condition_2)
    {}
    else if
    ...
    else
    {}
    //condition_x: 狀態和字符的笛卡兒積關系,以矩陣的形式表示了映射關系
}

這兩張狀態轉移圖很形象地說明了狀態機的邏輯意義,我們可以很容易地將這個概念思想應用到編程實踐中

 

2. 多模匹配算法簡介

了解了狀態機的基本概念之后,我們可以繼續學習多模匹配算法的基本概念了

多模式匹配在這里指的是在"一個字符串"中尋找"多個模式字符字串"的問題。
一般來說,給出一個長字串和很多短模式字符串,如何最快最省的求出哪些模式字符串出現在長字串中是我們需要思考的(因為基本上大多數情況下是在給定的長字串中出現我們給的模式字串的其中
幾個) 該算法的應用領域有很多,例如:
1) 關鍵字過濾 2) 入侵檢測 3) 病毒檢測 4) 分詞等 多模匹配算法是一個概念性的稱呼,它的具體實現有很多,例如: 1) Trie樹 2) AC算法 3) WM算法

簡單來說,多模匹配就是要我們實現下面的功能,用demo代碼進行演示

<?php
    public function CheckSql($db_string)
    {
      
        $express_1 = "/([A-Za-z])?(where)(.)*?(concat|char|(chr.*){4,}|case|floor|#.*|--)/i";   
        $express_2 = "/([A-Za-z])?(where).*(union)(\s)*(all)?(\s)*select(\s)*((\d|null),(\s)*){2,}/i"; 
        $express_3 = "/[^0-9a-z@\._-]{1,}(sleep|benchmark|load_file|outfile|(user\(\).*){1,})[^0-9a-z@\.-]{1,}/i";     
        $express_4 = "/([A-Za-z])?(where)(.|\s)*?(or|and|like)('\d'(=|>|<|like)'\d')/i";
        if (preg_match($express_1, $db_string) || preg_match($express_2, $db_string) || preg_match($express_3, $db_string) || 
preg_match($express_4, $db_string)) {
//die("detected!!!"); return 1; } else { //die("good"); return 0; } } ?>

文章 介紹了多模算法的基本原理,以及一種算法的實現: dictmatch

 

3. Aho-Corasick算法學習

0x1: Aho-Corasick(AC)算法的總體流程

1. 建立樹過程
    1) 根據關鍵字(即待搜索模式集合)建立起一顆Tire樹
    2) 設置第一層的不匹配的轉移節點
    3) 設置其余的不匹配轉移節點
    4) 設置Tire樹的各個節點的輸出字符(即當到達某個狀態時表明某個模式匹配成功,需要輸出對應的字符串)
    5) 初始化狀態到根節點

2. 查找過程
    1) 逐個輸入待搜索字符串
    2) 根據狀態機的轉移結果判斷是否有模式匹配成功

關於算法的原理、流程可以參閱文章頭部給出的鏈接,我就不做重復的引用了。在這里,我想依托一個PHP的AC算法的DEMO,一邊分析源代碼,一邊解析AC算法的流程

將下列文件放置在web目錄下,即可運行

0x2: Aho-Corasick(AC)代碼分析

demo.php

<?php 
/* 
    這個AC算法小程序的架構如下:
    1) demo.php: UI顯示層
    2) php.ac.app.php: 業務實現層,封裝了實現邏輯
    3) php.ac.search.php: AC多模式匹配的搜索查找算法的實現邏輯
    4) php.ac.pretreatment.php: AC算法的預處理(生成樹)的實現邏輯 
*/

//引入文件
include("php.ac.app.php");
$obj = new ACAppClass(); 

echo "<br>=================<br>";
$words2 = array("microsome", "cytochrome", "cytochrome P450 activity", "gibberellic acid biosynthesis", "GA3", "cytochrome P450", "oxygen binding", 
"AT5G25900.1", "protein", "RNA", "gibberellin", "Arabidopsis", "ent-kaurene oxidase activity", "inflorescence", "tissue"); $text2 = "The ga3 mutant of Arabidopsis is a gibberellin-responsive dwarf. We present data showing that the ga3-1 mutant is deficient in ent-kaurene
oxidase activity, the first cytochrome P450-mediated step in the gibberellin biosynthetic pathway. By using a combination of conventional map-based
cloning and random sequencing we identified a putative cytochrome P450 gene mapping to the same location as GA3. Relative to the progenitor line,
two ga3 mutant alleles contained single base changes generating in-frame stop codons in the predicted amino acid sequence of the P450. A genomic
clone spanning the P450 locus complemented the ga3-2 mutant. The deduced GA3 protein defines an additional class of cytochrome P450 enzymes. The
GA3 gene was expressed in all tissues examined, RNA abundance being highest in inflorescence tissue.
"; $res2 = $obj->findWordsInArray($words2, $text2); var_dump($res2); ?>

php.ac.app.php

<?php 

// 引入文件
include("php.ac.search.php");

class ACAppClass
{
    private $showtimeFlag;    // 是否顯示運行時間,false:不顯示;true:顯示,默認為false
    
    /**
     * @function 構造函數
     * @param
     * @return
     */
    public function ACAppClass()
    {
        $this->showtimeFlag = false;
    }

    /**
     * @function 從字符串中查找單個關鍵詞
     * @param string word 關鍵詞
     * @param string text 被查找的字符串
     * @return Array
     */
    public function findSingleWord($word, $text)
    {
        try
        {            
            if(strlen(trim($word))==0)
            {
                throw new Exception("Key word's content is empty.");
            }            
        }
        catch(Exception $e)
        {
            echo $e->getMessage(); 
            return;          
        }     
        //復用了從字符串中查找多個字符串的代碼邏輯(單個是多個的一種特殊情況)
        $arr = array(trim($word));
        return $this->findWordsInArray($arr, $text); 
    }


    /**
     * @function 從字符串中查找多個關鍵詞
     * @param Array words 關鍵詞數組
     * @param string text 被查找的字符串
     * @return Array
     */    
    public function findWordsInArray($words, $text)
    {
        $len = count($words);
        try
        {
            if($len==0)
            {
                throw new Exception("Array of keywords is empty.");
            }
        }
        catch(Exception $e)
        {
            echo $e->getMessage();
            return;
        }
        if($this->showtimeFlag)
        {
            $starttime = $this->getmicrotime();    
        }    
        /*
            構造AC算法"搜索關鍵詞字典樹",主要有兩個主要步驟
                1) 狀態轉移樹的建立(將需要查找的關鍵字模式轉換為一個樹的形式)
                2) 失效轉移樹的建立(在狀態轉移樹的基礎上進行完善添加當出現轉移失敗時,狀態應該跳轉到的下一個狀態)
                3) 狀態輸出的建立(在到達某個狀態的時候,可以表明某個模式匹配成功,可以輸出某個字符串)
        */
        $tree = new AhoCorasick();    
        try
        {    
            for ($i=0; $i<$len; $i++) 
            {
                if(trim($words[$i])=="")
                {
                    throw new Exception("Key word's content is empty.");
                }
                /*
                添加搜索詞
                將搜索詞(待匹配的模式)轉化到狀態轉移樹的形式中
                */
                $tree->add(trim($words[$i]));
            } 
        }
        catch(Exception $e)
        {
            echo $e->getMessage();
            return;
        }
        //向狀態轉移Tire樹中添加"失效轉移"部分
        $tree->prepare();
        //狀態轉移Tire樹准備完畢        
        $res = array();

        /*
        開始在目標字符串中進行多模式搜索
        本質上來說: 搜索的過程是以目標字符串為輸入參數,逐個輸入來使狀態在Tire樹中進行狀態轉移,最終得到輸出字符串
        */
        $obj = $tree->search($text);
        while($obj->hasNext())
        {
            $result = $obj->next();
            $res = array_unique(array_merge($res, $result->getOutputs()));
        }
        if($this->showtimeFlag)
        {        
            $endtime = $this->getmicrotime();    
            echo "<br>run time is: ".($endtime-$starttime)."ms<br>";    
        }
        return $res;    
    }
    
    /**
     * @function 從文件中查找關鍵詞
     * @param string $keyfile 關鍵詞所在的文件名稱及路徑
     * @param string $textfile 被查找的內容所在的文件名稱及路徑
     * @return Array
     */
    public function findWordsInFile($keyfile, $textfile){
        try{            
            if(!is_file($keyfile) || !is_file($textfile)){
                throw new Exception("Can not find the file.");
            }            
        }catch(Exception $e){
            echo $e->getMessage();   
            return;        
        }
        // 搜索詞所在的文件內容為空時,拋出異常
        try{        
            if(strlen(trim(file_get_contents($keyfile)))==0){
                throw new Exception("File's content is empty.");
            }
        }catch(Exception $e){
            echo $e->getMessage();  
            return;         
        }
        // 打開文件
        $handle1 = fopen($keyfile, "r");
        $handle2 = fopen($textfile, "r");
        $arr = array();
        $contents = "";
        try{
            while (!feof($handle1)) {
                $line = trim(fgets($handle1));
                if(strlen($line)!=0){
                    $arr[] = $line;
                }
            }
            
            while (!feof($handle2)) {
               $line = trim(fgets($handle2));
                if(strlen($line)!=0){
                    $contents .= $line;
                }
            }            
        }catch(Excption $e){
            echo $e->getMessage(); 
            return;                    
        }
        // 關閉文件
        fclose($handle1);
        fclose($handle2);    
        return $this->findWordsInArray($arr, $contents);            
    }
    /**
     * @function 獲取時間戳,單位為毫秒
     * @param
     * @return float
     */    
    function getmicrotime(){ 
        list($usec, $sec) = explode(" ",microtime());
        $value = (float)$usec*1000+(float)$sec;
        return round($value, 3); 
    }         
}
?>

php.ac.search.php

<?php
/**
 * @author: jessica.yang
 * @date: 2011-10-24
 * @filename: php.ac.search.php
 * @description: Aho Corasick多模式匹配算法,簡稱AC算法,包含兩個階段,第一個是預處理階段,即字典樹的生成;第二個是搜索查找階段,該文件完成第二階段的搜索查找功能
 */
// 引入文件
include("php.ac.pretreatment.php");
/**
 * @classname: AhoCorasick
 * @description: 用於實現AC多模式匹配的搜索查找算法
 */
class AhoCorasick 
{
    private $root;        // State對象,表示根節點
    private $prepared;    // boolean類型,表示搜索詞是否裝載完成。如果為true,則表示加載完成,並且不能再加載搜索詞
    private $arr_keys;    // Array對象,存放第一級的搜索詞
    
    /**
     * @function 構造函數
     * @param
     * @return
     */
    public function AhoCorasick() 
    { 
        $this->root = new State(0);                //構造字典樹的單個狀態節點
        $this->root->setFail($this->root);        //設置根節點的失效值,對於根節點來說,它的失效轉移節點就是它自己本身
        $this->prepared = false;                //搜索詞還未裝載完成
        $this->arr_keys = array();                //存放第一級搜索詞
    }

    /**
     * @function 獲取根節點對象
     * @param
     * @return State
     */    
    public function getRoot() 
    {
        return $this->root;
    }
       
    /**
     *@function 添加搜索詞
     *@param string $keywords 要查找的搜索詞
     *@return 
    **/
    public function add($keywords="")
    {
        // 如果裝載標志為true,則禁止再加載搜索詞
        try
        {
            if ($this->prepared)
            {
                throw new Exception("can't add keywords after prepare() is called.");
            }
        }
        catch(Exception $e)
        {
            echo $e->getMessage(); 
            return;          
        }
        
        // 如果搜索詞不是字符串類型,或者內容為空,則返回
        try
        {
            if(!is_string($keywords) || strlen(trim($keywords)) == 0)
            {
                throw new Exception("Added keywords is not string type, or content is empty.");    
            }
        }
        catch(Exception $e)
        {
            echo $e->getMessage();  
            return;        
        }
        $keywords = trim($keywords); 
        $words = $this->str_split_utf8($keywords);                                // 把搜索詞按字符為單位轉換成單字符數組(因為Tire樹的每個節點必須是單字符) 
        $this->arr_keys = array_unique(array_merge($this->arr_keys, $words));    // 設置第一層級的搜索字符 

        /*
        將搜索詞的單字符數組逐個"壓入"狀態轉移Tire樹中
        同時接收函數返回的: 添加完搜索詞之后的最后一個State值(這個State代表某個模式匹配成功,可以輸出對應的字符串)
        */
        $lastState = $this->root->extendAll($words); 

        /*
        向最后一個State值中添加輸出內容
        這里要着重理解: lastState代表某個匹配模式匹配成功到了最后,即表示匹配成功,則對應的這個狀態即為狀態機中的"可接受狀態",這時可輸出對應的搜索字符串
        */
        $lastState->addOutput($keywords);                                        
    }

    /**
     *@function 加載搜索詞add()完成之后調用
     *@param 
     *@return 
    **/    
    public function prepare() 
    {
        $this->prepareFailTransitions();
        //進行失效轉移處理之后就不允許繼續增加節點了,如果需要再次增加節點,則需要重頭構建這個Tire樹
        $this->prepared = true;
    }

    /**
     *@function 設置字典樹中每個State節點的失效值
     *@param 
     *@return 
    **/  
    private function prepareFailTransitions() 
    { 
        $q = array();    //存放第一層級的所有搜索詞                    
        foreach($this->arr_keys as $value)
        {
            if(is_null($this->root->get($value)))
            {
                // 如果搜索詞不存在於第一層級,則添加,並且設置失效值為根節點State對象
                $this->root->put($value, $this->root);
            }
            else
            {
                // 設置第一層級的失效值為根節點State對象,並且把搜索詞對應的State值添加到$q數組中
                $this->root->get($value)->setFail($this->root);
                array_push($q, $this->root->get($value));
            }
        }
        die(var_dump($q));
        // 設置所有State節點的失效值
        while(!is_null($q)) 
        { 
            $state = array_shift($q);            // 將數組$q第一個State值移出該數組,並返回移出的State值 
            if(is_null($state))                    // 如果取出的$state內容為空,則結束循環
            {
                break;
            } 
            $keys = $state->keys();                // 獲取$state值對應的下一級所有搜索詞        
            $cnt_keys = count($keys);
            for($i=0; $i<$cnt_keys; $i++) 
            {        
                $r = $state;
                $a = $keys[$i];
                $s = $r->get($a);
                array_push($q, $s);
                $r = $r->getFail(); 
                /*
                遞歸查找失效值,直到根節點為止
                這里要重點理解一下: 類似KMP算法,同樣采用實效實效函數推進的方法,假設當前狀態為s,s的一個孩子結點的根結點根節點
                t狀態,如果當前的失效函數已知為f(s),則顯然地,f(t)必定是f(s)的孩子結點狀態,所要做的就是在狀態f(s)處尋找接受字
                符同s->t下一個狀態,如果能找到,那就是f(t),否則說明到s處匹配串的前綴長度太長,需縮減,所以需要找到更短的后綴,
                於是就到f(s)處繼續,如果仍然找不到,則轉到f(f(s))處,形成狀態的遞歸轉移
                */
                while(is_null($r->get($a)))        
                {
                    //這是一個遞歸的過程,從根節點到當前節點,逐個向下,減少字符長度,並同時回溯查中啊是否有能夠包含這個字符串的子節點
                    $r = $r->getFail();
                } 
                $s->setFail($r->get($a));                                
                $s->setOutputs(array_unique(array_merge($s->getOutputs(), $r->get($a)->getOutputs())));                
            }
        }
    }

    /**
     *@function 查找函數
     *@param string words 被查找的字符串
     *@return Searcher
    **/ 
    public function search($words)
    {
        return new Searcher($this, $this->startSearch($words));
    }    
    /**
     *@function 查找函數
     *@param string words 被查找的字符串
     *@return SearchResult
    **/  
    public function startSearch($words) 
    {
        // 加載未完成時,不允許進行搜索查找    
        try
        {
            if (!$this->prepared)
            {
                throw new Exception("Can't start search until prepare().");
            }
        }
        catch(Exception $e)
        {
            echo $e->getMessage();  
            return;         
        }        
        // 轉換被查找的"字符串"為"單字符數組"。因為我們知道,AC搜索算法的過程是將被查找的字符串逐個的輸入狀態轉換樹,並根據轉移狀態結果進行判斷
        $arr_words = $this->str_split_utf8($words);
        // 搜索查找后結果集
        $res = $this->continueSearch(new SearchResult($this->root, $arr_words, 0));
        return $res;
    }

    /**
     *@function 真正的查找函數
     *@param SearchResult lastResult SearchResult對象
     *@return SearchResult or NULL
    **/          
    public function continueSearch($lastResult) 
    {
        // 如果lastResult搜索結果對象為null,則返回
        if(is_null($lastResult))
        {
            return NULL;
        }
        
        $words = $lastResult->words;            // 被查找的字符數組
        $state = $lastResult->lastMatchedState;    // 開始查找的State值
        $start = $lastResult->lastIndex;        // 開始查找的位置
        $len = count($words);
        for($i=$start; $i<$len; $i++) 
        {    
            $word = $words[$i];                    // 獲取單個字符
            // 如果獲取的搜索詞不存在,則遞歸轉向失效值進行搜索,直到根節點為止
            while (is_null($state->get($word)))
            {
                $state = $state->getFail();
                if($state===$this->root)
                {
                    break;
                }
            }
            
            if(!is_null($state->get($word)))
            {
                // 獲取搜索詞對應的State值,如果有輸出內容,則輸出
                $state = $state->get($word);
                if (count($state->getOutputs())>0)
                {    
                    return new SearchResult($state, $words, $i+1);
                }    
            }                        
        }        
        return NULL;
    }

    /**
     *@function 字符串轉換成字符數組,單位是字符
     *@param string str 轉換的字符串內容
     *@return Array
    **/     
    function str_split_utf8($str)
    {                
        $split=1;
        $array = array();
        for($i=0; $i < strlen($str); )
        {
            $value = ord($str[$i]);
            /*
            處理寬字節的情況
            http://zh.wikipedia.org/wiki/UTF-8
            1) 對於UTF-8編碼中的任意字節B,如果B的第一位為0,則B為ASCII碼,並且B獨立的表示一個字符;
            2) 如果B的第一位為1,第二位為0,則B為一個非ASCII字符(該字符由多個字節表示)中的一個字節,並且不為字符的第一個字節編碼;
            3) 如果B的前兩位為1,第三位為0,則B為一個非ASCII字符(該字符由多個字節表示)中的第一個字節,並且該字符由兩個字節表示;
            4) 如果B的前三位為1,第四位為0,則B為一個非ASCII字符(該字符由多個字節表示)中的第一個字節,並且該字符由三個字節表示;
            5) 如果B的前四位為1,第五位為0,則B為一個非ASCII字符(該字符由多個字節表示)中的第一個字節,並且該字符由四個字節表示;
            */
            if($value > 127)
            {
                if($value >= 192 && $value <= 223)
                {//雙字節
                    $split=2;
                } 
                else if($value >= 224 && $value <= 239)
                {//三字節
                    $split=3;
                }                    
                else if($value >= 240 && $value <= 247)
                {//四字節
                    $split=4;
                } 
            }
            else
            {//單字節
                $split=1;
            }
            
            $key = NULL;
            //根據當前字節長度來生成相應的單字符
            for($j = 0; $j < $split; $j++, $i++ ) 
            {
                $key .= $str[$i];
            }
            //http://www.w3school.com.cn/php/func_array_push.asp
            array_push( $array, $key );
        }
        return $array;
    }    
}

///////////////////////////////////////
/**
 * @classname: SearchResult
 * @description: 搜索結果類,用於存儲搜索查找后的結果集
 */
class SearchResult {
    var $lastMatchedState;// State對象,最后匹配的State值
    var $words;// Array對象,被搜索的內容
    var $lastIndex;// int類型,最后出現的位置
    /**
     * @function 構造函數
     * @param State state State對象
     * @param Array words 被查找的字符串
     * @param int index 查找位置
     * @return
     */
    public function SearchResult($state, $words=array(), $index=0) {
        $this->lastMatchedState = $state;
        $this->words = $words;
        $this->lastIndex = $index;
    }
    /**
     * @function 獲取輸出的內容
     * @param 
     * @return Array
     */
    public function getOutputs() {
        return $this->lastMatchedState->getOutputs();
    }
    /**
     * @function 獲取查找的位置
     * @param 
     * @return int
     */
    public function getLastIndex() {
        return $this->lastIndex;
    }
}
 


////////////////////////////
/**
 * @classname: Searcher
 * @description: 搜索類
 */
class Searcher
{
    private $tree;// AhoCorasick對象    
    private $currentResult;// SearchResult對象
    /**
     * @function 構造函數
     * @param AhoCorasick tree AhoCorasick對象    
     * @param SearchResult result SearchResult對象
     */
    public function Searcher($tree, $result) {
        $this->tree = $tree;
        $this->currentResult = $result;
    }
    /**
     * @function hasNext 用於判斷是否還有值存在
     * @param 
     * @param boolean true表示有值  false表示無值
     */
    public function hasNext() {
        return !is_null($this->currentResult);
    }
    /**
     * @function next 獲取下一個值
     * @param 
     * @param 如果有值則返回SearchResult對象,否則返回NULL
     */
    public function next() {
        if (!$this->hasNext()){
            return NULL;
        }
        $result = $this->currentResult;
        $this->currentResult = $this->tree->continueSearch($this->currentResult);
        return $result;
    }
}

?>

php.ac.pretreatment.php

<?php
/**
 * @author: jessica.yang
 * @date: 2011-10-24
 * @filename: php.ac.pretreatment.php
 * @description: Aho Corasick多模式匹配算法,簡稱AC算法,包含兩個階段,第一個是預處理階段,即字典樹的生成;第二個是搜索查找階段,該文件完成第一階段的預處理功能
 */

/**
 * @classname: State
 * @description: 狀態類,用於表示字典樹中的每一個狀態節點
 */
class State 
{
    private $depth;        // int類型,表示每一個狀態對象的深度,從0開始表示
    private $edgeList;    // 類似於列表,用於包含該狀態下所包含的下一級所有State對象
    private $fail;        // State對象,表示狀態對象失效之后要跳轉的地方
    private $outputs;    // array對象,存放某一狀態下可以輸出的內容
    
    /**
     * @function State 構造函數
     * @param int depth 狀態所處的深度
     * @return
     */
    public function State($depth) 
    {
        $this->depth = $depth;                    //初始化狀態當前狀態節點的深度 
        
        /*
        存儲State對象下一級對應的所有State內容,以數組形式存儲
        這里要着重理解的是: 樹是在數據結構是以一種鏈表的形式存儲的,鏈表中的每個節點都要存儲指向它的所有子節點的指針,或者以一種數組的形式存儲(只要能達到可尋址的目的即可)
        */
        $this->edgeList = new DenseEdgeList();    
        
        $this->fail = NULL;                        //初始化當前狀態節點發生失效時需要轉移到的下一個節點(即失效轉移指針)
        $this->outputs = array();                //初始化當前狀態節點的輸出字符串
    }

    /**
     *@function extend 添加單個搜索詞
     *@param char character 單個搜索詞,或者一個字母、數字、或者一個漢字等
     *@return State 
    **/
    public function extend($character) 
    { 
        if (!is_null($this->edgeList->get($character)))
        {
            return $this->edgeList->get($character);
        }
        /*
        新建一個新的State節點,作為當前節點的下級State節點
        */
        $nextState = new State($this->depth+1);
        $this->edgeList->put($character, $nextState);
        return $nextState;
    } 

    /**
     *@function extendAll 添加搜索詞
     *@param array contents 搜索詞數組
     *@return State
    **/
    public function extendAll($contents) 
    { 
        $state = $this;
        $cnt = count($contents);
        /*
        循環遍歷搜索詞單字符數組,逐個壓入Tire樹
        */ 
        for($i=0; $i < $cnt; $i++) 
        {
            // 如果搜索的關鍵詞存在,則直接返回該關鍵詞所處的State對象,否則添加該關鍵詞
            if(!is_null($state->edgeList->get($contents[$i])))
            {
                //如果搜索詞在當前節點中,則返回當前State節點
                $state = $state->edgeList->get($contents[$i]);
            }
            else
            {
                //如果搜索詞不在當前節點中,則新建一個新的State節點作為當前節點的下級節點,並返回新的State節點,接下來就繼續在新的State節點中插入搜索詞
                $state = $state->extend($contents[$i]);
            }
        }
        //完成狀態轉移樹的填充,最終的效果就是所有的搜索詞都被填充到了一個層級的樹狀轉移圖中
        return $state;
    }

    /**
     * @function 計算搜索詞的總長度
     * @param
     * @return int
     */
    public function size() {
        $keys = $this->edgeList->keys();
        $result = 1;
        $length = count($keys);
        for ($i=0; $i<$length; $i++){
            $result += $this->edgeList->get($keys[$i])->size();
        }
        return $result;
    }
    /**
     * @function 獲取單個關鍵詞所處的State對象
     * @param char character
     * @return State
     */
    public function get($character) 
    {
        $res = $this->edgeList->get($character);
        return $res;
    }
    /**
     * @function 向State對象中添加下一級的搜索詞及對應的State值
     * @param char character
     * @param State state
     * @return
     */
    public function put($character, $state) 
    {
        $this->edgeList->put($character, $state);
    }
    /**
     * @function 獲取State對象下一級的所有關鍵詞
     * @param
     * @return Array
     */
    public function keys() 
    {
        return $this->edgeList->keys();
    }
    /**
     * @function 獲取State對象失效時對應的失效值
     * @param
     * @return State
     */
    public function getFail() 
    {
        return $this->fail;
    } 

    /**
     * @function 設置State對象失效時對應的失效值(即失效時需要轉移到的下一個狀態)
     * @param
     * @return 
    */
    public function setFail($state) 
    {
        $this->fail = $state;
    }

    /**
     * @function 向State對象的outputs中添加輸出內容
     * @param
     * @return 
     */
    public function addOutput($str)
    {
        array_push($this->outputs, $str);
    }

    /**
     * @function 獲取State對象的輸出內容
     * @param
     * @return Array
     */
    public function getOutputs() {
        return $this->outputs;
    }
     /**
     * @function 設置State對象的輸出內容
     * @param
     * @return 
     */   
    public function setOutputs($arr=array()){
        $this->outputs = $arr;
    }
}






////////////////////////////////////////////////////////
/**
 * @classname: DenseEdgeList
 * @description: 存儲State對象下一級對應的所有State內容,以數組形式存儲
 */
class DenseEdgeList
{
    private $array;                // State對象,包含對應的搜索詞及State值
    
    /**
     * 構造函數
     */
    public function DenseEdgeList() 
    {
        $this->array = array();
    }

    /**
     * @function 從鏈表存儲形式的內容轉為數組存儲形式的內容
     * @param SparseEdgeList list
     * @return DenseEdgeList
     */
    public function fromSparse($list) 
    {
        $keys = $list->keys();
        $newInstance = new DenseEdgeList();
        for($i=0; $i < count($keys); $i++) 
        {
            $newInstance->put($keys[$i], $list->get($keys[$i]));
        }
        return $newInstance;
    }
    /**
     * @function 獲取搜索詞對應的State值
     * @param char word
     * @return 如果存在則返回對應的State對象,否則返回NULL
     */
    public function get($word) 
    {
        if(array_key_exists($word, $this->array))
        {
            return $this->array["$word"];            
        }
        else
        {
            return NULL;
        }
    }

    /**
     * @function 添加搜索詞及對應的State值到數組中
     * @param char word 單個搜索詞
     * @param State state 搜索詞對應的State對象
     * @return 
     */
    public function put($word, $state) 
    {
        $this->array["$word"] = $state;
    }

    /**
     * @function 獲取所有的搜索詞
     * @param 
     * @return Array
    */
    public function keys() 
    {
        return array_keys($this->array);
    }
}

///////////////////////////////////////
/**
 * @classname: SparseEdgeList
 * @description: 存儲State對象下一級對應的所有State內容,以鏈表形式存儲
 */
class SparseEdgeList{    
    private $head;// Cons對象
    /**
     * 構造函數
     */
    public function SparseEdgeList() {
        $this->head = NULL;
    }
    /**
     * @function 獲取搜索詞對應的State值
     * @param char word
     * @return 如果存在則返回對應的State對象,否則返回NULL
     */
    public function get($word) {
        $cons = $this->head;
        while(!is_null($cons)){
            if ($cons->word === $word){
                return $cons->state;
            }
            $cons = $cons->next;
        }
        return NULL;
    }
    /**
     * @function 添加搜索詞及對應的State值到鏈接中
     * @param char word 單個搜索詞
     * @param State state 搜索詞對應的State對象
     * @return 
     */
    public function put($word, $state){
        $this->head = new Cons($word, $state, $this->head);
    }
    /**
     * @function 獲取所有的搜索詞
     * @param 
     * @return Array
     */
    public function keys() {
        $result = array();
        $c = $this->head;
        while(!is_null($c)){
            array_push($result, $c->word);
            $c = $c->next;
        }
        return $result;
    }
    
}
/**
 * @classname: Cons
 * @description: 用於SparseEdgeList生成鏈表時表示的節點對象
 */
class Cons {
    var $word;// 單個搜索詞
    var $state;// State對象
    var $next;// Cons對象
    /**
     * 構造函數
     */
    public function Cons($word, $state, $next){
        $this->word = $word;
        $this->state = $state;
        $this->next = $next;
    }
}
?>

上面是一個AC算法的PHP實現,為了更加清晰的說明這個算法的思想,我們接下來看幾張圖來進一步理解一下

下圖是多模式he/ she/ his /hers構成的一個確定性有限狀態機,做幾點說明:

wps_clip_image-531

1. 該狀態機優先按照實線標注的狀態轉換路徑進行轉換,當所有實線標注的狀態轉換路徑條件不能滿足時,按照虛線的狀態轉換路徑進行狀態轉換。如:狀態0時,當輸入h,則轉換到狀態1;輸入s,則轉換到狀態3;否則轉換到狀態0。
2.  匹配過程如下:從狀態0開始進行狀態轉換,主串作為輸入。如主串為:ushers,狀態轉換的過程是這樣的:

wps_clip_image-720

3. 當狀態轉移到2,5,7,9等紅色狀態點時,說明發生了模式匹配。
如主串為:ushers,則在狀態5、2、9等狀態時發生模式匹配,匹配的模 式串有she、he、hers

0x3: 關於AC算法的注意點

1. AC算法是一款非常優秀的算法,在Unix的fgrep中使用的就是這個算法
2. 要理解AC算法,我們的重點應該放到它的Tire樹的建立上,將搜索詞轉換為一棵以字母順序為索引關系的層次樹(這里所謂的字母順序就是轉移方向)
3. 失效轉換的思想和KMP算法的思想很類似,核心的思想是避免不必要的回溯,讓搜索始終沿着向前的方向,盡最大可能減小時間復雜度
4. 輸出字符串的個數和搜索詞的數量是一致的,每個搜索詞都會對應一條狀態轉移路徑,這條路徑的最后一個State狀態會包含一個輸出字符串
5. AC算法的時間復雜度為: O(m+n+z),是線性的,所以非常高效

 

4. 多模正則匹配的編程學習

這里還有一些關於多模匹配算法的代碼學習資料

http://sourceforge.net/projects/multifast/
http://www.codeproject.com/Articles/12383/Aho-Corasick-string-matching-in-C
http://search.cpan.org/~dankogai/Regexp-Trie-0.02/lib/Regexp/Trie.pm

 

5. Snort入門學習

我們之前說過,AC算法是一種多模匹配算法,它適用於一次匹配需要同時匹配多個模式的應用場景。我們在有了AC算法的原理基礎之后,接下來可以繼續深入學習一下Snort高速多規則匹配的原理了

0x1: Snort簡介

Snort是一款開源的NIPS(Network Intrusion Prevention System)、NIDS(Network Intrusion Detection System)
http://www.snort.org/

NIDS(Network Intrusion Detection System)的功能點:
1) 實時流量分析
2) IP數據包Log記錄
3) 協議分析
4) 內容搜索
    4.1) 單模式搜索
    4.2) 多模式搜索
5) 對攻擊行為的檢測
    1) 掃描器探針
    2) 操作系統指紋探測
    3) CGI(common gateway interface)探測
    4) 緩沖區溢出
    5) SMB探測
    6) 端口遍歷掃描

Snort的工作模式
1) 嗅探模式(Sniffer): 對網絡數據進行實時嗅探並顯示
2) 數據包記錄(packet logger): 對捕獲到的數據包記錄都磁盤上
3) 網絡入侵檢測(Network Intrusion Detection): 對流量進行監控,並應用用戶定義的規則進行模式匹配,Snort會根據用戶的定義采取相應的相應動作action

0x2: Snort的相關架構

Snort是一種以攻擊特征為基礎的的入侵檢測系統,利用事先建立好的的已知的攻擊資料特征碼,來比對接收到的封包內容是否含有攻擊行為。若符合特征碼則觸發相對應的動作

Snort是一種以攻擊特征為基礎的的入侵檢測系統,利用事先建立好的的已知的攻擊資料特征碼,來比對接收到的封包內容是否含有攻擊行為。若符合特征碼則觸發相對應的動作

1. Packet Decoder(封包解碼器)
當Snort取得原始的網絡封包后,第一件事即將封包置入"封包解碼器"模組中進行封包解碼

2. Preprocessors(預處理器)
Snort的預處理器為外掛式程序(plusin)的架構,主要功能在於重新標准化網絡流量,如
    1) 重組封包
    2) 分段、重組TCP Stream
    3) 編碼的規范化、轉換等
以使得網絡流量能精確的被偵測引擎(Detection Engine)解析以及匹配特征碼

3. Detection Engine(偵測模組)
Detection Engine(偵測模組)主要功能在於"規則分析""特征偵測",Detection Engine將Snort的規則文件引入,並按照規則文件中的規則進行比對(單模比對、或者多模比對)。一旦發現
有符合規則文件定義的行為,即觸發該規則文件中所定義的處理方式,當所有的規則都不符合時,即會丟棄該封包。 Snort的規則文件分為兩個部分:
1) 規則表頭(Rule-Header) 1.1) 決定封包比對來源范圍(例如限定比對哪些范圍的IP) 1.2) 比對成功時的動作(log或者直接丟棄) 2) 規則選項(Rule-Options) 利用一至多個關鍵字設定欲偵測的流量特征,規則選項按照功能可以分為下列4個部分 2.1) Meta-data: 設定欲顯示的相關訊息,如當規則觸發時所要產生的訊息 2.2) Payload: 用來比對封包內容的規則 2.3) Non-Payload: 用來比對各種協定的欄位值 2.4) Post-Detection: 當封包內容與規則匹配時,除了在規則表頭(Rule-Header)所定義的動作外,另外會觸發的動作

Example:

alert tcp any any → any 5432 (msg:"someone access PSQL command:SELECT"; content:"select";)

alert(處理方式):    產生警示的 log
tcp(來源通訊協定):    偵測 TCP 的封包
any(來源 IP):        偵測任何的來源 IP
any(來源 port):        偵測任何的來源端口
any(目的 IP):        偵測任何的目的 IP
5432(目的 port):    偵測 5432 端口的封包
...(規則匹配內容):        若符合內容含有"select"的字串,則將 msg 后的字串記錄起來

0x3: Snort安裝、配置

http://linuxmantra.com/2010/10/install-snort-2-9-on-rhel-5.html
http://www.boyunjian.com/do/article/snapshot.do?uid=net.chinaunix.blog%2Fuid-522598-id-1764389.html
1. 下載准備環境
yum -y install mysql-bench mysql-devel php-mysql gcc pcre-devel php-gd gd glib2-devel gcc-c++ libpcap-devel
cd /root
mkdir  snort
cd snort
wget http://www.snort.org/downloads/867 
wget http://www.snort.org/downloads/860 


2. 安裝Snort(在安裝過程中需要解決依賴庫的問題方可正常安裝)
tar -zvxf snort-2.9.0.5.tar.gz
cd snort-2.9.0.5
./configure --with-mysql --enable-dynamicplugin
make && make install

遇到報錯:
"ERROR!  dnet header not found, go get it from http://code.google.com/p/libdnet/ or use the --with-dnet-* options, if you have it installed in an 
unusual place
" tar -xzvf libdnet-1.12.tgz cd libdnet-1.12.tgz ./configure make && make install 如果遇到報錯 "ERROR! daq_static library not found, go get it from http://www.snort.org/" cd /root/snort/ tar -xzvf daq-0.5.tar.gz cd daq-0.5 ./configure make && make install 如果遇到報錯: "ERROR! Libpcap library version >= 1.0.0 not found. Get it from http://www.tcpdump.org" cd /root/snort/ wget http://www.tcpdump.org/release/libpcap-1.1.1.tar.gz tar -zvxf libpcap-1.1.1.tar.gz cd libpcap-1.1.1 ./configure -prefix=/usr make && make install 3. 配置Snort groupadd snort useradd -g snort snort -s /sbin/nologin (添加一個不允許登錄的虛擬賬戶) mkdir /etc/snort mkdir /etc/snort/rules mkdir /etc/snort/so_rules mkdir /var/log/snort chown snort:snort /var/log/snort cd /root/snort/snort-2.9.0.5/etc cp * /etc/snort cd /root/snort/ tar -xzvf snortrules-snapshot-2.8.tar.gz cd ./rules cp * /etc/snort/rules cp ../so_rules/precompiled/CentOS-5.0/i386/2.6.1.5/* /etc/snort/so_rules wget https://www.snort.org/downloads/893 --no-check-certificate vim /etc/snort/snort.conf 修改如下 將 RULE_PATH 和 SO_RULE_PATH 改為絕對路徑 var RULE_PATH /etc/snort/rules var SO_RULE_PATH /etc/snort/so_rule 配置數據庫(在同一個文件中) 在配置數據庫信息之前先生成相應的數據庫 mysql -uroot -p111 create database snort; use snort; source /root/snort/snort-2.9.0.5/schemas/create_mysql; grant all privileges on snort.* to snort@'localhost' identified by 'snort'; flush privileges; (在.conf中添加信息) vim /etc/snort/snort.conf output database: log, mysql, user=snort password=snort dbname=snort host=localhost 4. 啟動Snort

1) 監聽模式(Sniffer)

監聽所有來往的封包,但不做攻擊模式的比對
snort -v

 2) NIDS(網絡型入侵檢測系統)
讓Snort不僅監聽所有來往的封包,並會對封包中是否包含攻擊模式進行比較
snort -u snort -g snort -c /etc/snort/snort.conf

0x3: Snort捕獲數據的可視化顯示

和我們之前學習Kippo蜜罐的學習中,將捕獲到的數據進行可視化展示很重要,對於Snort來說,可以使用BASE(Basic Analysis and Security Engine)進行數據可視化展示

http://freecode.com/projects/base-php

要使用BASE,需要准備一些環境:

1) ADOdb(PHP連接數據庫的通用接口)
2) BASE需要繪圖和Email的相關功能
    2.1) Mail
    2.2) Image_Color
    2.3) Image_Canvas
    2.4) Image_Graph

pear list
BASE本質上就是一些PHP文件組成的腳本
http://sourceforge.net/projects/secureideas/

啟動Snort的檢測模式

snort -u snort -g snort -c /etc/snort/snort.conf

用WVS進行模擬攻擊

 

后記

1. 本篇文章完成了對AC算法、Snort的入門學習
2. 我們現在已經知道,Snort、WAF高速正則規則匹配使用的是AC算法
3. 下一步准備研究一下Snort的開源源代碼,從源代碼的角度來深入學習一下Snort的檢測原理、入侵檢測相關模塊,AC算法在產品中的應用

 

Copyright (c) 2014 LittleHann All rights reserved

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM