魔方一代伪还原等于木马?知识盲区等于木马?真有你的

首先这是一篇被迫营业的文章,本来对于同行的流言蜚语,我们向来是无视的,建站第一天,某同行就到处宣传我们是骗子网站。一开始很气愤,到后来看破了,当你试图去抢别人生意时,别人肯定想办法搞你,搞不过只能走小人途径,到处抹黑以达到特定目的!事实证明,如今DZ盒子用户量破10万,这10万用户深知我们有没有骗过他们一分钱!

到后来出现的一些流言蜚语也有些用户反馈到我这,但是我们都是直接忽视,因为当你试图去与狗较真时,狗的目的已经达到了,就好比我只吃了一碗面为什么付2碗钱,如果你去较真,可能受伤的只是自己,而制作这些舆论的背后者却躲在人群中暗自窃喜!

因为有用户反馈说有个网址说我们的某个资源特意加了后门,之前也有类似的谣言,但是都是基础的文件,任何一个懂php代码的人都能看懂到底有没有后门,所以我们根本不回应!

这次涉及的知识可能比较深(魔方一代伪还原,魔方一代opcode提取,魔方一点带混淆调试),下面我们就详细解读一下我们是如何破解被魔方一代加密的应用的。

事情是这样的,因为我们破解了克米3.6全套应用,其中有一个文件,我们使用了伪还原去授权验证技术,什么是伪还原去授权技术?

就是将一个加密的文件(mfenc1)通过格式化代码进行相关调试,得到关键授权代码
然后通过提取格式化后mfenc1opcode单独保存到一个文件然后读取,
方便进行乱码调试并修改(此方法来自52破解大神,我认为这样的确更方便调试就借鉴了),
从而hook去除该授权或伪造授权,以继续执行!达到破解的目的!

具体如何操作,下面来看看吧,文章从这里开始可能会变的很无聊和枯燥乏味!我们就拿谣言中提到的那个文件来讲,comiis_wx - 副本.php 这个文件就是谣言中所说的原版文件,未进行破解的

破解">开始破解格式化代码


我用的是 nikic/php-parser 的 AST 分析器进行的代码格式化,因为我发现这个库对乱码变量名的支持很好。

<?php

use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;

ini_set('xdebug.max_nesting_level', 10000);

require 'vendor/autoload.php';

$input_file = $argv[1];
$output_file = $argv[1] . '.formatted.php';

$code = file_get_contents($input_file);

// 解析代码
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}

// 格式化代码
$prettyPrinter = new PrettyPrinter\Standard;
$prettyCode = $prettyPrinter->prettyPrintFile($ast);

file_put_contents($output_file, $prettyCode);

格式化后的代码!

分析代码


代码最前面是一堆拥有同样形式的函数,都是 7 个输入变量,都是以引用的方式传递参数。

function func1(&$arg0, &$eip, &$arg2, &$arg3, &$arg4, &$arg5, &$arg6);

后面是一个函数,包含一个静态变量,这个静态变量等于一个超长的字符串,一个由一堆字符串使用 . 点符号连接起来的超长的字符串。

function func0($arg0, $eip, $arg2 = null)

最后面是一句调用函数。  

func0(array(), 0)

然后进行单步调试,跟踪一会就回发现,这特么不是一个虚拟机的形式吗!以下为了方便描述,我就使用 x86 指令集的某些描述方式了(只研究过一些 x86 指令集)。

初步分析结果


经过一段时间的调试,我们大概弄明白了最后一个函数类似于 call 指令,其他的每个函数的 7 个参数分别为:数据内存指令指针栈指针基址指针报错等级栈报错等级栈指针。  将变量名替换成有意义的名称之后的代码如下

function func1($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer) {
    // 这里都没有 return 都是通过引用参数返回的
}

