PHP代码加密原理探索

开始之前
以我理解解释下CISCN线上初赛一题非预期解法

先抛出来有扩展加密方式

此种加密原理是把源码通过des或aes等加密算法进行加密,当加密的源码执行时扩展会截获加密的代码并解密后交给zend执行。

<?php
$code = file_get_contents('待加密的PHP');
$code = base64_encode(openssl_encrypt($code, 'aes-128-cbc', '密钥', false, 'IV'));
echo "<?php eval(openssl_decrypt(base64_decode($code), 'aes-128-cbc', '密钥', false, 'IV'));";
将其化成流程图方式讲解。

由于其中加密的密钥用户是可以自己修改的(以php-beast为例子)

static uint8_t key[] = {

0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c,

};
所以题目的流程图如下

那么我们要怎么去解这题呢?

zsx学长发现了一个sdk.php文件是用SourceGuardian加密的。因此zsx学长先下载了一个SourceGuardian扩展,并且再引入了一个sg11的破解脚本密钥,
sg11目的是为了破解这个openssl_private_encrypt达到实现正真题目环境也就是SDK让它能够正常运行,然后再让它满足sha1($key) === $this->getHash()条件即可。

所以截图如下

sg11的破解脚本这里起到的作用为openssl_private_decrypt,然后
直接修改zend_is_identical的返回值,直接让他return 1,使得sha1($key) === $this->getHash()条件成立满足sdk.php条件,直接var_dump出flag

zend_is_identical为控制===返回值

前置知识
目前编程语言可以分为两大类:

第一类是像C/C++, .NET, Java之类的编译型语言, 它们的共性是: 运行之前必须对源代码进行编译,然后运行编译后的目标文件。

第二类比如:PHP, Javascript, Ruby, Python这些解释型语言, 他们都无需经过编译即可”运行”,虽然可以理解为直接运行,

但它们并不是真的直接就被能被机器理解, 机器只能理解机器语言,那这些语言是怎么被执行的呢, 一般这些语言都需要一个解释器, 由解释器来执行这些源码, 实际上这些语言还是会经过编译环节, 只不过它们一般会在运行的时候实时进行编译。

为了效率,并不是所有语言在每次执行的时候都会重新编译一遍, 比如PHP的各种opcode缓存扩展(如APC, xcache, eAccelerator等),比如Python会将编译的中间文件保存成pyc/pyo文件, 避免每次运行重新进行编译所带来的性能损失。

一种语言被称为编译类语言,一般是由于在程序执行之前有一个翻译的过程, 其中关键点是有一个形式上完全不同的等价程序生成。 而PHP之所以被称为解释类语言,就是因为并没有这样的一个程序生成, 它生成的是中间代码,这只是PHP的一种内部数据结构。

当一段PHP代码进入Zend虚拟机,它会被执行两步操作:编译和执行。 对于一个解释性语言来说,这是一个创造性的举动,但是,现在的实现并不彻底。 现在当PHP代码进入Zend虚拟机后,它虽然会被执行这两步操作,但是这两步操作对于一个常规的执行过程来说却是连续的,

也就是说它并没有转变成和Java这种编译型语言一样:生成一个中间文件存放编译后的结果。 如果每次执行这样的操作,对于PHP脚本的性能来说是一个极大的损失。 虽然有类似于APC,eAccelerator等缓存解决方案。但是其本质上是没有变化的,并且不能将两个步骤分离,各自发展壮大。

PHP语言作为脚本语言的一种,由于不需要进行编译,所以通常PHP程序的分发都是直接发布源代码。对于开源软件来说这并没有什么问题,但是对于一些商业代码却是一个不太好的消息,正因为如此,导致PHP界涌现出了不少加密产品。

加密的本质
本质上程序在运行时都是在执行机器码,而基于虚拟机的语言的加密通常也是加密到这个级别, 也就是说PHP加密后的程序在执行之前都会解密成opcode来执行。

PHP在执行之前有一个编译的环节,编译的结果是opcode,然后由Zend虚拟机执行, 从这里看如果只要将源代码加密,然后在执行之前将代码解密即可。

