PHP基本架构原理及内核调试原理学习
一、PHP基本架构
以目前的 PHP 主流版本 PHP7 和 PHP5 来说架构是如上图所示,主要有四层体系构成,从下到上依次是
- Zend 引擎
- Extensions 扩展
- SAPI 接口
- 上层应用
0x1:Zend引擎
Zend 引擎是 PHP4 以后加入 PHP 的,是对原有 PHP 解释器的重写,整体使用 C 语言进行开发,也就是说可以把 PHP 理解成用 C 写的一个编程语言软件,引擎的作用是将 PHP 代码翻译为一种叫 opcode 的中间语言,它类似于 JAVA 的 ByteCode(字节码)。
引擎对 PHP 代码会执行四个步骤:
- 词法分析 Scanning(Lexing),将 PHP 代码转换为语言片段(Tokens)。
- 解析 Parsing, 将 Tokens 转换成简单而有意义的表达式。
- 编译 Compilation,将表达式编译成 Opcode。
- 执行 Execution,顺序执行 Opcode,每次一条,以实现 PHP 代码所表达的功能。
APC、Opchche 这些扩展可以将 Opcode 缓存以加速 PHP 应用的运行速度,使用它们就可以在请求再次来临时省略前三步。
引擎也实现了基本的数据结构、内存分配及管理,提供了相应的 API 方法供外部调用。
0x2:Extensions扩展
常见的内置函数、标准库都是通过 extension 来实现的,这些叫做 PHP 的核心扩展,用户也可以根据自己的要求安装 PHP 的扩展。
0x3:SAPI(Server Application Programming Interface)
SAPI 是 Server Application Programming Interface 的缩写,中文为服务端应用编程接口,它通过一系列钩子函数使得 PHP 可以和外围交换数据,SAPI 就是 PHP 和外部环境的代理器,它把外部环境抽象后,为内部的 PHP 提供一套固定的,统一的接口,使得 PHP 自身实现能够不受错综复杂的外部环境影响,保持一定的独立性。
通过 SAPI 的解耦,PHP 可以不再考虑如何针对不同应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式。
0x4:上层应用
程序员编写的 PHP 程序,无论是 Web 应用还是 Cli 方式运行的应用都是上层应用,PHP 程序员主要工作就是编写它们。
参考链接:
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/ https://learnku.com/articles/8395/an-overview-of-the-architecture-and-principles-of-what-php-is-php
二、PHP源码学习
PHP本质上都是一个C程序,但和我们传统意义上写的C程序不同的是,PHP具有很多强大的功能:
- 虚拟机的功能:和软件加密中的VMP概念类似,PHP的语法可以看成是一种虚拟指令集,我们程序员在编写PHP脚本的时候其实就是在使用这种虚拟指令集进行代码逻辑、功能调用的编程。同时PHP则提供了对这些虚拟指令的解析、翻译,并调用相应的底层代码去完成程序员的调用请求
- 内存管理:这里说的内存管理和操作系统层面的那个内存管理并不是一个概念。操作系统负责的是整个windows / linux 上全部应用的内存管理。而PHP的内存管理负责的是在每次请求的脚本的当前"运行空间(针对每个脚本而言都有自己的运行空间)”、PHP内核、以及扩展模块中的永久内存这些方面的内存管理,对于PHP来说,内存管理是一个非常底层、非常重要的部分。
- 服务器通信:PHP作为一个编译(这里的编译指的是编译为opcode的概念)、解释系统、同时提供虚拟运行时环境的C程序,它最重要的方面是能和服务器(WEB容器)进行通信,要注意的是,这里所说的"服务器"是一个概念上的意义,PHP对与上层"服务器"的通信进行了抽象,把所有的逻辑都抽象、封装到了SAPI(server application programming interface),对于上层的服务器来说,它们对PHP的调用就可以通过SAPI来进行,实现了"解耦和"。所有这些调用方式,对PHP来说本质上都是一样的,PHP把它们都看成是来自"服务器"的调用。这种架构方式提供了很美丽的解耦和方式,同时,为PHP在不同操作系统、不同服务器(WEB容器)上的跨平台操作提供了支持。常见的调用SAPI方式有:
- CGI:CGI即通用网关接口(Common Gateway Interface),它是一段程序, 通俗的讲CGI就象是一座桥,把网页和WEB服务器中的执行程序连接起来。每一个用户请求,都会先要创建CGI的子进程,然后处理请求,处理完后结束这个子进程,这就是Fork-And-Execute模式。所以用CGI方式的服务器有多少连接请求就会有多少CGI子进程,子进程反复加载是CGI性能低下的主要原因。
- Fastcgi模式:FAST-CGI 是CGI的升级版本,FastCGI 像是一个常驻(Long-Live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去Fork一次,解决了 CGI 最为人诟病的 Fork-And-Execute 模式
- CLI模式:CLI是PHP的命令行运行模式,php -f script.php
- 模块模式:以apache为例,模块模式是以mod_php模块的形式集成,此时mod_php模块的作用是接收Apache传递过来的PHP文件请求,并处理这些请求,然后将处理后的结果返回给Apache。如果我们在Apache启动前在其配置文件中配置好了PHP模块(mod_php5),PHP模块通过注册apache2的ap_hook_post_config挂钩,在Apache启动的时候启动此模块以接受PHP文件的请求。
我们可以从PHP的官网上下载到PHP的源代码:
http://cn2.php.net/downloads.php
解压后,我们先来看看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
以上的步骤是标准的步骤,我实际在安装的时候会遇到很多的问题,下面把我找到的这些相关问题的链接分享出来,希望对朋友有帮助
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代码对应的Opcode
对PHP脚本代码进行单步调试最好的工具就是xdebug了,它是一个PHP扩展,同时很多IDE都继承对PHP单步调试的支持,如:
- eclipse
- zend studio
- NetBeans
要注意的一点是,我们在使用xdebug的时候,调试的对象其实是PHP的中间语言:opcode,在开始学习PHP代码调试之前,先来了解一下PHP中的opcode。
0x1:PHP opcode是什么
类似于:
- C和汇编的关系
- C#和CIL(Common Intermeditate Language,中间语言)的关系
- java和JVM字节码的关系
PHP在执行的时候,会被Zend先翻译成opcode中间语言,然后再逐个opcode顺序执行,我们知道,PHP是构建在Zend VM(Zend虚拟机)之上的,所以这些opcode其实就是Zend VM中的指令(和VMP: virtual machine protect 的概念类似)。
既然opcode是一个指令系统,那么opcode就必然包含一个指令系统所必须的组成部分,包括:
- 指令集(各种操作符)
- 指令的格式和规范(由处理器指令规范规定)
- 操作数
- 显示的常量操作数
- 保存在寄存器中的操作数
- 保存在堆栈中的操作数
- 保存在内存中的操作数
- 保存在IO端口中的操作数
- 保存指令、及相关信息的数据结构(struct _zend_op)(PHP这类虚拟机语言特有的)
为了更好地理解opcode和PHP代码的关系,我们从一个脚本的完整执行流程来看看PHP是怎么对代码进行翻译、并执行的。
例如,我们在请求这段代码的执行的时候
<?php echo 1; ?>
PHP会做如下的事情:
- Zend启动引擎"词法"分析,将代码切分为一个个的标记Toekn,你可以用token_get_all()函数来查看PHP在这一步都做了什么
- 使用"语法"分析器(注意和词法做区分)(PHP使用bison生成语法分析器,规则见$PHP_SRC/Zend/zend_language_parser.y), bison根据规则进行相应的处理, 如果代码找不到匹配的规则,也就是语法错误时Zend引擎会停止,并输出错误信息,比如缺少括号,或者不符合语法规则的情况都会在这个环节检查
- Zend引擎对这些Token进行编译,将代码编译为opcode,并绑定相应的参数、和函数调用
- Zend引擎执行这些opcode,在执行opcode的过程中还有可能会继续重复进行编译-执行,例如执行eval,include/require等语句,因为这些语句还会包含或者执行其他文件或者字符串中的脚本
总的来说,我们的脚本代码最终会以opcode的形式在Zend RunTime中出现,我们知道,opcode本质上是一些"虚拟机指令",所以每一条opcode都对应着Zend中对应的一个函数调用,即"opcode-Zend_Function",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的虚拟指令是一样的。
参考链接:
https://netbeans.org/kb/docs/php/debugging_zh_CN.html http://www.zvv.cn/blog/show-101-1.html http://www.cnblogs.com/LittleHann/p/3344261.html 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/
0x2:如何查看PHP代码对应的opcode
使用vld(Vulcan Logic Dumper)这款php扩展,可以查看对应PHP代码的opcode
1、下载VLD源代码
http://pecl.php.net/package/vld
我下的是 0.12.0 版本,下载后,解压到一个文件夹中
2、编译源代码
phpize 生成makefile文件: ./configure --with-php-config=/usr/bin/php-config --enable-vld (这里的php-config路径可能会不一样,根据自己的机器上的情况修改) 编译完成后,复制.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服务
vim /etc/php.ini
在文件中加上 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代码
关于对底层C代码的单步调试,我觉得有以下几点需要理解:
- PHP在底层是用C实现的
- PHP本质上是一个C程序(php.exe),负责对脚本进行解释和执行
- 所有的PHP脚本代码,都会被翻译为opcode,并对应于某个zend_funtion,所有我们对这个相应的zend_function进行下断点,就可以针对执行的PHP脚本代码,单步调试它对应的底层C代码
结合我们写C程序的概念,我们需要单步调试一个程序,就需要编译出一个debug版本的exe程序。对于PHP的编译也是同样的道理。为了debug单步调试PHP,我们需要做如下准备工作:
- 编译个debug版本的PHP:编译出的可执行程序会比较大,附带了调试信息
- 用于单步调试PHP的IDE:这里选用NetBeans,eclipse也是可以的
0x1:编译一个PHP(Debug)
我们在文章的开始已经学习了怎么从源代码编译个debug版本的PHP,注意到:
./configure --enable-debug
这里的 "--enable-debug"是一个关键,只有这样编译出的PHP版本才是可以单步调试的
0x2:在NetBeans中创建用于PHP单步调试的工程
将我们进行编译的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的运行机制、语言的特性。
假设有一种场景,你编写了一个PHP脚本,其中遇到一个PHP的特性不是很理解,在使用vld翻译为opcode后,根据opcode去推测和寻找(有时候不一定能准确地找到)这个语法对应的zend_function,在指定的zend_function中下断点,从C的层面单步跟踪,帮助我们更好、更深入地理解问题的成因。
参考链接:
http://www.nowamagic.net/librarys/veda/detail/1285
五、从PHP内核层面,PHP脚本执行步骤
0x1:一切的开始: SAPI接口
命令行程序和Web程序类似,命令行参数传递给要执行的脚本,相当于通过url请求一个PHP页面.。
PHP脚本完成执行后返回响应结果,只不过命令行响应的结果是显示在终端上,而WEB服务器的返回结果回传给浏览器。
脚本执行的开始都是通过SAPI接口进行的。
0x2:MINIT -- 扩展初始化
当给定的SAPI启动时,例如在对/usr/local/apache/bin/apachectl start的响应中,PHP从初始化其内核子系统开始。
在接近启动例程的末尾,它加载"每个"(逐一调用)扩展模块的初始化例程(MINIT)。
这使得每个扩展可以初始化内部变量、分配资源、注册资源处理器、以及向ZE注册自己的函数,以便于脚本调用某个函数的时候这个函数的代码的入口地址在哪里。
0x3:RINIT -- 请求初始化处理
接下来,PHP等待SAPI层请求要处理的页面。
- 对于CGI或CLI这种类型的SAPI来说,这将立刻发生且只发生一次
- 对于Apache、IIS或其他WEB容器来说,每次远程用户请求页面(请求执行脚本)时都将发生,因此重复很多次,也可能并发。
不管请求如何产生,PHP要求ZE建立脚本的运行环境后再开始接受请求(PHP负责和WEB服务器的通信)。
在某个请求到达的时候,PHP会逐个调用"每个"扩展的请求初始化(RINIT)函数。
RINIT使得扩展有机会设定特定的环境变量、根据请求分配资源、执行其他任务(如审核)。
以session扩展为例:如果启用session.auto_start,RINIT将自动触发用户空间的session_start()函数以及"预组装"$_SESSION变量。
0x4:执行PHP代码
一旦请求被初始化了,ZE开始接管控制权,将PHP脚本翻译成符号(Token),最终形成操作码(Opcode)并逐步执行之。如果操作码中有涉及到扩展的调用,ZE将会把参数绑定到该函数,并且临时交出控制权直到函数运行结束。
0x5:RSHUTDOWN - 脚本结束
每一次的脚本运行结束后,PHP会逐一调用"每个"扩展的请求关闭(RSHUTDOWN)函数以执行最后的清理工作(如将session变量存入磁盘)。
接下来,ZE执行清理过程(垃圾收集),即有效地对之前的请求期间用到的每个变量执行unset()。
0x6:MSHUTDOWN -- SAPI关闭
一旦完成,PHP继续等待SAPI的其他文档请求、或者关闭信号(对于CGI和CLI等SAPI,没有"下一个请求",所以SAPI立刻开始关闭)。
关闭期间,PHP再次遍历每个扩展,调用其模块关闭(MSHUTDOWN)函数,并最终关闭自己的内核子系统
参考链接:
http://sebug.net/paper/pst_WebZine/pst_WebZine_0x05/0x07_浅谈从PHP内核层面防范PHP_WebShell.html
六、PHP扩展基本概念
0x1:PHP在PHP整体架构中的位置
关于PHP的扩展,PHP.NET官方给出了一个详细的解释
http://www.php.net/manual/zh/internals2.ze1.zendapi.php
我们可以把PHP的扩展理解为C编程中的库(.dll、.so)(PHP的扩展本质上也是库),在需要使用这些库中的"导出函数"的时候,就在代码中使用以下方式调用
HMODULE LoadLibrary(LPCTSTR lpFileName);
FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);
BOOL FreeLibrary(HMODULE hModule);
而PHP中的扩展在本质上也是这个意思,我们在编译出相应的库文件(.dll、.so)文件后,可以使用以下方式引入这个库文件
- 修改php.ini,增加 extension=lib_name.so
- 在代码中调用dl()函数,去动态地加载外部库文件;如果扩展模块已经注册了php内置函数,则在php代码中可以直接调用
0x2:编译PHP扩展
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、测试新扩展的效果
以上就是一个基本的PHP扩展的编译过程,PHP的扩展是在zend的基础上进行的,所以我们在编写源代码的时候会涉及到大量的zend的宏调用,涉及到很多的数据结构,这一块也是需要重点学习的。
参考链接:
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
七、基于PHP扩展技术实现PHP API Hook技术
谈到PHP API Hook技术,我们就要回到我写这篇文章的目的了"WebShell攻防对抗",我们可以很容易地想到以下几点来进行WebShell动态检测:
对PHP中可能产生安全问题的函数进行Hook劫持,在代码执行的之前,先运行我们的"代码安全检测模块",从而达到动态检测的目的
该怎么实现API Hook的目的呢,我觉得有以下几种思路
- 利用zend本身提供的函数回调机制对代码流进行Hook绑定
- PHP中一定有一个数据结构存储的是所有函数的入口地址的(类似windows内核中的IDT一样),我们找到这个"函数调用地址表",然后修改其中某个函数的入口指针,修改为我们自己定义的函数,这样,原本的函数调用就会先执行我们自定义的函数,然后我们再把控制权交回原本在这个位置的函数,完成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/