function func_call(array $args, $eip, $ret = null)
{
    static $memory;
    if (strlen($memory) == 0) {
        $memory = '......';
    }
    $stack = array();
    $error_level_stack = array(); // 用于保存 error_reporting level
    $esp = $error_level_stack_pointer = 0;
    $stack[++$esp] = $ret;
    foreach ($args as $item) {
        $stack[++$esp] = $item;
    }
    $stack[++$esp] = count($args);
    $stack[++$esp] = -1;
    $stack[++$esp] = 0;
    $ebp = $esp;
    while ($eip >= 0) {
        $func = 'func' . ($memory[$eip] ^  $memory[$eip + 1]) . ($memory[$eip] ^  $memory[$eip + 4]) . ($memory[$eip] ^  $memory[$eip + 5]);
        $eip += ord($memory[$eip] ^ $memory[$eip + 3]);
        $func($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer);
    }
    if ($eip == -1) {
        return $stack[$esp];
    }
}

func_call(array(), 0);

修复局部变量名


经过观察发现,这个加密算法的函数中只有局部变量,所以我们可以轻松地进行变量名替换,而不会影响函数执行结果。

我这里依旧使用 PHP-Parser 先进行语法分析,然后再替换变量名,最后格式化输出。 

在解析代码与格式化输出之间添加如下代码

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;

// 变量名替换
class VariableRenameNodeVisitor extends NodeVisitorAbstract
{
    protected $paramsMap = [];

    public function __construct(&$params_map)
    {
        $this->paramsMap = $params_map;
    }

    public function generateNewVariableName()
    {
        $values = array_values($this->paramsMap);
        $i = 0;
        while (in_array('v' . $i, $values)) {
            ++$i;
        }
        return 'v' . $i;
    }

    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Expr\Variable) {
            if (!isset($this->paramsMap[$node->name])) {
                $this->paramsMap[$node->name] = $this->generateNewVariableName();
            }
            $node->name = $this->paramsMap[$node->name];
        }
    }
}

class FunctionParamsRenameNodeVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Stmt\Function_) {
            $params_map = [];
            foreach ($node->params as $i => &$param) {
                $params_map[$param->name] = 'arg' . $i;
                $param->name = $params_map[$param->name];
            }
            $visitor = new VariableRenameNodeVisitor($params_map);
            $traverser = new NodeTraverser;
            $traverser->addVisitor($visitor);
            $node->stmts = $traverser->traverse($node->stmts);
        }
    }
}

$traverser = new NodeTraverser;
$traverser->addVisitor(new FunctionParamsRenameNodeVisitor);
$ast = $traverser->traverse($ast);

或者直接使用我们提供的混淆修复功能

进行修复后

修复变量名之后之后我们继续调试。 

调试的过程中,可以看出这套指令集中,数据与指令是混在一起的,并不是 .text 与 .data 分开的,或者说使用了大量的立即数。

在所有的函数调用、eval、include 或 require 处下断点,单步调试看看到底是什么逻辑。 

然后单步跟踪一会就回发现每个函数的作用,所有的函数都是通过栈来进行数据交换的。 

有的函数负责申请栈空间,有的负责清除栈空间,有的函数负责跳转 je、jnz 之类的,有的负责函数调用,总之前面的 65 个函数可以称之为 mfenc 的指令集。 

我们要破解这个文件必须把他的指令集中每一条指令都分析一下,然后对他的 VM 中间函数进行 Hook 操作,提取出关键 Opcode,然后根据 Opcode 对应的操作还原出原始代码,这个过程和 IDA 的还原代码很像,这个过程靠的是脑子和经验,但是最费的还是体力。 

提取原始 Opcode

先把 $memory 变量输出出来,免得原来的文件看起来费劲。 

$memory 赋值语句之后插入 file_put_contents

$memory = '......';file_put_contents('opcode.php', $memory);

执行一次,之后改成

$memory = file_get_contents('opcode.php');

函数重命名


为了消除程序乱码,我想了一个方法,就是把所有的函数名称改成 func1 之类的名字,然后动态地把乱码函数名代{过}{滤}理到我们替换之后的函数。

这样做之后有几个好处,我可以随意地修改程序,而不用担心编码错误。因为有代{过}{滤}理这一层,我可以随意 Hook 其中的步骤。

