把明文 PHP 源码打包成 swoole_loader 扩展可以直接加载执行的加密文件。
和官方 Swoole Compiler 是同一类东西:离线加密器(源码不出本机)+ 配套 loader。
2.0 相比 1.0 的三件事:
| 1.0 | 2.0 | |
|---|---|---|
| 依赖 | 要装 Python3 + 目标版本 PHP + opcache | 零依赖:自带 Python + PHP 7.0–8.4 运行时(Linux 还自带 glibc) |
| 授权 | 无 | 机器码 + 有效期 + 版本权益,离线校验(RSA-2048 签名,改一个字就失效) |
| 产物控制 | 只能加密 | 绑定域名 / IP / 到期,产物换个站或过了期就跑不起来 |
| 平台 | Linux | Linux x86_64 + Windows x64 |
# 0) 授权:先拿机器码去换授权文件
./bin/swcenc machine
# 机器码: SWC-XXXX-XXXX-XXXX
# 把机器码发给签发者,拿到 license.json 后放到 ./license/license.json
./bin/swcenc license # 看授权状态
./bin/swcenc doctor # 环境自检:能用哪些 (loader, PHP) 组合
# 1) 加密单个文件(PHP 8.3 / loader 3.2)
./bin/swcenc pack app/common.php -o out/common.php --php 8.3
# 2) 整目录加密 + 逐个用真实 loader 验证
./bin/swcenc batch ./app -o ./app_enc --php 8.3 --verify
# 3) 绑定域名与到期(客户最常用的两个开关)
./bin/swcenc batch ./app -o ./app_enc --php 8.3 \
--domains "example.com,*.example.com" --expire 2027-01-01
Windows 上是同一个用法,把 ./bin/swcenc 换成 bin\swcenc.exe:
bin\swcenc.exe machine
bin\swcenc.exe pack app\common.php -o out\common.php --php 8.3 --domains "example.com"
bin\swcenc.exe doctor
bin\swcenc.exe(同目录还有 bin\swcenc.cmd,等价)。%LOCALAPPDATA%\swcenc(用户级,不弹 UAC),桌面与开始菜单会有「swcenc 加密器」快捷方式;命令行用完整路径
"%LOCALAPPDATA%\swcenc\bin\swcenc.exe" doctor。
想让任意目录直接敲 swcenc,把 %LOCALAPPDATA%\swcenc\bin 自己加进 PATH 即可
(安装器刻意不动你的 PATH)。
不带参数运行就是交互式菜单(1 自检 / 2 看授权 / 3 加密单文件 / 4 加密整目录 / 5 机器码)。
装到系统里(可选):
sudo ./install.sh # Linux:装到 /opt/swcenc 并建 /usr/local/bin/swcenc
产物只有在目标机上装了对应那一个版本的 loader 才能执行:
; php.ini
extension=swoole_loader83_nts.so ; Linux
三条硬规则,先看清再动手:
| 规则 | 说明 |
|---|---|
| 单版本 | 产物只能跑在与加密时相同的 PHP 主次版本上。8.3 加的密只能在 8.3 跑。 |
| 单代数 | loader 3.1 与 3.2 的产物互不通用,头块里写着代数。 |
| Pro 独立 | --pro 产物只能由 swoole_loader_pro 加载;官方 loader 加载会直接段错误。 |
./bin/swcenc versions 会打印当前包里 loader × PHP 的完整支持矩阵。
swcenc <命令> [选项]
命令
pack <文件> -o <输出文件> 打包单个文件
batch <目录> -o <输出目录> 批量打包,保持相对路径
info <文件> 查看容器信息
unpack <文件> -o <输出二进制> 解出容器载荷
versions loader × PHP 支持矩阵
caps 各版本能力分层(JSON)
doctor 环境自检(含授权状态与内置运行时)
machine 打印本机机器码(换授权用)
license 查看授权状态
通用选项(pack / batch)
--php 7.3 | 8.1 ... 目标 PHP 版本(必须与目标服务器一致)
--loader-version 3.1|3.2 写进头块的 loader 代数(默认 3.2)
--mode strong|weak strong=字节码载荷(默认);weak=源码随容器下发
--verify 打包后用真实 loader 执行一遍做验证(Linux)
-j, --jobs N batch 的并发数
产物绑定
--domains a.com,*.b.com 只允许这些域名运行(Web 请求;CLI 默认放行)
--ips 1.2.3.4,... 只允许这些来访 IP
--expire 2027-01-01 到期日(当天结束前仍可用;Web 与 CLI 都会拦)
--bind-cli CLI 也检查域名(默认不检查,见下)
--bind-quiet 绑定不通过时不提示原因,只回 403
加固 / Pro
--pro Pro 层(只能配 Pro loader)
--swc-strict / --swc-calibrate / --swc-encode-ops
与官方同款的诱饵字节 / 编码态 op 等加固
--guard / --no-guard 运行期反分析自检层(--pro 默认开)
--scatter off|light|full|mangle 粉碎性重组打散(--pro 默认 full)
授权
--license <文件> 指定授权文件路径
为什么需要:加密只保证「代码不被读」,不保证「谁都能跑」。客户把加密包交付给下线之后,
真正要控制的是「这份产物能在哪个站、到什么时候为止运行」。
./bin/swcenc pack app.php -o app.enc.php --php 8.3 \
--domains "customer.com,*.customer.com" \
--ips "1.2.3.4" \
--expire 2027-01-01
行为(三条都是刻意的设计):
那是售后灾难而不是保护。要连 CLI 一起绑,加 --bind-cli。
*.example.com 匹配 a.example.com、b.a.example.com,也匹配 example.com 本身。
绑定层与源码一起被编译进载荷(不是可读的配置),命中不允许的环境时输出:
This script is not licensed to run here (host evil.com).
This script is not licensed to run here (expired on 2027-01-01).
绑定检查发生在每次请求,开销是一次数组比较,可以忽略。
授权是离线的:客户机器不需要联网,签发者也不需要部署授权服务器。
./bin/swcenc machine # 客户把这一行发给签发者
./bin/swcenc license # 授权状态、功能位、可绑定域名
授权文件(license/license.json)由签发者用私钥签名,工具内置公钥校验:
machines(机器码列表)与 not_after(到期日),换机器或过期就用不了;edition)决定哪些功能可用:| edition | strong | batch | pro | bind | decoy |
|---|---|---|---|---|---|
| trial | ✅ | ||||
| std | ✅ | ✅ | |||
| pro | ✅ | ✅ | ✅ | ✅ | |
| enterprise | ✅ | ✅ | ✅ | ✅ | ✅ |
授权文件按这个顺序查找:--license 指定 → SWCENC_LICENSE → 包内 license/license.json
→ ~/.swcenc/license.json → /etc/swcenc/license.json。
机器码由「系统 id + 第一块物理网卡 MAC + CPU 型号」算出(Windows 用 MachineGuid +
系统卷序列号 + MAC)。容器里 MAC 每次重建都会变,所以别把加密器装在会重建的容器里,
否则会一直要重新授权。
| 组件 | Linux | Windows |
|---|---|---|
| 解释器 | 自带 Python 3.12(含标准库) | 自带免安装 Python 3.12 |
| PHP 运行时 | 自带 PHP 7.0–8.4(runtime/php/<版本>/) | 自带官方 NTS x64 的 php.exe + php_opcache.dll |
| 系统库 | 自带 glibc 与全部依赖 .so(runtime/lib/,用自带的 ld.so 启动) | 自带 VC 运行时(vcruntime140*.dll) |
所以客户机上什么都不用装 —— 实测在一台「没有 glibc、没有 PHP、没有 Python」的
Alpine 容器里,license / doctor / pack --verify 与产物执行全部正常。
引擎不是以 .py 形式交付的:
| 平台 | 引擎形态 | 包里 .py 数量 |
|---|---|---|
| Linux | 12 个模块 Cython 编译成 .so(原生机器码) | 0 |
| Windows | 编译成 .pyc 字节码 | 0 |
授权校验写在编译产物里:ENFORCED_BY_BUILD 这个常量在打包时被烘焙成 True,
所以 SWCENC_ENFORCE=0 ./bin/swcenc pack ... 这种绕过尝试照样被授权拦住 ——
"要不要校验授权"只存在于编译出来的机器码里,既改不了也没有源码可改。
doctor 会明确告诉你用的是内置运行时还是系统里找到的:
✅ 内置运行时:/opt/swcenc/runtime
PHP 10 个版本,Python:自带 —— 客户机不需要装任何东西
2.0.0 这一轮的引擎不是 Python 源码,也不是中间字节码,而是原生机器码:
| 平台 | 引擎形态 | 说明 |
|---|---|---|
| Linux | Cython 编译的 .so(每个模块一个)+ engine/libswcenc.so(入口) | 入口导出 swcenc_main(argc, argv) |
| Windows | .pyd 原生扩展(mingw 交叉编译,链接官方 python312.dll) | 不再是 .pyc 字节码 |
外面只剩两层壳,都不含加密逻辑:
bin/swcenc 37 行 shell —— 用自带 glibc 加载器 + 自带库目录把下面那个拉起来
└─ bin/swcenc.bin 40 行 C —— dlopen(engine/libswcenc.so) → swcenc_main(argc, argv)
└─ engine/libswcenc.so 全部逻辑(嵌入式 CPython + 引擎模块)
Linux 上可以直接调这个 .so(不经命令行):
void *h = dlopen("<root>/engine/libswcenc.so", RTLD_NOW);
int (*run)(int, char **) = dlsym(h, "swcenc_main");
char *argv[] = {"swcenc", "pack", "a.php", "-o", "b.php", "--php", "8.3"};
run(5, argv);
前提是进程能用到自带运行时:SWCENC_ROOT=<root>、PYTHONHOME=<root>/runtime/python,
并让 <root>/runtime/lib 可被加载(PHP FFI / ctypes / 你自己的 C 程序都可以)。
./bin/swcenc --engine-diag 会打印引擎环境诊断,排障先看它。
打散 / 符号分析 / __FILE__ 路径修正这三段核心逻辑是 PHP 写的,必须由目标 PHP 执行,
所以不能编译。它们现在以 .enc(HMAC-SHA256 计数器流加密 + 认证标签) 的形式随包,
运行时解密到同目录的隐藏临时文件、用完立即删除;密钥在构建时烘焙进编译产物
(包里既没有明文密钥,也没有可读源码)。
loader31/32、loaderpro31/32、rev/opdump、以及引擎入口 libswcenc.so
都用 phpshield 打成自解密容器:磁盘上只有壳,真正的镜像在调用时才解密进匿名内存。
已知边界:phpshield 目前是一个进程一个壳的设计(PHP 扩展正是这种模型)。
引擎的 Python 模块要在同一进程里加载十几个,实测只有第一个能加载成功,
因此它们保持 Cython 机器码、不做容器加固。要在 Python 场景也套容器,
需要让 phpshield 支持"同进程多实例"(内部镜像的加载与去重那块),这个还没做。
每个用户下载到的副本,engine/pack/wm.so(Windows 为 wm.pyd)里的三段占位串
被等长替换成该用户的标记(另有启动横幅、WATERMARK.txt、WATERMARK.json)。
水印在编译产物里,删掉那两个文本文件也带不走。
swcenc-2.0.0-linux-x86_64/ swcenc-2.0.0-win64/
├─ bin/swcenc 启动器 ├─ bin/swcenc.exe 启动器(原生,双击出菜单)
│ └─ bin/swcenc.cmd 备用启动器
├─ runtime/ ├─ runtime/
│ ├─ lib/ 自带 glibc + .so │ ├─ php/<版本>/bin/php.exe + php8.dll
│ ├─ php/70..84/ │ │ ext/php_opcache.dll
│ └─ python/ 自带解释器 │ └─ python/ 免安装解释器
├─ engine/ 引擎(无 .py:Linux 为 .so,Windows 为 .pyc)
│ ├─ pack/ 打包器 + 自检层 + 绑定层 + 授权模块(编译产物)
│ ├─ rev/ 逆向数据(op 表 / 诱饵表 / vm 规格,编译产物)
│ ├─ loader31/ loader32/ 官方 loader(--verify 与交付给目标机)
│ └─ loaderpro31/ loaderpro32/ Pro loader
├─ license/ public_key.pem + 放 license.json 的地方
├─ examples/
└─ README.md USAGE.md INSTALL.md
Q:--verify 报「真实 loader 加载执行通过 ✅」是什么意思?
产物真的被对应版本的 loader 加载并执行了一遍,输出与原文件一致。这是最硬的验证。
Q:Windows 上为什么不做 --verify?
swoole_loader 是 Linux 扩展(.so),Windows 的 PHP 加载不了它。Windows 上打包会明确
提示跳过验证,请把产物部署到目标服务器验证。需要逐次验证的 PRO 产物建议在 Linux 机器上打包。
Q:哪些 PHP 版本实测能跑通?
发行包里带真实 loader 逐个组合跑过的结果(Alpine 容器,--verify 通过才算数):
| 目标 PHP | loader 3.1 | loader 3.2 | 说明 |
|---|---|---|---|
| 7.0 / 7.1 / 7.2 | ❌ | ❌ | 加载时段错误(见下一条) |
| 7.3 / 7.4 | ✅ | ✅ | 含 --swc-strict |
| 8.0 / 8.1 | ✅ | ✅ | |
| 8.2 / 8.3 / 8.4 | —(官方 loader 3.1 没有这几个版本) | ✅ | 含 --pro(8.3 实测) |
Q:7.0 / 7.1 / 7.2 为什么不行?
这是引擎本身的现状,不是本工具引入的:用 swoole_loader70/71/72_nts.so 加载对应版本
产出的产物会在载入时段错误(实测环境:PHP 7.0.33 / 7.1.33 / 7.2.33,带与不带 opcache
都一样;同样的问题在线上服务上一模一样地复现)。所以:
--pro / --swc-strict 另有限制:引擎会 fail-closed(宁可报错也不产出被弱化的"PRO 产物")。
Q:Windows 上能用 --pro / --swc-strict 吗?
只能配 PHP 7.0 / 7.1。7.2+ 的加固层要用一个只在 Linux 上构建的转储扩展,Windows 的 PHP
加载不了它(.so 不是 .dll),所以那两档在 Windows 上会被明确拒绝(不会偷偷降级成弱产物)。
要出加固 / Pro 产物、或者要逐文件 --verify,在 Linux 上打包 —— 那边是全组合支持的。
Windows 上出普通 strong 产物、做产物绑定、取机器码、管理授权都没问题。
Q:产物换目录部署,路径会不会错?
不会。打包时会抹掉载荷里内嵌的构建路径(__FILE__ / __DIR__ 相关),
产物放到哪台机器、哪个目录都能跑。
Q:为什么引擎目录里看不到 .py 文件?
故意的。引擎已编译(Linux .so / Windows .pyc),授权校验也在编译产物内。
改动这些文件会让工具无法启动。
Q:Windows 上装的是 setup.exe,不想装怎么办?
用便携包 swcenc-2.0.0-win64.zip,解压后运行 bin\swcenc.exe,不写注册表也不进 PATH。
安装包同样可以随时在「程序和功能」里卸载干净。
Q:weak 模式是什么?
源码随容器下发(有 loader 守卫,但源码以明文存在于文件里)。适合本机调试或
只要「不被随手改」的场景。正式交付请用默认的 strong。
Q:同一个项目要同时支持 PHP 7.4 和 8.3 怎么办?
Swoole 是单版本加密,所以分别打包两次(--php 7.4 / --php 8.3),
在目标机上按 PHP 版本选用对应产物。
--pro + --scatter full + --guard是当前最强的组合。
license/public_key.pem 只能用于校验,不能签发。docs/README.md)。
下载发行包后可离线查看同样的内容。