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的处理方法。