經過php extension使disable_function支持通配符

  本人學C語言不久,對指針內存管理等都還沒入門,php擴展的編寫更是胡亂在拼湊,如下是我「亂搞」的一點記錄,但願你們指點和輕噴。 php

  一天翻php.ini的時候看到了一堆「同族」的函數 html

; This directive allows you to disable certain functions for security reasons.
; It receives a comma-delimited list of function names. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off.
; http://php.net/disable-functions
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,
pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,
pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,
pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority

當時就想要是支持通配符那麼直接寫成 pcntl_* 這樣就簡便多了。想法是有了,可是不知道怎麼實現好。偶然的機會看到了《淺談從PHP內核層面防範PHP WebShell》這文章,當中提到 zend_disable_function 這個函數,因而感受先前的通配符想法能夠實現了。 前端

說一下簡單的思路吧:在php.ini讀取配置,遍歷函數表,正則匹配函數而後刪除掉,註冊一個同名函數以便給前端提示。 nginx

先用C模擬一下實現吧,代碼以下 web

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pcre.h>

#define OVERCCOUNT 30
#define MAX_REGEX_COUNT 50 //最大支持規則數量

char *replace_start(char *src) { //替換通配符*號
    static char buffer[4096];
    char *p, *str;
    char *orig = "*";
    char *rep = "(\\w+)";
    
    str = (char *)malloc(4096);
    p = strstr(src, orig);
    if (p == src) {
        sprintf(str, "%s%s", src, "$");
    } else {
        sprintf(str, "%s%s%s", "^", src, "$");
    }
    if (!(p = strstr(str, orig))) {
        return str;
    }
    strncpy(buffer, str, p-str); // Copy characters from 'str' start to 'orig' st$
    buffer[p-str] = '\0';

    sprintf(buffer+(p-str), "%s%s", rep, p+strlen(orig));
    free(str);
    return buffer;
}

int matchpattern(char *src, char *pattern) {
    pcre *re;
    const char *error;
    int erroffset;
    int ovector[OVERCCOUNT];
    int rc;
    re = pcre_compile(pattern, PCRE_CASELESS|PCRE_DOTALL, &error, &erroffset, NULL);
    if (re == NULL)
        return 0;
    rc = pcre_exec(re, NULL, src, strlen(src), 0, 0, ovector, OVERCCOUNT);
    free(re);
    return rc;
}

int main(int argc, char **argv)
{
    char *function_table[] = \
        {"array_diff", "array_pop", "array_shift", "var_dump", "time", \
         "date", "str_replace", "strstr", "test", "abc_str"};
    char *ini = "array_*, *str test";

    char *s, *p;
    char *delim = ", ";//這裏支持,號和空格來分割規則
    char *regex_list[MAX_REGEX_COUNT] = {0};

    int i = 0;
    s = strndup(ini, strlen(ini));
    p = strtok(s, delim);
    if (p) {
        do {
            p = replace_start(p);
            regex_list[i] = strndup(p, strlen(p));
            i++;
        } while ((p = strtok(NULL, delim)));
    }
   
    int match = -1, k;
    char *func, *regex;
    for (i = 0; i < 10; i++) {
        func = function_table[i];
        for (k = 0; k < MAX_REGEX_COUNT; k++) {
            regex = regex_list[k];
            if (!regex) break;
            //printf("regex:%s\n", regex);
            match = matchpattern(func, regex);
            if (match >= 0) {
                printf("function:%s() are disabled!!\n", func);
            }
        }
    }
   
    //free memory
    for(i = 0; i < MAX_REGEX_COUNT; i++) {
        regex = regex_list[i];
        if (regex) {
            free(regex);
            regex_list[i] = NULL;
        }
    }
    free(s);
    s = NULL;
    return 0;    
}
由於要使用正則,我在這裏選擇了pcre庫,因而咱們編譯的時候要帶上 -lpcre。運行看看咱們的效果。


嗯,好像還不錯的樣子。接下來就是關鍵了,怎麼改編成php擴展。 shell

至於怎麼快速建立一個php 擴展的就不介紹了,能夠參考《快速開發一個PHP擴展》,我在這裏新建了一個叫"solutest"的擴展。接着咱們把上面的函數(main函數對應的改一下名字,我這裏改成 static void remove_function())貼到solutest.c(文件名對應你建立時候輸入的名字)裏面,對應的內存操做函數能夠換成由php內核提供的e*系列函數,malloc->emalloc, free->efree ...還有一點是用e*系列申請的內存才用efree來釋放,要否則不會有錯,囧在這裏吃過虧。(詳細參考《PHP擴展開發與內核應用》- 內存管理)。而後在 PHP_MINIT_FUNCTION 裏面調用咱們的 remove_function,爲何選擇 PHP_MINIT_FUNCTION ?或者你能夠嘗試在 PHP_RINIT_FUNCTION 調用 (參考《PHP擴展開發與內核應用》- PHP啓動與終結)。編譯看看效果,別忘了須要pcre庫的支持,因此要加上 pcre.h 後,而後編輯 Makefile 在EXTRA_LIBS 加上 -lpcre。 apache

