作者:c0d3p1ut0s & s1m0n

RASP概念

RASP(Runtime Application self-protection)是一種在運行時檢測攻擊并且進行自我保護的一種技術。早在2012年,Gartner就開始關注RASP,惠普、WhiteHat Security等多家國外安全公司陸續推出RASP產品,時至今日,惠普企業的軟件部門出售給了Micro Focus,RASP產品Application Defender隨之易主。而在國內,去年知道創宇KCon大會兵器譜展示了JavaRASP,前一段時間,百度開源了OpenRASP,去年年底,360的0kee團隊開始測試Skywolf,雖然沒有看到源碼和文檔,但它的設計思路或許跟RASP類似。而商業化的RASP產品有OneAPM的OneRASP和青藤云的自適應安全產品。在國內,這兩家做商業化RASP產品做得比較早。

那么RASP到底是什么呢?它到底是怎樣工作的呢?

我的WAF世界觀

為了表述方便,暫且把RASP歸為WAF的一類。從WAF所在的拓撲結構,可以簡單將WAF分為如下三類,如下圖所示:

  • 以阿里云為代表的云WAF以中間人的形式,在HTTP請求到達目標服務器之前進行檢查攔截。
  • 以ModSecurity為代表的傳統WAF在HTTP請求到達HTTP服務器后,被Web后端容器解釋/執行之前檢查攔截HTTP請求。
  • RASP工作在Web后端解釋器/編譯器中,在漏洞代碼執行前阻斷執行流。

從上圖中WAF所處的位置可以看出,云WAF和傳統WAF的檢查攔截HTTP請求的主要依據是HTTP Request,其實,如果站在一個非安全從業者的角度來看,這種檢測方式是奇怪的。我們可以把Web服務看做是一個接受輸入-處理-輸出結果的程序,那么它的輸入是HTTP請求,它的輸出是HTTP響應。靠檢測一個程序的輸入和輸出來判斷這個程序的運行過程是否有害,這不奇怪嗎?然而它又是可行且有效的,大多數的Web攻擊都能從HTTP請求中找到蛛絲馬跡。這種檢測思路是云WAF和傳統WAF能有效工作的原因,也是它們的缺點。

筆者一直認為,問題發生的地方是監控問題、解決問題的最好位置。Web攻擊發生在Web后端代碼執行時,最好的防護方法就是在Web后端代碼執行之前推測可能發生的問題,然后阻斷代碼的執行。這里的推測并沒有這么難,就好像云WAF在檢查包含攻擊payload的HTTP請求時推測它會危害Web服務一樣。這就是RASP的設計思路。

好了,上面談了一下筆者個人的一些看法,下面開始談一談PHP RASP的實現。

RASP在后端代碼運行時做安全監測,但又不侵入后端代碼,就得切入Web后端解釋器。以Java為例,Java支持以JavaAgent的方式,在class文件加載時修改字節碼,在關鍵位置插入安全檢查代碼,實現RASP功能。同樣,PHP也支持對PHP內核做類似的操作,PHP支持PHP擴展,實現這方面的需求。你可能對JavaAgent和PHP擴展比較陌生,實際上,在開發過程中,JavaAgent和PHP擴展與你接觸的次數比你意識到的多得多。

PHP擴展簡介

有必要介紹一下PHP解釋的簡單工作流程,根據PHP解釋器所處的環境不同,PHP有不同的工作模式,例如常駐CGI,命令行、Web Server模塊、通用網關接口等多個模式。在不同的模式下,PHP解釋器以不同的方式運行,包括單線程、多線程、多進程等。

為了滿足不同的工作模式,PHP開發者設計了Server API即SAPI來抹平這些差異,方便PHP內部與外部進行通信。

雖然PHP運行模式各不相同,但是,PHP的任何擴展模塊,都會依次執行模塊初始化(MINIT)、請求初始化(RINIT)、請求結束(RSHUTDOWN)、模塊結束(MSHUTDOWN)四個過程。如下圖所示:

