做爲WebShell檢測、CMS修復、WebShell攻防研究學習的第三篇文章php
本文旨在研究PHP內核的原理、PHP Zend擴展的編寫方法、PHP API Hook技術。但願能給研究這一領域的朋友帶來一點點幫助,同時拋磚引玉,和你們共同討論更多的技術細節, 共同窗習成長html
上一篇咱們學習了WebShell攻防、服務器安全加固、以及文件磁盤可疑行爲的檢測相關的知識java
http://www.cnblogs.com/LittleHann/p/3558685.htmlnode
本文主要分爲3個部分 1. PHP內核、.php腳本的執行流程 2. PHP Zend擴展原理及編寫方法 3. PHP API Hook技術
相關學習資料mysql
http://www.nowamagic.net/librarys/veda/detail/1325 http://www.nowamagic.net/librarys/veda/detail/1322 http://rapheal.sinaapp.com/2013/11/20/php_zend_hello_world/ http://hilojack.sinaapp.com/?p=891 http://www.nowamagic.net/librarys/veda/detail/1285 http://www.php.net/manual/zh/internals2.ze1.zendapi.php http://blog.csdn.net/heiyeshuwu/article/details/3453854 http://www.php.net/manual/zh/internals2.buildsys.configunix.php http://www.php.net/manual/zh/internals2.buildsys.php http://www.ccvita.com/496.html http://blog.csdn.net/taft/article/details/596291 http://weizhifeng.net/write-php-extension-part1.html http://blog.csdn.net/hguisu/article/details/7414724 http://sebug.net/paper/pst_WebZine/pst_WebZine_0x05/0x07_淺談從PHP內核層面防範PHP_WebShell.html http://security.tencent.com/index.php/blog/msg/19 http://www.laruence.com/2012/02/18/2560.html https://wiki.php.net/rfc/taint http://www.php-internal.com/
1. PHP內核學習linux
從個人理解上來講,PHP、包括zend(咱們這裏暫時統稱爲PHP,以後會對他們的組成概念進行說明)本質上都是一個C程序。程序員
但和咱們傳統意義上寫的C程序不一樣的是,PHP具備不少強大的功能: 1) 虛擬機的功能: 和軟件加密中的VMP概念相似,PHP的語法能夠當作是一種虛擬指令集,咱們程序員在編寫PHP腳本的時候其實就是在使用這種虛擬指令集進行代碼邏輯、功能調用的編程。
同時PHP則提供了對這些虛擬指令的解析、翻譯,並調用相應的底層代碼去完成程序員的調用請求
2) 內存管理: 這裏說的內存管理和操做系統層面的那個內存管理並非一個概念。操做系統負責的是整個windows / linux 上所有應用的內存管理。而PHP的內存管理負責的是
2.1) 在每次請求的腳本的當前"運行空間(針對每一個腳本而言都有本身的運行空間)" 2.2) 以及PHP內核 2.3) 擴展模塊中的永久內存 這些方面的內存管理,對於PHP來講,內存管理是一個很是底層、很是重要的部分
3) 和服務器通訊: PHP做爲一個編譯(這裏的編譯指的是編譯爲opcode的概念)、解釋系統、同時提供虛擬運行時環境的C程序,它最重要的方面是能和服務器(WEB容器)進行通訊,
要注意的是,這裏所說的"服務器"是一個概念上的意義,PHP對與上層"服務器"的通訊進行了抽象,把全部的邏輯都抽象、封裝到了SAPI(server application programming
interface),對於上層的服務器來講,它們對PHP的調用就能夠經過SAPI來進行,實現了"解耦和"。常見的調用SAPI方式有: 1) CGI: CGI即通用網關接口(Common Gateway Interface),它是一段程序, 通俗的講CGI就象是一座橋,把網頁和WEB服務器中的執行程序鏈接起來 每有一個用戶請求,都會先要建立CGI的子進程,而後處理請求,處理完後結束這個子進程,這就是Fork-And-Execute模式 因此用CGI方式的服務器有多少鏈接請求就會有多少CGI子進程,子進程反覆加載是CGI性能低下的主要緣由 2) Fastcgi模式 FAST-CGI 是CGI的升級版本,FastCGI 像是一個常駐(Long-Live)型的CGI,它能夠一直執行着,只要激活後,不會每次都要花費時間去Fork一次
(解決了 CGI 最爲人詬病的 Fork-And-Execute 模式) 3) CLI模式 CLI是PHP的命令行運行模式
php -f script.php 4) 模塊模式 模塊模式是以mod_php5模塊的形式集成,此時mod_php5模塊的做用是接收Apache傳遞過來的PHP文件請求,並處理這些請求,而後將處理後的結果返回給Apache。 httpd.conf: ... LoadModule php5_module "E:/wamp/bin/php/php5.4.12/php5apache2_4.dll" ... 若是咱們在Apache啓動前在其配置文件中配置好了PHP模塊(mod_php5),PHP模塊經過註冊apache2的ap_hook_post_config掛鉤,在Apache啓動的時候啓動此模塊以接受PHP文件
的請求。 全部這些調用方式,對PHP來講本質上都是同樣的,PHP把它們都當作是來自"服務器"的調用。這種架構方式提供了很美麗的解耦和方式,同時,爲PHP在不一樣操做系統、不一樣服務
器(WEB容器)上的跨平臺操做提供了支持。
咱們能夠從PHP的官網上下載到PHP的源代碼:sql
http://cn2.php.net/downloads.phpapache
解壓後編程
咱們先來看看PHP的源碼的目錄結構:
build: 和編譯有關的目錄 ext: 擴展庫代碼 例如 Mysql、zlib、iconv 等咱們熟悉的擴展庫。咱們在程序中調用mysql_connect()這個函數就是由於在php.ini加入"extension=php_mysql.dll" 來加載php_mysql.dll這個擴展,
從而使咱們在代碼空間中可使用mysql_connect()這個導出函數 main: 主目錄,其中包含了: 1) php中最重要頭文件php.h 2) 輸入輸出函數spprintf.h 3) 服務器抽象層SAPI.h 4) 內存管理alloca.c 5) 流式邏輯的實現\streams\.. .. pear: 這裏是Pear核心文件。http://pear.php.net/ sapi: 各類服務器的接口調用,例如apache、IIS等,也包含通常的fastcgi、cgi等。PHP經過SAPI來封裝對服務器通訊的抽象 scripts: Linux 下的腳本目錄 tests: 測試腳本目錄,官方默認提供了一些基本的測試.php腳本供咱們學習之用 win32: Windows 下編譯 PHP 有關的腳本。在windows下編譯PHP使用了WSH(windows script hosting) Zend: 核心的引擎,zend是PHP的核心,它主要實現了: 1) 把人類能夠理解的腳本解析成機器能夠理解的符號(token), 而後在一個進程空間內執行這些符號。 2) ZE還負責內存管理 3) 變量做用域 4) 以及函數調用的調度
瞭解了PHP的源碼目錄
接下來咱們學習一下怎麼本身動手對這些源代碼進行編譯
os: RedHat Linux Enterprice 5 kernal: 2.6.18-371.4.1.el5 php-version: php-5.5.9.tar.gz http://cn2.php.net/downloads.php make: GNU Make 3.81 gcc: gcc version 4.1.2 20080704 (Red Hat 4.1.2-54)
選定一個文件夾做爲源碼編譯的目錄,這裏選擇,我這裏的狀況是/php/..(路徑、文件名能夠任選)
1. 切換目錄: cd /php
2. 解壓源代碼壓縮包: tar -zvxf php-5.5.9.tar.gz
3. 解壓完畢後,進入源碼目錄: cd php-5.5.9
4. 強制從新生成配置文件configure(不是必要、可選): autoconf -f http://man.chinaunix.net/linux/lfs/htmlbook/appendixa/autoconf.html
5. 更改腳本的執行權限: chmod 777 configure
6. 根據configure生成makefile文件: ./configure --enable-debug (--enable-debug表示編譯時附帶GDB調試信息,這樣編譯出來的版本會比release更大,但同時也容許咱們用NetBeans這樣的工具進行C源碼級別的單步調試)
7. 根據上一步生成的makefile文件進行編譯: make
8. 安裝完成,進入CLI測試一下,由於咱們如今尚未和apache等服務器"關聯"起來,因此這裏就使用CLI接口的PHP進行測試
cd sapi/cli
9. 運行測試語句:
./php -m (若是你本機已經安裝了PHP、或者lamp的話,這裏須要明確指定調用當前路徑下的php可執行文件)
./php -v
10. 安裝成功
11. 安裝遇到的問題: 以上的步驟是標準的步驟,我實際在安裝的時候會遇到不少的問題,下面把我找到的這些相關問題的連接分享出來,但願對朋友有幫助
http://asange.blog.51cto.com/7125040/1229976
http://linux.chinaitlab.com/administer/850484.html
configure: error: xml2-config not found. Please check your libxml2 installation. ---- 安裝libxml2-dev依賴包便可: yum -y install libxml2-devel.i386
./configure --enable-debug 若是你想要以後能夠debug調試這個PHP的內核代碼的話,這裏必定要注意加上--enable-debug
http://man.chinaunix.net/linux/lfs/htmlbook/appendixa/autoconf.html
在windows下編譯PHP不太方便,會遇到編譯器、編譯環境版本、額外的依賴庫、配置環境、甚至還有字符編碼等問題,不太推薦在windows進行PHP源碼的編譯
http://blog.linuxphp.org/archives/1592/
http://www.kissthink.com/archive/zai--i-n-d-o-w-s-shang-bian-yi-zi-ji-de----yin-qing.html
http://ms.n.blog.163.com/blog/static/185953520134992322690/
https://wiki.php.net/internals/windows/stepbystepbuild
編譯完成PHP的C源代碼以後,咱們繼續學習怎麼調試PHP代碼
這裏要注意一下,若是直接google"調試PHP代碼"這幾個字,搜索到的結果可能會讓人產生歧義,這裏實際上是兩個概念:
1) 單步調試腳本語言級別的PHP代碼,例如咱們寫一下幾句話:
<?php $i = 'Hello world!'; echo $i;
咱們能夠對這兩行PHP腳本代碼作單步調試,並查看它們對應的opcode。
怎麼對"腳本代碼"做單步調試
對PHP腳本代碼(爲了區分以後要說的PHP的C源代碼,因此這裏稱之爲PHP腳本代碼)進行單步調試最好的工具就是xdebug了,它是一個PHP擴展,同時不少IDE都繼承對PHP單步調試的支持(eclipse、zend studio、NetBeans)
多是我研究的還太淺了,我在學習的時候,感受腳本代碼級的單步調試對個人幫助不是特別大,我基本均可以用die、var_dump來打到個人需求,因此並無深刻研究單步調試的東西,這裏給出NetBeans配置單步調試的相關資料,留待之後學習
https://netbeans.org/kb/docs/php/debugging_zh_CN.html http://www.zvv.cn/blog/show-101-1.html
說到這裏,咱們要注意的一點是,咱們在使用xdebug的時候,調試的對象實際上是PHP的中間語言: opcode.
關於PHP代碼和opcode的關係
咱們能夠結合
1) C和彙編的關係(雖然不是很準確) 2) C#和CIL: Common Intermeditate Language中間語言的關係 3) java和JVM字節碼的關係
PHP在執行的時候,會被Zend先翻譯成opcode中間語言(opcode就是一行行的代碼),而後再一條條的單獨執行,咱們知道,PHP是構建在Zend VM(Zend虛擬機)之上的,因此這些opcode其實就是Zend VM中的指令(和VMP: virtual machine protect 的概念相似)。
既然opcode是一個指令系統,那麼opcode就必然包含一個指令系統所必須的組成部分,包括:
1) 指令集(各類操做符) 2) 指令的格式和規範(由處理器指令規範規定) 3) 操做數 3.1) 顯示的常量操做數 3.2) 保存在寄存器中的操做數 3.3) 保存在堆棧中的操做數 3.4) 保存在內存中的操做數 3.5) 保存在IO端口中的操做數 4) 保存指令、及相關信息的數據結構(struct _zend_op)(PHP這類虛擬機語言特有的)
爲了更好地理解opcode和PHP代碼的關係,咱們從一個腳本的完整執行流程來看看PHP是怎麼對代碼進行翻譯、並執行的
例如,咱們在請求這段代碼的執行的時候
<?php echo 1; ?>
PHP會作以下的事情:
1) Zend啓動引擎"詞法"分析,將代碼切分爲一個個的標記Toekn,你能夠用token_get_all()函數來查看PHP在這一步都作了什麼 2) 使用"語法"分析器(注意和詞法作區分)(PHP使用bison生成語法分析器,規則見$PHP_SRC/Zend/zend_language_parser.y), bison根據規則進行相應的處理, 若是代碼找不到匹配
的規則,也就是語法錯誤時Zend引擎會中止,並輸出錯誤信息。 好比缺乏括號,或者不符合語法規則的狀況都會在這個環節檢查 3) Zend引擎對這些Token進行編譯, 將代碼編譯爲opcode,並綁定相應的參數、和函數調用 4) Zend引擎執行這些opcode,在執行opcode的過程當中還有可能會繼續重複進行編譯-執行,例如執行eval,include/require等語句,由於這些語句還會包含或者執行其餘文件或者字符串中
的腳本 5) 總的來講,咱們的腳本代碼最終會以opcode的形式在Zend RunTime中出現,咱們知道,opcode本質上是一些"虛擬機指令",因此每一條opcode都對應着Zend中對應的一個函數調用,
即"opcode-Zend_Function" 6) Zend_Function就是在底層用C實現的一些代碼邏輯,PHP的全部上層功能最終由底層的C來實現
1. 詞法分析
array (size=7) 0 => array (size=3) 0 => int 372 1 => string '<?php ' (length=6) 2 => int 1 1 => array (size=3) 0 => int 316 1 => string 'echo' (length=4) 2 => int 1 2 => array (size=3) 0 => int 375 1 => string ' ' (length=1) 2 => int 1 3 => array (size=3) 0 => int 305 1 => string '1' (length=1) 2 => int 1 4 => string ';' (length=1) 5 => array (size=3) 0 => int 375 1 => string ' ' (length=1) 2 => int 1 6 => array (size=3) 0 => int 374 1 => string '?>' (length=2) 2 => int 1
2. 語法分析(這一步是在檢查代碼的語法合理性)
3. 編譯Token
在PHP實現內部,opcode由以下的結構體表示:
struct _zend_op { opcode_handler_t handler; // 執行該opcode時調用的處理函數 znode result; znode op1; znode op2; ulong extended_value; uint lineno; zend_uchar opcode; // opcode代碼,咱們能夠根據這個opcode代碼推測出它可能調用的Zend函數 };
下面這個函數是在"編譯器"遇到echo語句的時候進行編譯的函數(注意,這裏是編譯過程,即(3)步,編譯器在這一步的目的是生成相應的opcode,並綁定參數)
void zend_do_echo(const znode *arg TSRMLS_DC) { zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); //首先經過get_next_op在當前的op_array的最後邊"生成"一條opcode opline->opcode = ZEND_ECHO; //opcode代碼 opline->op1 = *arg; //綁定這個opcode須要的參數 SET_UNUSED(opline->op2); }
PHP腳本編譯爲opcode保存在op_array中(注意,這仍是第(3)步),其內部存儲的結構以下:
struct _zend_op_array { /* Common elements */ zend_uchar type; char *function_name; // 若是是用戶定義的函數則,這裏將保存函數的名字 zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsigned char return_reference; /* END of common elements */ zend_bool done_pass_two; zend_uint *refcount; zend_op *opcodes; // opcode數組 zend_uint last,size; zend_compiled_variable *vars; int last_var,size_var; // ... }
opcodes保存在op_array後,在執行的時候由下面的execute函數執行
ZEND_API void execute(zend_op_array *op_array TSRMLS_DC) { // ... 循環執行op_array中的opcode或者執行其餘op_array中的opcode }
4. Zend引擎逐條(實際上是op_array中的逐個元素項)執行opcode
咱們知道,opcode本質上是一鞋"虛擬指令",這些虛擬指令必須在底層有具體的實現代碼,這個系統才能正常運轉,而咱們在第(3)步已經由zend_do_echo生成了相應的opcode,如今是執行的時候了
"echo 1;"是一條輸出常量的PHP腳本代碼,它在Zend中對應的調用以下:
static int ZEND_FASTCALL ZEND_ECHO_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *z; SAVE_OPLINE(); z = opline->op1.zv; // if (IS_CONST == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) { INIT_PZVAL(z); } zend_print_variable(z); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); }
代碼最終獲得了執行,看到這裏,我以爲咱們能夠想的更多一點,PHP的這種opcode本質上和VMP的虛擬指令是同樣的,可能不太準確,可是我以爲不少技術實際上是相同的,去除表面的特性,回到技術自己的原理,它們具備很大的類似性
http://www.cnblogs.com/LittleHann/p/3344261.html
更多詳細細節能夠參閱這些paper
http://www.nowamagic.net/librarys/veda/detail/1322 http://www.nowamagic.net/librarys/veda/detail/1325 http://rapheal.sinaapp.com/2013/11/20/php_zend_hello_world/
瞭解了PHP代碼和opcode的基本關係,咱們接下來學習一下:
怎麼查看PHP代碼對應的opcode
使用vld(Vulcan Logic Dumper)這款php擴展,能夠查看對應PHP代碼的opcode
1. 下載VLD源代碼
http://pecl.php.net/package/vld
我下的是 0.12.0 版本,下載後,解壓到一個文件夾中
2. 編譯源代碼
2.1) phpize
2.2) 生成makefile文件愛你: ./configure --with-php-config=/usr/bin/php-config --enable-vld
(這裏的php-config路徑可能會不同,根據本身的機器上的狀況修改)
2.3) 編譯完成後,複製.so文件到執行的目錄
cp modules/vld.so /usr/lib/php/modules
cp modules/vld.so /usr/include/php/ext
關於PHP的擴展目錄能夠在php.ini中查看到(在php.ini中搜索extention_dir)
3. 修改.php.ini配置,並重啓httpd服務
3.1) vim /etc/php.ini
3.2) 在文件中加上 extension=vld.so
4. 測試運行效果
vim test.php
<?php
echo 1;
?>
php -dvld.active=1 ./test.php
Finding entry points Branch analysis from position: 0 Return found filename: /var/www/html/index.php function name: (null) number of ops: 4 compiled vars: none line # * op fetch ext return operands --------------------------------------------------------------------------------- 2 0 > ECHO 1 5 1 ECHO '%0A' 2 > RETURN 1 3* > HANDLE_EXCEPTION branch: # 0; line: 2- 5; sop: 0; eop: 3 path #1: 0, 1
瞭解了PHP的單步調試、opcode的概念以後,咱們回到最初的問題上來. 咱們以前說過:
若是直接google"調試PHP代碼"這幾個字,搜索到的結果可能會讓人產生歧義,這裏實際上是兩個概念: 1) 單步調試腳本語言級別的PHP代碼 ...
這裏就要引入另外一個概念了
2) 對PHP的底層C代碼的單步調試
關於對底層C代碼的單步調試,我以爲有如下幾點須要理解: 1) PHP在底層是用C實現的 2) PHP本質上是一個C程序(php.exe),負責對腳本進行解釋和執行 3) 全部的PHP腳本代碼,都會被翻譯爲opcode,並對應於某個zend_funtion,全部咱們對這個相應的zend_function進行下斷點,就能夠針對執行的PHP腳本代碼,單步調試它對應的底層C代碼
結合咱們寫C程序的概念,咱們須要單步調試一個程序,就須要編譯出一個debug版本的exe程序。對於PHP的編譯也是一樣的道理。爲了debug單步調試PHP,咱們須要作以下準備工做:
1) 編譯個debug版本的PHP: 編譯出的可執行程序會比較大,附帶了調試信息 2) 用於單步調試PHP的IDE: 這裏選用NetBeans,eclipse也是能夠的
1. 編譯一個PHP(Debug),咱們在文章的開始已經學習了怎麼從源代碼編譯個debug版本的PHP,注意到:
./configure --enable-debug
這裏的 "--enable-debug"是一個關鍵,只有這樣編譯出的PHP版本纔是能夠單步調試的
2.在NetBeans中建立工程
將咱們進行編譯的PHP源碼目錄添加進工程中
選擇好類型,並選擇利用現存的源代碼建立工程
填寫上源代碼的路徑,我這裏是 /php/php-5.5.9
添加成功後,能夠開始下斷點調試了(在第一次運行的時候NetBeans會讓咱們選擇要debug的可執行程序,咱們選cli.exe便可),咱們的測試語句是:
<?php echo 1; ?>
咱們已經知道,這個語句對應的opcode調用的zend中的ZEND_ECHO_SPEC_CONST_HANDLER函數,故咱們的斷點應該下在這個函數體中
點擊開始debug,當PHP解析這段腳本的時候,由於會調用這個函數,因此天然斷了下來
以後,咱們就能夠像咱們平時編程同樣,去單步跟蹤、調試PHP的內部運行機制。掌握了對PHP底層代碼的單步調試對咱們深刻學習PHP是頗有幫助的,這可讓咱們從一個不一樣的角度去研究、學習PHP的運行機制、語言的特性,例以下面的情景
1) 你編寫了一個PHP腳本 2) 其中遇到一個PHP的特性不是很理解 3) 使用vld翻譯爲opcode 4) 根據opcode去推測、尋找(有時候不必定能準確地找到)這個語法對應的zend_function 5) 在指定的zend_function中下斷點,從C的層面單步跟蹤,幫助咱們更好、更深刻地理解問題的成因
文章寫到這裏,我再看看個人標題: "PHP內核、.php腳本的執行流程".......當真是以爲我有些蚍蜉撼樹,誇下海口了,PHP內核博大精深,並非在一篇文章中能研究地清楚的,最好的方法應該是針對某個的問題、某個特性做針對性的研究,但我認爲這篇文章會是一個很好的開始,這裏分享一些不錯的資料,也留待之後研究繼續
http://www.nowamagic.net/librarys/veda/detail/1285
PHP腳本的執行步驟
一切的開始: SAPI接口 命令行程序和Web程序相似, 命令行參數傳遞給要執行的腳本,至關於經過url 請求一個PHP頁面.。 PHP腳本完成執行後返回響應結果,只不過命令行響應的結果是顯示在終端上,而WEB服務器的返回結果回傳給瀏覽器。 腳本執行的開始都是經過SAPI接口進行的 1) 啓動apache: 當給定的SAPI啓動時,例如在對/usr/local/apache/bin/apachectl start的響應中,PHP從初始化其內核子系統開始。 在接近啓動例程的末尾,它加載"每一個"(逐一調用)擴展的代碼並調用其模塊初始化例程(MINIT)。這使得: 1.1) 每一個擴展能夠初始化內部變量 1.2) 分配資源 1.3) 註冊資源處理器 1.4) 以及向ZE註冊本身的函數,以便於腳本調用某個函數的時候這個函數的代碼的入口地址在哪裏 2) 請求初始化處理: 接下來,PHP等待SAPI層請求要處理的頁面 2.1) 對於CGI或CLI這種類型的SAPI來講,這將馬上發生且只發生一次 2.2) 對於apache、IIS或其餘WEB容器來講,每次遠程用戶請求頁面(請求執行腳本)時都將發生,所以重複不少次,也可能併發。 無論請求如何產生,PHP要求ZE創建腳本的運行環境後再開始接受請求(PHP負責和WEB服務器的通訊)。 在某個請求到達的時候,PHP會逐個調用"每一個"擴展的請求初始化(RINIT)函數。RINIT使得擴展有機會: 1) 設定特定的環境變量 2) 根據請求分配資源 3) 執行其餘任務: 如審覈 session擴展中有個RINIT做用的典型示例: 若是啓用session.auto_start,RINIT將自動觸發用戶空間的session_start()函數以及"預組裝"$_SESSION變量 3) 執行PHP代碼: 一旦請求被初始化了,ZE開始接管控制權,將PHP腳本翻譯成符號(Token),最終造成操做碼(Opcode)並逐步執行之。若是操做碼中有涉及到擴展的調用,ZE將會把參數綁定到該函數,而且臨時
交出控制權直到函數運行結束 4) 腳本結束: 每一次的腳本運行結束後,PHP會逐一調用"每一個"擴展的請求關閉(RSHUTDOWN)函數以執行最後的清理工做(如將session變量存入磁盤)。接下來,ZE執行清理過程(垃圾收集): 即有效地對以前
的請求期間用到的每一個變量執行unset() 5) SAPI關閉: 一旦完成,PHP繼續等待SAPI的其餘文檔請求、或者關閉信號(對於CGI和CLI等SAPI,沒有"下一個請求",因此SAPI馬上開始關閉)。 關閉期間,PHP再次遍歷每一個擴展,調用其模塊關閉(MSHUTDOWN)函數,並最終關閉本身的內核子系統 MINIT->RINIT->RSHUTDOWN->MSHUTDOWN
還能夠參閱一下這篇文章
http://sebug.net/paper/pst_WebZine/pst_WebZine_0x05/0x07_淺談從PHP內核層面防範PHP_WebShell.html
以上就是google告訴咱們的PHP的腳本的執行步驟,可是在這裏小瀚以爲咱們能夠更深刻地學習一下,但願接下來能親自動手分析、調試一下源碼,從源碼的角度去分析一下這個過程,我在google上沒有找到相關的資料,若是有這方面源碼調試方面資料的朋友,但願能在留言中分享一下,不勝感激
2. PHP擴展學習
在學習了PHP內核的基本知識,咱們接下來能夠學習一下PHP的一個很重要的功能(或者叫架構): 擴展
關於PHP的擴展,PHP.NET官方給出了一個詳細的解釋
http://www.php.net/manual/zh/internals2.ze1.zendapi.php
paper中,我以爲最重要的是那張圖,它幫助咱們理解了PHP的總體架構、以及PHP和Zend的關係
Zend 和PHP的關係 Zend 指的是語言引擎,PHP 指的是咱們從外面看到的一套完整的系統。 爲了實現一個 WEB 腳本的解釋器,你須要完成如下三個部分的工做 1. 解釋器部分,負責對輸入代碼的分析、翻譯和執行: (zend: 100%) 1.1 Zend 構成了語言的核心 1.2 同時也包含了一些最基本的 PHP 預約義函數的實現 1.3 將PHP腳本代碼翻譯爲Token、以及將opcode翻譯爲對zend_function的調用 1.4 提供opcode的虛擬機運行時runtime 1.5 內存管理 也就是說,zend至關於真個整個程序的大架構,起核心做用 2. 功能性部分,負責具體實現語言的各類功能(好比它的函數等等): (zend: 50%、 php: 50%) 2.1 IO管理 2.2 流式輸入輸入Stream的實現 3. 接口部分,負責同 WEB 服務器的會話等功能: (php: 100%) 3.1 實現SAPI,封裝了PHP和上層WEB容器的通訊 3.2 包含了全部創造出語言自己各類顯著特性的模塊(也就是常說的module模塊) 3.2.1) xml 3.2.2) mysql模塊等 PHP的"不少"(並非所有)核心函數功能、代碼邏輯的實現都依靠擴展來實現 以上3個部分,他們合起來稱之爲 PHP 包
咱們能夠把PHP的擴展理解爲C編程中的庫(.dll、.so)(PHP的擴展本質上也是庫),在須要使用這些庫中的"導出函數"的時候,就在代碼中使用如下方式調用
HMODULE LoadLibrary(LPCTSTR lpFileName);
FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);
BOOL FreeLibrary(HMODULE hModule);
而PHP中的擴展在本質上也是這個意思,咱們在編譯出相應的庫文件(.dll、.so)文件後,可使用如下方式引入這個庫文件
1. 修改php.ini,增長 extension=lib_name.so 2. 在代碼中調用dl()函數,去動態地加載外部庫文件
接下來,咱們一塊兒來學習一下怎麼本身動手編譯一個最簡單的PHP擴展(代碼庫),而後調用PHP中實現的函數(導出函數)
編譯PHP擴展
google上關於PHP擴展的編譯的文章有不少,我在這裏儘量地給出一些實驗步驟,並分享出一些我在學習過程當中的學習資料
http://blog.csdn.net/heiyeshuwu/article/details/3453854
http://www.php.net/manual/zh/internals2.buildsys.configunix.php
http://www.php.net/manual/zh/internals2.buildsys.php
http://www.ccvita.com/496.html
http://blog.csdn.net/taft/article/details/596291
http://weizhifeng.net/write-php-extension-part1.html
1. 在PHP的源代碼目錄下生成一個新的文件夾: hello
咱們知道,擴展的框架生成可使用ext_skel這個腳本幫助咱們構建,可是爲了理解更加清楚理解各個代碼文件的意義、及它們之間的關係,咱們這裏所有用手工建立
這裏ext就是源碼目錄下的ext文件夾,咱們就是要在這裏新建咱們的擴展目錄,以後咱們會把擴展須要的文件都統一放在這個文件夾中
2. 新建配置文件: config.m4
config.m4 PHP_ARG_ENABLE(hello, whether to enable Hello World support, [ --enable-hello Enable Hello World support]) if test "$PHP_HELLO" = "yes"; then AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World]) PHP_NEW_EXTENSION(hello, hello.c, $ext_shared) fi
3. 新建擴展所須要用到的頭文件: php_hello.h
php_hello.h #ifndef PHP_HELLO_H #define PHP_HELLO_H 1 #define PHP_HELLO_WORLD_VERSION "1.0" #define PHP_HELLO_WORLD_EXTNAME "hello" PHP_FUNCTION(hello_world); extern zend_module_entry hello_module_entry; #define phpext_hello_ptr &hello_module_entry #endif
4. 新建擴展所實現功能的代碼邏輯的文件: hello.c
hello.c #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_hello.h" static function_entry hello_functions[] = { PHP_FE(hello_world, NULL) {NULL, NULL, NULL} }; zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_HELLO_WORLD_EXTNAME, hello_functions, NULL, NULL, NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_HELLO_WORLD_VERSION, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_HELLO ZEND_GET_MODULE(hello) #endif PHP_FUNCTION(hello_world) { //RETURN_STRING("Hello World", 1); char *arg = NULL; int arg_len, len; char *strg; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) { return; } len = spprintf(&strg, 0, "Your input string: %s/n", arg); RETURN_STRINGL(strg, len, 0); }
5. 編譯擴展源代碼
phpize
./configure --enable-hello
make
vim /etc/php.ini
增長這一條: extension=hello.so
cp modules/hello.so /usr/lib/php/modules/
service httpd restart
6. 測試新擴展的效果
7. 實驗成功
以上就是一個基本的PHP擴展的編譯過程,PHP的擴展是在zend的基礎上進行的,因此咱們在編寫源代碼的時候會涉及到大量的zend的宏調用,涉及到不少的數據結構,這一塊也是須要重點學習的,更多的信息能夠參考這篇文章
http://blog.csdn.net/hguisu/article/details/7414724
3. PHP API Hook技術
談到PHP API Hook技術,咱們就要回到我寫這篇文章的目的了"WebShell攻防對抗",咱們能夠很容易地想到如下幾點來進行WebShell動態檢測:
1) 對PHP中可能產生安全問題的函數進行Hook劫持,在代碼執行的以前,先運行咱們的"代碼安全檢測模塊",從而達到動態檢測的目的 2) 該怎麼實現API Hook的目的呢,我以爲有如下幾種思路 2.1) 利用zend自己提供的函數回調機制對代碼流進行Hook綁定 2.2) PHP中必定有一個數據結構存儲的是全部函數的入口地址的(相似windows內核中的IDT同樣),咱們找到這個"函數調用地址表",而後修改其中某個函數的入口指針,修改成咱們本身
定義的函數,這樣,本來的函數調用就會先執行咱們自定義的函數,而後咱們再把控制權交回本來在這個位置的函數,完成Hook的效果
以上幾點是咱們本身猜想的,具體的狀況還有待繼續深刻學習,PHP API Hook的研究將放到下一篇文章中繼續學習,這裏分享幾個這方面的連接
http://security.tencent.com/index.php/blog/msg/19 http://www.laruence.com/2012/02/18/2560.html https://wiki.php.net/rfc/taint http://www.php-internal.com/
Copyright (c) 2014 LittleHann All rights reserved