PHPJM混淆原理与还原

写在前面

最近在工作中遇到了一些高度混淆的PHP恶意文件,经分析发现其使用了第三方通用加密平台提供的加密服务,文件特征分别对应 phpjm.net 与 phpjiami.com 两个平台。本文以 phpjm.net 为研究对象,对其加密机制展开学习与分析,并进行解密还原。

混淆分析

先编写一段示例代码用来做测试,代码如下

<?php
if ($_GET['display'] == true) {
    phpinfo();
}

需要注意的是phpjm.net的加密算法只能使用php<7的版本使用,根据报错推测原因是加密过程中会掺杂一些随机的字符,而这些字符对于php7来说都是非法字符,识别上较为严格,无法执行,php5则会静默忽略或跳过。经过phpjm混淆过后会变成了下面样子 方法名字,变量名字全都已经被不可见字符混淆过了,在vsc和phpstorm中显示的都是。分析前使用phpstorm进行格式化一下,分析起来会更加方便一些,格式化后的代码如下

<?php
/*
������������Ϣ�����DZ�php�ļ������ߣ����Ա��ļ�����������Ϣֻ���ṩ�˶Ա�php�ļ����ܡ������Ҫ��PHP�ļ����м��ܣ��밴������Ϣ��ϵ��
Warning: do not modify this file, otherwise may cause the program to run.
QQ: 1833596
Website: http://www.phpjm.net/
Copyright (c) 2012-2026 phpjm.net All Rights Reserved.
*/
if (!defined("ECFFAFDC")) {
    define("ECFFAFDC", __FILE__);
    global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
    function ��($��, $��� = "")
    {
        global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
        if (empty($���)) {
            return base64_decode($��);
        } else {
            return ��($����������($��, $���, $����($���)));
        }
    }

    $���� = ��("c3RycmV2�");
    $���������� = ��("c3RydHI=�");
    $�� = ��("G3p1bwNvbXByGX�Nz�", "ZwCmG");
    $��� = ��("SzYzOWIzMmY0MaMwED�FjYjY2MDU0YzlhYTUy�NDNhODgwS�2U=�", "LaZEGS");
    $�������� = ��("NXNhbA==�", "ZWHON");
    $��������� = ��("UmFzZTU0�X2RlU29k�ZC==�", "YCvGgJQU");
    $��������������� = ��("IHJlZ19yZXBsYVNl�", "ctWOLdVhI");
    function ����(&$����)
    {
        global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
        $���������������� = ��("SGLL�", "ZTLNlCS");
        @$���������������($���, $�������� . "(@$��($���������('eNptkm9P2lAU�xr8KaXhxm1Wl�FgeE3GzMqNgs�mDIchW0h9A9Q�pQsKQ7tpplAM�0iGFFoqA0I+6�3hbJCHt5z/M7�555znuM3oGED�jJYjV3Q6CFWM�wLLXF1yVxvCo�v6M+Grqr19nS�FXcUOa8zEE4Q�wzP7dYSo2lR7�7Ho14tlyJs3A�PtJLR9+V+EnM�LWPoU2tozl2o�JperNTZREeVT�aCIww9drCBoO�dH0+WfS1qcsl�qc9SkuKVscPw�qYt0EjFdtTFo�tpptoz1vuRSf�ZsieQ3Cf9i9L�Z4c0j6jnXtN8�Gt11Fo3pg9d9�Jb1XyLC0kmH1�c3fEPF9M/mCk�CsKnrRfrSVON�3tgazu5cXs4q�VY5SyjPUIHOs�JCRBRmi/83vc�th7Ml4k6GkwM�b3XFOF1WyEgt�w6rJs3wMwmfU�snIgnaayRXd8�G+Zyh8cfD3K5�qFQAG1UAtgO2�37zDv4IdjHBo�wq/1bBz/5R8M�7rs2RK8v5Lfo�rViuiqugYUdv�l8tHL+DFCR92�ya2ccwSPAK8k�8erFksc35l+h�a5aAZUFii6Rw�CNeUf00Ba8p/�8gPEFrWLb2O8�EBYEURQLITL0�Npjf3QtSlBAu�hIJhIVAIiCGM�IHHc/c13c+Pb�WJkNnPbuR6YN�3zvrAf6ZqS4a�uvpHBW4UjbUK�2bAmySJK0Hre�NVBZkpMTdY79�kKpYbOJnC9ms�xFLoHP4CagIz�Aw==�')));", "���
������639b32f40c0d1cb66054c9aa5243a880�����");
        return "/";
    }
} else {
    global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
    $���� = ��("c3RycmV2�");
    $���������� = ��("c3RydHI=�");
    $�� = ��("G3p1bwNvbXByGX�Nz�", "ZwCmG");
    $��� = ��("SzYzOWIzMmY0MaMwED�FjYjY2MDU0YzlhYTUy�NDNhODgwS�2U=�", "LaZEGS");
    $�������� = ��("NXNhbA==�", "ZWHON");
    $��������� = ��("UmFzZTU0�X2RlU29k�ZC==�", "YCvGgJQU");
    $��������������� = ��("IHJlZ19yZXBsYVNl�", "ctWOLdVhI");
}
$��������� = ��("rU5vejrm�VXdBd0FE�UVFGQr8=�", "ZYeLr");
$�������� = ����($���������);
@$���������������($���, $�������� . "(@$��($���������('eNotjM1q�g0AURveFvsMsLkTh�voFNXJW+�QHellEIT�KpRUmmRR�SknU0Yyj�0dEZf+IkzqvWRZff�4ZzPcRd3�rv/u3954�K2LBy8P9�49Pszdv" . $��������� . $�������� . "fs2cy�n5Pt125p�kx8ySd56�9WnZDvkl�/xXZrTfL�rQUSISsR�glYh1HVw�QuA0lQKB�ZjpLpymF�7ho1Trzs�jT4GFcJF�URMKmlOE�phZiHEyV�aQQWiCTf�m1wfC4QT�Desojphk�Y4xwLiNVtAduQp1M�3zq+dkVG�Zdl3zeWAkMamNIVS�I28YP0cI�Fd/3rEvU�daBtPUhp�O+7iD7ZA�Z0Q=�')));", "���
������639b32f40c0d1cb66054c9aa5243a880������");
return true; ?>0247589c0301a1ae1ea887304a867032

直接看虽然大部分都看不懂,但中间部分的变量仍能看出明显的 base64 编码特征,并且里面有一个位置base64_decode并没有混淆,这部分代码如下

    // define("ECFFAFDC", __FILE__);
    // global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
    function ��($��, $��� = "")
    {
        // global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
        if (empty($���)) {
            return base64_decode($��);
        } else {
            return ��($����������($��, $���, $����($���)));
        }
    }

    $���� = ��("c3RycmV2�");
    $���������� = ��("c3RydHI=�");
    $�� = ��("G3p1bwNvbXByGX�Nz�", "ZwCmG");
    $��� = ��("SzYzOWIzMmY0MaMwED�FjYjY2MDU0YzlhYTUy�NDNhODgwS�2U=�", "LaZEGS");
    $�������� = ��("NXNhbA==�", "ZWHON");
    $��������� = ��("UmFzZTU0�X2RlU29k�ZC==�", "YCvGgJQU");
    $��������������� = ��("IHJlZ19yZXBsYVNl�", "ctWOLdVhI");
    function ����(&$����)
    {
        // global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;
        $���������������� = ��("...");
        return "/";
    }

里面注释的部分可以先忽略掉,因为没有实质性的逻辑,对于分析,不看也可以。在IF中函数定义的部分代码大致可以分为三个部分看,��方法部分,赋值部分,����方法部分,实际这样看也不算太多内容,单独看第一个函数,大致看逻辑如下

    function ��($��, $��� = "")
    {
	    // 如果第二个变量为空
        if (empty($���)) {
	        // 把第一个参数base64解码后返回
            return base64_decode($��);
        } else {
	        // 否则做运算,其中里面是嵌套了两层当前函数
            return ��($����������($��, $���, $����($���)));
        }
    }

在第二部分都是通过第一个方法进行解密的,在这部分能看懂的基本就是很多参数都是base64编码的,结合第一个函数分析的,如果只传入了一个值,那么他就会直接返回base64解码的内容,并且在第一个方法中,如果传入了两个值会做重复运算,运算的函数和第二部分的变量是基本匹配的,我们现把能拿到的数据转换一下,如下

    $���� = ��("c3RycmV2�"); // strrev
    $���������� = ��("c3RydHI=�"); // strtr 
    $�� = ��("G3p1bwNvbXByGX�Nz�", "ZwCmG");
    $��� = ��("SzYzOWIzMmY0MaMwED�FjYjY2MDU0YzlhYTUy�NDNhODgwS�2U=�", "LaZEGS");
    $�������� = ��("NXNhbA==�", "ZWHON");
    $��������� = ��("UmFzZTU0�X2RlU29k�ZC==�", "YCvGgJQU");
    $��������������� = ��("IHJlZ19yZXBsYVNl�", "ctWOLdVhI");

后续替换到第一个方法中,可以发现方法1就还原了

    function ��($��, $��� = "")
    {
        if (empty($���)) {
            return base64_decode($��);
        } else {
            return ��(strtr($��, $���, strrev($���)));
        }
    }
	// 美化后
	function func0($var1, $var2 = "")
    {
        if (empty($var2)) {
            return base64_decode($var1);
        } else {
            return func0(strtr($var1, $var2, strrev($var2)));
        }
    }

后续通过下面脚本复原其他第二部分内容

<?php
function func0($var1, $var2 = "")
{
    if (empty($var2)) {
        return base64_decode($var1);
    } else {
        return func0(strtr($var1, $var2, strrev($var2)));
    }
}
$���� = func0("c3RycmV2�"); 
$���������� = func0("c3RydHI=�"); 
$�� = func0("G3p1bwNvbXByGX�Nz�", "ZwCmG");
$��� = func0("SzYzOWIzMmY0MaMwED�FjYjY2MDU0YzlhYTUy�NDNhODgwS�2U=�", "LaZEGS");
$�������� = func0("NXNhbA==�", "ZWHON");
$��������� = func0("UmFzZTU0�X2RlU29k�ZC==�", "YCvGgJQU");
$��������������� = func0("IHJlZ19yZXBsYVNl�", "ctWOLdVhI");

print_r(array_slice(get_defined_vars(), -7));

// 输出如下
// Array
// (
//     [锟斤拷锟斤拷] => strrev
//     [锟斤拷锟斤拷锟?锟斤拷锟斤拷锟浇] => strtr
//     [锟斤拷] => gzuncompress
//     [锟斤拷锟絔 => /639b32f40c0d1cb66054c9aa5243a880/e
//     [锟斤拷锟斤拷锟斤拷锟斤拷] => eval
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟絔 => base64_decode
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟絔 => preg_replace
// )

第二部分也已经完全解密,我们看一下第三部分

    function ����(&$����)
    {
        $���������������� = ��("SGLL�", "ZTLNlCS");
        @$���������������($���, $�������� . "(@$��($���������('eNptkm9P2lAU�xr8KaXhxm1Wl�FgeE3GzMqNgs�mDIchW0h9A9Q�pQsKQ7tpplAM�0iGFFoqA0I+6�3hbJCHt5z/M7�555znuM3oGED�jJYjV3Q6CFWM�wLLXF1yVxvCo�v6M+Grqr19nS�FXcUOa8zEE4Q�wzP7dYSo2lR7�7Ho14tlyJs3A�PtJLR9+V+EnM�LWPoU2tozl2o�JperNTZREeVT�aCIww9drCBoO�dH0+WfS1qcsl�qc9SkuKVscPw�qYt0EjFdtTFo�tpptoz1vuRSf�ZsieQ3Cf9i9L�Z4c0j6jnXtN8�Gt11Fo3pg9d9�Jb1XyLC0kmH1�c3fEPF9M/mCk�CsKnrRfrSVON�3tgazu5cXs4q�VY5SyjPUIHOs�JCRBRmi/83vc�th7Ml4k6GkwM�b3XFOF1WyEgt�w6rJs3wMwmfU�snIgnaayRXd8�G+Zyh8cfD3K5�qFQAG1UAtgO2�37zDv4IdjHBo�wq/1bBz/5R8M�7rs2RK8v5Lfo�rViuiqugYUdv�l8tHL+DFCR92�ya2ccwSPAK8k�8erFksc35l+h�a5aAZUFii6Rw�CNeUf00Ba8p/�8gPEFrWLb2O8�EBYEURQLITL0�Npjf3QtSlBAu�hIJhIVAIiCGM�IHHc/c13c+Pb�WJkNnPbuR6YN�3zvrAf6ZqS4a�uvpHBW4UjbUK�2bAmySJK0Hre�NVBZkpMTdY79�kKpYbOJnC9ms�xFLoHP4CagIz�Aw==�')));", "���
������639b32f40c0d1cb66054c9aa5243a880�����");
        return "/";
    }

第三部分代码掺杂了第一部分内容,但是最终返回内容仅为/(根据加密代码更变,不固定),初次分析时由于关注点在最终返回值上,忽略了这段代码实际执行的内容,但是他与后面执行流高度相似,这里把从上面找到的函数全都替换过来,这部分代码如下

function func2(&$var1){
        // $���������������� = func0("SGLL�", "ZTLNlCS");
        @preg_replace("/639b32f40c0d1cb66054c9aa5243a880/e", "eval" . "(@gzuncompress(base64_decode('eNptkm9P2lAU�xr8KaXhxm1Wl�FgeE3GzMqNgs�mDIchW0h9A9Q�pQsKQ7tpplAM�0iGFFoqA0I+6�3hbJCHt5z/M7�555znuM3oGED�jJYjV3Q6CFWM�wLLXF1yVxvCo�v6M+Grqr19nS�FXcUOa8zEE4Q�wzP7dYSo2lR7�7Ho14tlyJs3A�PtJLR9+V+EnM�LWPoU2tozl2o�JperNTZREeVT�aCIww9drCBoO�dH0+WfS1qcsl�qc9SkuKVscPw�qYt0EjFdtTFo�tpptoz1vuRSf�ZsieQ3Cf9i9L�Z4c0j6jnXtN8�Gt11Fo3pg9d9�Jb1XyLC0kmH1�c3fEPF9M/mCk�CsKnrRfrSVON�3tgazu5cXs4q�VY5SyjPUIHOs�JCRBRmi/83vc�th7Ml4k6GkwM�b3XFOF1WyEgt�w6rJs3wMwmfU�snIgnaayRXd8�G+Zyh8cfD3K5�qFQAG1UAtgO2�37zDv4IdjHBo�wq/1bBz/5R8M�7rs2RK8v5Lfo�rViuiqugYUdv�l8tHL+DFCR92�ya2ccwSPAK8k�8erFksc35l+h�a5aAZUFii6Rw�CNeUf00Ba8p/�8gPEFrWLb2O8�EBYEURQLITL0�Npjf3QtSlBAu�hIJhIVAIiCGM�IHHc/c13c+Pb�WJkNnPbuR6YN�3zvrAf6ZqS4a�uvpHBW4UjbUK�2bAmySJK0Hre�NVBZkpMTdY79�kKpYbOJnC9ms�xFLoHP4CagIz�Aw==�')));", "���
������639b32f40c0d1cb66054c9aa5243a880�����");
        return "/";
}

对于中间这部分代码进行解密,代码如下

echo(base64_encode(@gzuncompress(base64_decode('eNptkm9P2lAU�xr8KaXhxm1Wl�FgeE3GzMqNgs�mDIchW0h9A9Q�pQsKQ7tpplAM�0iGFFoqA0I+6�3hbJCHt5z/M7�555znuM3oGED�jJYjV3Q6CFWM�wLLXF1yVxvCo�v6M+Grqr19nS�FXcUOa8zEE4Q�wzP7dYSo2lR7�7Ho14tlyJs3A�PtJLR9+V+EnM�LWPoU2tozl2o�JperNTZREeVT�aCIww9drCBoO�dH0+WfS1qcsl�qc9SkuKVscPw�qYt0EjFdtTFo�tpptoz1vuRSf�ZsieQ3Cf9i9L�Z4c0j6jnXtN8�Gt11Fo3pg9d9�Jb1XyLC0kmH1�c3fEPF9M/mCk�CsKnrRfrSVON�3tgazu5cXs4q�VY5SyjPUIHOs�JCRBRmi/83vc�th7Ml4k6GkwM�b3XFOF1WyEgt�w6rJs3wMwmfU�snIgnaayRXd8�G+Zyh8cfD3K5�qFQAG1UAtgO2�37zDv4IdjHBo�wq/1bBz/5R8M�7rs2RK8v5Lfo�rViuiqugYUdv�l8tHL+DFCR92�ya2ccwSPAK8k�8erFksc35l+h�a5aAZUFii6Rw�CNeUf00Ba8p/�8gPEFrWLb2O8�EBYEURQLITL0�Npjf3QtSlBAu�hIJhIVAIiCGM�IHHc/c13c+Pb�WJkNnPbuR6YN�3zvrAf6ZqS4a�uvpHBW4UjbUK�2bAmySJK0Hre�NVBZkpMTdY79�kKpYbOJnC9ms�xFLoHP4CagIz�Aw==�'))));

// JJM9k6AoIkptOXdKVzQ9hCIsIlp4cWJzSiIpOySKhImTkD2ToCgidlhod2JHOWt2UT09miIsIlpjUUN2Iik7JISLm4uJjT2ToCgiSkhabFlXUT2RIiwiWmhHbnlIT0FKIik7JJOQm5eVlJ49k6AoInRtbHN0WE5wZW1VPZQiLCJaWWN2dCIpOySVkpCQnpqfkYubPZOgKCJSM1ZpUjNjeZgiLCJjVHFXUiIpOySNhIKSg4WDiJOInoU9k6AoImNXUTGPIiwiYlNDcmhqRkpjIik7JJmPg5SOloCKn4KbhpA9k6AoInBXNWZZWEp5WViQaz2EIiwiYWNnUnVRaXAiKTskm4Wdl46LhJOPmJeVnIA9k6AoIm1aeXNiM3lsnCIsIlpRSXlOaWRtIik7JJGKf5iIl4aUnZqElpKak5M9k6AoImdISmx5MTl0WViEUmphQT09mSIsImN5RWlVVFpnIik7JJOgPV9fRklMRV9fO2lmKCSRin+YiJeGlJ2ahJaSmpOTKCIvKC4rPylcKC8iLCSToCwki4+gKSl7JJKSgY2gPSSLj6BbMV07fWVsc2V7JJKSgY2gPSSToDt9JISLm4uJjaA9JJMoJJKSgY2gLCAicmIiKTskioSJk5CgPSSEi5uLiY0oJISLm4uJjaAsJJOQm5eVlJ4oJJKSgY2gKSk7JJuFnZeOi4STj5iXlZyAKCSEi5uLiY2gKTsklZKQkJ6an5GLmygkioSJk5CgLC0xMyk9PSSVkpCQnpqfkYubKCSNhIKSg4WDiJOInoUoJJWSkJCemp+Ri5soJJWSkJCemp+Ri5soJIqEiZOQoCwwLC0zMikuImNkOGRkZWVlZjcxNzY0YTI1NDMzZDhmNzQ4ZDBmMGU3IiwxKSksLTEzKSB8fCAkkYp/mIiXhpSdmoSWkpqTk6AoKTskgZaUoD1AJIuPKCSclISfgpCEjIQoJIGWlKApKTsknJSEn4KQhIyEoD10aW1lKCk7JIuPPZOgKCJwM1oxYm1OdmJYQlRwl1hOeoUiLCJaeUFUcCIpOw==

之所以加入 base64_encode,是因为输出内容同样经过了混淆处理,直接粘贴会出问题,格式化过后的样子如下

<?php
$� = ��("Jm9wJW4=�", "ZxqbsJ");
$����� = ��("vXhwbG9kvQ==�", "ZcQCv");
$������ = ��("JHZlYWQ=�", "ZhGnyHOAJ");
$������� = ��("tmlstXNpemU=�", "ZYcvt");
$���������� = ��("R3ViR3cy�", "cTqWR");
$������������ = ��("cWQ1�", "bSCrhjFJc");
$������������� = ��("pW5fYXJyYX�k=�", "acgRuQip");
$�������������� = ��("mZysb3yl�", "ZQIyNidm");
$��������������� = ��("gHJly19tYX�RjaA==�", "cyEiUTZg");
$�� = __FILE__;
if ($���������������("/(.+?)\(/", $��, $���)) {
    $����� = $���[1];
} else {
    $����� = $��;
}
$������� = $�($�����, "rb");
$������ = $������($�������, $�������($�����));
$��������������($�������);
$����������($������, -13) == $����������($������������($����������($����������($������, 0, -32) . "cd8ddeeef71764a25433d8f748d0f0e7", 1)), -13) || $����������������();
$���� = @$��($���������($����));
$���������� = time();
$�� = ��("p3Z1bmNvbXBTp�XNz�", "ZyATp");

这里的解密逻辑和刚才一样,方法��实际就是我们第一部分代码,这里再次进行解密,代码还原大概成了这样

$� = func0("Jm9wJW4=�", "ZxqbsJ");
$����� = func0("vXhwbG9kvQ==�", "ZcQCv");
$������ = func0("JHZlYWQ=�", "ZhGnyHOAJ");
$������� = func0("tmlstXNpemU=�", "ZYcvt");
$���������� = func0("R3ViR3cy�", "cTqWR");
$������������ = func0("cWQ1�", "bSCrhjFJc");
$������������� = func0("pW5fYXJyYX�k=�", "acgRuQip");
$�������������� = func0("mZysb3yl�", "ZQIyNidm");
$��������������� = func0("gHJly19tYX�RjaA==�", "cyEiUTZg");
$func0 = func0("p3Z1bmNvbXBTp�XNz�", "ZyATp"); // 最后

print_r(array_slice(get_defined_vars(), -10));

// Array
// (
//     [锟絔 => fopen
//     [锟斤拷锟斤拷锟絔 => explode
//     [锟斤拷锟斤拷锟斤拷] => fread
//     [锟斤拷锟斤拷锟斤拷锟絔 => filesize
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷] => substr
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷] => md5
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟絔 => in_array
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷] => fclose
//     [锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟絔 => preg_match
//     [func0] => gzuncompress
// )

// 还原后美化
$fp = fopen(__FILE__, "rb");
$data = fread($fp, filesize(__FILE__));
fclose($fp);
substr($data, -13) == substr(md5(substr($data, 0, -32) . "cd8ddeeef71764a25433d8f748d0f0e7",1), -13) || preg_match();

// 要注意的是这里的var1是复函数通过&引用过来的 
// gzuncompress为第二部分解密得来
$var1 = @gzuncompress($base64_decode($var1));
// $���������� = time(); // 目前看无意义
// func0 置换
$func0 = "gzuncompress";

主要作用为校验md5是否为cd8ddeeef71764a25433d8f748d0f0e7,如果不对则直接执行preg_match()进行错误抛出,代码就不执行了,如果判断成功则处理父函数传入的var1,然后把func0置换成gzuncompress。对此第三部分也解密完了。我们回到正常的执行流,代码如下

$��������� = ��("rU5vejrm�VXdBd0FE�UVFGQr8=�", "ZYeLr");
$�������� = ����($���������);
@$���������������($���, $�������� . "(@$��($���������('eNotjM1q�g0AURveFvsMsLkTh�voFNXJW+�QHellEIT�KpRUmmRR�SknU0Yyj�0dEZf+IkzqvWRZff�4ZzPcRd3�rv/u3954�K2LBy8P9�49Pszdv" . $��������� . $�������� . "fs2cy�n5Pt125p�kx8ySd56�9WnZDvkl�/xXZrTfL�rQUSISsR�glYh1HVw�QuA0lQKB�ZjpLpymF�7ho1Trzs�jT4GFcJF�URMKmlOE�phZiHEyV�aQQWiCTf�m1wfC4QT�Desojphk�Y4xwLiNVtAduQp1M�3zq+dkVG�Zdl3zeWAkMamNIVS�I28YP0cI�Fd/3rEvU�daBtPUhp�O+7iD7ZA�Z0Q=�')));", "���
������639b32f40c0d1cb66054c9aa5243a880������");
return true; ?>0247589c0301a1ae1ea887304a867032

这里基于上面已经分析过的内容继续进行置换,代码如下

<?php
function func0($var1, $var2 = "")
{
    if (empty($var2)) {
        return base64_decode($var1);
    } else {
        return func0(strtr($var1, $var2, strrev($var2)));
    }
}
function func2(&$var1)
{
	$var1 = gzuncompress(base64_decode($var1));
    return "/";
}
$data1 = func0("rU5vejrm�VXdBd0FE�UVFGQr8=�", "ZYeLr");
$data2 = func2($data1);
@$preg_replace("/639b32f40c0d1cb66054c9aa5243a880/e", "eval" . "(@gzuncompress(base64_decode('eNotjM1q�g0AURveFvsMsLkTh�voFNXJW+�QHellEIT�KpRUmmRR�SknU0Yyj�0dEZf+IkzqvWRZff�4ZzPcRd3�rv/u3954�K2LBy8P9�49Pszdv" . $data1 . $data2 . "fs2cy�n5Pt125p�kx8ySd56�9WnZDvkl�/xXZrTfL�rQUSISsR�glYh1HVw�QuA0lQKB�ZjpLpymF�7ho1Trzs�jT4GFcJF�URMKmlOE�phZiHEyV�aQQWiCTf�m1wfC4QT�Desojphk�Y4xwLiNVtAduQp1M�3zq+dkVG�Zdl3zeWAkMamNIVS�I28YP0cI�Fd/3rEvU�daBtPUhp�O+7iD7ZA�Z0Q=�')));", "���
������639b32f40c0d1cb66054c9aa5243a880������");
return true; ?>0247589c0301a1ae1ea887304a867032

通过之前的办法把data1解密出来,然后data2说白了就直接写/即可,然后替换部分就不替换了,这里/e参数是直接执行了,我们把执行内容进行输出,美化如下

<?php
$data1 = gzuncompress(base64_decode("eNoz6fUwAwADQQFA"));
$data2 = "/";
echo gzuncompress(base64_decode("eNotjM1q�g0AURveFvsMsLkTh�voFNXJW+�QHellEIT�KpRUmmRR�SknU0Yyj�0dEZf+IkzqvWRZff�4ZzPcRd3�rv/u3954�K2LBy8P9�49Pszdv" . $data1 . $data2 . "fs2cy�n5Pt125p�kx8ySd56�9WnZDvkl�/xXZrTfL�rQUSISsR�glYh1HVw�QuA0lQKB�ZjpLpymF�7ho1Trzs�jT4GFcJF�URMKmlOE�phZiHEyV�aQQWiCTf�m1wfC4QT�Desojphk�Y4xwLiNVtAduQp1M�3zq+dkVG�Zdl3zeWAkMamNIVS�I28YP0cI�Fd/3rEvU�daBtPUhp�O+7iD7ZA�Z0Q=�"));

因为字符存在乱码,直接运行会报错,需要通过16进制编辑器进行把关键位置进行提取,实际解密脚本如下

$str1 = "654e6f746a4d31718567304155527665467f76734d734c6b546890766f464e584a572b945148656c6c4549548c4b7052556d6d52529f536b6e553059796a943064455a662b496b7f7a717657525a666692345a7a50635264338e72762f7533393534894b324c427938503986343950737a6476";
$str2 = "66733263798b6e35507431323570876b783879536435368739576e5a44766b6c912f78585a7254664c84725155534953735296676c5968314856778c517541306c514b429e5a6a704c70796d469c37686f3154727a738c6a54344746634a468055524d4b6d6c4f458870685a6948457956866151515769435466846d317766433451549a4465736f6a70686b8c593478774c694e567f744164755170314d9b337a712b646b5647925a646c337a6557417f6b4d616d4e4956538c49323859503063498646642f33724576558c64614274505568708b4f2b376944375a419c5a30513d8d";
$var1 = gzuncompress(base64_decode("eNoz6fUwAwADQQFA"));
$var2 = "/";
echo gzuncompress(base64_decode(hex2bin($str1) . $var1 . $var2 . hex2bin($str2)));

输出结果如下,源码已经是输出 后半部分为 phpjm 附加的变量销毁代码,用于防止变量残留,这里可直接忽略。

解密逻辑

根据分析内容,他的加密逻辑如下

源代码压缩 -》增添注释信息 -》 对源代码进行zlib压缩 -》base64转码 -》 对数据提取变量A -》 对数据提取变量B -》 变量A通再次zlib加密+base转码-》处理过的A再进行func0加密方案进行加密 -》变量B通过func1直接返回 -》 替换数据模板

和程序加壳很类似,严格来说这并不算加密,更类似于在执行流程上附加了一层解包逻辑。根据上面逻辑,开始编写解密脚本逻辑,要注意的是,原文中不可见字符较多,如果直接文本匹配会出问题,代码编写与逻辑全部基于hex处理,下面进行正则进行匹配,提取出解密时的必要数据

array - 数据碎片A提取逻辑(str1) = last(a02822(.*?)222c22(.*?)22293b)
str - 数据碎片B提取逻辑(str2) = 72657475726e2022(.*?)223b7d7d
str - 数据C提取 = last(2827654e(.*?)272929293b22)
// 数据C匹配654e原因是因为处理源代码前会增加 `?><?php` 这类字样前面7个是固定的,经过多次测试,前面的654e也是固定的,可以作为标识使用

这里以rust代码为例,实际提取逻辑代码如下

fn parser_fragment_a(content: &[u8]) -> Option<(String, String)> {
    let re = Regex::new(r"a02822(?P<data>[a-f0-9]+?)222c22(?P<key>[a-f0-9]+?)22293b")
        .expect("parser fragment a error");
    re.captures_iter(content).last().map(|caps| {
        (
            String::from_utf8_lossy(&caps["data"]).to_string(),
            String::from_utf8_lossy(&caps["key"]).to_string(),
        )
    })
}
fn parser_fragment_b(content: &[u8]) -> Option<String> {
    let re = Regex::new(r"72657475726e2022(?P<data>[a-f0-9]+?)223b7d7d")
        .expect("parser fragment c error");

    re.captures(content)
        .map(|caps| String::from_utf8_lossy(&caps["data"]).to_string())
}

fn parser_fragment_c(content: &[u8]) -> Option<String> {
    let re = Regex::new(r"2827(?P<data>654e.*?)272929293b22").expect("parser fragment b error");

    re.captures_iter(content)
        .last()
        .map(|caps| String::from_utf8_lossy(&caps["data"]).to_string())
}

提取后将A数据根据func0进行解码,然后替换数据,func0的逻辑如下

fn decode(data: &str, key: &str) -> String {
    let mut data_bytes = hex::decode(data).expect("invalid hex input");
    let key_bytes = hex::decode(key).expect("invalid hex key");

    let mut reversed_key = key_bytes.clone();
    reversed_key.reverse();

    // strtr($var1, $var2, strrev($var2));
    for byte in data_bytes.iter_mut() {
        if let Some(pos) = key_bytes.iter().position(|&x| x == *byte) {
            *byte = reversed_key[pos];
        }
    }

    // 嵌套func0 - base64_decode
    hex::encode(
        base64::engine::general_purpose::STANDARD
            .decode(&filter_base64(&data_bytes))
            .expect("base64_decode fragment error"),
    )
}

这里在base64解码的时候做了一层处理,原文中的base64掺杂了很多非base64直接可识别的数据,这些数据会被base64_decode忽略,rust不会,这里需要新增一个函数,把这些数据过滤出来filter_base64实现如下

fn filter_base64(data: &[u8]) -> Vec<u8> {
    let re = Regex::new(r"(?-u)[^A-Za-z0-9+/=]").unwrap();
    re.replace_all(data, &b""[..]).to_vec()
}

A数据提取后,需要对此二次base64解码然后通过zlib进行解压缩的操作拿到最终的数据A,具体解压缩操作如下

use zlib_rs::{InflateConfig, ReturnCode, decompress_slice};

fn decompress(hex_input: &str) -> Result<String, Box<dyn Error>> {
    let bin_data = hex::decode(hex_input)?;

    let decoded_b64 =
        base64::engine::general_purpose::STANDARD.decode(&filter_base64(&bin_data))?;

    let mut decompressed_buf = vec![0u8; decoded_b64.len() * 10];
    let (decompressed, rc) = decompress_slice(
        &mut decompressed_buf,
        &decoded_b64,
        InflateConfig::default(),
    );

    if rc == ReturnCode::Ok {
        Ok(String::from_utf8_lossy(decompressed).into_owned())
    } else {
        Err(format!("zlib-rs decompression failed: {:?}", rc).into())
    }
}

后开始进行完整的数据拼接,将C中的两个变量替换成A和B,替换方案为识别字符串中的".$vara.$varb.",可以直接通过正则222e24(.*?)2e22识别,然后把这部分替换掉即可,代码实现如下

fn splicing_data(fragment_c: &str, decoded_a: &str, fragment_b: &str) -> String {
    let re = regex::Regex::new(r"222e24[a-f0-9]+2e22").expect("splicing regex error");
    let replacement = format!("{}{}", decoded_a, fragment_b);

    re.replace_all(fragment_c, replacement.as_str()).to_string()
}

替换好的数据再次进行解压缩操作即可完成代码的解密。

解密脚本

可直接下载使用

https://github.com/UNSAFE-TEAM/phpjm_decode

完整代码如下

use base64::Engine as _;
use regex::bytes::Regex;
use std::error::Error;
use std::fs;
use std::path::Path;
use zlib_rs::{InflateConfig, ReturnCode, decompress_slice};

fn print_banner() {
    println!(" _   _ _   _ ____    _    _____ _____    _____ _____    _    __  __ ");
    println!("| | | | \\ | / ___|  / \\  |  ___| ____|  |_   _| ____|  / \\  |  \\/  |");
    println!("| | | |  \\| \\___ \\ / _ \\ | |_  |  _| _____| | |  _|   / _ \\ | |\\/| |");
    println!("| |_| | |\\  |___) / ___ \\|  _| | |__|_____| | | |___ / ___ \\| |  | |");
    println!(" \\___/|_| \\_|____/_/   \\_\\_|   |_____|    |_| |_____/_/   \\_\\_|  |_|");
    println!();
    println!("  phpjm_decode  |  @Github UNSAFE-TEAM");
    println!("{}", "- ".repeat(35));
}

fn read_file_to_hex<P: AsRef<Path>>(path: P) -> String {
    fs::read(path)
        .expect("read file error")
        .iter()
        .map(|b| format!("{:02x}", b))
        .collect()
}

fn filter_base64(data: &[u8]) -> Vec<u8> {
    let re = Regex::new(r"(?-u)[^A-Za-z0-9+/=]").unwrap();
    re.replace_all(data, &b""[..]).to_vec()
}

fn decode(data: &str, key: &str) -> String {
    let mut data_bytes = hex::decode(data).expect("invalid hex input");
    let key_bytes = hex::decode(key).expect("invalid hex key");

    let mut reversed_key = key_bytes.clone();
    reversed_key.reverse();

    // strtr($var1, $var2, strrev($var2));
    for byte in data_bytes.iter_mut() {
        if let Some(pos) = key_bytes.iter().position(|&x| x == *byte) {
            *byte = reversed_key[pos];
        }
    }

    // 嵌套func0 - base64_decode
    hex::encode(
        base64::engine::general_purpose::STANDARD
            .decode(&filter_base64(&data_bytes))
            .expect("base64_decode fragment error"),
    )
}

fn parser_fragment_a(content: &[u8]) -> Option<(String, String)> {
    let re = Regex::new(r"a02822(?P<data>[a-f0-9]+?)222c22(?P<key>[a-f0-9]+?)22293b")
        .expect("parser fragment a error");
    re.captures_iter(content).last().map(|caps| {
        (
            String::from_utf8_lossy(&caps["data"]).to_string(),
            String::from_utf8_lossy(&caps["key"]).to_string(),
        )
    })
}
fn parser_fragment_b(content: &[u8]) -> Option<String> {
    let re = Regex::new(r"72657475726e2022(?P<data>[a-f0-9]+?)223b7d7d")
        .expect("parser fragment c error");

    re.captures(content)
        .map(|caps| String::from_utf8_lossy(&caps["data"]).to_string())
}

fn parser_fragment_c(content: &[u8]) -> Option<String> {
    let re = Regex::new(r"2827(?P<data>654e.*?)272929293b22").expect("parser fragment b error");

    re.captures_iter(content)
        .last()
        .map(|caps| String::from_utf8_lossy(&caps["data"]).to_string())
}

fn splicing_data(fragment_c: &str, decoded_a: &str, fragment_b: &str) -> String {
    let re = regex::Regex::new(r"222e24[a-f0-9]+2e22").expect("splicing regex error");
    let replacement = format!("{}{}", decoded_a, fragment_b);

    re.replace_all(fragment_c, replacement.as_str()).to_string()
}

fn decompress(hex_input: &str) -> Result<String, Box<dyn Error>> {
    let bin_data = hex::decode(hex_input)?;

    let decoded_b64 =
        base64::engine::general_purpose::STANDARD.decode(&filter_base64(&bin_data))?;

    let mut decompressed_buf = vec![0u8; decoded_b64.len() * 10];
    let (decompressed, rc) = decompress_slice(
        &mut decompressed_buf,
        &decoded_b64,
        InflateConfig::default(),
    );

    if rc == ReturnCode::Ok {
        Ok(String::from_utf8_lossy(decompressed).into_owned())
    } else {
        Err(format!("zlib-rs decompression failed: {:?}", rc).into())
    }
}

fn main() {
    print_banner();

    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        eprintln!("用法: {} <file>", args[0]);
        std::process::exit(1);
    }

    let input_path = Path::new(&args[1]);

    if !input_path.exists() {
        eprintln!("错误: 文件不存在 -> {}", input_path.display());
        std::process::exit(1);
    }

    if !input_path.is_file() {
        eprintln!("错误: 路径不是一个文件 -> {}", input_path.display());
        std::process::exit(1);
    }

    let input_path = Path::new(&args[1]);

    let file_hex = read_file_to_hex(input_path);

    let mut vara = parser_fragment_a(file_hex.as_bytes())
        .map(|(data, key)| decode(&data, &key))
        .unwrap();
    let varb = parser_fragment_b(file_hex.as_bytes()).unwrap();
    let varc = parser_fragment_c(file_hex.as_bytes()).unwrap();

    vara = hex::encode(decompress(&vara).unwrap());

    let result = splicing_data(&varc, &vara, &varb);
    let decoded = decompress(&result).unwrap();

    let output_path = {
        let stem = input_path.file_stem().unwrap_or_default().to_string_lossy();
        let ext = input_path.extension().unwrap_or_default().to_string_lossy();
        let new_filename = if ext.is_empty() {
            format!("{}.decode", stem)
        } else {
            format!("{}.decode.{}", stem, ext)
        };
        input_path.with_file_name(new_filename)
    };

    fs::write(&output_path, &decoded).expect("写入文件失败");
    println!("已保存至 {}", output_path.display());
}

写在后面

对于PHP这种解释型语言来说,如果仅靠单文件的形式,基本不存在什么加密了,落地即开源,后面会继续介绍phpjiami.com的处理方法。