加密的目的就是为了防止轻易获取程序源码的一种手段,对于PHP来说, 将源码编译为opcode已经能达到目的了,因为PHP引擎最终都是需要执行opcode的。 虽然可以将加密进一步,但是如果需要修改Zend引擎,那么成本就有点大了,因为需要修改 Zend引擎了,而这是无法通过简单的扩展机制来实现了,所以解密的成本也会变的太大, 也就没有实际意义了。

opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定。
PHP中的opcode则属于前面介绍中的后者,PHP是构建在Zend虚拟机(Zend VM)之上的。PHP的opcode就是Zend虚拟机中的指令。

PHP源码加密/解密的初步探索
无扩展加密
此种加密方式不依赖其他扩展,可以虽然看上去无法阅读,但是可以直接执行。
原理基本上就是以eval(*_decode(…))为核心辅以各种字符串混淆和小技巧。不外乎以下几类

1.采用多种编码

base64_decode,urldecode,gzuncompress

源码

<?php
eval("echo 'hello by museljh';");
?>
<?php
$a=base64_encode("echo 'hello by museljh';");
eval(base64_decode($a));
?>
<?php
function phpencode($code) {
$code = str_replace(array('<?php','?>','<?PHP'),array('','',''),$code);
$encode = base64_encode(gzdeflate($code));// 开始编码
$encode = '<?php'."\neval(gzinflate(base64_decode("."'".$encode."'".")));n?>";
return $encode;
}
function phpdecode($code) {
$code = str_replace(array('<!?php','<?PHP',"eval(gzinflate(base64_decode('","')));",'?>'),array('','','','','',''),$code);
$decode = base64_decode($code);
$decode = @gzinflate($decode);
return $decode;
}

$a="echo 'hello by museljh';";
echo phpencode($a);
?>
结果
<?php
eval(gzinflate(base64_decode('S03OyFdQz0jNyclXSKpUyC0tTs3JylC3BgA=')));
?>

eval(base64_decode('S03OyFdQz0jNyclXSKpUyC0tTs3JylC3BgA='));
KMÎÈWPÏHÍÉÉWHªTÈ--NÍÉÊP·

(PHP 4 >= 4.0.4, PHP 5, PHP 7) gzdeflate — Deflate a string

gzdeflate ( string $data [, int $level = -1 [, int $encoding = ZLIB_ENCODING_RAW ]] ) : string

(PHP 4> = 4.0.4,PHP 5,PHP 7) gzinflate -

gzinflate ( string $data [, int $length = 0 ] ) : string

此函数会使收缩的字符串膨胀。
2.使用变量代替函数名称

比如定一串字符串变量通过从这串字符串中提取题目拼接而成

3.变量采用相似容易混淆或无法辨认的字符命名

如$O00OO0 $O00O0O $O0OO00甚至使用ASCII码作为变量名

4.反劫持

加密后增加判断当前文件MD5和原始加密文件的MD5是否相同的逻辑,防止尝试通过美化代码破解文件的操作

5.字符替换
通过随机秘钥替换字符达到混淆的目的

6.多层加密
通过多层的编码混淆增加破解复杂度

优点:不用安装额外扩展,可以直接运行

缺点:加密效果比较差,因为代码最终都是通过eval执行,如果劫持eval可以100%还原。所以一般不推荐此种方法。

phpjiami、zhaoyuanma的免费版本等

比如 phpjiami加密源码的整个流程是:

加密流程:源码 -> 加密处理(压缩,替换,BASE64,转义)-> 安全处理(验证文件 MD5 值,限制 IP、限域名、限时间、防破解、防命令行调试)-> 加密程序成品,
再简单的说:源码 + 加密外壳 == 加密程序

加密处理无非是多次的压缩处理,转换加密我见过某变态程序对源码不集的加密转换,足足进行了50次操作。 要破解的人失去耐心。

MD5的方式,此方式较复杂。一般会将一段加密后的代码,判断MD5值,写入到PHP中。程序运行的时间读取这一段MD5值,并判断。如果值不相同则停止运行。