我又重新修改了我的 format.php,不过试了一下感觉效果并不是特别好,所以这个暂时就先放弃(不过之后肯定还是要把 Opcode 翻译成汇编语言的)。

function func51(&$memory, &$eip, &$stack, &$esp, &$ebp, &$error_level_stack, &$error_level_stack_pointer)
{
    eval("function " . $stack[$esp] . '(){return func65(func_get_args(),' . (int) ($memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++]) . ');}');
}
function func65(array $args, $eip, $ret = null)
{
    static $memory, $func_name_map;
    if (strlen($memory) == 0) {
        $memory = file_get_contents('1.php.opcode.bin');
        $func_name_map = include '2.php.formatted.php.func_name_map.php';
    }
    $stack = [];
    $error_level_stack = [];
    $esp = $error_level_stack_pointer = 0;
    $stack[++$esp] = $ret;
    foreach ($args as $item) {
        $stack[++$esp] = $item;
    }
    $stack[++$esp] = count($args);
    $stack[++$esp] = -1;
    $stack[++$esp] = 0;
    $ebp = $esp;
    while ($eip >= 0) {
        $func = base64_decode('zb+8') . ($memory[$eip] ^ $memory[$eip + 1]) . ($memory[$eip] ^ $memory[$eip + 2]) . ($memory[$eip] ^ $memory[$eip + 4]) . ($memory[$eip] ^ $memory[$eip + 5]);
        $eip += ord($memory[$eip] ^ $memory[$eip + 3]);
        if (isset($func_name_map[$func])) {
            $func = $func_name_map[$func];
        }
        $func($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer);
    }
    if ($eip == -1) {
        return $stack[$esp];
    }
    exit;
}
include 'func_map.php';

到这里 我们基本上伪还原出了魔方1整个文件的可调试修改文件了

其中 opcode.phpfunc_map.php  把这2个文件合并成一个,避免多余的文件

opcode.php是关键的代码,func_map是函数名映射地图代码!

调试时,一步步调试会很吃力,除非是第一次调试不得已的情况,经常对此加密的了解之后,我们抓取关键指令通常使用打印

得到关键代码,然后进行hook进行强制或移除授权

此文介绍的只是我们最初研究魔方1代的时候 使用的方法,经过深入研究,我们现在已经有更好的破解方法,甚至可以已经实现在线解密!



写在最后



为什么克米中我们用到了这最原始的方法?因为懒,当时破解这个模板时花费了很大的时间和精力,到剩下最后几个文件的时候懒的弄了,就采用了这个最简单且原始的方法。
以至于连调试代码都没删除,因为可能弄了时间长了,头已经昏了。正常发布的时候  -副本.php 一般我是会删除的,但是没有删除,连破解时的注释都忘记删除了。


整个事情差不多就是这样!这个文件按这个逻辑在继续调试是可以得到可阅读的源码的。不过此文的目的并不是教大家解密,只是为了澄清,所以就不继续写下去了。
对于此事的后续我也不会在理会,如果有人在发此类文章,我想目的就是为了让我继续写这个文件的后面步骤得到源码的教程!



感言



我们网站没有写正式的文章发布系统,每次写文章都得发布html文件,很麻烦,目的就是不想和别人打口水战!


并且说我们这个文件含木马的人只是简单的几十个字说 这个文件有木马具体分析流程也没有。


还懂的都懂,我想问 你懂

 echo 'hellow word';

这句是啥意思吗?如果要实锤木马请附上具体流程,木马是怎么一个执行的流程!自己不清楚的情况,就不要瞎哔哔,啥也不懂就叫唤,你的知识盲区就是木马?

喜欢说XX是木马的话,麻烦专业一点给个分析过程,无凭无据就说是木马?比如像这篇文章样,拿出真凭实据,嘴上说说谁不会?(如果我说:发谣言的站长和小偷长的一样,因为他会我看不懂的东西)
你认为你的网站还很安全吗?风靡全网的DZ超级后门木马你了解过吗?


网传的那个附件是18M,而我们的原版的20M!说明那个是被别人恶意修改的,如果使用那个模板出现问题和我们没有关系