PHP內核源代碼、PHP Zend擴展、API Hook學習筆記

做爲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

相關文章
相關標籤/搜索