在PHP實例啟動時,PHP解釋器會依次加載每個PHP擴展模塊,調用每個擴展模塊的MINIT函數,初始化該模塊。當HTTP請求來臨時,PHP解釋器會調用每個擴展模塊的RINIT函數,請求處理完畢時,PHP會啟動回收程序,倒序調用各個模塊的RSHUTDOWN方法,一個HTTP請求處理就此完成。由于PHP解釋器運行的方式不同,RINIT-RSHUTDOWN這個過程重復的次數也不同。當PHP解釋器運行結束時,PHP調用每個MSHUTDOWN函數,結束生命周期。

PHP核心由兩部分組成,一部分是PHP core,主要負責請求管理,文件和網絡操作,另一部分是Zend引擎,Zend引擎負責編譯和執行,以及內存資源的分配。Zend引擎將PHP源代碼進行詞法分析和語法分析之后,生成抽象語法樹,然后編譯成Zend字節碼,即Zend opcode。即PHP源碼->AST->opcode。opcode就是Zend虛擬機中的指令。使用VLD擴展可以看到Zend opcode,這個擴展讀者應該比較熟悉了。下面代碼的opcode如圖所示

<?php
$a=1;
$b=2;
print $a+$b;
>

Zend引擎的所有opcode在http://php.net/manual/en/internals2.opcodes.list.php 中可以查到,在PHP的內部實現中,每一個opcode都由一個函數具體實現,opcode數據結構如下

struct _zend_op {
    opcode_handler_t handler;//執行opcode時調用的處理函數
    znode result;
    znode op1;
    znode op2;
    ulong extended_value;
    uint lineno;
    zend_uchar opcode; 
};

如結構體所示,具體實現函數的指針保存在類型為opcode_handler_t的handler中。

設計思路

PHP RASP的設計思路很直接,安全圈有一句名言叫一切輸入都是有害的,我們就跟蹤這些有害變量,看它們是否對系統造成了危害。我們跟蹤了HTTP請求中的所有參數、HTTP Header等一切client端可控的變量,隨著這些變量被使用、被復制,信息隨之流動,我們也跟蹤了這些信息的流動。我們還選取了一些敏感函數,這些函數都是引發漏洞的函數,例如require函數能引發文件包含漏洞,mysqli->query方法能引發SQL注入漏洞。簡單來說,這些函數都是大家在代碼審計時關注的函數。我們利用某些方法為這些函數添加安全檢查代碼。當跟蹤的信息流流入敏感函數時,觸發安全檢查代碼,如果通過安全檢查,開始執行敏感函數,如果沒通過安全檢查,阻斷執行,通過SAPI向HTTP Server發送403 Forbidden信息。當然,這一切都在PHP代碼運行過程中完成。

這里主要有兩個技術問題,一個是如何跟蹤信息流,另一個是如何安全檢查到底是怎樣實現的。

我們使用了兩個技術思路來解決兩個問題,第一個是動態污點跟蹤,另一個是基于詞法分析的漏洞檢測。

動態污點跟蹤

對PHP內核有一些了解的人應該都知道鳥哥,鳥哥有一個項目taint,做的就是動態污點跟蹤。動態污點跟蹤技術在白盒的調試和分析中應用比較廣泛。它的主要思路就是先認定一些數據源是可能有害的,被污染的,在這里,我們認為所有的HTTP輸入都是被污染的,所有的HTTP輸入都是污染源。隨著這些被污染變量的復制、拼接等一系列操作,其他變量也會被污染,污染會擴大,這就是污染的傳播。這些經過污染的變量作為參數傳入敏感函數以后,可能導致安全問題,這些敏感函數就是沉降點。

做動態污點跟蹤主要是定好污染源、污染傳播策略和沉降點。在PHP RASP中,污染源和沉降點顯而易見,而污染傳播策略的制定影響對RASP的準確性有很大的影響。傳播策略過于嚴格會導致漏報,傳播策略過于寬松會增加系統開銷。PHP RASP的污染傳播策略是變量的復制、賦值和大部分的字符串處理等操作傳播污染。

