<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    淺談從PHP內核層面防范PHP WebShell

    By 咖啡(k4kup8_0x4154_gmail.com)
    
    
    [目錄]
    
    1. 簡述
    2. php的執行流程
    3. php的生命周期
    4. php源代碼分析以及功能性代碼的實現
    5. 總結
    6. 參考資料
                                           
    
    一、簡述
    
        依據php特定運行環境、php某些特定函數缺陷、php普通函數可以實現變化多端的php 
    webshell,php版本的scanwebshell也不是太給力。php webshell功能最大化就是實現文件、
    目錄、命令、數據庫等操作,這些都是基于php代碼實現的。把相關功能化的php函數運行參
    數提取出來,然后做一個判斷,這樣就能從本質上防范php webshell,在php這個層面實現
    其安全的最大化。這里介紹下通過編寫php擴展來實現這個思路,當然需要的話也可以重新
    編譯php源代碼來實現。
    
        首先我們了解下php的執行流程、php生命周期,接下來通過分析具體函數的php源代碼
    來實現功能性代碼。
        	
    
    二、php的執行流程
    
    2.1 scanner
        
        將PHP代碼轉換為Tokens,詳見代碼Zend/zend_language_scanner.l。
           
    2.2 parser
        
        將Tokens轉換成表達式,詳見代碼Zend/zend_language_parser.y。
           
    2.3 compile
        
        將表達式編譯成opcode。opcode存放在op_array中。
           
    2.4 execute
        
        Zend Engine調用zend_execute來執行op_array,輸出結果。
    
    
    三、php的生命周期
    
    3.1 STARTUP
    
        1、初始化引擎和核心組件。
        2、解析php.ini。
        3、初始化靜態構建的模塊(MINIT)。
        4、初始化共享模塊(MINIT)。
    
    3.2 ACTIVATION
    
        1、初始化環境變量、變量。
        2、激活靜態構建的模塊(RINIT) 。
        3、激活共享模塊(RINIT) 。
        
    3.3 RUNTIME
        
        1、編譯和執行php.ini中auto_prepend_file選項指定的文件。
        2、編譯和執行所請求的文件。
        3、編譯和執行php.ini中auto_append_file選項指定的文件。
        
    3.4 DEACTIVATION
          
        1、調用用戶指定的退出函數。
        2、銷毀對象實例。
        3、停用模塊(RSHUTDOWN)。
        4、清空輸出。
        5、清理環境。
        6、釋放剩余的非持久內存。
    
    3.5 SHUTDOWN
          
        1、關閉啟動的全部模塊(MSHUTDOWN)。
        2、關閉引擎。
    
    
    四、php源代碼分析以及功能性代碼的實現
        
        php函數分為兩種:一種是Zend的函數,這類函數數量比較少,比如eval函數。第二種
    是由PHP_FUNCTION宏編寫的,這類函數數量比較多,比如system函數。實現對兩類函數在提
    取運行時的參數的方式也不相同,比如處理eval函數用重寫zend_compile_string的方式,
    而處理system函數則對HashTable操作。下邊就以eval函數和system函數為例進行分析、代
    碼實現。
       
    4.1 eval函數代碼分析與代碼實現
        
        首先我們看php源代碼中eval函數是如何實現的,部分代碼如下:
    
    --code-------------------------------------------------------------------------                     
        // PHPSRC/Zend/zend_vm_def.h
            
        if (inc_filename->type!=IS_STRING) {
    		tmp_inc_filename = *inc_filename;
    		zval_copy_ctor(&tmp_inc_filename);
    		convert_to_string(&tmp_inc_filename);
    		inc_filename = &tmp_inc_filename;
    	}    
            
        case ZEND_EVAL: {
                    /* 調用zend_make_compiled_string_description函數 */
    				char *eval_desc = zend_make_compiled_string_description("eval()"d code" TSRMLS_CC);
                    /* 調用zend_compile_string函數 */
    				new_op_array = zend_compile_string(inc_filename, eval_desc TSRMLS_CC);
    				efree(eval_desc);
    			}
        /* 執行op_array */
        zend_execute(new_op_array TSRMLS_CC);
        
        //PHPSRC/Zend/zend.c
             
        #define COMPILED_STRING_DESCRIPTION_FORMAT "%s(%d) : %s"
        ZEND_API char *zend_make_compiled_string_description(char *name TSRMLS_DC)
        {
        	zend_spprintf(&compiled_string_description, 0, COMPILED_STRING_DESCRIPTION_FORMAT, cur_filename, cur_lineno, name);
        	return compiled_string_description; //返回值包含"eval()"d code"字符串
        }
     
        //PHPSRC/Zend/zend_compile.c
             
        ZEND_API zend_op_array *(*zend_compile_string)(zval *source_string, char *filename TSRMLS_DC);
        
        zend_compile_string一個函數指針。下邊看下引擎初始化的時候對zend_compile_string的操作。
            
        int zend_startup(zend_utility_functions *utility_functions, char **extensions, int start_builtin_functions)
        {
        	zend_compile_string = compile_string; //對zend_compile_string函數的地址賦值
    -------------------------------------------------------------------------------    	
                
        只要檢查op_array中是否含有"eval()"d code"字符串,就能判斷是否是在執行eval函數。
        在引擎初始化的時候,默認會將compile_string函數的地址賦值給zend_compile_string,
    compile_string函數則返回一個指向zend_op_array的指針。如果能在php代碼編譯之前對
    zend_compile_string進行重寫,那么就能達到劫持的目的。根據php的生命周期,對
    zend_compile_string進行重寫應該放在STARTUP或者ACTIVATION這兩個階段,而編寫php擴
    展所使用到的PHP_MINIT_FUNCTION和PHP_RINIT_FUNCTION宏就分別處在STARTUP和ACTIVATION
    這個兩個階段,這是為什么呢?我們先看下php.h代碼中對PHP_MINIT_FUNCTION宏的定義。
        
    --code-------------------------------------------------------------------------    
        #define PHP_MINIT_FUNCTION		ZEND_MODULE_STARTUP_D
        //ZEND_MODULE_STARTUP_D定義在zend_API.h
        #define ZEND_MODULE_STARTUP_D(module)		int ZEND_MODULE_STARTUP_N(module)(INIT_FUNC_ARGS)
        //ZEND_MODULE_STARTUP_N定義在zend_API.h
        #define ZEND_MODULE_STARTUP_N(module)       zm_startup_##module
        //INIT_FUNC_ARGS定義在zend_modules.h
        #define INIT_FUNC_ARGS		int type, int module_number TSRMLS_DC
    -------------------------------------------------------------------------------
        
        PHP_MINIT_FUNCTION(module)的原型就是:
    
    --code-------------------------------------------------------------------------    
    zm_startup_module(int type, int module_number TSRMLS_DC)
    -------------------------------------------------------------------------------
    
        同樣的PHP_RINIT_FUNCTION(module)的原型為:
        
    --code-------------------------------------------------------------------------     
    zm_activate_module(int type, int module_number TSRMLS_DC)
    -------------------------------------------------------------------------------
    
        關于對eval函數運行參數截取分析的實現代碼如下:
    
    --code-------------------------------------------------------------------------
        #define OVECCOUNT 30
        /* 具體正則表達式要按照具體的需求來寫,下面正則僅為測試用 */
        #define eval_regex_value   "(((chr\\(\\d*?\\)|base64_decode\\(|eval|gzinflate\\(|system|shell_exec|popen|pclose|proc_close|proc_get_status|proc_nice|proc_terminate|exec|passthru|show_source|escapeshellcmd|escapeshellarg system|shell_exec|popen|pclose|proc_open|proc_close|proc_get_status|proc_nice|proc_terminate|exec|passthru|show_source|escapeshellcmd|escapeshellarg)\\([{}"$\\w\\s]*?\\));).*?"
        
        static zend_op_array* (*old_compile_string)(zval *source_string, char *filename TSRMLS_DC);
        static zend_op_array* safe_compile_string(zval *source_string, char *filename TSRMLS_DC);
        
        PHP_RINIT_FUNCTION(safe) //PHP_MINIT_FUNCTION(safe)也可
        {
        	safe_hook_execute();
        	return SUCCESS;
        }
        
        PHP_RSHUTDOWN_FUNCTION(safe) //PHP_MSHUTDOWN_FUNCTION(safe)也可
        {
        	safe_unhook_execute();
        	return SUCCESS;
        }   
        
        int matchpattern(char *src, char *pattern, int i) //正則匹配函數
        {              
        	pcre *re;
        	const char *error;
        	int erroffset;
        	int ovector[OVECCOUNT];
        	int rc;
        	char *substring_start;
        	int substring_length;
        	TSRMLS_FETCH();
        	
        	re = pcre_compile(pattern, PCRE_CASELESS|PCRE_DOTALL, &error, &erroffset, NULL);
        	if(re == NULL) {
        		//printf("PCRE compilation failed at offset %d: %s\n", erroffset, error);
        		return 1;
        	}
        	
        	rc = pcre_exec(re, NULL, src, strlen(src), 0, 0, ovector, OVECCOUNT);	
        	if(rc >= 0) {
        		substring_start = src + ovector[2*i];
        		substring_length = ovector[2*i+1] - ovector[2*i];
        		
        		printf("Match_result: %.*s\n", substring_length, substring_start);
        		printf("Filename    : %-40s\n", zend_get_executed_filename(TSRMLS_C));
        		printf("Line        : %-50i\n", zend_get_executed_lineno(TSRMLS_C));
        	}
        	free(re);
        	return rc;
        }    
        
        static zend_op_array *safe_compile_string(zval *source_string, char *filename TSRMLS_DC)
        {
        	char *eval_strings;
        	int x;
            zend_op_array *op_array;
         	
            op_array = old_compile_string(source_string, filename TSRMLS_CC);
        
            /* 過濾非eval函數 */
        	if(!strstr(op_array->filename, "eval()"d code")) {
        		return old_compile_string(source_string, filename TSRMLS_CC);
        	}
        	/* 將source_string字符串賦值給eval_strings */
        	eval_strings = estrndup(Z_STRVAL_P(source_string), Z_STRLEN_P(source_string));
        
        	printf("%s","\n");
        	printf("Function    : %-40s\n", "eval");	
        	x = matchpattern(eval_strings, eval_regex_value, 1);
        	if (x < 0)
        	{
        		return old_compile_string(source_string, filename TSRMLS_CC);
        	}
        	else if(x >= 0)
        		return FALSE;
        }
        
        int safe_hook_execute()
        { 			
        	old_compile_string = zend_compile_string;
        	zend_compile_string = safe_compile_string;
            system_hook_system();  //對應后邊對system函數的操作   
        	return TRUE;
        }
        
        int safe_unhook_execute()
        {	
        	zend_compile_string = old_compile_string;    
        	return TRUE;
        }
    -------------------------------------------------------------------------------    
        
    4.2 system函數代碼分析與代碼實現 
        
        首先我們看php源代碼中system函數是如何實現的,部分代碼如下:
    
    --code-------------------------------------------------------------------------         
        //PHPSRC\ext\standard\exec.c
          
        PHP_FUNCTION(system)
        {  
            /* 調用php_exec_ex函數 */
            php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
        }
           
        static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode)
        {
        	char *cmd;
    
        	if (!ret_array) {
    		/* 調用php_exec函數 */
        		ret = php_exec(mode, cmd, NULL, return_value TSRMLS_CC);
        	} else {
        		if (Z_TYPE_P(ret_array) != IS_ARRAY) {
        			zval_dtor(ret_array);
        			array_init(ret_array);
        		}
        		ret = php_exec(2, cmd, ret_array, return_value TSRMLS_CC);
    -------------------------------------------------------------------------------
    
        接下來看php_exec函數的定義。
    
    --code-------------------------------------------------------------------------  
        int php_exec(int type, char *cmd, zval *array, zval *return_value TSRMLS_DC)
        {
        	char *cmd_p, *b, *c, *d=NULL;
    
        	if (PG(safe_mode)) {
        		cmd_p = php_escape_shell_cmd(d);
        		efree(d);
        		d = cmd_p;
        	} else {
        		cmd_p = cmd;
    
            #ifdef PHP_WIN32
        	    fp = VCWD_POPEN(cmd_p, "rb"); //調用VCWD_POPEN函數
    -------------------------------------------------------------------------------    	    
     
        接下來看VCWD_POPEN函數的定義。
    
    --code-------------------------------------------------------------------------     
        //TSRM\tsrm_virtual_cwd.c
        
        #ifdef TSRM_WIN32 //以windows平臺為例
        
        CWD_API FILE *virtual_popen(const char *command, const char *type TSRMLS_DC)
        {
        	return popen_ex(command, type, CWDG(cwd).cwd, NULL);//調用popen_ex函數
        }
    -------------------------------------------------------------------------------
        
        接下來看popen_ex函數的定義。
        
    --code-------------------------------------------------------------------------
    //TSRM\tsrm_win32.c
    
    TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, char *env)
    {
    	char *cmd;
        
    	cmd = (char*)malloc(strlen(command)+strlen(TWG(comspec))+sizeof(" /c "));
    	sprintf(cmd, "%s /c %s", TWG(comspec), command);
    	if (!CreateProcess(NULL, cmd, &security, &security, security.bInheritHandle, NORMAL_PRIORITY_CLASS|CREATE_NO_WINDOW, env, cwd, &startup, &process)) {
    		return NULL;
    	}
    	free(cmd); 	
    -------------------------------------------------------------------------------    	
    
        上邊是system函數執行的參數傳遞的過程,在這個過程中如果可以截取函數執行的參數
    的話,就可以分析參數是否包含危險的關鍵字。為了方便編寫擴展程序,我們直接在exec.c
    中php_exec函數中添加截取代碼,也就是在php_exec_ex函數調用php_exec函數之前的位置。
    添加如下代碼即可獲取執行的參數:
    
    --code-------------------------------------------------------------------------   
       	x = matchpattern(cmd, system_regex_value, 0);//調用正則函數進行判斷
    	if(x >= 0) RETURN_FALSE;
    -------------------------------------------------------------------------------
    	
        下邊我們分析怎么實現對system函數的重寫,這里主要參照main.c文件中實現php.ini
    中disable_functions功能的php_disable_functions函數,它調用了zend_disable_function。
    
    --code-------------------------------------------------------------------------   
    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);
    }
    -------------------------------------------------------------------------------
        
        同樣我們可以用zend_hash_del函數將system從function_table中刪除,然后注冊新的
    zend函數,以達到對system函數劫持的目的。
    
        system作為一個執行系統命令的函數,在這里進行了禁用操作,沒有使用正則處理函數
    進行參數的檢查,當然也可以根據具體的需求進行具體的操作。
      
        實現代碼如下:
        
    --code-------------------------------------------------------------------------    
        /* 聲明導出函數 */
        PHP_FUNCTION(system1)
        {  
        	printf("%s","\n");
        	printf("Function    : %-40s\n", "system");
        	printf("Filename    : %-40s\n", zend_get_executed_filename(TSRMLS_C));
        	printf("Line        : %-50i\n", zend_get_executed_lineno(TSRMLS_C));
        	printf("%s","system function is disabled.");  
        }
        
        /* 聲明 Zend 函數塊 */
        zend_function_entry hook_system_functions[] = {      
          PHP_FALIAS(system, system1, NULL) // 創建system別名
          {NULL, NULL, NULL}
        };
        
        /* 創建system hook函數 */
        int safe_hook_system()
        {
          TSRMLS_FETCH();
          /* 刪除function_table中的system函數 */
          zend_hash_del(CG(function_table), "system", sizeof("system"));
        
        /* 注冊新zend函數 */
        #ifndef ZEND_ENGINE_2
          zend_register_functions(hook_system_functions, NULL, MODULE_PERSISTENT TSRMLS_CC);
        #else
          zend_register_functions(NULL, hook_system_functions, NULL, MODULE_PERSISTENT TSRMLS_CC);
        #endif
          return 0;
        }
    -------------------------------------------------------------------------------
        
    4.3 demo運行效果
        
    4.3.1 加載php擴展
    
    -------------------------------------------------------------------------------
    C:\phpext>type php.ini | findstr "^extension="
    extension=php_safe.dll 
    
    C:\phpext>php 3.php
    
    Function    : system
    Filename    : C:\phpext\3.php
    Line        : 3
    
    system function is disabled.
    
    Function    : eval
    Match_result: exec("ver");
    Filename    : C:\phpext\3.php
    Line        : 9
    -------------------------------------------------------------------------------
    
    4.3.2 不加載php擴展   
        
    -------------------------------------------------------------------------------
    C:\phpext>type php.ini | findstr "^extension="
    
    
    C:\phpext>php 3.php
    
    Microsoft Windows XP [版本 5.1.2600]
    -------------------------------------------------------------------------------
    
     
    五、總結
        
        實現php webshell的功能性函數眾多,我們做到控制關鍵性的函數足以。當然實現函數
    截取要根據函數的情況進行一一的分析,然后做相應的判斷。
    
        最后要感謝下SuperHei,文章不足之處請斧正。
    
    
    六、參考資料
    
    [1] php源代碼  http://www.php.net
    [2] PHP Extension Writing http://talks.somabo.de/200903_montreal_php_extension_writing.pdf
    
    -EOF-
    
    97av