PHP解密:EnPHP mzphp2加密 无法还原变量名的混淆加密
本帖最后由 Ganlv 于 2019-7-13 14:09 编辑## 问题引出
[原求助帖](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=770762&pid=24232635)
## EnPHP
(https://github.com/djunny/enphp) 官方已开源
## 分析过程
### 简单分析一下原理
我使用 VSCode 打开这个文件,这个文件全都是不可读字符,如果用 UTF-8 来显示的话很不友好。
使用 Ctrl + Shift + P 打开快捷指令,输入 `encoding`,选择用 Change File Encoding,选择 Reopen with Encoding,选择 Western (Windows 1252)。
> Windows 1252 是个单字节的字节集,不会出现任何 2 个字节被显示成 1 个字符的问题,其他的单字节集通常也可以。
我们只看代码部分,不看乱码部分。
```php
error_reporting(E_ALL ^ E_NOTICE);
define('字符串1', '字符串2');
一堆乱码1;
$GLOBALS[字符串1] = explode('字符串3', gzinflate(substr('字符串4', 0x0a, -8)));
一堆乱码2;
include $GLOBALS{字符串1};
include $GLOBALS{字符串1}{0x001}(__FILE__) . $GLOBALS{字符串1};
```
解释一下我们分析出来的代码的含义
1. 抑制错误显示
2. 定义一个全局常量作为被加密字符串储存的名称
3. 一个不知道什么常量,毫无意义
4. `gzinflate` 就是 gzip 解压缩,把一个二进制的字节串还原成原始字符串,并用 `explode` 分成一堆小字符串。
5. 一个不知道什么常量,毫无意义
6. `$GLOBALS{字符串1}` 就是那一堆小字符串的储存位置,从中提取出第一个元素 `$GLOBALS{字符串1}` 就是我们要还原的内容了。
### PHP-Parser
既然是乱码,我们又得请出我们的重量级选手了 (https://github.com/nikic/php-parser)
> 这个库的作者是 nikic,其实他是 PHP 核心开发组的人员,这个解释器真的堪称完美。
新建一个文件夹作为这个工程的文件夹
创建 Composer 文件,安装 PHP-Parser
```bash
composer init
composer require nikic/php-parser
```
然后新建一个 `index.php` 先把 AST 解析写好。
> 这个初始代码来自 <https://github.com/nikic/php-parser#quick-start>
> 看乱码我用 VSCode,但是写代码我还是选择 PHPStorm。
```php
<?php
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
require 'vendor/autoload.php';
$code = file_get_contents(__DIR__ . '/tests/assets/admin.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse($code);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return;
}
$dumper = new NodeDumper;
echo $dumper->dump($ast) . "\n";
```
### 找出有用的参数
我们必须从 AST 中把有用信息提取出来。
什么是有用的呢?
就是上面的字符串 1、3、4,不包括字符串 2,因为代码中根本就没用到,他只是一个临时的变量名称。还有 `substr` 的参数 `0x0a` 和 `-8`。
我们根据他在 AST 中的位置编写代码
```php
$str1 = $ast->expr->args->value->value;
$str3 = $ast->expr->expr->args->value->value;
$str4 = $ast->expr->expr->args->value->args->value->args->value->value;
$int1 = $ast->expr->expr->args->value->args->value->args->value->value;
$int2 = -$ast->expr->expr->args->value->args->value->args->value->expr->value;
```
如何知道他的位置?
> 调试必须得配置好 XDebug,配置过程请自行百度。
然后就可以得到 `$ast->expr->args->value->value` 这个了。
### 先看看解密之后的字符串是什么样子的
在原来的代码之后添加
```php
$string_array = explode($str3, gzinflate(substr($str4, $int1, $int2)));
print_r($string_array);
```
再次调试,看调试输出
```plain
Array
(
=> config.php
=> dirname
=> /../include/class.db.php
=> filter_has_var
=> type
=> json_encode
=> success
=> icon
=> m
=> 请勿非法调用!
=> filter_input
......
=> id错误,没有找到id!
=> ua
=> Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
=> curl
)
```
的确不出所料,我们要的字符串都出来了。
### 逐步还原
我们需要把代码中所有的 `$GLOBALS{字符串1}`,都换成原来的字符串。
我们需要用到 NodeTraverser 了,他负责遍历 AST 的每一个节点
当他发现任何一个 Node 是下面这种结构的时候
```plain
Expr_ArrayDimFetch(
var: Expr_ArrayDimFetch(
var: Expr_Variable(
name: GLOBALS
)
dim: Expr_ConstFetch(
name: Name(
parts: array(
0: �
)
)
)
)
dim: Scalar_LNumber(
value: 0
)
)
```
他将直接把这个 Node 替换成
```plain
Scalar_String(
value: $string_array
)
```
```php
class GlobalStringNodeVisitor extends NodeVisitorAbstract
{
protected $globalVariableName;
protected $stringArray;
public function __construct($globals_name, $string_array)
{
$this->globalVariableName = $globals_name;
$this->stringArray = $string_array;
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayDimFetch
&& $node->var instanceof Node\Expr\ArrayDimFetch
&& $node->var->var instanceof Node\Expr\Variable
&& $node->var->var->name === 'GLOBALS'
&& $node->var->dim instanceof Node\Expr\ConstFetch
&& $node->var->dim->name instanceof Node\Name
&& $node->var->dim->name->parts === $this->globalVariableName
&& $node->dim instanceof Node\Scalar\LNumber
) {
return new Node\Scalar\String_($this->stringArray[$node->dim->value]);
}
return null;
}
}
$nodeVisitor = new GlobalStringNodeVisitor($str1, $string_array);
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
$prettyPrinter = new Standard;
echo $prettyPrinter->prettyPrintFile($ast);
```
运行结果
### 美化代码
我们看到 `('dirname')(__FILE__)` 这种代码不太符合正常代码书写习惯,我们需要把它改成 `dirname(__FILE__)`。
```php
class BeautifyNodeVisitor extends NodeVisitorAbstract
{
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\FuncCall
&& $node->name instanceof Node\Scalar\String_) {
$node->name = new Node\Name($node->name->value);
}
return null;
}
}
$nodeVisitor = new BeautifyNodeVisitor();
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
```
运行结果
### 函数内部字符串
前面全局部分的代码看上去还不错,但是后面的函数内部代码还是有些乱码
它使用一个 `$局部变量1 =& $GLOBALS[字符串1]` 把这个全局变量变成局部变量了,我们必须遍历所有函数,把这些字符串替换掉。
原理就是发现 `$局部变量1 =& $GLOBALS[字符串1]` 则把 `局部变量1` 保存下来,之后再发现 `$局部变量1` 则替换成 `$string_array`。
(代码较长,此处省略)
运行结果
### 函数局部变量名
这里的原理就是把所有参数名统一替换成 `$arg0`, `$arg1`,所有变量名统一替换成 `$v0`, `$v1`。
(代码较长,此处省略)
### 类的方法
由于样例文件中没有包含类的方法,所以对类方法变量名的去除乱码可能并不是很好。
### 去除无用常量语句
这个代码里面有一堆无用的调用常量的语句,完全不知道是干什么的,毫无意义,去掉。
(代码较长,此处省略)
### 自动寻找全局字符串变量
```php
$str1 = $ast->expr->args->value->value;
$str3 = $ast->expr->expr->args->value->value;
$str4 = $ast->expr->expr->args->value->args->value->args->value->value;
$int1 = $ast->expr->expr->args->value->args->value->args->value->value;
$int2 = -$ast->expr->expr->args->value->args->value->args->value->expr->value;
```
这个代码用的是固定的 `$ast`, `$ast`,但是实际上他并不一定总是 `1` 或 `3`,所以我们做得适用性强一些。自动寻找
```php
$GLOBALS = explode('delimiter', gzinflate(substr('data', start, -length)))
```
这个句话的位置。
### 结果对比
## 总结
只有局部变量的变量名不能还原。其他所有的标识符(函数名、类名、方法名、函数调用、常量名)、字符串、数字全部都能成功还原。代码结构完全没有加密,只需替换被混淆的名称即可。
使用这种方法保护 php 代码并不是一种明智的选择,代码中能看到的乱码都是 gzdeflate 引起的,这个是公开的算法,通过简单的 debug 方法即可得到各种字符串常量的内容,加以修改也不是什么难事,并不建议大家使用这种方式保护自己的代码。
这个加密并不是特别难,算上截图和编写这篇文章,编写全自动解密脚本,我总共大概用了 5 个小时。和 mfenc 那种东西差远了,mfenc 光分析原理就用了 4 天时间,一周才能初步解出来,一两个月才能搞出来像样的全自动反编译器。对于一个认真学习过 php 编程的人来说,这个 enphp 大概一个小时就能恢复到能看能用的代码。
其实,使用这种混淆变量名的方式来“加密” php 代码并不难。使用 PHP-Parser 这个现成的 AST 库,字符串常量提取,变量名替换,编写一个功能完全一致的东西可能只需要一天时间,请不必将其当做一种高深的技术。
> 没有破解不了的软件,只有不值得破解的软件。
> 破解的目的并不是搞破坏,那些没被公开的漏洞才是需要担心的。
## 附件
### 样本
### 自动解密脚本
**接下来就是伸手党的时刻啦**
[![](https://static.52pojie.cn/static/image/filetype/zip.gif)下载链接](https://www.52pojie.cn/thread-883976-1-1.html#24279182_相关链接) _(494 KB, 下载次数: 105)_
下载 `dist.zip`,解压,进入该文件夹,然后右键在此处打开命令行,然后执行
```bash
php bin/decode.php 待解密的文件.php 解密之后输出文件名.php
```
#### 2019-05-23: v1.3
增加了几种匹配模式
```php
$GLOBALS
$GLOBALS['FOO']
explode('DELIMITER', gzinflate(substr('DATA', 0x0a, -8))
explode('DELIMITER', 'DATA')
```
增加对一个文件夹及所有子文件夹的批量解密
增加对 EnPHP bug 的检测(EnPHP 在使用单引号作为字符串分隔符时会出错,导致加密后的 php 文件无法正常运行),当出错时会提示 `explode delimiter with apostrophe`,但是本程序并不会修复这个 bug。因为加密后的文件本来就不能正常运行,解密之后的文件不应该有不同的运行结果。
## 相关链接
* [原求助帖](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=770762&pid=24232635)
* (http://enphp.djunny.com/)
* (https://github.com/djunny/enphp)
* (https://github.com/nikic/php-parser)
* [下载链接](https://github.com/ganlvtech/php-enphp-decoder/releases)
* [在线解密](http://enphp.ganlvtech.cn/)
* (https://www.52pojie.cn/thread-693641-1-1.html)
* (https://www.52pojie.cn/thread-770762-1-1.html)
* (https://www.52pojie.cn/thread-695189-1-1.html)
* (https://www.52pojie.cn/thread-696901-1-1.html)
本帖最后由 zsx 于 2019-7-27 23:39 编辑
bj2018 发表于 2019-6-2 23:19
@Ganlv 这么晚打扰了,得到了一个样本,实在是感觉有点牛,看得云里雾里,叫**,不知道能不能贴**的网 ...
这个加密的作者飘过(
免费加密的最低版本要求是PHP 5.3,并没有到PHP 7;PHP 7.2是使用国密SM4的限制,一般应该用不上的。
使用OpenSSL的缘故是考虑到大部分主机都安装了这一扩展,加解密是计算密集型操作,我不太乐意为了降低仅仅一丝被破解的可能性,而使得整个程序变得极为缓慢;过了十年,带JIT的PHP普及之后(我没测试过PHP 8下的性能,在HHVM下跑我的加密性能确实比PHP 7.3有数倍提升),才有抛弃OpenSSL的可能。不过,我也有提供不需要使用OpenSSL的版本,以支持PHP 5.2就是了。
本帖最后由 zhangya4548 于 2019-5-18 20:47 编辑
Ganlv 发表于 2019-5-18 18:41
请提供样本
```
<?php
namespace app\admin\controller;error_reporting(E_ALL^E_NOTICE);define('��', '��');��͛���������ζ;$_SERVER[��] = explode('|||', gzinflate(substr('� K-*�/�/J-�/*��K�a�a�aKIM��K���_��P��9�)P5@������̼����TqiRqI�-���