動態污點跟蹤的一個小小好處是如果一些敏感函數的參數沒有被污染,那么我們就無需對它進行安全檢查。當然,這只是它的副產物,它的大作用在漏洞檢測方面。

動態污點跟蹤的實現比較復雜,有興趣的可以去看看鳥哥的taint,鳥哥的taint也是以PHP擴展的方式做動態污點跟蹤。PHP RASP中,這部分是基于鳥哥的taint修改、線程安全優化、適配不同PHP版本實現的。在發行過程中,我們也將遵守taint的License。

在PHP解釋器中,全局變量都保存在一個HashTable類型的符號表symbol_table中,包括預定義變量_GET、_GET,_SERVER等數組中的值標記為污染,這樣,我們就完成了污染源的標記。

污染的傳播過程其實就是hook對應的函數,在PHP中,可以從兩個層面hook函數,一是通過修改zend_internal_function的handler來hook PHP中的內部函數,handler指向的函數用C或者C++編寫,可以直接執行。zend_internal_function的結構體如下:

//zend_complie.h
typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
    uint32_t fn_flags;
    zend_string* function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_internal_arg_info *arg_info;
    /* END of common elements */

    void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //函數指針,展開:void (*handler)(zend_execute_data *execute_data, zval *return_value)
    struct _zend_module_entry *module;
    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;

我們可以通過修改zend_internal_function結構體中handler的指向,待完成我們需要的操作后再調用原來的處理函數即可完成hook。 另一種是hook opcode,需要使用zend提供的API zend_set_user_opcode_handler來修改opcode的handler來實現。

我們在MINIT函數中用這兩種方法來hook傳播污染的函數,如下圖所示

當傳播污染的函數被調用時,如果這個函數的參數是被污染的,那么把它的返回值也標記成污染。以hook內部函數str_replace函數為例,hook后的rasp_str_replace如下所示

PHP_FUNCTION(rasp_str_replace)
{
    zval *str, *from, *len, *repl;
    int tainted = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zzz|z", &str, &repl, &from, &len) == FAILURE) {
        return;
    }//取參

    if (IS_STRING == Z_TYPE_P(repl) && PHP_RASP_POSSIBLE(repl)) {
        tainted = 1;
    } else if (IS_STRING == Z_TYPE_P(from) && PHP_RASP_POSSIBLE(from)) {
        tainted = 1;
    }//判斷

    RASP_O_FUNC(str_replace)(INTERNAL_FUNCTION_PARAM_PASSTHRU);//調用原函數執行

    if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) {
        TAINT_MARK(Z_STR_P(return_value));
    }//污染標記
}

首先獲取參數,判斷參數from和repl是否被污染,如果被污染,將返回值標記為污染,這樣就完成污染傳播過程。

當被污染的變量作為參數被傳入關鍵函數時,觸發關鍵函數的安全檢查代碼,這里的實現其實跟上面的類似。PHP的中函數調用都是由三個Zend opcode:ZEND_DO_FCALL,ZEND_DO_ICALL 和 ZEND_DO_FCALL_BY_NAME中某一個opcode來進行的。每個函數的調用都會運行這三個 opcode 中的一個。通過劫持三個 opcode來hook函數調用,就能獲取調用的函數和參數。這里我們只需要hook opcode,就是上面第二幅圖示意的部分,為了讓讀者更加清晰,我把它復制下來。

如圖,在MINIT方法中,我們利用Zend API zend_set_user_opcode_handler來hook這三個opcode,監控敏感函數。在PHP內核中,當一個函數通過上述opcode調用時,Zend引擎會在函數表中查找函數,然后返回一個zend_function類型的指針,zend_function的結構如下所示

union _zend_function {
    zend_uchar type;    /* MUST be the first element of this struct! */

