PHP解密:EnPHP混淆加密

前天收到一个任务,解密一个文件,然后就有了这个故事。

想了解EnPHP同学的移步 EnPHP官网

解密需要使用的工具:PHP-Parser
使用composer安装PHP-Parser

mkdir php-parser
cd php-parser
composer init
composer require nikic/php-parser

EnPHP加密的原理简介:
把需要加密的字符串保存到一个全局数组$_SERVER[STR]中,然后把字符串替换成数组变量,接着把$_SERVER[STR]分割成字符串,然后gzip压缩生成一个乱码字符串,然后在文件开头定义得到STR数组(这里的STR实际上也会被加密成乱码)。
例如:
源代码

$a = 'hello world';

加密后代码:

$_SERVER[STR] = explode('分隔符', gzinflate(substr('乱码', 0xa, -8)));
$x =& $_SERVER[STR][0];

解码原理简介:PHP-Parser可以用php代码生成ast(抽象语法树),然后再用ast生成php代码,只需要基于ast,就可以把代码逐步还原
基于这个理论,开始解密文件:
加密文件打开后是一堆乱码(文件一部分截图):

第一步:生成ast(抽象语法树)、格式化源码

use PhpParser\Error;
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结构

            name: Expr_ArrayDimFetch(
                var: Expr_ArrayDimFetch(
                    var: Expr_Variable(
                        name: _SERVER
                    )
                    dim: Expr_ConstFetch(
                        name: Name(
                            parts: array(
                                0: òÎò
                            )
                        )
                    )
                )
                dim: Scalar_LNumber(
                    value: 0
                )
            )

我们要把这种结构替换成字符串结构,字符串结构是这样的

            name: Scalar_String(
                value: error_reporting // $string_array[0]
            )

替换代码如下

use PhpParser\Node;
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结构如下:

                expr: Expr_AssignRef(
                    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)

            name: Scalar_String(
                value: invalid code // 表示这一行是无效代码
            )

同时,我们需要把$var变量都替换成全局变量($string_array)中对应的字符串

class LocalStringNodeVisitor extends NodeVisitorAbstract
{
    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

接下来我们把不符合规范的代码替换一下

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;
    }
}
/* 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
这时候我们的代码中还有一些局部变量是乱码,只需要把局部变量名替换为个正常编码的字符串即可,增加可读性,代码如下

class LocalVarNodeVisitor extends NodeVisitorAbstract
{
    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混淆加密

赞 (33) 评论 (0) 分享 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址