有的人想当然地认为修改eval为echo就能输出源码了,但实际上是不可以的因为他会校验其的MD5值。

限IP地址,某一些程序在某一些IP 上是不可运行的。某网站提供破解服务,这个网站的ip地址为:42.121.57.XX,在程序中发现这个IP黑名单则直接拒绝执行。

大概画风如这样(以phpjiami为例子)

<?php
œèÒ9œèÒ9œèÒ9œèÒ9œèÒ9;
œèÒ9œèÒ9œèÒ9œèÒ9;
function a(一堆乱码){

b=乱码解密;
....

}
œèÒ9œèÒ9œèÒ9œèÒ9;
œèÒ9œèÒ9œèÒ9œèÒ9;
function b(b,...){

    由乱码解密构成一堆全局变量 
‡¾Üö´=eval;
œèÒ9æ8î=base64;
xxxx

}
œèÒ9œèÒ9œèÒ9;œèÒ9
œèÒ9œèÒ9œèÒ9;
function c (){

安全处理

}
œèÒ9œèÒ9œèÒ9œèÒ9;
最后 ‡¾Üö´(œèÒ9æ8î(xxx(™¡óšÝï($§‡¾Üö´Õ°¾ãÑ('Ä4œèÒ9æ8î²4 ª78Øè6ŽDÔðìê´ŽÌ´òئèØÐ’ÌŽ3ÊÔ6ð”’Ô8–7ΰ¢26ŒÂÂâ¤0ô¨0¨Î2®â¤°7œ8ÄÈòØäª8جŠÜÊÒè5B¨......)));
?>
如何进行解密?

这里我将提供两种我已知的思路进行讲解

1.调用 eval 等代码执行的函数,最终会调用 php 内核的zend_compile_string函数。

为什么最终会调用zend_compile_string函数呢?

在中间代码的执行的时候,会经过zend_execute(EG(active_op_array) TSRMLS_CC);

如果你是使用VS查看源码的话,将光标移到zend_execute并直接按F12, 你会发现zend_execute的定义跳转到了一个指针函数的声明(Zend/zend_execute_API.c)。

ZEND_API void (zend_execute)(zend_op_array op_array TSRMLS_DC);

这是一个全局的函数指针,它的作用就是执行PHP代码文件解析完的转成的zend_op_array。

在zend_execute函数指针赋值时,还有PHP的中间代码编译函数zend_compile_file(文件形式)和zend_compile_string(字符串形式)。

所以呢,我们只用Hook住这个函数,就差不多了/其实貌似也可以直接修改zend_compile_file进行输出。

比如说

原来是这样

eval('echo 1;');

最后得出结果为1 ,

但是eval换成var_dump

var_dump('echo 1;');

最后结果就是

string(7) "echo 1;"

这样就可以获得源码了。

2.PHP在执行之前有一个编译的环节,编译的结果是opcode,然后由Zend虚拟机执行, 从这里看如果只要将源代码加密,然后在执行之前将代码解密即可。

3.手工dump法

4.动态调试法

5.代码审计Getshell

<?php
include "index.php";
var_dump(get_defined_vars());
从这里看,只要代码能被解密为opcode,那么总有可能反编译出来源代码, 其他的语言中也是类似,比如objdump程序能将二进制程序反汇编出来, .NET、Java的程序也是一样,都有一些反编译的程序,不过通常这些厂商同时还会 附带代码混淆的工具,经过混淆的代码可读性极差,很多人都留意过Gmail等网站 经过混淆的JS代码吧,他们阅读起来非常困难,经过混淆的代码即使反编译出来, 读者也很难通过代码分析出代码中的逻辑,这样也就极大的增加了应用的安全性。

简化的代码示例:

<?php
$code = file_get_contents('待加密的PHP');
$code = base64_encode(openssl_encrypt($code, 'aes-128-cbc', '密钥', false, 'IV'));
echo "<?php eval(openssl_decrypt(base64_decode($code), 'aes-128-cbc', '密钥', false, 'IV'));";

对于第一种思路,我们不需要知道数据的加密算法到底是什么,因为真实代码在执行时总会被解密出来,各位只需要知道PHP到底执行了什么,从这儿拿出代码。

不管是eval、assert、preg_replace('//e'),还是这类PHP加密扩展,想要动态执行代码就必须经过zend_compile_string这一个函数。只需要编写一个dll/so,给zend_compile_string挂上“钩子”,就能直接拿到完整的代码。

当然类似这样原理的加密在网上已经有很多非常成熟的在线解密方式。UnPHP,当然我们主要是介绍如何如何hook住zend_compile_string
理论上,只要我们在php内核执行eval函数的时候,将其dump出来,就可以得到源代码。在 php 扩展中, module init 的时候替换掉zend_compile_string,主要代码如下(tool.lu的站长xiaozi的代码),在php.ini中添加extension=hookeval.so,然后直接访问加密过的php代码即可

主要代码(全部代码 evalhook

static zend_op_array edump_compile_string(zval source_string, char *filename TSRMLS_DC)
{

int c, len;
char *copy;

if (Z_TYPE_P(source_string) != IS_STRING) {
    return orig_compile_string(source_string, filename TSRMLS_CC);
}

len  = Z_STRLEN_P(source_string);
copy = estrndup(Z_STRVAL_P(source_string), len);
if (len > strlen(copy)) {
    for (c=0; c<len; c++) if (copy[c] == 0) copy[c] == '?';
}

php_printf("----- [tool.lu start] -----\n");
php_printf("%s\n", copy);
php_printf("----- [tool.lu end] -----\n");

yes = 1;

return orig_compile_string(source_string, filename TSRMLS_CC);

}

PHP_MINIT_FUNCTION(edump)
{

if (edump_hooked == 0) {
    edump_hooked = 1;
    orig_compile_string = zend_compile_string;
    zend_compile_string = edump_compile_string;
}
return SUCCESS;

}
想要自己写出如上代码我们首先需要知道PHP是如何解析一个PHP文件的。
依旧是这个图

即:词法分析 => 语法分析 => opcode编译 => 执行

1.PHP的词法分析和语法分析的实现分别位于Zend目录下的zend_language_scanner.l和 zend_language_parser.y 文件,使用r2ec&flex来编译。

分析下语句

1.static zend_op_array edump_compile_string(zval source_string, char *filename TSRMLS_DC)

在PHP中,函数分为俩种,

1.一种是zend_internal_function, 这种函数是由扩展或者Zend/PHP内核提供的,用’C/C++’编写的,可以直接执行的函数。

2.另外一种是zend_user_function, 这种函数呢,就是我们经常在见的,用户在PHP脚本中定义的函数,这种函数最终会被ZE翻译成opcode array来执行

首先在zend_compile.h可以看到如下结构

1.typedef struct _zend_internal_function {

2.struct _zend_op_array {

3.typedef union _zend_function {

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_uint *refcount;

zend_op *opcodes;
zend_uint last, size;

zend_compiled_variable *vars;
int last_var, size_var;

zend_uint T;

zend_brk_cont_element *brk_cont_array;
zend_uint last_brk_cont;
zend_uint current_brk_cont;

zend_try_catch_element *try_catch_array;
int last_try_catch;

/* static variables support */
HashTable *static_variables;

zend_op *start_op;
int backpatch_count;

zend_bool done_pass_two;
zend_bool uses_this;

char *filename;

 zend_uint line_start;
zend_uint line_end;
char *doc_comment;
zend_uint doc_comment_len;

void *reserved[ZEND_MAX_RESERVED_RESOURCES];

};
2.

PHP_MINIT_FUNCTION(edump)
{

if (edump_hooked == 0) {
    edump_hooked = 1;
    orig_compile_string = zend_compile_string;
    zend_compile_string = edump_compile_string;
}
return SUCCESS;

}
PHP开始执行以后会经过两个主要的阶段:

处理请求之前的开始阶段和请求之后的结束阶段。

开始阶段有两个过程:第一个过程是模块初始化阶段(MINIT),

在整个SAPI生命周期内(例如Apache启动以后的整个生命周期内或者命令行程序整个执行过程中), 该过程只进行一次。第二个过程是模块激活阶段(RINIT),该过程发生在请求阶段,

例如通过url请求某个页面,则在每次请求之前都会进行模块激活(RINIT请求开始)。 例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的MINIT函数。 模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等等。 模块在实现时可以通过如下宏来实现这些回调函数:

PHP_MINIT_FUNCTION(myphpextension)
{

// 注册常量或者类等初始化操作
return SUCCESS; 

}
如何自己写一个类似的PHP解密扩展呢?

这里主要原理我觉得应该是先抄个var_dump函数,最后将这整个函数传给
PHP_MINIT_FUNCTION,其中把
zend_compile_string换成这个函数就可以了

实验将代码eval('echo 1;');输出的结果从1 变成string(7) "echo 1;"

有扩展的加密
此种加密原理是把源码通过des或aes等加密算法进行加密,当加密的源码执行时扩展会截获加密的代码并解密后交给zend执行。

优点:1.相比较免扩展加密更加安全,如果只是源码被窃取没有扩展与加密的key也是无法被破解的。 2.客户从目标机上down下来代码+beast.so扩展,因为绑定MAC地址的缘故,也是无法正常启动php-fpm的。

缺点:需要安装扩展才能使用,扩展可能会被破解拿到密钥。在zend层也可能会被劫持解密的源码。

php-beast、php_screw、screw_plus、ZoeeyGuard、tonyenc等市面上几乎所有的开源PHP加密扩展。

有扩展加密中,php_screw因加密方式太弱,容易被已知明文攻击(举例:大部分PHP文件的开头均为<?php)推测出密钥。其他的加密就都需要手动逆向,过于麻烦,直接使用通用方案来反而是更简单的破解方式。

转换为代码形式如下。

<?php
$code = file_get_contents('待加密的PHP');
$code = base64_encode(openssl_encrypt($code, 'aes-128-cbc', '密钥', false, 'IV'));
echo "<?php eval(openssl_decrypt(base64_decode($code), 'aes-128-cbc', '密钥', false, 'IV'));";
root@iZwz9hyvb5rjm3oubt8o15Z:~# cd things/php-7.2.0/ext/
root@iZwz9hyvb5rjm3oubt8o15Z:~/things/php-7.2.0/ext# ./ext_skel --extname=php_museljh
Creating directory php_museljh
Creating basic files: config.m4 config.w32 .gitignore php_museljh.c php_php_museljh.h CREDITS EXPERIMENTAL tests/001.phpt php_museljh.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/php_museljh/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-php_museljh
5.  $ make
6.  $ ./sapi/cli/php -f ext/php_museljh/php_museljh.php
7.  $ vi ext/php_museljh/php_museljh.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/php_museljh/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.

root@iZwz9hyvb5rjm3oubt8o15Z:~/things/php-7.2.0/ext#

扩展加密Opcodes(近似加密)
此种加密原理是先编译成opcode再压缩执行.

优点:是三种加密方式中最安全的方式,理论上是无法被破解得到源码的。

缺点: 不是绝对安全,可以通过OPCODE逆向转回PHP原代码,好的逆向效果在98%以上。

Zend Guard、老版本ionCube和部分配置下的Swoole Compiler

swoole complier是对编译以后的opcode作了手脚,也就是zend引擎在执行opcode之前需要完成解密的,或者是在执行过程中动态解密

混淆加密
无扩展虚拟机加密
Zend虚拟机
虚拟机(Virtual Machine),在计算机科学中的体系结构里,是指一种特殊的软件, 他可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于这个软件所创建的环境来操作软件。 在计算机科学中,虚拟机是指可以像真实机器一样运行程序的计算机的软件实现。虚拟机是一种抽象的计算机,它有自己的指令集,有自己的内存管理体系。

Zend引擎的核心文件都在$PHP_SRC/Zend/目录下面。不过最为核心的文件只有如下几个:

1.PHP语法实现

Zend/zend_language_scanner.l

Zend/zend_language_parser.y

2.Opcode编译

Zend/zend_compile.c

3.执行引擎

Zend/zend_vm_*

Zend/zend_execute.c

Leave a Comment