hgyula73/stinger

GitHub: hgyula73/stinger

一款 Nuitka 编译前的 Python 源代码预处理器,负责隐藏字符串字面量并将可恢复的 AES-256-GCM 加密源码托管嵌入二进制文件。

Stars: 0 | Forks: 0

# stinger — 字符串隐藏预处理器 [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/hgyula73/stinger/actions/workflows/ci.yml) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) 一款构建前的 Python 工具,用于在你使用 Nuitka 将**你自己的源文件**编译为原生二进制文件之前,剥离其中的可读文本。它主要做两件事: 1. **移除注释和文档字符串** —— 这些也是可读的、可能具有敏感性的文本。 2. **隐藏字符串字面量** —— 通过 base64+XOR 组合对其进行混淆,因此对二进制文件运行 `strings` 将不再会泄露你的查询语句、日志消息或 endpoint。 运行时结果**保持不变**:每个编码后的字符串都会在运行时解码还原。 ``` original .py ──[string_encoder.py]──> encoded .py ──[Nuitka]──> native binary ``` ## 安装说明 字符串隐藏功能**没有外部依赖** —— 你只需要 Python 3.9+(`ast.unparse`)。`requirements.txt` 仅用于运行测试: ``` pip install -r requirements.txt # pytest, tests only ``` ## 用法 ``` python string_encoder.py [options] ``` `` 可以是 `.py` 文件或目录(递归处理)。默认情况下,它会在源文件旁边生成一个 `.enc.py` 文件 —— **它不会覆盖原始文件**(除非显式指定 `--in-place`)。 ``` # 一个文件 -> demo.enc.py 位于源文件旁边 python string_encoder.py app/demo.py # 整个目录到一个单独的输出树,使用可重现的 key python string_encoder.py src/ --output build/encoded --key deadbeefcafe1234 --report ``` ### 选项 | 选项 | 含义 | |---|---| | `--in-place` | 覆盖原始文件(默认关闭) | | `--output ` | 输出目录,保留原有目录结构 | | `--exclude ` | 要跳过的 glob 匹配模式(将附加到默认规则中) | | `--min-len ` | 长度小于此值的字符串不会被编码(默认为 3) | | `--keep-docstrings` | 保留文档字符串(默认删除) | | `--encode-dict-keys` | 同时编码字典的字符串键(默认关闭) | | `--encode-i18n` | 同时编码 i18n 的 msgid (`_(...)`) 字符串 | | `--per-file-key` | 每个文件使用单独的密钥(默认为共享密钥) | | `--key ` | 以十六进制表示的固定密钥 —— 用于可重现的输出(**切勿提交至版本库**) | | `--seal ` | 源代码托管:将项目的整个源代码加密后嵌入到 `` 的编码输出中(见下文) | | `--version ` | 构建标记;**与 `--seal` 一起使用时为必填项**。作为版本密钥的标识符;二进制文件的 `--source`/`--version` 会打印该标识 | | `--dry-run` | 不写入任何文件,仅报告结果 | | `--report` | 在结束时显示统计信息 | 默认情况下,`.venv`、`venv`、`site-packages`、`__pycache__`、`*.build`、`*.dist`、`*.onefile-build` 都会被排除。 ## 它如何决定要编码的内容 替换逻辑始终为 `"text"` → `_sd("garbled")`,其中 `_sd(...)` 返回一个与原始字符串 **`==`** 相等的 str。这就是为什么运行时行为完全相同的原因 —— **除了以下四种情况**,所有的跳过规则均由此衍生: 1. **对象身份**发生改变(产生新对象) → `is` / `is not` 的操作数、`__main__` 保护代码块保持不变。 2. **静态可见性**丢失 → 动态导入(`import_module`, `__import__`)、`__all__`/`__version__` 及其他 dunder 方法、注解、`TYPE_CHECKING` 块、`getattr` 的名称保持不变。 3. **类型**发生改变 → `bytes` 字面量(`b"..."`)保持不变。 4. **解析位置**至关重要 → 文档字符串、f-string、`match`/`case` 模式保持不变。 保守原则:如果字符串的上下文不确定,则将其**跳过**。按类别划分的详细列表请参阅 [`stinger.md`](stinger.md) 规范。 可安全编码的部分(通常也是敏感信息):SQL 查询、日志消息、endpoint、错误文本、`%`/`.format()` 模板。 ## 源代码托管 (`--seal`) 这一机制旨在协调两个对立的需求:**代码保护**(客户不应看到源代码)和**可恢复性**(开发者在需要时可以将其移交)。解决方案是:将项目的整个源代码以**高强度加密**的形式随二进制文件一起分发,并且只能使用**正确的 UUID 密钥**才能打开 —— 该密钥由开发者保管或存放在合法的托管机构中,**绝不**包含在发布的二进制文件内。 这是经典的**软件托管**模式:客户拿到二进制文件;只有当他们付款,或者触发了托管条件并释放密钥时,他们才能解锁源代码。 ### 一个 seal(信封)上的多重密钥 源代码使用随机的内容密钥(CEK)加密**一次**;随后 CEK 会被多个 UUID 封装,因此**多个密钥可以打开同一个二进制文件**: - **主密钥 (Master key)** —— 归开发者所有;它会注入到**每一个**构建版本中 → 它始终可以打开任何二进制文件(真正的托管)。 - **版本密钥 (Version key)** —— 每次构建唯一,与 `--version` 绑定。它**只能**打开其对应的构建版本,对其他版本无效(每次构建使用独立的 CEK)。该密钥与**版本**绑定,而不是与客户绑定 —— 反正你也无法阻止密钥被传递。 ``` source ──[zip → AES-256-GCM(CEK) once; CEK wrap: master + version]──> seal in the main file ──[Nuitka]──> binary ``` ``` python string_encoder.py src/ --seal src/worker.py --version 1.4.2 --output build/encoded ``` - **密钥块:** stinger 会在主文件顶部写入一个**清晰可见的注释块** —— 最顶部是 `STINGER-ESCROW-MASTER:`,在其下方是 `STINGER-ESCROW-VERSION :` 密钥,每行一个。该注释在 `ast.unparse` 期间会消失 → **没有任何密钥**会被编译进二进制文件。它会复用主密钥;对于新版本,它会追加一行新内容。它还会将新密钥打印到控制台(请记下来/妥善移交)。 - **使用 `--seal` 时必须提供 `--version`** —— 这是版本密钥的标识符,也是二进制文件向客户打印的内容。 **提取 —— `--source`(交互式):** ``` ./worker --source stinger escrow — version: 1.4.2 ← this is what the client sees; they request the key based on it enter the escrow key: _ ← stdin, no echo (does not end up in ps/history) ``` 使用正确的密钥时,源代码将被解包到二进制文件旁边(`_source/`),随后程序退出。如果密钥错误,则显示错误消息并以非零状态码退出。**如果不指定 `--source`**,worker 将**正常运行**(延迟导入 → 零开销)。单独运行 `./worker --version` 仅打印版本号。 **密码学技术:** 16字节的 UUID(122 位) → **Scrypt** KDF → 32字节的 KEK,用于封装 CEK;源代码使用 **AES-256-GCM** 加密。安全性依赖于密钥的熵值:攻击者能看到二进制文件中的所有内容(blob、salt、nonce、算法、版本标记),唯独看不到 UUID 密钥 —— 在物理层面排除了暴力破解这些密钥的可能性。详细格式请参阅:[`escrow-v2.md`](escrow-v2.md)。 ## 保证 - 编码后的输出**始终是有效的 Python 代码**(该工具会重新解析它;绝不会写入损坏的文件,遇到错误时会以非零状态码停止运行)。 - **幂等性**:运行两次不会重复编码(基于哨兵机制的检测)。 - **往返转换**:对于每个编码的字符串,满足 `_sd(encode(s)) == s`,包括 unicode。 ## 测试 ``` pytest -q ``` 它们涵盖了所有四个正确性类别、幂等性、注释/文档字符串处理以及 CLI 的覆盖保护。`test_escrow.py` 涵盖了托管的加密核心、运行时 stub 以及 `--seal` 的集成测试 —— 包括**铁律**(UUID 绝不会泄露到编码后的输出中)。 ## Nuitka 集成与对比 混淆后的源代码可以像原始代码一样编译和运行。其效果可以通过 `strings` 进行验证: ``` python string_encoder.py examples/demo_source.py --key deadbeefcafe1234 python -m nuitka --standalone --onefile examples/demo_source.enc.py strings demo_source.bin | grep -iE 'select|password|/api/|token' # no matches ``` 该示例(`examples/demo_source.py`)涉及了许多情况;生成的 `*.enc.py` 和构建输出未纳入版本控制(参见 `.gitignore`)。 ## 常见问题 **这是真正的安全吗?难道不能有人直接恢复隐藏的字符串吗?** 这完全是两码事。**字符串隐藏只是一种混淆,而非密码学** —— XOR 密钥就存在于二进制文件中,因此任何拥有调试器或内存转储的人都能恢复出文本。它只是为了阻止随意的 `strings | grep`,文档也明确指出了这一点。**而托管功能(`--seal`)才是真正的密码学** —— 使用 AES-256-GCM 以及基于 UUID 派生的密钥进行加密,且该 UUID 绝不会包含在二进制文件中;如果没有该 UUID,即便在客户端机器上的运行时环境中,源代码 blob 也会保持加密状态。 **为什么不直接用 PyArmor 或 Nuitka Commercial?** 如果你需要字节码加密、反调试或许可证强制执行,请使用那些工具 —— 这个工具并不在此竞争。stinger 是一个小巧、免费、AST 正确的**字符串字面量**预处理器,外加其他工具所不具备的**源代码托管**机制。请与 Nuitka 配合使用,而不是用来替代专业的安全加固产品。 **它会隐藏我的 API endpoint 吗?** 如果是纯字符串字面量,则会隐藏(如 SQL、日志消息、错误文本、分配给变量的 URL 字符串)。但有两种情况被刻意保留不动,并且**会泄露**:**装饰器参数**(例如 `@app.route("/api/users")` —— 路径保持明文可读)和 **f-string 的静态部分**(`f"/api/{uid}"` → `/api/` 会保留为明文)。如果这些内容很重要,请在源代码层面重写它们。请参阅 [`stinger.md`](stinger.md) 中的跳过列表。 **为什么要用自定义的托管格式,而不是 `age` / libsodium?** 这是一种经过深思熟虑的权衡。该设计希望解锁密钥是一个**简短的、人类可输入的 token**(一个可以通过电话读出或在提示符下粘贴的 UUID),这也就决定了我们会采用对称加密方案,而不是 PEM/密钥文件。其加密原语是标准的(AES-256-GCM,Scrypt);封装格式很小且易于审查(参见 [`escrow-v2.md`](escrow-v2.md))。它**没有经过外部审计** —— 请参见下文说明。 **为什么要对 122 位的 UUID 使用 Scrypt?** 坦白说,这属于双重保险:`uuid4` 本身就已经防暴力破解了,因此严格来说在这里并不需要使用内存困难型 KDF。保留它是为了实现深度防御,并且因为参数存储在头部,以后还可以进行调整。 **`--seal` 能否替代合法的软件托管协议?** 不能。它是一种**技术**机制(源代码以加密形式包含在实际发布的二进制位中,无需第三方持有并保持源代码副本的最新状态)。而**信任和法律框架** —— 即由谁持有密钥,以及在何种条件下释放密钥 —— 仍取决于你和你的律师。 **如果我丢失了主密钥怎么办?** 被密封的源代码将无法恢复。设计上绝不留后门。请像保管任何不可恢复的机密一样保管好主 UUID(开发者机器 + 合法托管)。 **加密经过了审计吗?** 没有。它通过 `cryptography` 库使用了广为人知的加密原语,附带了测试,并且格式设计得有意保持简单 —— 但它从未经过外部安全审计。请据此谨慎评估使用风险。 **我必须使用 Nuitka 吗?** 不需要。它是一个**源代码预处理器**,因此可以在任何编译器/打包工具之前运行。Nuitka 是主要适配目标(原生二进制文件 + 抵御 `strings` 提取),但编码后的输出是有效的 Python 代码,同样适用于 PyInstaller 或纯 CPython。 **它会减慢我的程序运行速度吗?** `_sd(...)` 每次调用时都会进行解码,且没有进行 memoization 处理。对于模块级常量来说影响可以忽略不计;只有在热点循环中的字符串字面量才会产生可测量的影响 —— 可以通过 `--min-len` 以及在需要时将常量提取到循环外来缓解。 **它会改变我的源代码格式吗?** 仅限**编码输出**,那本来就是喂给 Nuitka 的一次性产物。`ast.unparse` 会对其重新排版,并丢弃 `# type:` 注释。你维护的源代码永远不会被触碰(除非你传入了 `--in-place`)。 ## License 基于 Apache License, Version 2.0 授权 —— 详见 [LICENSE]()。 版权所有 2026 hgyula73。
标签:Nuitka, Python, 代码混淆, 字符串加密, 无后门, 源码保护, 编译预处理, 逆向工具