初探php擴充套件層面(一)
前段時間想寫一個靜態程式碼審計工具,需要對php擴充套件熟悉一些,那麼自己從零開始接觸這一塊,如果有錯誤的地方,麻煩師傅們指正。
另外呢網上雖然有一些文章,但是感覺都不是特別細,對於剛入門的我來說有些難以理解,因此詳細的記錄下自己的學習過程。
我在mac環境上折騰了兩天gdb,還是沒折騰好,無奈選擇docker,這裡推薦一個
https://github.com/hxer/php-debug/blob/master/Dockerfile
這個dockerfile的vld和php版本不匹配,需要更換下低版本的vld。
啟動命令
docker run -i -d --security-opt seccomp=unconfined -v /Users/p0desta/Desktop/code:/home php5-debug
編寫最簡單的php擴充套件
-
在ext目錄下執行命令
./ext_skel --extname=p0desta
-
然後進入到擴充套件目錄下,編輯config.m4檔案
16 dnl PHP_ARG_ENABLE(foobar, whether to enable foobar support, 17 dnl Make sure that the comment is aligned: 18 dnl [--enable-foobarEnable foobar support])
刪除第16-18行的註釋
-
然後去php_p0desta.h檔案,新增函式宣告
PHP_FUNCTION(confirm_foobar_compiled); PHP_FUNCTION(p0desta);
-
然後到p0desta.c中
const zend_function_entry p0desta_functions[] = { PHP_FE(p0desta, NULL) PHP_FE(confirm_p0desta_compiled,NULL)/* For testing, remove later. */ PHP_FE_END/* Must be the last line in p0desta_functions[] */ };
新增如下
PHP_FE(p0desta, NULL)
-
然後到最底下編寫函式
PHP_FUNCTION(p0desta) { php_printf("hello world"); }
-
然後在當前目錄下執行命令
phpize ./configure --enable-p0desta --enable-debug make
然後會在modules資料夾下生存 so
檔案,在php.ini中新增拓展
extension=p0desta.so
然後就可以呼叫自寫的函式。
php程式碼的大致執行流程
開始 -> Scanning,將php程式碼轉換為語言片段(Tokens) -> Parsing,將tokens轉化為簡單而有意義的表示式 -> Compilation,將表示式編譯成opcode -> Execution,順次執行opcodes,從而實現php指令碼的功能。
hook最簡單的opcode
關於一些巨集的解釋參考: https://github.com/pangudashu/php7-internal/blob/master/7/hook.md
這裡我使用 zend_set_user_opcode_handler
函式來hook echo
函式
zend_set_user_opcode_handler(ZEND_ECHO, ppecho);
主要原理是將對應的Zend op的handler函式替換成我們自己定義的來實現HOOK
首先我在擴充套件.h中定義如下
#define ZEND_OPCODE_HANDLER_ARGS void PHP_FUNCTION(confirm_foobar_compiled); int ppecho(ZEND_OPCODE_HANDLER_ARGS);
擴充套件.c中
PHP_MINIT_FUNCTION(p_echo) { /* If you have INI entries, uncomment these lines REGISTER_INI_ENTRIES(); */ zend_set_user_opcode_handler(ZEND_ECHO, ppecho); return SUCCESS; } int ppecho(ZEND_OPCODE_HANDLER_ARGS) { php_printf("hook success"); return ZEND_USER_OPCODE_RETURN; }
如果打算放行繼續執行的話 return ZEND_USER_OPCODE_DISPATCH
,如果不繼續執行的話 return ZEND_USER_OPCODE_RETURN
編譯完之後看一下效果
Webshell簡單防禦初探
關於一些PHP核心中的定義詳情請參考 https://www.kancloud.cn/kancloud/php-internals/42755
這裡我們暫時需要了解的有
-
全域性變數
EG()、這個巨集可以用來訪問符號表,函式,資源資訊和常量 CG() 用來訪問核心全域性變數 PG() PHP全域性變數。我們知道php.ini會對映一個或者多個PHP全域性結構。舉幾個使用這個巨集的例子:PG(register_globals), PG(safe_mode), PG(memory_limit) FG() 檔案全域性變數。大多數檔案I/O或相關的全域性變數的資料流都塞進標準擴展出口結構。
-
函式型別
Zend引擎將函式分為以下幾個型別
#define ZEND_INTERNAL_FUNCTION 1 #define ZEND_USER_FUNCTION 2 #define ZEND_OVERLOADED_FUNCTION 3 #define ZEND_EVAL_CODE 4 #define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5
-
ZEND_USER_FUNCTION (使用者函式:使用者定義的函式)
<?php function test(){ } ?>
-
ZEND_INTERNAL_FUNCTION (內部函式:由擴充套件、PHP核心、Zend引擎提供的內部函式)
-
變數函式
$func = 'print_r'; $func('i am print_r function.');
-
匿名函式
-
-
php7的_zend_execute_data
struct _zend_execute_data { const zend_op*opline;/* executed opline*/ zend_execute_data*call;/* current call*/ zval*return_value; zend_function*func;/* executed function*/ zvalThis;/* this + call_info + num_args*/ zend_execute_data*prev_execute_data; zend_array*symbol_table; #if ZEND_EX_USE_RUN_TIME_CACHE void**run_time_cache;/* cache op_array->run_time_cache */ #endif #if ZEND_EX_USE_LITERALS zval*literals;/* cache op_array->literals*/ #endif };
我們看一下如下程式碼的opcode
<?php eval("system('whoami');");
我們hook掉 INCLUDE_OR_EVAL
修改 php_hook_eval.h
增加
PHP_FUNCTION(confirm_foobar_compiled); static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS); # define ZEND_OPCODE_HANDLER_ARGS zend_execute_data *execute_data
修改 hook_eval.c
增加
static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS) { zend_execute_data *tmp = &execute_data; zend_op *opline = execute_data->opline; return ZEND_USER_OPCODE_DISPATCH; }
直接在 execute_data
中往下找呼叫的函式 system
這個也就是運算元
string型變數比較特殊,因為核心在儲存String型變數時,不僅儲存了字串的值,還儲存了它的長度,所以它有對應的兩種巨集組合STRVAL和STRLEN,即:Z_STRVAL、Z_STRVAL_P、Z_STRVAL_PP與Z_STRLEN、Z_STRLEN_P、Z_STRLEN_PP。
編寫 HOOK_INCLUDE_OR_EVAL
如下
static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS) { zend_op *opline = execute_data->opline; zval *operands = opline->op1.zv; char *cmd = Z_STRVAL_P(operands); if(cmd){ if((strstr(cmd, "system")==NULL)&&(strstr(cmd, "exec")==NULL)&&(strstr(cmd, "shell_exec")==NULL)&&(strstr(cmd, "passthru")==NULL)&&(strstr(cmd, "roc_open")==NULL)){ return ZEND_USER_OPCODE_DISPATCH; }else{ return ZEND_USER_OPCODE_RETURN; } } return ZEND_USER_OPCODE_DISPATCH; }
看下執行流程
當然,只hook掉 ZEND_INCLUDE_OR_EVAL
是很難防禦的,比如說
<?php eval('echo `whoami`;');
這種就必須再去hook DO_FCALL
為了不影響業務並且去做更好的防禦,還需要更深入的研究。
參考:
http://drops.xmd5.com/static/drops/web-7333.html https://www.cnblogs.com/iamstudy/articles/php_code_rasp_1.html