OK,make && sudo make install,接着編輯php.ini加上咱們的擴展(我測試環境是nginx + php-fpm,對應php.ini在 /etc/php5/fpm/php.ini,若是不肯定你加載的配置文件路徑能夠查看phpinfo的Loaded Configuration File
服務器

[solutest]
extension=solutest.so

sudo /etc/init.d/php5-fpm restart 函數

咱們重啓fpm看看效果(若是apache環境直接重啓apache服務器便可)

嘛嘛~跑起來了。 php-fpm

  怎麼獲取系統的函數呢?咱們能夠參考一下zend_disable_function的實現

//file:"Zend/zend_API.c" line:2524
ZEND_API int zend_disable_function(char *function_name, uint function_name_length TSRMLS_DC) /* {{{ */
{
	if (zend_hash_del(CG(function_table), function_name, function_name_length+1)==FAILURE) {
		return FAILURE;
	}
	disabled_function[0].fname = function_name;
	return zend_register_functions(NULL, disabled_function, CG(function_table), MODULE_PERSISTENT TSRMLS_CC);
}
/* }}} */
   嗯,從函數咱們能夠知道CG(function_table)保持了咱們要的函數表,並且它是一個 HashTable 結構,咱們能夠經過 zend_hash_del 刪除函數表內某個函數。跟進去 zend_hash_del函數看看,
//file:"Zend/zend_hash.h" line:154
#define zend_hash_del(ht, arKey, nKeyLength) \
		zend_hash_del_key_or_index(ht, arKey, nKeyLength, 0, HASH_DEL_KEY)
是一個宏,繼續展開深刻在 file:"Zend/zend_hash.c" line:486,函數有點就不貼了,能夠看出是對HashTable的遍歷和一些鏈表刪除的操做,還有獲得一個重要信息是函數名保存在了Bucket的arKey。如下是HashTbale的定義
//file:"Zend/zend_hash.h" line:52
struct _hashtable;

typedef struct bucket {
	ulong h;						/* Used for numeric indexing */
	uint nKeyLength;
	void *pData;
	void *pDataPtr;
	struct bucket *pListNext;
	struct bucket *pListLast;
	struct bucket *pNext;
	struct bucket *pLast;
	const char *arKey;
} Bucket;

typedef struct _hashtable {
	uint nTableSize;
	uint nTableMask;
	uint nNumOfElements;
	ulong nNextFreeElement;
	Bucket *pInternalPointer;	/* Used for element traversal */
	Bucket *pListHead;
	Bucket *pListTail;
	Bucket **arBuckets;
	dtor_func_t pDestructor;
	zend_bool persistent;
	unsigned char nApplyCount;
	zend_bool bApplyProtection;
#if ZEND_DEBUG
	int inconsistent;
#endif
} HashTable;

詳細的解釋能夠參考《深刻理解PHP內核》- PHP哈希表實現

  好吧,依葫蘆畫瓢,嘗試遍歷一下function_table。把 remove_function 函數對應修改成

static void remove_function() {
#ifdef ZEND_SIGNALS
    TSRMLS_FETCH();
#endif

    char *ini = "array_*, *str test";

    char *s, *p;
    char *delim = ", ";//這裏支持,號和空格來分割規則
    char *regex_list[MAX_REGEX_COUNT] = {0};

    int i = 0;
    s = estrndup(ini, strlen(ini));
    p = strtok(s, delim);
    if (p) {
        do {
            //p = replace_str(p, "*", "(\\w+)");
            p = replace_start(p);
            regex_list[i] = estrndup(p, strlen(p));
            i++;
        } while ((p = strtok(NULL, delim)));
    }
   
    int match = -1, k;
    char *regex;
    HashTable ht_func, *pht_func;
    Bucket *pBk;
    //拷貝一份CG(function_table)進行操做
    zend_hash_init(&ht_func, zend_hash_num_elements(CG(function_table)), NULL, NULL, 0);
    zend_hash_copy(&ht_func, CG(function_table), NULL, NULL, sizeof(zval*));
    pht_func = &ht_func;

    for (pBk = pht_func->pListHead; pBk != NULL; pBk = pBk->pListNext) {
 		printf("%s()\n", pBk->arKey);
    }

    //free memory
    zend_hash_destroy(&ht_func); //銷燬HashTable
    pht_func = NULL;
    for(i = 0; i < MAX_REGEX_COUNT; i++) {
        regex = regex_list[i];
        if (regex) {
            efree(regex);
            regex_list[i] = NULL;
        }
    }
    efree(s);
    s = NULL;   
}
保存之後又是一輪的  make && sudo make install。sudo /etc/init.d/php5-fpm restart,刷啦啦的一大片,嚇壞了吧,保存下來看看有多少。

應該差很少了吧,後面有...省略號是否是buffer什麼的滿了因此還沒輸出完呢???

  OK,下面是重點了,刪除對應的函數。其實咱們抄一下zend_disable_function就OK了,有同窗會問爲何不直接調用zend_disable_function,別急,下面我會說道。再次修改咱們的remove_function函數,此次修改便利的循環體和 char *ini 就好

char *ini = "array_p*,"; //使用array族函數測試


for (pBk = pht_func->pListHead; pBk != NULL; pBk = pBk->pListNext) {
        for (k = 0; k < MAX_REGEX_COUNT; k++) {
            regex = regex_list[k];
            if (!regex) break;
            //regex = "^array_p(\\w+)";
            match = matchpattern(pBk->arKey, regex);
            if (match >= 0) {
                printf("function:%s are disabled!!\n", pBk->arKey);
                //zend_disable_function(func, sizeof(func));
                if (zend_hash_del(CG(function_table), pBk->arKey, strlen(pBk->arKey)+1) == FAILURE) {
                    printf("disable %s error\n", pBk->arKey);
                };
                disabled_function[0].fname = pBk->arKey;
                zend_register_functions(NULL, disabled_function, CG(function_table), MODULE_PERSISTENT TSRMLS_CC);
            }
        }
    }
由於把系統的函數刪除了,不知請者調用會產生一個php函數不存在的錯誤,腳本也會中止運行,因而須要註冊一個同名的函數回去,而這個函數什麼也不作,輸出提示就好。那麼咱們須要在 remove_function 函數以前定義函數入口和提示函數
PHP_FUNCTION(print_disabed_info)
{  
    //I don't know why I can't use get_active_function_name in here
    // Maybe "EG"
    zend_error(E_WARNING, "*** function has been disabled! (°Д°≡°д°)エッ!?"); //get_active_function_name(TSRMLS_C)
}

static zend_function_entry disabled_function[] = {
    PHP_FALIAS(display_disabled_function, print_disabed_info, NULL)
    PHP_FE_END
};

估計有同窗吐槽爲何用***代替了顯示的函數名,這就是爲何我不調用zend_disable_function的緣由。當時卡在這裏好久,一直段錯誤,後來無心中註釋了 get_active_function_name(TSRMLS_C) 就跑起來了╯-__-)╯ ╩╩,求告知。。和上面一個編譯重啓服務器什麼的,而後看效果,由於咱們配置寫的是array_p*,因此一下函數被禁用了。(測試完之後記得關閉輸出)

