基於PHP擴展的WAF實現

lightless · 2015/07/24 11:24php

0x00 前言


最近上(ri)網(zhan)上(ri)多了,各類狗啊盾啊看的好心煩,好多蜜汁shell都被殺了,搞的我本身也想開發這麼一個斬馬刀,順便看成畢設來作了。web

未知攻,焉知防。咱們先來看看shell們都是怎麼躲過查殺的:加密、變形、回調、隱藏關鍵字……總之就是一句話,讓本身變得沒有特徵,這樣就能夠躲過狗和盾的查殺。可是萬變不離其宗,不管怎麼變形,最終都會回到相似這樣的格式:shell

#!php
$_GET($_POST)
複製代碼

分爲執行數據部分($_GET)和傳遞數據的部分($_POST),也就是說,不管怎麼變形,在執行的過程當中都會變成這個樣子,接下來仍是去執行相似system(), exec(), eval()等等函數,那麼咱們就直接定位到這裏,檢測該腳本是否調用危險函數,或是在腳本調用這些函數的時候進行分析,判斷該腳本是否爲非法用戶的shell,會取到很不錯的防護效果。安全

0x01 如何獲取先機


既然分析出來了問題的根結,那麼下一步就是要控制住這些危險函數的入口點,即system()等函數的底層入口,就像Windows APIhookzw系列函數同樣,咱們也要hooksystem()這些函數。服務器

這裏咱們須要用到PHP Extension, 即PHP擴展,位於PHP內核zend和PHP應用代碼之間,很明顯,擴展能夠監控應用層代碼的執行細節,也能夠調用內核提供的ZEND API接口,包括禁用類、禁用函數等等。圖爲PHP結合其餘必要組件的基本結構。less

在ZEND中提供了接口供咱們進行這樣的操做,經過zend_set_user_opcode_handler就能夠達到目的,根據某大牛所說,只須要hook三個OPCODE便可。函數

#!c
ZEND_INCLUDE_OR_EVAL        處理eval()、require等
ZEND_DO_FCALL               函數執行system()等
ZEND_DO_FCALL_BY_NAME       變量函數執行 $func = "system";$func();等
複製代碼

ZEND_DO_FCALL爲例,在MINIT中將ZEND_DO_FCALL替換掉:網站

#!c
zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL);
複製代碼

接着定義本身的處理函數,這裏是LIGHT_DO_FCALLui

#!c
static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS)
{
    /* 檢測是否爲合法調用 */

    if (/*非法調用*/)
    {
        /*攔截掉,不執行。*/
    }
    else
    {
        /*合法調用,繼續執行*/
        return ZEND_USER_OPCODE_DISPATHC;
    }   
}
複製代碼

這樣一個骨架就完成了,其他的內容能夠自行添加。這樣每當調用system()等函數時,都會通過你本身的LIGHT_DO_FCALL,因而就能夠對上層的代碼執行進行檢查了。加密

0x02 上面的方法太粗暴


上面的方法當然是好,可是誤判會不少,甚至一個普通的函數調用都會直接被ban掉。這樣的WAF放在業務中必定會被噴出翔的。咱們須要換個思路。

以前已經說過了,不管如何變形、加密、隱藏關鍵字等,最終都須要調用到system()eval()等函數,終究是逃不過這些的。那麼咱們把剛纔的思路的變一下,不要直接hook那幾個OPCODE,向上走一層。

eval()函數在底層是要調用zend_compile_string函數的,那麼咱們是否是能夠在底層重載掉zend_compile_string函數,或是用本身的函數替換掉呢?答案確定是能夠的,咱們只須要重寫本身的zend_compile_string函數便可。

可是對於system來講有些不同,上面咱們hook掉了ZEND_DO_FCALL,可是太粗暴了,咱們這裏能夠在function_table中刪除掉system,並用咱們本身的函數註冊system函數,這樣就能夠作到了實時檢查,若是爲合法調用,能夠調用舊的system函數並繼續執行。

