七零部落格
思则大道至简,疑则谜团重重!
思则大道至简,疑则谜团重重!
现在常见的对PHP代码进行加密的方式主要分为两大类:
? 不需要加载php扩展的;
? 需要加载php扩展的
第一种方式使用方便,不需要对php服务器进行配置,或加载其他模块,因此可以方便地部署在租用空间的服务器上。对文件进行加密也很简单,只需要把php文件上传到加密网站,就可以生成加密之后的文件,替换原来的php文件之后就可以正常运行了。
第二种方式使用起来相对复杂一些, php服务器上需要加载额外的模块。对于租用空间,没有管理员权限的用户来说,这是一个问题。
无论哪种方式,要分析它的加解密过程,我们都需要简单了解一下php文件的执行过程。
下面的分析都是基于PHP5.4.15的代码,运行在Apache2.4之上。
1、 PHP的执行过程
简单来说,PHP文件大致按照下图的流程进行执行:
图1
php文件作为参数传入Zend\zend.c文件中第1283行的zend_execute_scripts函数,这个函数会首先调用Zend\zend_language_scanner.c文件中第720行的compile_string函数,把文件的内容编译成一个zend_op_array对象,这个对象记录了php文件编译之后生成的中间代码。然后,zend_op_array会被作为参数传入Zend\zend_vm_execute.h文件中第342行的execute函数进行执行。
需要说明的几点:
l zend_execute_scripts中实际调用的是zend_compile_string函数,而不是compile_string函数。是在Zend\zend.c文件中第640行的zend_startup函数中进行初始化时,把compile_string的函数指针赋值给了zend_compile_string。
l 同样的,zend_execute_scripts实际调用的是zend_execute,是在zend_startup中,把execute的函数指针赋值给了zend_execute。
l execute函数按照zend_op_array进行执行,各个op实际对应的处理函数(handler)是在Zend\zend_vm_execute.h的第36481行的zend_init_opcodes_handlers函数中初始化变量zend_opcode_handlers时设定的。
2、 不需要加载php扩展的加解密方式的分析
通过分析,我们得出以下显而易见的一些结论:
l 因为没有加载php扩展,因此加密之后的文件的执行过程仍然完全遵循图1中的流程。因此,加密之后的文件本身应该是一个完全符合php语法要求的,可以被php编译器正确解析,并可以被php vm正确执行的文件。
l 那么,解密的过程只能是在这个加密之后的文件的执行过程中进行的,也就是说,这个加密之后的php文件是一个可以自解密的php文件,这个文件中包含了所有解密过程中需要用到的信息。因此,拥有了这个加密之后的文件,其实也同时拥有了解密所需的所有信息:解密的方法、和解密过程中所需的数据。
l 无论解密过程如何,最后一定是解密出原来的明文内容,然后进行执行的,否则就不符合图1的执行流程了。
现在,我们可以思考如何进行解密了:
l 一种方式,模拟执行这个加密的php文件,因为这个文件是自解密的,因此,最后一定可以得到明文。
这种方式是最容易想到的,但比较难于执行,并且这个文件中可能包含多个执行陷阱,扰乱模拟执行的过程。
l 另一种方式,直接在PHP环境中真实地执行这个加密的php文件,在php的内部得到解密之后的明文。
在php中,把一段文本作为代码进行执行,我知道的有以下几种方式:
u 把文本保存到另一个文件,include这个文件进行。
u 使用eval函数,直接执行这段文本。
参考这个eval的处理函数,Zend\zend_vm_execute.h中2529行的ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER函数,和Zend\zend_execute_API.c中第1156行的zend_eval_string1函数。
u 使用包含执行功能的其它函数。
比如,preg_replace函数,在pattern中指定e(PREG_REPLACE_EVAL)作为参数,就会先执行参数中的文本,然后执行替换(这个参数在php5.5将不再支持)。参考preg_replace函数的处理函数,ext\pcre\php-pcre.c中第1003行的php_pcre_replace_impl函数,和第897行preg_do_eval函数。
为了看起来更安全,很多进行加密的都不会选择include方式,而会选择后两种。其实三种方式的安全性是一样的,没有任何差别。因为无论哪种方式,都遵循图1中的执行流程。
那么,我们在图1中的Zend\zend_language_scanner.c中的compile_string函数中增加一个输出,把所有需要编译文本输出出来,其中一定有解密之后的明文。
一些问题:
l 为什么不使用include/require?
因为使用include方式,需要生成一个包含明文的文件,可能加密厂商认为这样不安全,怕别人找到这个文件,所以大部分不使用这种方式。
l 为什么不拆分明文?
我几乎没有看到先拆分明文,再分别进行加密的。具体原因不太清楚,我猜测的原因是,php不是一个强制格式化的语言,因此php文件内的格式是很复杂的,如果简单的按照文本分割,很容易导致其中的某一部分无法正确运行。
如果按照语句来分割,首先必须对文件进行词法、语法的分析,这个比较复杂和麻烦,并且还可能需要重新组织文件结构(比如一个很大的函数,怎么分割),有点得不偿失,所以加密厂商没有先拆分。
l 为什么无法混淆明文中的变量名、函数名?
与拆分明文那个问题相似,要对原始明文中的变量名、函数名进行混淆,就必须对文件进行词法、语法的分析,在此基础上还要进行语义分析,建立变量表和函数表,这个复杂度更高。
另外,如果是跨几个文件的变量,或者被其它文件中调用的函数,要混淆变量名、函数名,就必须和其它文件一起分析,一起加密。这样既增加复杂度,也使加密的使用上更加麻烦,不能像现在这样,一个一个文件单独加密了。
l 为什么加密之后的文本是乱码?
如果使用文本编辑器打开加密之后的文件,会看到一些文本字符,而同时还有大量的无法正常显示的乱码,这是为什么呢?这些乱码主要包括两部分:
u 故意使用的在纯英文和中文环境下无法正确显示字符,比如,一些常量的名字,变量的名字等。
PHP在解析一个文件时,是完全按照单字节码读取的,不会去分析双字节码,而我们的文本编辑器则会按照双字节码进行分析和显示。比如,如下用16进制表示的字符串:
27 B9 33 36 8C 27
这个对于PHP来说,就是一个用单引号(0x27)括起来的字符串。而B9和8C如果按照双字节码分析,应该和后边的另一个双字节码组成一个字符,而它们的后边那个字节并不是一个双字节码,因此无法正确显示出来。尽管无法显示,但对于PHP来说,就是一个普通的字符串。
u 加密之后的内容不是可以显示的字符。
l 为什么看不到解密过程中的用到的函数名?
为了掩盖运行中自我解密的过程,大部分加密厂商都通过某种方式对解密过程中需要用得函数名进行了编码或加密,有一些只是简单的使用base64进行了编码,有些则使用了复杂些的算法。
从解密的角度看,复杂度没有变化。
3、 需要加载PHP扩展的加解密
这种方式的具体实现机制差别很大,因为PHP扩展给了解密很大的自由度,也就给了加密很大的可能性。但简单划分的话,也可以分为两类:
l 使用Zend\zend_language_scanner.c中的compile_string函数进行编译php文件的;
l 和不使用这个函数进行编译的
对于仍然使用Zend\zend_language_scanner.c中的compile_string函数进行编译的,基本分析方法与不需要加载php扩展的加解密方式相似,就不多说了。
对于不使用compile_string这个函数的这种方式,我们以Zend Guard为例来分析加解密的机制。
准确来说,Zend Guard并没有对PHP代码进行加密,它只是对php代码进行了编译、优化和混淆。它的加密过程执行了以下操作:
a) 首先对php明文文件进行编译,生成zend_op_array对象。这一步操作类似于Zend\zend_language_scanner.c中的compile_string函数的功能。
b) 对代码进行优化。
c) 因为进行了编译,生成了变量表和函数表,因此,可以简单的对变量名、函数名进行混淆。
d) 把处理之后的zend_op_array对象序列化到一个文件,这就是加密之后的文件。
需要说明的是,如果多个文件之间有函数调用、公共变量,这些文件必须一起用Zend Guard进行加密。
在执行的时候,需要把Zend Loader作为php扩展进行加载。这些加密之后的文件的执行流程与图1中的流程基本一致,唯一的差别就是,Zend\zend.c中的zend_execute_scripts调用的不再是Zend\zend_language_scanner.c中的compile_string函数,而是Zend Loader中的一个函数。这个函数从加密之后的文件中反序列化zend_op_array,然后返回这个zend_op_array给zend_execute_scripts,继续下边的执行。
因此,解密Zend Guard,其实就是按照zend_op_array生成代码的过程。这个过程很复杂吗?不太确定。不同的中间代码设计思路不同,由中间代码生成源代码的难度差别也很大。如果我们把机器码也作为中间代码来看待,由机器码生成源代码难度最大;Java、.NET的中间代码相对来说接近机器码,但比机器码更加接近逻辑结构,因此难度相对较小,但仍有较大难度。
Php的中间码我没有仔细分析,无法评估难度大小。有兴趣的可以参考Zend\zend_compile.h中255行的_zend_op_array结构,以及相关的其它结构体。
4、 PHP代码的编译
要进行PHP代码的解密,掌握PHP代码的编译方法是必须的,我是在Windows7下进行的编译,参考的是https://wiki.php.net/internals/windows/stepbystepbuild中介绍的步骤。
因为我是用XAMPP 1.8.1搭建的环境,这个里边的PHP加载了所有模块,所以自己编译的时候,也必须加载所有模块,否则或者xampp运行不起来,或者就需要自己修改配制文件。我使用的build步骤如下:
windows sdk 6.1 shell
setenv /x86 /xp /release
cd c:\php-sdk\
bin\phpsdk_setvars.bat
bin\phpsdk_buildtree.bat php5.4.15
cd C:\php-sdk\php5.4.15\vc9\x86\php-5.4.15-src
buildconf
configure --enable-pdo
nmake
这样生成的是thread-safety的build。如果要加载Zend Loader,必须使用non-thread-safety的build,所以configure改成如下:
configure --enable-pdo --disable-zts
5、 遇到的一些问题
l 在apache2.4上无法加载Zend Loader6.0
这是一个比较绕的问题,Zend Loader要求PHP必须是non-thread-safety的,而apache2.4 module必须是thread-safety才能编译。因此,如果要加载Zend Loader,PHP就不能以apache module模式运行,而只能是CGI模式。
这个模式的切换是在C:\xampp\apache\conf\extra\httpd-xampp.conf文件中配制的。打开这个文件,注释掉类似于LoadModule php5_module的这一行,以及上下相关几行。把PHP-CGI setup里边的那几行的注释去掉就可以了。