使用 Pyarmor 混淆 Python 代码

简介

Pyarmor 是支持如下功能的命令行工具:


通过 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

可以使用 ggenerate 替换命令 gen

pyarmor g foo.py
pyarmor generate foo.py

该命令生成混淆脚本 dist/foo.py。可以用 Python 解释器运行该脚本:

python dist/foo.py

检查默认输出路径中的所有生成文件:

$ ls dist/
foo.py  pyarmor_runtime_000000

Python 包 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,也可以运行混淆脚本。

混淆单个包

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.py

4.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"