样本结构
样本第一层省略长字符串后如下:
<?php
$table = base64_decode("...");
$decoder = $table[3].$table[6].$table[33].$table[30];
$decoder .= $table[22].$table[36].$table[29].$table[26];
eval($decoder("..."));
?>
函数名通过字符表索引动态拼接,真实代码放在 eval() 参数中。
第一层
把 eval 改成 echo:
-eval($decoder("..."));
+echo($decoder("..."));
输出仍然是 PHP:
$payload = "...";
eval('?>'.$decoder($transform($payload)));
第二层出现两个结构相同的代码块,输出分别位于 ?> 前后。继续替换新一层的 eval,把输出拼回当前 PHP。
逐层处理
每轮保留一份中间文件:
layer-01.php
layer-02.php
layer-03.php
layer-04.php
执行环境使用无网络容器:
docker run --rm \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
-v "$PWD/work:/work:rw" \
-w /work \
php:8.2-cli \
php layer-01.php > layer-01.out
本次样本的循环逻辑:
from pathlib import Path
import re
import subprocess
work = Path("work")
source = (work / "sample.php").read_text(encoding="utf-8")
for index in range(1, 9):
instrumented = re.sub(r"\beval\s*\(", "print(", source)
layer = work / f"layer-{index:02d}.php"
layer.write_text(instrumented, encoding="utf-8")
result = subprocess.run(
["php", str(layer)],
check=True,
capture_output=True,
text=True,
).stdout
(work / f"layer-{index:02d}.out").write_text(
result,
encoding="utf-8",
)
if result.lstrip().startswith("<?php"):
source = result
else:
source = source.removesuffix("?>") + result + "?>"
该脚本只对应这份样本的输出结构。实际执行时,subprocess.run() 位于上面的无网络容器中。
结果
第四轮输出恢复为正常 PHP 源码,与原分析帖给出的最终结果一致。每层变化固定为:
动态拼接函数名
-> 解码长字符串
-> eval 执行下一层
-> 新的动态拼接函数名

原 Joplin 笔记末尾混入了一段导出元数据,本次整理时已删除。
参考
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。