    struct {
        zend_uchar type;  /* never used */
        zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
        uint32_t fn_flags;
        zend_string *function_name;
        zend_class_entry *scope;
        union _zend_function *prototype;
        uint32_t num_args;
        uint32_t required_num_args;
        zend_arg_info *arg_info;
    } common;

    zend_op_array op_array;
    zend_internal_function internal_function;
};

其中,common.function_name指向這個函數的函數名,common.scope指向這個方法所在的類,如果一個函數不屬于某個類,例如PHP中的fopen函數,那么這個scope的值是null。這樣,我們就獲取了當前函數的函數名和類名。

以上的行文邏輯是以RASP的角度來看的,先hook opcode和內部函數,來實現動態污點跟蹤,然后通過hook函數調用時運行的三個opcode來對監控函數調用。實際上,在PHP內核中,一個函數的調用過程跟以上的行文邏輯是相反的。

當一個函數被調用時,如上文所述,根據這個函數調用的方式不同,例如直接調用或者通過函數名調用,由Zend opcode,ZEND_DO_FCALL,ZEND_DO_ICALL 和 ZEND_DO_FCALL_BY_NAME中的某一個opcode來進行。Zend引擎會在函數表中搜索該函數,返回一個zend_function指針,然后判斷zend_function結構體中的type,如果它是內部函數,則通過zend_internal_function.handler來執行這個函數,如果handler已被上述hook方法替換,則調用被修改的handler;如果它不是內部函數,那么這個函數就是用戶定義的函數,就調用zend_execute來執行這個函數包含的zend_op_array。

現在我們從RASP的角度和PHP內核中函數執行的角度來看了動態污點跟蹤和函數的hook,接下來,我們需要對不同類型的關鍵函數進行安全檢測。

基于詞法分析的攻擊檢測

傳統WAF和云WAF在針對HTTP Request檢測時有哪些方法呢?常見的有正則匹配、規則打分、機器學習等,那么,處于PHP解釋器內部的PHP RASP如何檢測攻擊呢?

首先,我們可以看PHP RASP可以獲取哪些數據作為攻擊檢測的依據。與其他WAF一樣,PHP RASP可以獲取HTTP請求的Request。不同的是,它還能獲取當前執行函數的函數名和參數,以及哪些參數是被污染的。當然,像傳統WAF一樣,利用正則表達式來作為規則來匹配被污染的函數參數也是PHP RASP檢測的一種方法。不過,對于大多數的漏洞,我們采用的是利用詞法分析來檢測漏洞。準確的來說,對于大多數代碼注入漏洞,我們使用詞法分析來檢測漏洞。

代碼注入漏洞,是指攻擊者可以通過HTTP請求將payload注入某種代碼中,導致payload被當做代碼執行的漏洞。例如SQL注入漏洞,攻擊者將SQL注入payload插入SQL語句中,并且被SQL引擎解析成SQL代碼,影響原SQL語句的邏輯,形成注入。同樣,文件包含漏洞、命令執行漏洞、代碼執行漏洞的原理也類似,也可以看做代碼注入漏洞。

對于代碼注入漏洞,攻擊者如果需要成功利用,必須通過注入代碼來實現,這些代碼一旦被注入,必然修改了代碼的語法樹的結構。而追根到底,語法樹改變的原因是詞法分析結果的改變,因此,只需要對代碼部分做詞法分析,判斷HTTP請求中的輸入是否在詞法分析的結果中占據了多個token,就可以判斷是否形成了代碼注入。

在PHP RASP中,我們通過編寫有限狀態機來完成詞法分析。有限狀態機分為確定有限狀態機DFA和非確定有限狀態機NFA,大多數的詞法分析器,例如lex生成的詞法分析器,都使用DFA,,因為它簡單、快速、易實現。同樣,在PHP RASP中,我們也使用DFA來做詞法分析。

詞法分析的核心是有限狀態機,而有限狀態機的構建過程比較繁瑣,在此不贅述,與編譯器中的詞法分析不同的是,PHP RASP中詞法分析的規則并不一定與這門語言的詞法定義一致,因為詞法分析器的輸出并不需要作為語法分析器的輸入來構造語法樹,甚至有的時候不必區分該語言的保留字與變量名。