很明顯這樣作的話,確實減小了誤殺,可是也增長了風險,由於除了system()以外還有不少能夠用於執行命令的函數。除了執行命令,還有遍歷目錄、讀敏感文件的問題,須要考慮不少的方面,偏僻的函數都要考慮在內,還有PHP SPLDirectoryIterator等等各類方面。

0x03 說了這麼多咱們看看實際效果吧


咱們來看看hook三個OPCODE的方式會產生什麼樣子的效果。首先咱們在MINIT函數中將這三個OPCODE的處理函數換成咱們本身的。

#!c
ZEND_MINIT_FUNCTION(lightWAF)
{
    /*
     * hook掉ZEND_DO_FCALL
     * 處理system函數等
     */
    zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL);

    /*
     * hook掉ZEND_DO_FCALL_BY_NAME
     * 處理$func='system';$func();等類型
     */
    zend_set_user_opcode_handler(ZEND_DO_FCALL_BY_NAME, LIGHT_DO_FCALL_BY_NAME);

    /*
     * hook掉ZEND_INCLUDE_OR_EVAL
     * 處理eval, require等
     */
    zend_set_user_opcode_handler(ZEND_INCLUDE_OR_EVAL, LIGHT_INCLUDE_OR_EVAL);

    return SUCCESS;
}
複製代碼

LIGHT_DO_FCALLLIGHT_DO_FCALL_BY_NAMELIGHT_INCLUDE_OR_EVAL的內容相似,均爲判斷腳本文件是否在upload目錄中,若是在的話就禁止危險函數的執行,不然放行。這裏咱們用LIGHT_DO_FCALL說明一下:

#!c
/* LIGHT_DO_FCALL */
static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS)
{
    char *filePath;

    filePath = zend_get_executed_filename(TSRMLS_C);
    php_printf("[Debug] filePath: %s\n<br />", filePath);

    if (strstr(filePath, "/upload/"))
    {
        /* 非法調用,攔截 */   
        php_printf("[Warning] Execute command via system() etc.\n<br />");
        return ZEND_USER_OPCODE_RETURN;
    }
    else    /* Not Found */
    {
        /* 合法調用,放行 */
        return ZEND_USER_OPCODE_DISPATCH;
    }
}
複製代碼

這裏咱們看到,一旦檢測到在upload目錄中,就返回ZEND_USER_OPCODE_RETURN,實際做用就是不繼續執行危險函數了,若是不在upload目錄中,就返回ZEND_USER_OPCODE_DISPATCH,實際做用就是繼續執行該函數。

擴展編寫完成後,編譯便可獲得so文件,若是對PHP擴展開發以及編譯不熟悉的同窗,請參考這個擴展開發教程的第五章:http://www.walu.cc/phpbook/ ,開發方面的知識超出了本文的內容,因此這裏再也不贅述了。

編譯完成後,將獲得的lightWAF.so文件複製到php的擴展目錄下,我這裏是/usr/local/php/lib/php/extensions/,根據本身的實際狀況進行調整。接下來修改php.ini,讓PHP自動加載咱們的擴展,在其中加上一行:

extension=/path/to/our/lightWAF.so
複製代碼

我這裏是:

extension=/usr/local/php/lib/php/extensions/lightWAF.so
複製代碼

根據以前so文件放的位置不一樣,請自行修改路徑。

最後咱們重啓PHP服務,讓設置生效。PHP重啓完成後,在終端中執行php -m,來驗證擴展是否加載成功,若是在結果中看到了lightWAF,則說明加載成功。(請無視圖中的錯誤信息,那個是我本地環境的問題,對lightWAF以及PHP沒什麼影響)

成功加載後,咱們看一下實際效果,這裏有一個上傳文件的頁面,能夠上傳任何類型的文件並將上傳的文件保存在upload目錄中。如今模擬一下getshell的過程。

上傳頁面

咱們寫個小馬,內容以下:

#!php
<?php
    system($_GET["cmd"]);
?>
複製代碼

進行上傳,並訪問shell,結果以下:

