写在前面
最近在工作中遇到了一些高度混淆的PHP恶意文件,经分析发现其使用了第三方通用加密平台提供的加密服务,文件特征分别对应 phpjm.net 与 phpjiami.com 两个平台。本文以 phpjm.net 为研究对象,对其加密机制展开学习与分析,并进行解密还原。
混淆分析
先编写一段示例代码用来做测试,代码如下
<?phpif ($_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: 1833596Website: http://www.phpjm.net/Copyright (c) 2012-2026 phpjm.net All Rights Reserved.*/if (!defined("ECFFAFDC")) {define("ECFFAFDC", __FILE__);global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;function ��($��, $��� = ""){global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;if (empty($���)) {returnbase64_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������");returntrue; ?>0247589c0301a1ae1ea887304a867032
直接看虽然大部分都看不懂,但中间部分的变量仍能看出明显的 base64 编码特征,并且里面有一个位置base64_decode并没有混淆,这部分代码如下
// define("ECFFAFDC", __FILE__);// global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;function ��($��, $��� = ""){// global $�, $��, $���, $����, $�����, $������, $�������, $��������, $���������, $����������, $����������, $������������, $�������������, $��������������, $���������������, $���������������;if (empty($���)) {returnbase64_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解码后返回returnbase64_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($���)) {returnbase64_decode($��); } else {return ��(strtr($��, $���, strrev($���))); } }// 美化后functionfunc0($var1, $var2 = ""){if (empty($var2)) {returnbase64_decode($var1); } else {returnfunc0(strtr($var1, $var2, strrev($var2))); } }
后续通过下面脚本复原其他第二部分内容
<?phpfunctionfunc0($var1, $var2 = ""){if (empty($var2)) {returnbase64_decode($var1); } else {returnfunc0(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"/"; }
第三部分代码掺杂了第一部分内容,但是最终返回内容仅为/(根据加密代码更变,不固定),初次分析时由于关注点在最终返回值上,忽略了这段代码实际执行的内容,但是他与后面执行流高度相似,这里把从上面找到的函数全都替换过来,这部分代码如下
functionfunc2(&$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������");returntrue; ?>0247589c0301a1ae1ea887304a867032
这里基于上面已经分析过的内容继续进行置换,代码如下
<?phpfunctionfunc0($var1, $var2 = ""){if (empty($var2)) {returnbase64_decode($var1); } else {returnfunc0(strtr($var1, $var2, strrev($var2))); }}functionfunc2(&$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������");returntrue; ?>0247589c0301a1ae1ea887304a867032
通过之前的办法把data1解密出来,然后data2说白了就直接写/即可,然后替换部分就不替换了,这里/e参数是直接执行了,我们把执行内容进行输出,美化如下
<?php$data1 = gzuncompress(base64_decode("eNoz6fUwAwADQQFA"));$data2 = "/";echogzuncompress(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 = "/";echogzuncompress(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(.*?)223b7d7dstr - 数据C提取 = last(2827654e(.*?)272929293b22)// 数据C匹配654e原因是因为处理源代码前会增加 `?><?php` 这类字样前面7个是固定的,经过多次测试,前面的654e也是固定的,可以作为标识使用
这里以rust代码为例,实际提取逻辑代码如下
fnparser_fragment_a(content: &[u8]) ->Option<(String, String)> {letre = 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(), ) })}fnparser_fragment_b(content: &[u8]) ->Option<String> {letre = 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())}fnparser_fragment_c(content: &[u8]) ->Option<String> {letre = 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的逻辑如下
fndecode(data: &str, key: &str) ->String {letmut data_bytes = hex::decode(data).expect("invalid hex input");letkey_bytes = hex::decode(key).expect("invalid hex key");letmut reversed_key = key_bytes.clone(); reversed_key.reverse();// strtr($var1, $var2, strrev($var2));forbytein data_bytes.iter_mut() {ifletSome(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实现如下
fnfilter_base64(data: &[u8]) ->Vec<u8> {letre = 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};fndecompress(hex_input: &str) ->Result<String, Box<dyn Error>> {letbin_data = hex::decode(hex_input)?;letdecoded_b64 = base64::engine::general_purpose::STANDARD.decode(&filter_base64(&bin_data))?;letmut 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识别,然后把这部分替换掉即可,代码实现如下
fnsplicing_data(fragment_c: &str, decoded_a: &str, fragment_b: &str) ->String {letre = regex::Regex::new(r"222e24[a-f0-9]+2e22").expect("splicing regex error");letreplacement = 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};fnprint_banner() {println!(" _ _ _ _ ____ _ _____ _____ _____ _____ _ __ __ ");println!("| | | | \\ | / ___| / \\ | ___| ____| |_ _| ____| / \\ | \\/ |");println!("| | | | \\| \\___ \\ / _ \\ | |_ | _| _____| | | _| / _ \\ | |\\/| |");println!("| |_| | |\\ |___) / ___ \\| _| | |__|_____| | | |___ / ___ \\| | | |");println!(" \\___/|_| \\_|____/_/ \\_\\_| |_____| |_| |_____/_/ \\_\\_| |_|");println!();println!(" phpjm_decode | @Github UNSAFE-TEAM");println!("{}", "- ".repeat(35));}fnread_file_to_hex<P: AsRef<Path>>(path: P) ->String { fs::read(path) .expect("read file error") .iter() .map(|b| format!("{:02x}", b)) .collect()}fnfilter_base64(data: &[u8]) ->Vec<u8> {letre = Regex::new(r"(?-u)[^A-Za-z0-9+/=]").unwrap(); re.replace_all(data, &b""[..]).to_vec()}fndecode(data: &str, key: &str) ->String {letmut data_bytes = hex::decode(data).expect("invalid hex input");letkey_bytes = hex::decode(key).expect("invalid hex key");letmut reversed_key = key_bytes.clone(); reversed_key.reverse();// strtr($var1, $var2, strrev($var2));forbytein data_bytes.iter_mut() {ifletSome(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"), )}fnparser_fragment_a(content: &[u8]) ->Option<(String, String)> {letre = 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(), ) })}fnparser_fragment_b(content: &[u8]) ->Option<String> {letre = 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())}fnparser_fragment_c(content: &[u8]) ->Option<String> {letre = 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())}fnsplicing_data(fragment_c: &str, decoded_a: &str, fragment_b: &str) ->String {letre = regex::Regex::new(r"222e24[a-f0-9]+2e22").expect("splicing regex error");letreplacement = format!("{}{}", decoded_a, fragment_b); re.replace_all(fragment_c, replacement.as_str()).to_string()}fndecompress(hex_input: &str) ->Result<String, Box<dyn Error>> {letbin_data = hex::decode(hex_input)?;letdecoded_b64 = base64::engine::general_purpose::STANDARD.decode(&filter_base64(&bin_data))?;letmut 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()) }}fnmain() {print_banner();letargs: Vec<String> = std::env::args().collect();if args.len() < 2 {eprintln!("用法: {} <file>", args[0]); std::process::exit(1); }letinput_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); }letinput_path = Path::new(&args[1]);letfile_hex = read_file_to_hex(input_path);letmut vara = parser_fragment_a(file_hex.as_bytes()) .map(|(data, key)| decode(&data, &key)) .unwrap();letvarb = parser_fragment_b(file_hex.as_bytes()).unwrap();letvarc = parser_fragment_c(file_hex.as_bytes()).unwrap(); vara = hex::encode(decompress(&vara).unwrap());letresult = splicing_data(&varc, &vara, &varb);letdecoded = decompress(&result).unwrap();letoutput_path = {letstem = input_path.file_stem().unwrap_or_default().to_string_lossy();letext = input_path.extension().unwrap_or_default().to_string_lossy();letnew_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的处理方法。