在經過詞法分析之后,我們可以得到一串token,每個token都反映了對應的代碼片段的性質,以SQL語句

select username from users where id='1'or'1'='1'

為例,它對應的token串如下

select <reserve word>
username <identifier>
from <reserve word>
users    <identifier>
where <reserve word>
id  <identifier>
=   <sign>
'1' <string>
or  <reserve word>
'1' <string>
=   <sign>
'1' <string>

而如果這個SQL語句是被污染的(只有SQL語句被污染才會進入安全監測這一步),而且HTTP請求中某個參數的值是1'or'1'='1,對比上述token串可以發現,HTTP請求中參數橫跨了多個token,這很可能是SQL注入攻擊。那么,PHP RASP會將這條HTTP請求判定成攻擊,直接阻止執行SQL語句的函數繼續運行。如果上述兩個條件任一不成立,則通過安全檢查,執行SQL語句的函數繼續運行。這樣就完成了一次HTTP請求的安全檢查。其他代碼注入類似,當然,不同的代碼注入使用的DFA是不一樣的,命令注入的DFA是基于shell語法構建的,文件包含的DFA是基于文件路徑的詞法構建的。

在開發過程中有幾個問題需要注意,一個是\0的問題,在C語言中,\0代表一個字符串的結束,因此,在做詞法分析或者其他字符串操作過程中,需要重新封裝字符串,重寫一些字符串的處理函數,否則攻擊者可能通過\0截斷字符串,繞過RASP的安全檢查。

另一個問題是有限狀態自動機的DoS問題。在一些非確定有限狀態機中,如果這個自動機不接受某個輸入,那么需要否定所有的可能性,而這個過程的復雜度可能是2^n。比較常見的例子是正則表達式DoS。在這里不做深入展開,有興趣的朋友可以多了解一下。

討論

在做完這個RASP之后,我們回頭來看看,一些問題值得我們思考和討論。

RASP有哪些優點呢?作為縱深防御中的一層,它加深了縱深防御的維度,在Web請求發生時,從HTTP Server、Web解釋器/編譯器到數據庫,甚至是操作系統,每一層都有自己的職責,每一層也都是防護攻擊的陣地,每一層也都有對應的安全產品,每一層的防護側重點也都不同。

RASP還有一些比較明顯的優點,一是對規則依賴很低,如果使用詞法分析做安全檢測的話基本不需要維護規則。二是減少了HTTP Server這層攻擊面,繞過比較困難,絕大多數基于HTTP Server特性的繞過對RASP無效。例如HPP、HPF、畸形HTTP請求、各種編碼、拆分關鍵字降低評分等。三是誤報率比較低。從比較理想的角度來說,如果我的后端代碼寫得非常安全,WAF看到一個包含攻擊payload的請求就攔截,這也屬于誤報吧。

RASP的缺點也很明顯,一是部署問題,需要在每個服務器上部署。二是無法像云WAF這樣,可以通過機器學習進化檢驗規則。三是對服務器性能有影響,但是影響不大。根據我們對PHP RASP做的性能測試結果來看,一般來說,處理一個HTTP請求所消耗的性能中,PHP RASP消耗的占3%左右。

其實,跳出RASP,動態污點跟蹤和hook這套技術方案在能做的事情很多,比如性能監控、自動化Fuzz、入侵檢測系統、Webshell識別等等。如果各位有什么想法,歡迎和我們交流。

參考文獻

關于作者

兩位作者水平有限,如文章有錯誤疏漏,或者有任何想討論交流的,請隨時聯系

  • c0d3p1ut0s c0d3p1ut0s@gmail.com
  • s1m0n simonfoxcat@gmail.com

License

在PHP RASP中,我們使用了一部分taint和PHP內核的代碼。兩者的License都是PHP License。因此,在軟件發行過程中,我們將遵守PHP License的相關限制。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/449/