前天收到一个任务,解密一个文件,然后就有了这个故事。
想了解EnPHP同学的移步 EnPHP官网
解密需要使用的工具:PHP-Parser
使用composer安装PHP-Parser
cd php-parser
composer init
composer require nikic/php-parser
EnPHP加密的原理简介:
把需要加密的字符串保存到一个全局数组$_SERVER[STR]中,然后把字符串替换成数组变量,接着把$_SERVER[STR]分割成字符串,然后gzip压缩生成一个乱码字符串,然后在文件开头定义得到STR数组(这里的STR实际上也会被加密成乱码)。
例如:
源代码
加密后代码:
$x =& $_SERVER[STR][0];
解码原理简介:PHP-Parser可以用php代码生成ast(抽象语法树),然后再用ast生成php代码,只需要基于ast,就可以把代码逐步还原
基于这个理论,开始解密文件:
加密文件打开后是一堆乱码(文件一部分截图):
第一步:生成ast(抽象语法树)、格式化源码
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
require 'vendor/autoload.php';
$filename = 'enphp.php';
$code = file_get_contents($filename);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse($code);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return;
}
$filename = "enphp";
$i = 2;
$dumper = new NodeDumper;
file_put_contents($filename.$i.".ast", $dumper->dump($ast));
$prettyPrinter = new PrettyPrinter\Standard;
file_put_contents($filename.$i.".php", $prettyPrinter->prettyPrintFile($ast));
/* 接下来需要用到$_SERVER[òÎò]数组,可以从ast中获取explode时对应的参数 */
$str1 = $ast[1]->expr->args[0]->value->value;
$str3 = $ast[2]->expr->expr->args[0]->value->value;
$str4 = $ast[2]->expr->expr->args[1]->value->args[0]->value->args[0]->value->value;
$int1 = $ast[2]->expr->expr->args[1]->value->args[0]->value->args[1]->value->value;
$int2 = -$ast[2]->expr->expr->args[1]->value->args[0]->value->args[2]->value->expr->value;
//$string_array 即为 $_SERVER[òÎò]
$string_array = explode($str3, gzinflate(substr($str4, $int1, $int2)));
生成的ast文件为enphp2.ast,php文件为enphp2.php
打开enphp2.php:
可以看到代码已经格式化了,代码里有类似$_SERVER[òÎò][0]、$_SERVER[òÎò][0x1]的变量,我们要做的就是把这些变量替换成原来的字符串。
首先,我们打印出$string_array(后面代码多次出现,请读者自动脑补为$_SERVER[òÎò]) 或者 $_SERVER[òÎò]数组
Array
(
[0] => error_reporting
[1] => HTTP_HOST
[2] => getTopDomainhuo
[3] => http://check.ieasynet.com/update.php
[4] => ?a=client_check&u=
[5] => curl_init
[6] => curl_setopt
......
[306] => /^\/admin.php/
[307] => /index.php
[308] => admin_url
[309] => /^\/index.php/
[310] => /admin.php
)
可以看到,$_SERVER[òÎò][0]其实就是error_reporting字符串,我们要做的就是替换所有的$_SERVER[òÎò][i]模式为原来的字符串。
打开enphp2.ast
找到$_SERVER[òÎò][i]的ast结构
var: Expr_ArrayDimFetch(
var: Expr_Variable(
name: _SERVER
)
dim: Expr_ConstFetch(
name: Name(
parts: array(
0: òÎò
)
)
)
)
dim: Scalar_LNumber(
value: 0
)
)
我们要把这种结构替换成字符串结构,字符串结构是这样的
value: error_reporting // $string_array[0]
)
替换代码如下
use PhpParser\Node\Stmt\Function_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
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 enterNode(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 === '_SERVER'
&& $node->var->dim instanceof Node\Expr\ConstFetch
&& $node->var->dim->name instanceof Node\Name
&& $node->var->dim->name->parts[0] === $this->globalVariableName
&& $node->dim instanceof Node\Scalar\LNumber
) {
return new Node\Scalar\String_($this->stringArray[$node->dim->value]);
}
return null;
}
}
/* translate all variable */
$nodeVisitor = new GlobalStringNodeVisitor($str1, $string_array);
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
$filename = "enphp";
$i = 3;
$dumper = new NodeDumper;
file_put_contents($filename.$i.".ast", $dumper->dump($ast));
$prettyPrinter = new PrettyPrinter\Standard;
file_put_contents($filename.$i.".php", $prettyPrinter->prettyPrintFile($ast));
生成的ast文件为enphp3.ast,php文件为enphp3.php
打开enphp3.php可以看到这一部分已经替换过来了,不过有些是这样的(‘string’),不符合编码习惯,还得处理一次,这里先不处理,因为方法里还有类似$var =& $_SERVER[òÎò]的代码,$var变量在函数内做操作,还需替换一次,这种代码的ast结构如下:
var: Expr_Variable(
name: á<8d>
)
expr: Expr_ArrayDimFetch(
var: Expr_Variable(
name: _SERVER
)
dim: Expr_ConstFetch(
name: Name(
parts: array(
0: òÎò
)
)
)
)
)
我们要把这种结构替换掉(php-parser的删除节点功能在最近的版本中好像去掉了,不能删除,所以我们替换成invalid code)
value: invalid code // 表示这一行是无效代码
)
同时,我们需要把$var变量都替换成全局变量($string_array)中对应的字符串
{
protected $globalVariableName;
protected $stringArray = [];
protected $localArray = [];
public function __construct($globals_name, $string_array)
{
$this->globalVariableName = $globals_name;
$this->stringArray = $string_array;
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\AssignRef
&& $node->var instanceof Node\Expr\Variable
&& $node->expr instanceof Node\Expr\ArrayDimFetch
&& $node->expr->var instanceof Node\Expr\Variable
&& $node->expr->var->name == "_SERVER"
&& $node->expr->dim instanceof Node\Expr\ConstFetch
&& $node->expr->dim->name instanceof Node\Name
&& $node->expr->dim->name->parts[0] === $this->globalVariableName
) {
$this->localArray[$node->var->name] = 1;
return new Node\Scalar\String_('invalid code');
} elseif (
$node instanceof Node\Expr\ArrayDimFetch
&& $node->var instanceof Node\Expr\Variable
&& isset($this->localArray[$node->var->name])
&& $node->dim instanceof Node\Scalar\LNumber
) {
return new Node\Scalar\String_($this->stringArray[$node->dim->value]);
}
return null;
}
}
/* translate function local string */
$nodeVisitor = new LocalStringNodeVisitor($str1, $string_array);
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
$filename = "enphp";
$i = 4;
$dumper = new NodeDumper;
file_put_contents($filename.$i.".ast", $dumper->dump($ast));
$prettyPrinter = new PrettyPrinter\Standard;
file_put_contents($filename.$i.".php", $prettyPrinter->prettyPrintFile($ast));
生成的ast文件为enphp4.ast,php文件为enphp4.php
接下来我们把不符合规范的代码替换一下
{
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;
}
}
/* beautify function string */
$nodeVisitor = new BeautifyNodeVisitor();
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
$filename = "enphp";
$i = 5;
$dumper = new NodeDumper;
file_put_contents($filename.$i.".ast", $dumper->dump($ast));
$prettyPrinter = new PrettyPrinter\Standard;
file_put_contents($filename.$i.".php", $prettyPrinter->prettyPrintFile($ast));
生成的ast文件为enphp5.ast,php文件为enphp5.php
这时候我们的代码中还有一些局部变量是乱码,只需要把局部变量名替换为个正常编码的字符串即可,增加可读性,代码如下
{
protected $paramsArray = [];
protected $i = 0;
protected $varArray = [];
protected $k = 0;
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\Variable && $node->name != '_SERVER') {
if (!isset($this->varArray[$node->name])) {
$this->varArray[$node->name] = 'arg'.$this->k;
$this->k++;
}
$node->name = $this->varArray[$node->name];
}
if ($node instanceof Node\Stmt\Function_) {
$this->i = 0;
$this->k = 0;
}
return null;
}
}
/* translate function local variable */
$nodeVisitor = new LocalVarNodeVisitor();
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);
$filename = "enphp";
$i = 6;
$dumper = new NodeDumper;
file_put_contents($filename.$i.".ast", $dumper->dump($ast));
$prettyPrinter = new PrettyPrinter\Standard;
file_put_contents($filename.$i.".php", $prettyPrinter->prettyPrintFile($ast));
生成的ast文件为enphp6.ast,php文件为enphp6.php
打开enphp6.php,后面的代码部分已经正常可读,只有上面的$_SERVER[òÎò] = explode…部分代码是乱码,怎么办呢?直接删掉就好了,代码中的$_SERVER[òÎò][*]我们都替换过了,所以这个全局变量没有用了,至此,代码已经解码完成(还有可能存在不符合编码规范的代码,已经不是乱码了,读者可自行处理或不处理)。
转载请注明:小Y » PHP解密:EnPHP混淆加密