而後隨便寫個腳本,調用一下array_pop函數什麼的,而後執行之。

It's work!! :)

  呼,不知不覺寫了這麼長了,也懶得分兩篇了。接下來把讀取php.ini配置代碼寫上就完成了。其實這部門工做在擴展自動生成的代碼已經有了,只要稍微加工一下就好。

/* 
  	Declare any global variables you may need between the BEGIN
	and END macros here:     
*/
ZEND_BEGIN_MODULE_GLOBALS(solutest)
	char *disable_functions;
ZEND_END_MODULE_GLOBALS(solutest)
php_solutest.h 大概47行左右的樣子,去掉註釋加入咱們的disable_functions變量
/* If you declare any globals in php_solutest.h uncomment this:*/
ZEND_DECLARE_MODULE_GLOBALS(solutest)
solutest.c 30行左右,去掉註釋
/* {{{ PHP_INI
 */
/* Remove comments and fill if you need to have entries in php.ini*/
PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("solutest.disable_functions", "", PHP_INI_ALL, OnUpdateString, disable_functions, zend_solutest_globals, solutest_globals)
PHP_INI_END()

/* }}} */
solutest.c 71行左右,去掉註釋,修改成咱們的變量
/* If you have INI entries, uncomment these lines */
	REGISTER_INI_ENTRIES();
PHP_MINIT_FUNCTION 函數裏面,去掉註釋
/* Remove comments if you have entries in php.ini */
	DISPLAY_INI_ENTRIES();
PHP_MINFO_FUNCTION  函數裏面,去掉註釋

而後編輯你的php.ini文件,加入配置

[solutest]
extension=solutest.so
solutest.disable_functions = array_p*,
編譯重啓服務器,而後瀏覽phpinfo會發現咱們的配置已經被讀取了。
最後把咱們的配置利用上,能夠經過SOLUEXT_G(disable_functions)宏來訪問,對應修改 remove_function 函數。去掉 char *ini 由於已經不須要了,配置從php.ini 讀取,而後修改 s
s = estrndup(SOLUTEST_G(disable_functions), strlen(SOLUTEST_G(disable_functions)));
OK,保存編譯重啓服務器測試。

:)預期的效果達到了。打完收工。

PS:
  此擴展是本人YY的產物,沒有通過嚴格測試,請勿在生產機上使用。

代碼下載: http://pan.baidu.com/share/link?shareid=207778&uk=436715329

參考資料:
淺談從PHP內核層面防範PHP WebShell
PHP擴展開發及內核應用
鳥哥博客
快速開發一個PHP擴展
深刻理解PHP內核

相關文章
相關標籤/搜索