在PHP脚本审计、数据迁移或接口调试中,常能看到一些看似无规律的字符串。它们可能来自简单的Base64编码,也可能经过自定义异或、字符移位或多层嵌套。逆向解密不是要暴力破解,而是先判断密文特征,再逐步恢复明文。比如尾部一个或两个等号通常是Base64的填充标志,大量百分号说明经过URL编码,只有数字和a到f的连续字符多半是十六进制。理解这些基础特征,可以快速确定第一层处理方式。

一、从字符集识别常见编码
字符串编码识别是最容易入手的一步。Base64的输出字符集包含大写字母、小写字母、数字、加号和斜杠,并且长度通常为4的倍数,不足时用等号补足。PHP内置的base64_decode函数可以处理严格模式,建议加上第二个参数true,当输入包含非法字符时直接返回false,避免静默错误。例如下面这段代码先通过正则判断,再执行解码。
<?php
$str = 'SGVsbG8gV29ybGQ=';
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $str)) {
$result = base64_decode($str, true);
if ($result !== false) {
echo $result;
}
}
?>
URL编码的特征更明显,空格会被替换成加号,非ASCII字符会被替换成百分号加两位十六进制。PHP的urldecode能将百分号序列还原,但需要注意若字符串里同时存在加号,urldecode会把加号转成空格;而rawurldecode不会转换加号,适用于更精确的场景。十六进制编码只包含0到9和a到f,通常成对出现,对应一个字节。hex2bin可以完成解码,但遇到奇数长度会返回false。
实际逆向时,不一定只存在一种编码。很多开发者会把JSON或序列化对象先Base64处理,再整体URL编码,目的是方便放入GET参数。此时应先用urldecode去掉百分号层,再尝试base64_decode,最后再反序列化或解析JSON。顺序判断错误会导致解码结果仍然是乱码,这也是新手容易卡住的地方。
二、理解并逆写异或加密
异或加密是PHP里很常见的轻量级加密方式。它的核心原理是使用一个密钥字节与明文字节做异或运算,相同位不同则结果为1,相同则为0。异或最大的特点是自反性:如果cipher = plain ^ key,那么plain = cipher ^ key。因此加密函数和解密函数完全一致。下面这个函数演示了循环使用密钥进行异或。
<?php
function xorCrypt($data, $key) {
$out = '';
$keyLen = strlen($key);
if ($keyLen === 0) {
return $data;
}
for ($i = 0; $i < strlen($data); $i++) {
$out .= $data[$i] ^ $key[$i % $keyLen];
}
return $out;
}
$plain = 'PHP逆向解密测试';
$cipher = xorCrypt($plain, 'secret');
echo bin2hex($cipher) . "\n";
echo xorCrypt($cipher, 'secret');
?>
如果只拿到密文而没有密钥,逆向难度会明显增加。此时可以从已知明文片段入手,反推密钥。比如知道密文前面原本是{"code":这类JSON开头,就可以用已知明文字节与密文字节异或,得到密钥片段,再通过循环补齐。这个方法在简单的接口数据包分析中非常有效。异或加密的强度取决于密钥长度和随机性,固定短密钥很容易被这种方式恢复。
除了异或,常见的还有ASCII位移。比如加密时把每个字符的ASCII码加3,解密时再减3。这种算法没有密钥保护,几乎没有安全性,但常常被用来做混淆。可以写一个通用遍历脚本,尝试1到25的位移,观察输出是否包含正常文本。字符反转和替换表也属于同类可逆操作,还原时只需要反向调用strrev或把替换表对调。
三、多层编码与自定义变换的还原步骤
真实场景中,字符串往往不是单一算法处理的结果,而是多层嵌套。例如开发者先对原文做异或,再Base64编码,最后把字符串反转。如果直接Base64解码会得到乱码,因为顺序错了。正确的做法是先反转,再Base64解码,最后异或解密。因此逆向的第一步是识别最外层处理。一个实用的技巧是:把可疑字符串丢进调试脚本,依次尝试strrev、base64_decode、hex2bin、urldecode,每一步输出长度和可打印字符比例。
下面这个脚本同时对输入做多种候选解包,并自动过滤掉明显不可打印的结果。它虽然不是万能钥匙,但能帮助快速发现第一层编码。
<?php
$cipher = '...';
$candidates = [];
$candidates[] = base64_decode($cipher, true);
$candidates[] = base64_decode(strrev($cipher), true);
$candidates[] = hex2bin($cipher);
$candidates[] = urldecode($cipher);
$candidates[] = rawurldecode($cipher);
foreach ($candidates as $i => $value) {
if ($value === false || $value === '') {
continue;
}
$printable = preg_match('/^[\x20-\x7E\r\n\t]+$/u', $value);
echo '候选' . $i . ' 长度' . strlen($value) . ' 可打印:' . ($printable ? '是' : '否') . ' 内容:' . $value . "\n";
}
?>
正则里的\x20表示空格,\x7E表示波浪号,这段区间基本覆盖了常见可打印ASCII字符。若候选结果包含中文,可改用mb_check_encoding判断UTF-8。并不是所有密文解出来都必须是可打印文本,但多数字符串加密的目标是传输文本,因此可打印性可以作为有效过滤条件。当结果中只有少量乱码时,也可以人工判断是否缺少了某个步骤。
多层还原的另一个要点是记录每一步的变换,不要尝试一次性写完所有代码。可以先用临时变量保存每一层结果并打印,确认无误后再合并成完整解密函数。这样做能避免因为顺序错误或字符集问题导致后续分析跑偏。
四、编写一个通用的PHP逆向解密函数
当把上面的思路整理成函数后,可以快速处理常见组合。下面这个示例定义了一个smartDecode函数,它按优先级尝试Base64、反转Base64、URL解码后Base64、十六进制,并返回第一个可读结果。实际使用中可以根据日志或抓包数据调整顺序。
<?php
function smartDecode($str) {
$checks = [];
$checks[] = base64_decode($str, true);
$checks[] = base64_decode(strrev($str), true);
$checks[] = base64_decode(urldecode($str), true);
$checks[] = hex2bin($str);
$checks[] = hex2bin(urldecode($str));
foreach ($checks as $value) {
if ($value !== false && $value !== '' && preg_match('/^[\x20-\x7E\r\n\t]+$/u', $value)) {
return $value;
}
}
return $str;
}
$target = 'bWZzSEhJTUIlMkYlMkY=';
echo smartDecode($target);
?>
这个函数并不能覆盖所有场景,但提供了一个可扩展的框架。你可以把自定义异或解密、替换表反转、位移恢复等操作加进去。