使用 Pyarmor 混淆 Python 代码
简介
Pyarmor 是支持如下功能的命令行工具:
- 混淆 Python 脚本。
- 将混淆脚本绑定到特定机器。
- 为混淆脚本设置过期时间。
通过 PyPI 安装
Pyarmor 包发布在 PyPI 上。推荐使用 pip 从 PyPI 安装包。所有现代版本的 Python 都自带该工具。
在 Linux 或 macOS 上,打开终端,运行以下命令:
pip install -U pyarmor在 Windows 上,打开命令提示符(按 Win + R,输入 cmd),然后运行相同的命令:
pip install -U pyarmor安装完成后,在命令提示符中输入 pyarmor --version。如果一切正常,将看到刚刚安装的 Pyarmor 包的版本号。
快速入门
如果想要了解更多内容,请访问:https://pyarmor.readthedocs.io/zh/latest/。
混淆单个脚本
下面的命令混淆单个脚本 foo.py:
pyarmor gen foo.py可以使用 g 或 generate 替换命令 gen :
pyarmor g foo.py
pyarmor generate foo.py该命令生成混淆脚本 dist/foo.py。可以用 Python 解释器运行该脚本:
python dist/foo.py检查默认输出路径中的所有生成文件:
$ ls dist/
foo.py pyarmor_runtime_000000Python 包 pyarmor_runtime_000000 是运行混淆脚本所必需的。
分发混淆脚本
仅将 dist/foo.py 复制到另一台机器上无法正常运行。应当复制 dist/ 目录中的所有文件。
检查 dist/foo.py 的内容后可以清楚地看到原因:
from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, ...)可以将混淆脚本视为普通 Python 脚本,只不过其依赖 pyarmor_runtime_000000 包。使用方式与未混淆脚本相同。
重要提示:请在相同 Python 版本和相同平台的机器上运行混淆脚本,否则将无法正常工作。因为
pyarmor_runtime_000000包含扩展模块,该模块依赖具体平台,并且绑定到 Python 版本。
注意:目标设备不用安装 Pyarmor。Python 解释器无需 Pyarmor,也可以运行混淆脚本。
混淆单个包
O用于设置不同于默认值的输出路径,比如dist2:
pyarmor gen -O dist2 src/mypkg检查输出:
$ tree dist2
dist2
├── mypkg
│ ├── __init__.py
│ └── test.py
└── pyarmor_runtime_000000
├── __init__.py
└── pyarmor_runtime.so
2 directories, 4 files所有混淆脚本都位于 dist2/mypkg 中,可以按如下方式进行测试:
cd dist2/
python -c 'import mypkg'如果存在子包,可以使用 -r 启用递归模式:
pyarmor gen -O dist2 -r src/mypkg分发混淆包
可以将整个 dist2 目录复制到另一台机器上运行,但这种方式不够方便。更好的做法是使用 -i,将所有必需文件生成到包目录内部:
pyarmor gen -O dist3 -r -i src/mypkg检查输出:
$ tree dist3
dist3
└── mypkg
├── __init__.py
├── pyarmor_runtime_000000
│ ├── __init__.py
│ └── pyarmor_runtime.so
└── test.py
2 directories, 4 files现在所有文件都位于包目录 dist3/mypkg 中,只需将整个目录复制到任意目标机器即可。
注意:将当前的
dist3/mypkg/__init__.py与上一节中的dist2/mypkg/__init__.py进行比较,以进一步理解混淆脚本的结构。
设置混淆脚本的过期时间
可以使用 -e 为混淆脚本设置过期日期。比如生成有效期为 30 天的混淆脚本:
pyarmor gen -O dist4 -e 30 foo.py运行混淆脚本 dist4/foo.py 进行验证:
python dist4/foo.py下面使用另一种形式,将过期日期设置为过去的日期 2020-12-31:
pyarmor gen -O dist4 -e 2020-12-31 foo.py此时,dist4/foo.py 应无法正常运行:
python dist4/foo.py分发带有过期限制的脚本与前文相同,只需将整个 dist4/ 目录复制到目标机器即可。
自 v8.5.0 起,默认检查本地时间。如果需要检查网络时间,可以将 nts 配置为任意 NTP 服务器。比如:
pyarmor cfg nts=pool.ntp.org实际上,这是旧版本中的默认配置。有时 NTP 服务器可能返回 RuntimeError: Resource temporarily unavailable,此时可以尝试使用 HTTP 服务。比如:
pyarmor cfg nts=http://worldtimeapi.org/api将混淆脚本绑定到设备
自 Pyarmor 8.4.6 起,可以通过以下命令获取目标机器的硬件信息:
python -m pyarmor.cli.hdinfo示例输出:
Default Harddisk Serial Number: 'HXS2000CN2A'
Default Mac address: '00:16:3e:35:19:3d'
Default IPv4 address: '128.16.4.10'在 Pyarmor 8.4.6 之前,可以使用 pyarmor-7 hdinfo 获取硬件信息。
可以使用 -b 将硬件信息绑定到混淆脚本。比如将 dist5/foo.py 绑定到以太网 MAC 地址:
pyarmor gen -O dist5 -b 00:16:3e:35:19:3d foo.py这样,dist5/foo.py 只能在目标机器上运行。
同样,也可以绑定 IPv4 地址和硬盘序列号:
pyarmor gen -O dist5 -b 128.16.4.10 foo.py
pyarmor gen -O dist5 -b HXS2000CN2A foo.py也可以组合使用多个硬件信息。比如:
pyarmor gen -O dist5 -b "00:16:3e:35:19:3d HXS2000CN2A" foo.py只有当机器的以太网 MAC 地址和硬盘序列号都匹配时,才可以运行该混淆脚本。
分发绑定到设备的脚本与前文相同,只需将整个 dist5/ 目录复制到目标机器即可。
打包混淆脚本
再次说明,混淆后的脚本本质上仍是普通 Python 脚本,可以像未混淆脚本一样使用。
假设包 mypkg 的结构如下:
projects/
└── src/
└── mypkg/
├── init.py
├── utils.py
└── config.json首先,为混淆后的包创建输出路径 projects/dist6:
cd projects
mkdir dist6然后,将包中的数据文件复制到输出路径:
cp -a src/mypkg dist6/接着,对脚本进行混淆,覆盖 dist6/mypkg 中的所有 .py 文件:
pyarmor gen -O dist6 -i src/mypkg最终输出如下:
projects/
├── README.md
├── src/
│ └── mypkg/
│ ├── __init__.py
│ ├── utils.py
│ └── config.json
└── dist6/
└── mypkg/
├── __init__.py
├── utils.py
├── config.json
└── pyarmor_runtime_000000/
└── __init__.py与 src/mypkg 相比,唯一的区别是 dist6/mypkg 中额外包含子包 pyarmor_runtime_000000。最后,只需按照你偏好的方式对 dist6/mypkg 进行打包即可。
示例 1 - Thin CLI
项目结构
obfuscated-example/
├── README.md
├── pyproject.toml
├── scripts
│ └── build-obfuscated-wheel.sh
└── src
└── obfuscated_example
├── __init__.py
├── cli.py
└── core.py4.2 README.md
# Pyarmor Thin CLI 示例
这个示例演示适合工程项目的 Pyarmor 使用方式:
- `cli.py` 作为薄入口,只负责解析命令行参数,调用库函数。
- 业务逻辑放在包内部的 Library 代码中。
- Pyarmor 只混淆包目录。
- 将混淆后的包重新组织成临时项目目录,再用 `python -m build` 构建 `wheel`。
这种结构接近 Rust 的 Thin Binary 风格:入口尽量薄,核心逻辑都在 `lib` 中。
## 项目结构
源码目录如下:
```text
obfuscated-example/
├── pyproject.toml
├── README.md
├── scripts/
│ └── build-obfuscated-wheel.sh
└── src/
└── obfuscated_example/
├── __init__.py
├── cli.py
└── core.py
```
其中:
- `src/obfuscated_example/cli.py` 是命令行入口。
- `src/obfuscated_example/core.py` 是需要保护的业务逻辑。
- `pyproject.toml` 负责声明包元数据、命令行入口和 Pyarmor Runtime 的二进制文件。
## 安装构建依赖
建议在干净的 Python 环境中执行:
```bash
python -m pip install -U pip
python -m pip install build pyarmor
```
## 一键构建混淆 `wheel`
在 `obfuscated-example` 目录下执行:
```bash
./scripts/build-obfuscated-wheel.sh
```
脚本将生成临时构建目录 `build-obfuscated`:
```text
build-obfuscated/
├── pyproject.toml
├── README.md
└── src/
└── obfuscated_example/
├── __init__.py
├── cli.py
├── core.py
└── pyarmor_runtime_000000/
├── __init__.py
└── pyarmor_runtime.so
```
随后脚本在 `build-obfuscated` 中执行:
```bash
python -m build
```
最终生成的 `wheel` 位于:
```text
build-obfuscated/dist/
```
## 测试 wheel
可以在新虚拟环境中测试生成的 `wheel`:
```bash
python -m venv /tmp/obfuscated-example-test
source /tmp/obfuscated-example-test/bin/activate
python -m pip install build-obfuscated/dist/*.whl
obfuscated-example --name Bob --repeat 2
```
如果在 `obfuscated-example` 目录外测试,请将 `wheel` 路径替换为实际路径。pyproject.toml
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "obfuscated-example"
version = "0.1.0"
description = "A thin CLI plus library example for packaging Pyarmor-obfuscated code."
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
obfuscated-example = "obfuscated_example.cli:main"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
"obfuscated_example.pyarmor_runtime_000000" = ["*.so", "*.pyd", "*.dll", "*.dylib"]scripts/build-obfuscated-wheel.sh
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_ROOT="${PROJECT_ROOT}/build-obfuscated"
PACKAGE_NAME="obfuscated_example"
PYARMOR_BIN="${PYARMOR:-pyarmor}"
rm -rf "${BUILD_ROOT}"
mkdir -p "${BUILD_ROOT}/src"
cp "${PROJECT_ROOT}/pyproject.toml" "${BUILD_ROOT}/"
cp "${PROJECT_ROOT}/README.md" "${BUILD_ROOT}/"
cp -a "${PROJECT_ROOT}/src/${PACKAGE_NAME}" "${BUILD_ROOT}/src/"
"${PYARMOR_BIN}" gen -O "${BUILD_ROOT}/src" -i -r "${PROJECT_ROOT}/src/${PACKAGE_NAME}"
(
cd "${BUILD_ROOT}"
python -m build
)
echo "Built wheel files:"
ls "${BUILD_ROOT}/dist"src/obfuscated_example/core.py
"""Business logic kept in the library layer."""
from dataclasses import dataclass
@dataclass(frozen=True)
class GreetingConfig:
name: str
repeat: int = 1
excited: bool = False
def build_greeting(config: GreetingConfig) -> str:
punctuation = "!" if config.excited else "."
line = f"Hello, {config.name}{punctuation}"
return "\n".join(line for _ in range(config.repeat))src/obfuscated_example/cli.py
"""Thin command-line entry point."""
from argparse import ArgumentParser
from obfuscated_example.core import GreetingConfig, build_greeting
def main() -> None:
parser = ArgumentParser(description="Run the obfuscated-example greeting demo.")
parser.add_argument("--name", default="Pyarmor", help="Name used in the greeting.")
parser.add_argument("--repeat", type=int, default=1, help="Number of greetings.")
parser.add_argument("--excited", action="store_true", help="Use an exclamation mark.")
args = parser.parse_args()
config = GreetingConfig(name=args.name, repeat=args.repeat, excited=args.excited)
print(build_greeting(config))
if __name__ == "__main__":
main()src/obfuscated_example/__init__.py
__version__ = "0.1.0"