首先这是一篇被迫营业的文章,本来对于同行的流言蜚语,我们向来是无视的,建站第一天,某同行就到处宣传我们是骗子网站。一开始很气愤,到后来看破了,当你试图去抢别人生意时,别人肯定想办法搞你,搞不过只能走小人途径,到处抹黑以达到特定目的!事实证明,如今DZ盒子用户量破10万,这10万用户深知我们有没有骗过他们一分钱!
到后来出现的一些流言蜚语也有些用户反馈到我这,但是我们都是直接忽视,因为当你试图去与狗较真时,狗的目的已经达到了,就好比我只吃了一碗面为什么付2碗钱,如果你去较真,可能受伤的只是自己,而制作这些舆论的背后者却躲在人群中暗自窃喜!
因为有用户反馈说有个网址说我们的某个资源特意加了后门,之前也有类似的谣言,但是都是基础的文件,任何一个懂php代码的人都能看懂到底有没有后门,所以我们根本不回应!
这次涉及的知识可能比较深(
魔方一代
伪还原,魔方一代opcode
提取,魔方一点带混淆调试),下面我们就详细解读一下我们是如何破解被魔方一代加密的应用的。
事情是这样的,因为我们破解了克米3.6全套应用,其中有一个文件,我们使用了伪还原去授权验证技术,什么是伪还原去授权技术?
就是将一个加密的文件(
mfenc1
)通过格式化代码进行相关调试,得到关键授权代码
然后通过提取格式化后
mfenc1
的opcode
单独保存到一个文件然后读取,
方便进行乱码调试并修改(此方法来自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.php
和 func_map.php
把这2个文件合并成一个,避免多余的文件
opcode.php
是关键的代码,func_map
是函数名映射地图代码!
调试时,一步步调试会很吃力,除非是第一次调试不得已的情况,经常对此加密的了解之后,我们抓取关键指令通常使用打印
得到关键代码,然后进行hook进行强制或移除授权
此文介绍的只是我们最初研究魔方1代
的时候 使用的方法,经过深入研究,我们现在已经有更好的破解方法,甚至可以已经实现在线解密!
写在最后
为什么克米中我们用到了这最原始的方法?因为懒,当时破解这个模板时花费了很大的时间和精力,到剩下最后几个文件的时候懒的弄了,就采用了这个最简单且原始的方法。
以至于连调试代码都没删除,因为可能弄了时间长了,头已经昏了。正常发布的时候 -副本.php
一般我是会删除的,但是没有删除,连破解时的注释都忘记删除了。
整个事情差不多就是这样!这个文件按这个逻辑在继续调试是可以得到可阅读的源码的。不过此文的目的并不是教大家解密,只是为了澄清,所以就不继续写下去了。
对于此事的后续我也不会在理会,如果有人在发此类文章,我想目的就是为了让我继续写这个文件的后面步骤得到源码的教程!
感言
我们网站没有写正式的文章发布系统,每次写文章都得发布html文件,很麻烦,目的就是不想和别人打口水战!
并且说我们这个文件含木马的人只是简单的几十个字说 这个文件有木马具体分析流程也没有。
还懂的都懂,我想问 你懂
echo 'hellow word';
这句是啥意思吗?如果要实锤木马请附上具体流程,木马是怎么一个执行的流程!自己不清楚的情况,就不要瞎哔哔,啥也不懂就叫唤,你的知识盲区就是木马?
喜欢说XX是木马的话,麻烦专业一点给个分析过程,无凭无据就说是木马?比如像这篇文章样,拿出真凭实据,嘴上说说谁不会?(如果我说:发谣言的站长和小偷长的一样,因为他会我看不懂的东西)
你认为你的网站还很安全吗?风靡全网的DZ超级后门木马你了解过吗?
网传的那个附件是18M,而我们的原版的20M!说明那个是被别人恶意修改的,如果使用那个模板出现问题和我们没有关系