能夠看到被成功攔截了,好吧,咱們換隻牛逼的馬兒,試試反射型的。

#!php
<?php
    $func = new ReflectionFunction("system");
    echo $func->invokeArgs(array("$_GET[c][/c]"));
?>
複製代碼

繼續上傳並訪問,看看結果:

依然被攔截了,只不過此次觸發的是ZEND_DO_FCALL_BY_NAME這個OPCODE。 接下來咱們看看正常的文件會不會被攔截,在lightWAF目錄下寫入一個test.php,用於模擬正常的業務文件,內容以下:

#!php
<?php
    system('ls');
?>
複製代碼

訪問一下看看結果:

能夠看到ls命令成功的執行了,也就是說咱們的正常文件是不會被攔截的,而只有upload目錄中的文件會被攔截,這樣作又會引起另外一個弊端,假若攻擊者經過某種方法將shell寫入正常的文件中,或是與業務結合起來,那麼這種防護手段就很難生效了。具體如何防護還要結合其餘的特徵進行檢測,並非沒有辦法了,實際應用中不能只依靠檢測文件路徑這一條規則,須要結合業務進行部署防護方案。

另外一種方法與這個hook三個OPCODE的方法相似,無非就是麻煩一點,感興趣的同窗能夠圍觀下面的參考文獻:http://security.tencent.com/index.php/blog/msg/19 ,這裏描述了hook函數的比較詳細的思路。

0x04 不談業務的安全都是耍流氓


總結一下,攔截的方法大概就是上面兩種,可是攔截的依據尚未決定,如何判斷一個調用system()的腳本是否爲webshell呢?若是咱們的WAF放到生產環境,啥都無論亂殺,頗有可能形成正常的業務沒法工做。我本身總結了幾個方法,可能聯合起來使用效果更好一些。

  • 根據目錄判斷 一般狀況下,上傳的文件通常都有專門的目錄進行存放,例如upload/等,正常的業務文件是不在這其中的,因而咱們能夠簡單的只對這個目錄進行處理,其餘目錄的文件一概放行。

  • 根據文件權限判斷 這須要網站的維護者對網站的權限進行嚴格的控制,例如,全部的web文件均是644,且爲root:root全部。上傳的文件爲www:www全部,根據權限的不一樣進行查殺,對於默認的web文件進行放行,對屬於非root:root的文件進行查殺。也能夠根據w標誌判斷,經過未知方式getshell的文件不少是帶有w標誌的,因此能夠根據這些特徵進行查殺。

  • 變形檢測 若是發現一個文件調用了assertsystempreg_replace /e等等,可是在源文件中沒有發現這些關鍵字,還等什麼,這個文件很大的可能就是shell了。(zend_get_executed_filename能夠得到文件的標誌以及文件的路徑,例如出現了assert,說明該腳本使用了assert執行代碼。)

  • 黑名單檢測 就像傳統的殺軟同樣,總會有那麼一部分的特徵病毒庫,咱們也能夠創建一部分的webshell特徵庫,先依靠特徵庫殺掉一部分,再根據其餘的狀況進行判斷。若是將動態檢測和靜態檢測結合起來,查殺、攔截效果應當都會有顯著的改善。

0x05 因此這WAF有啥用


安全須要和業務結合起來進行,不談業務的安全都是耍流氓。由於是擴展級別的WAF,在部署的時候可能須要從新編譯,修改配置文件等等。批量式部署可能顯得不是那麼方便,並且要根據業務須要進行各類細微的調整。若是僅僅是幾臺服務器,相信這種WAF仍是十分棒的,調整起來也十分方便。

若是能開發出適合批量部署的基於擴展的WAF,那麼可能會比較容易普及,畢竟在業務上部署WAF不是一個簡單的事情。

  • 參考資料:

http://security.tencent.com/index.php/blog/msg/57 http://security.tencent.com/index.php/blog/msg/19 http://www.walu.cc/phpbook/ http://www.php-internals.com/book/?p=index http://www.nowamagic.net/librarys/veda/detail/1543

相關文章
相關標籤/搜索