简介:这份PHP在线解密工具源码V2.0面向需要还原加密PHP代码的开发者与安全研究人员,提供一套无需数据库、上传至服务器访问域名即可使用的在线解密方案。工具支持Zend(PHP5.2/5.3/5.4)、易盾1.x与2.x、phpjm、tianyiw、zym、威盾/微盾等多种加密方式,并可尝试处理部分goto、Enphp、vidun、神盾等不常见混淆,同时具备代码美化、混淆修复与ionCube识别能力。压缩包共11个文件,以js脚本、css样式与html页面为主,另有url说明文件,整体约398KB,结构轻量便于快速部署。目前已有196人学习下载。读者可获得完整的在线解密站点源码,直接搭建属于自己的解密服务,并通过更新日志了解魔方二代、多层eval解密等功能的迭代思路,适合用于代码还原、安全分析与学习研究场景。
1. 在线 PHP 解密工具到底在解什么:从混淆代码到可读源码的那条路
拿到一份 PHP 文件,打开一看全是eval(gzinflate(base64_decode('...')))套娃,或者满屏$O0O0O0这种变量名,这是很多做二次开发、代码审计、老系统维护的人都会遇到的场景。所谓「在线 PHP 解密工具」,本质是把经过编码、压缩、混淆的 PHP 代码还原成人类可读形式的一套流程,常见处理对象包括 base64 多层嵌套、gzinflate/gzuncompress 压缩、字符串拼接混淆、变量名替换这几类。它解决的不是「破解授权」,而是让你能看懂自己手上这份代码到底在干什么。适合谁?接手遗留系统的后端、做源码审计的安全同学、需要二次开发但拿不到原始工程的人。这篇就把这套工具从原理到落地拆开讲,重点放在你能自己复现的代码和参数上。
2. 先搞清楚 PHP 混淆的四种常见形态与识别方法
在动手写解密工具之前,得先能判断手上这份代码属于哪一类混淆。判断错了,后面的解码步骤全是白费功夫。我一般拿到文件先做三件事:看开头特征、数嵌套层数、跑一次静态扫描。
2.1 四种形态的特征与对应解码思路
第一种是纯 base64 编码,特征是文件里出现大段[A-Za-z0-9+/=]组成的字符串,通常被base64_decode包裹。这种最好处理,直接解码就能看到原文,但要注意它经常是多层的,解一次还是 base64。
第二种是压缩类,常见函数是gzinflate、gzuncompress、gzdecode。特征是解码后的内容是二进制乱码,需要用对应的解压函数再处理一次。这类往往和 base64 组合出现,形成gzinflate(base64_decode(...))的经典结构。
第三种是字符串拼接混淆,把关键函数名和变量拆成多个片段再用.拼接,比如$a = 'ba'.'se'.'64_'.'de'.'code';。这种不能靠单纯解码,需要做常量折叠,把拼接结果算出来再替换。
第四种是变量名与结构混淆,典型是$O0O0O0、$lIlIlI这种,配合goto或者大量if跳转打乱控制流。这类最难自动化还原,工具能做的通常只是把变量名规范化,逻辑还得人工读。
| 形态 | 识别特征 | 解码方式 | 自动化难度 |
|---|---|---|---|
| base64 编码 | 长串 base64 字符 | base64_decode | 低 |
| 压缩类 | 解码后二进制乱码 | gzinflate/gzuncompress | 低 |
| 字符串拼接 | 函数名被.拆分 | 常量折叠 | 中 |
| 变量名混淆 | $O0O0O0类命名 | 重命名规范化 | 高 |
2.2 用一段 PHP 脚本自动识别混淆类型
与其一个个手动看,不如写个识别脚本。下面这段代码扫描文件内容,输出它命中的混淆特征,作为后续解码的入口判断。
<?php // 混淆类型识别脚本 function detectObfuscation($code) { $result = []; // 检测 base64:连续 40 字符以上的 base64 串 if (preg_match('/[A-Za-z0-9+\/]{40,}={0,2}/', $code)) { $result[] = 'base64'; } // 检测压缩函数 foreach (['gzinflate', 'gzuncompress', 'gzdecode'] as $fn) { if (stripos($code, $fn) !== false) { $result[] = 'compress:' . $fn; } } // 检测 eval 嵌套 if (preg_match('/eval\s*\(/i', $code)) { $result[] = 'eval'; } // 检测字符串拼接混淆:形如 'xx'.'yy' if (preg_match("/'[^']{1,6}'\s*\.\s*'[^']{1,6}'/", $code)) { $result[] = 'concat'; } // 检测变量名混淆:$O0O0 或 $lIlI 类 if (preg_match('/\$[O0Il1]{4,}/', $code)) { $result[] = 'varname'; } return $result ?: ['unknown']; } $code = file_get_contents($argv[1]); print_r(detectObfuscation($code));逻辑说明:preg_match里的{40,}是 base64 串长度阈值,设太小会误判普通字符串,设太大漏掉短编码块,40 是我实测比较稳的值。concat那条正则限制单段长度 1 到 6,是因为混淆器通常把函数名拆成 2 到 4 字符的小块,太长会匹配到正常的字符串拼接。varname用[O0Il1]字符集,正好覆盖视觉混淆常用的几个字符。
参数调整建议:如果你的样本 base64 串普遍较短,把 40 降到 24;如果误报多,升到 60。识别结果只是入口,不代表最终解码路径,实际处理时按compress优先、base64其次的顺序剥。
3. 用 PHP 写一个可复现的多层解码器
识别完类型,接下来是核心:把嵌套的编码一层层剥开。这里的关键是「循环解码直到不再变化」,而不是写死解码次数,因为不同样本嵌套层数不一样。
3.1 解码主循环的写法与终止条件
下面这段是解码器的主干,思路是每轮尝试 base64 和 gzinflate,只要内容发生变化就继续,直到稳定。
<?php // 多层解码器核心 function decodeLayer($code) { $changed = true; $round = 0; $maxRound = 20; // 防止死循环 while ($changed && $round < $maxRound) { $changed = false; $round++; // 尝试提取 base64 串并解码 if (preg_match_all('/[A-Za-z0-9+\/]{40,}={0,2}/', $code, $m)) { foreach ($m[0] as $b64) { $decoded = base64_decode($b64, true); if ($decoded !== false && $decoded !== $b64) { // 只替换可打印或含压缩特征的结果 if (isReadable($decoded) || hasCompressMark($decoded)) { $code = str_replace($b64, $decoded, $code); $changed = true; } } } } // 尝试解压 foreach (['gzinflate', 'gzuncompress', 'gzdecode'] as $fn) { if (function_exists($fn)) { $tmp = @$fn($code); if ($tmp !== false && $tmp !== $code) { $code = $tmp; $changed = true; } } } } return $code; } function isReadable($s) { // 可打印字符占比超过 85% 视为可读 $printable = preg_replace('/[^\x20-\x7E\r\n\t]/', '', $s); return strlen($s) > 0 && strlen($printable) / strlen($s) > 0.85; } function hasCompressMark($s) { // gzip/zlib 头特征 return strpos($s, "\x1f\x8b") === 0 || strpos($s, "\x78\x9c") === 0; }逻辑说明:$maxRound = 20是硬性保险,防止某些自引用样本导致死循环,实测正常样本 3 到 6 轮就稳定。base64_decode第二个参数传true是严格模式,遇到非法字符返回 false,避免把普通字符串误当 base64 解出乱码。isReadable用可打印字符占比判断,85% 这个阈值是经验值,太低会把二进制当文本,太高会漏掉含少量控制字符的合法代码。hasCompressMark检查 gzip 的\x1f\x8b和 zlib 的\x78\x9c头,命中就直接交给解压函数。
3.2 处理 eval 嵌套:把动态执行转成静态文本
很多混淆样本最后一步是eval($decoded),工具要做的不是执行它,而是把eval里的内容提取出来当文本处理。下面这段负责把eval参数抽出来。
<?php // 提取 eval 内部表达式,不执行 function extractEval($code) { $results = []; $offset = 0; while (($pos = stripos($code, 'eval', $offset)) !== false) { $start = strpos($code, '(', $pos); if ($start === false) break; // 括号配对计数 $depth = 0; $i = $start; $len = strlen($code); for (; $i < $len; $i++) { if ($code[$i] === '(') $depth++; elseif ($code[$i] === ')') { $depth--; if ($depth === 0) break; } } $inner = substr($code, $start + 1, $i - $start - 1); $results[] = $inner; $offset = $i; } return $results; }逻辑说明:这段用括号配对而不是正则,是因为eval内部经常嵌套多层括号和字符串,正则很难正确匹配。$depth计数从(开始加、)减,归零时就是表达式结束位置。提取出来的$inner是字符串形式的表达式,可以继续丢回decodeLayer处理。注意这里全程不调用eval,避免执行未知代码带来的风险,这是做这类工具的安全底线。
参数说明:如果样本里eval后面跟的是变量而不是直接表达式,比如eval($a),这段只能提取到$a,需要配合变量赋值追踪才能拿到真实内容,那属于进阶部分,后面章节讲。
4. 避坑与排查:解码工具最容易翻车的五个地方
这类工具看着简单,实际踩坑不少。下面五条是我和身边人真实遇到过的,按「现象 → 原因 → 解决」写清楚。
4.1 解码后还是乱码
现象:跑完解码器,输出依然是一堆看不懂的字符。原因通常是漏了某一层,比如样本是base64 → gzinflate → base64三层,而你的循环里解压和 base64 的先后顺序不对,导致中间结果被跳过。解决:把解码顺序改成「先扫 base64 再扫压缩」交替进行,而不是固定顺序,或者干脆每轮两种都试一遍,这也是 3.1 里主循环的写法。
4.2 严格模式把合法 base64 判成非法
现象:base64_decode($s, true)返回 false,但肉眼看着就是合法 base64。原因:样本里的 base64 串带了换行或空格,严格模式不容忍这些字符。解决:解码前先preg_replace('/\s+/', '', $s)去掉空白,再走严格模式。别直接关掉严格模式,否则普通字符串会被误解码成乱码,反而更难排查。
4.3 死循环把内存跑满
现象:脚本卡住不返回,内存持续上涨。原因:某些样本解码后内容里又包含了自己,形成自引用,循环永远不终止。解决:除了$maxRound上限,再加一个「内容哈希去重」,每轮记录md5($code),如果哈希重复就跳出。这个后悔药一定要提前加,不然只能强杀进程。
4.4 变量名混淆还原后逻辑对不上
现象:变量名规范化成$var1、$var2后,代码能读了,但执行逻辑和预期不符。原因:变量名混淆常配合控制流打乱,单纯重命名不改变跳转顺序,读起来还是乱的。解决:这类样本别指望全自动,工具只做「去混淆第一层」,把goto和跳转标签列出来,人工重建控制流。认清工具边界,比硬堆功能有用。
4.5 处理大文件时超时
现象:几十 MB 的样本跑几分钟没结果。原因:主循环里每轮都做全文件正则匹配,复杂度是 O(轮数 × 文件大小)。解决:先按行或按块切分,只对含编码特征的块做解码,普通代码行直接跳过。另外把preg_match_all的结果缓存起来,避免重复扫描同一段。
提示:做这类工具时,永远不要在服务端直接执行样本代码,所有处理都当纯文本对待。这是安全红线,也是这类工具能放心用的前提。
5. 从能解到好用:批量处理与结果校验的两个技巧
单文件解码跑通后,实际工作里更多是批量处理一整个目录,而且解完还得验证结果对不对。这一章讲两个让工具从「能用」到「好用」的技巧。
5.1 批量解码时的目录遍历与结果落盘
批量处理的核心是遍历、解码、按原路径落盘,同时记录处理日志,方便回溯哪些文件解失败。
<?php // 批量解码入口 function batchDecode($srcDir, $outDir) { $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($srcDir) ); $log = []; foreach ($it as $file) { if ($file->getExtension() !== 'php') continue; $path = $file->getPathname(); $rel = substr($path, strlen($srcDir) + 1); $code = file_get_contents($path); $decoded = decodeLayer($code); $target = $outDir . '/' . $rel; @mkdir(dirname($target), 0755, true); file_put_contents($target, $decoded); // 记录是否还有 eval 残留,作为校验信号 $log[$rel] = [ 'size_before' => strlen($code), 'size_after' => strlen($decoded), 'eval_left' => substr_count($decoded, 'eval'), ]; } file_put_contents($outDir . '/decode_log.json', json_encode($log, JSON_PRETTY_PRINT)); return $log; }逻辑说明:RecursiveIteratorIterator配合RecursiveDirectoryIterator是遍历目录树的标准写法,比手写递归稳。getExtension() !== 'php'过滤非 PHP 文件,避免把图片、日志也当代码处理。日志里记size_before和size_after,如果两者相等,说明这个文件根本没解出东西,需要单独看。eval_left统计解码后还剩几个eval,正常应该降到 0 或很少,残留多说明还有没剥干净的层。
参数说明:mkdir的0755是目录权限,按你的部署环境调整。日志用 JSON 落盘而不是直接打印,是因为批量场景下文件多,落盘后可以用脚本再分析,比如筛出size_before == size_after的文件重点排查。
5.2 用「可读性评分」快速筛出没解干净的文件
批量跑完,几百个文件不可能一个个看。我一般用一个简单的可读性评分,把可疑文件排到前面。
| 评分项 | 权重 | 说明 |
|---|---|---|
| 可打印字符占比 | 40% | 低于 85% 扣分 |
| eval 残留数量 | 30% | 每残留 1 个扣 10 分 |
| base64 长串数量 | 20% | 每命中 1 个扣 5 分 |
| 变量名混淆密度 | 10% | $O0O0类变量占比 |
评分低于 60 的文件,基本可以判定没解干净,优先人工介入。这个表不是标准,是我自己调出来的经验权重,你可以按样本特点改。比如你的样本压缩层特别多,就把 base64 长串的权重提上去。
5.3 一个容易被忽略的校验点:编码一致性
解码后的文件如果原本是 UTF-8,解出来变成乱码中文,多半是编码在解码过程中被破坏了。解决:解码前先检测原文件编码,用mb_detect_encoding判断,解码后如果可读性评分正常但中文乱码,尝试用mb_convert_encoding转回原编码。这个坑不常遇到,但遇到一次能查半天,因为你会以为是解码逻辑错了,其实是编码问题。
我自己的习惯是,每处理一批新样本,先拿 3 到 5 个文件手动跑一遍完整流程,确认解码路径和评分都正常,再放开批量。这样比一上来就全量跑、出问题再回头查要省时间。这套工具的价值不在于「一键还原所有混淆」,而在于把重复的、机械的剥壳工作自动化,把人的精力留给真正需要理解的控制流和业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取