SoftwareLock
Provide offline, no-network, no-password copy protection for a Python
application by binding it to a machine. Distribute the protected build to
friends without issuing individual passwords; a copied build simply will not
open on another machine. The bundled machine_lock.py plus protect.py
deliver this with minimal antivirus false positives (UPX never used).
Headline mode — activation (zero-server, interactive licensing): you
hand the skill a source entry (program B); one command returns a
copy-protected A.exe (ships with no token) plus a uniquely paired
genkey.exe — your double-click tool for issuing per-machine activation
codes. Forwarding A.exe alone does nothing; only genkey.exe + your private
key (kept locally, never shipped) can activate a given machine.
When to use
- The user wants to ship a Python program (folder, zip, or single exe) and
prevent it from running after being copied to another PC. - The user wants to package with Nuitka / PyInstaller while minimizing
antivirus false positives. - The user mentions machine-code binding, software lock, or "防君子不防小人"
style lightweight protection.
The one-shot flow (headline use case)
You invoke the skill with one source entry (program B); it hands you
back a complete, paired set of artifacts in the output folder — no server, no
accounts, no per-user passwords:
<out>/
├─ <name>_dist/ ← 发给朋友的分发包(activation 模式只有 exe,无令牌)
├─ genkey.exe ← 你的「激活码签发工具」(图形界面,双击即用)
└─ sl_private_key.pem ← 你的私钥(仅你本地保管,切勿外发)
One command:
python assets/protect.py myapp/main.py --gui --mode activation \
--activation-passphrase "<你的口令>"
genkey.exeis uniquely paired to this build. Each run generates a fresh
Ed25519 keypair; the public key is baked into A.exe, and the matching
genkey.exe+sl_private_key.pemare dropped next to it. So the genkey.exe
you get can only sign codes for that A.exe — it is not a generic tool.- The private key is never embedded in genkey.exe; the exe auto-detects the
adjacentsl_private_key.pem(or derives it from the passphrase). Keep the
.pemsecret — anyone with it can forge activation codes for your software. - After building: send
<name>_dist/to the friend; each time they send you a
machine code, double-clickgenkey.exe, paste it, copy the activation code
back. See "Activation mode" below for the full handshake and the two FAQ.
Bundled components
assets/machine_lock.py— the binding component (withgate/
make_token/self_hash). You normally never edit
it by hand;protect.pycopies and configures it automatically per build.assets/protect.py— one-command packager: auto-injects the lock into a
copy of the user's entry, compiles with Nuitka (preferred) or PyInstaller
(--noupx forced), generates the shipping token (folder mode), and assembles a
clean distribution folder (exe + token, no source), optionally
Authenticode-signs the output. In activation mode it also builds the
developer signing toolgenkey.exe(see below) so you get every artifact in
one run.assets/genkey.py— developer-side activation-code signer (CLI). Used by
protect.pyto buildgenkey.exe; you can also run it directly.assets/genkey_gui.py— GUI wrapper aroundgenkey.py;protect.pycompiles
it intogenkey.exe(the author's double-click signing tool). The private
key is NOT embedded —genkey.exeauto-detectssl_private_key.pemin its
own folder (or derives it from a passphrase you type in).assets/fuse.py— EXE 融合器(开发者端 CLI):把一个已有 exe 用你的密钥
AES-256 加密后,编进一个「壳 exe」,并嵌入 Ed25519 激活门。产物fused_xxx.exe
运行时先完成机器激活(令牌存 appdata),通过才解密原 exe 临时释放运行。assets/fuse_gui.py—fuse.py的 GUI 包装(双击即用),复用fuse.build()。assets/fuse_wrapper.py— 融合 exe 的运行时壳模板(由fuse.py注入常量后编译)。README.md— full reference (mode comparison, compatibility matrix, AV
strategy, honest limitations).
Quick workflow (one command — preferred)
The user hands you their source entry; you produce the protected build in one
step. They do not need to add gate() to their own code — protect.py
injects it into a build copy automatically, leaving their source untouched.
# activation mode (零服务器、交互式授权) —— 当前唯一保护模式,也是默认
python assets/protect.py myapp/main.py --gui --collect pystray
# …可选加 --activation-passphrase "<口令>" 让密钥可复现
# (sl_private_key.pem 丢了也能凭口令还原;同口令重建 exe 旧激活码仍有效)
# 用开发者工具签发激活码(需要私钥):
python assets/genkey.py sign --key sl_private_key.pem
# …可选 --hours / --days 签发「限时激活码」(自对方首次运行起算,到期须换新码)
python assets/genkey.py sign --key sl_private_key.pem --days 7
# …或凭口令恢复 / 重新生成私钥:
python assets/genkey.py keygen --passphrase "<口令>" --out sl_private_key.pem
# fuse 模式(EXE 融合器):把一个已有 exe 加密融合成受保护的新 exe
python assets/fuse.py target.exe --key "你的口令或 sl_private_key.pem" --out fused_target
# …或双击 assets/fuse_gui.py 用图形界面操作;两者都复用 genkey 签激活码
protect.py then: (1) copies + configures machine_lock.py into a staging dir;
(2) copies the entry and injects import machine_lock; machine_lock.gate() at
the top (source untouched); (3) compiles; (4) builds genkey.exe (the author's
signing tool) next to sl_private_key.pem; (5) assembles <name>_dist/
(exe only, no .sl_token, no .py); (6) optionally signs.
Output layout & what to ship
The one-shot command and its artifact tree are described in the
The one-shot flow section above. In short: <name>_dist/ goes to the
friend; genkey.exe + sl_private_key.pem stay with you. genkey.exe
auto-reads the adjacent .pem (or derives it from the build passphrase) — no
command line needed.
Distribute:
- activation: ship <name>_dist/ (exe only, no token). The recipient's
first launch shows a "machine code" dialog; they send it to you, you
double-click genkey.exe (built alongside the output) and paste the
machine code to get an activation code, they paste it back. The code is bound
to their hardware (Ed25519-signed by your private key) and cannot be forged or
reused on another PC. The build ships un-activated, so you may even launch it
yourself to self-activate your own copy — just never ship a folder that
already contains a .sl_token.
Manual workflow (only for special cases)
If the entry cannot use the auto-injected bootstrap (e.g. unusual startup
logic, custom pre-lock UI), instead copy assets/machine_lock.py next to
main.py and add at the very top, before any other imports or windows:
import machine_lock
machine_lock.gate() # exits with a popup if the check fails
Then build with protect.py (it injects the Ed25519 public key from your
generated keypair and compiles): python assets/protect.py main.py --gui.
The activation gate needs no secret config block — security comes entirely from
your Ed25519 private key, which is never embedded in the shipped build. You may
set a custom APP_ID in the config block at the top of machine_lock.py to
namespace different programs; otherwise the default softwarelock is fine.
Important constraints
- Never use UPX — it is the single largest source of heuristic false positives.
Nuitka native PE plus a code-signing certificate yields near-zero Windows
Defender false positives. gate()locates the distribution directory viasys.argv[0], never
_MEIPASS, so state survives onedir / onefile / Nuitka / source runs.- Windows fingerprinting uses PowerShell
Get-CimInstance(with fallback),
compatible with Win11 24H2 wherewmic.exewas removed. - Non-Windows falls back to
/etc/machine-id, sopython machine_lock.py
self-checks without crashing during development. - This is "honest people" protection: a determined reverse engineer can still
bypass Python-level checks. Nuitka / obfuscation only raises the bar. - Ship the compiled build only — never the source. If the recipient gets
machine_lock.py/main.pysource, they can delete thegate()call and
bypass the check instantly. The activation binds a code to one machine's
hardware, not against source access. - Built-in hardening:
SELF_CHECK = Trueby default makesgate()refuse if the
core check logic was neutered (behavioral check ongate's referenced
functions), ifSECRETis still the template default, or — when you fill the
optionalSELF_CHECK_HASHwith this file's SHA256 — if the file hash no longer
matches. Generate the hash with
python -c "import machine_lock; print(machine_lock.self_hash())". Source runs
(non-frozen) skip the checks entirely.protect.py --regen-secretinjects a
fresh randomSECRETper build (strongly recommended). - Source runs are unlocked by design:
gate()detects a raw.pysource run
(orSL_SIM_FROZENunset) and returns immediately, so the developer's own machine
runs the unlocked source freely. The lock only takes effect for the compiled build
or when testing withSL_SIM_FROZEN=1. Only the distributed build is protected —
this is intended. - Multi-component machine fingerprint (activation mode): the activation code
and the stored token are bound to a set of strong, unique hardware traits — not
a single ID. Strong fields (all used, any present): system UUID, motherboard
serial, BIOS serial, first disk serial, Windows MachineGuid. CPU/GPU/RAM serials
are deliberately NOT used because they are usually empty or non-unique in the
wild (using them would falsely lock legitimate users). The activation code is
Ed25519-signed by your private key, so a hand-edited or forged token is refused. - Empty-fingerprint guard: if the strong traits (UUID / motherboard / BIOS /
disk / MachineGuid) are all empty or placeholders — WMI broken, no permission,
sandbox —gate()refuses (never binds to a worthless empty fingerprint, never
silently opens when hardware can't be read). Trade-off: a machine that cannot
read any core hardware at all cannot run the protected build (safe-by-default). - Per-machine, re-activate on change: an activation code is signed for one
specific machine's hardware hash; a major hardware change (motherboard / system
disk) or moving to another PC invalidates it → the friend must request a fresh
machine code and you sign a new activation code. If the friend deletes the
.sl_tokenthemselves, the build re-prompts for activation (no silent bypass) —
just re-issue a code. There is intentionally no migration / unbind backdoor.
Activation mode (since v1.3.0) — zero-server, interactive licensing
For users who do not want to pay for a server yet still need to stop
unlimited propagation of the distributed package. The package ships with no
token at all; instead the recipient activates once, interactively, with a
code you sign offline.
Flow:
1. You build with --mode activation. protect.py generates an Ed25519
keypair: the public key is baked into the exe; the private key
(sl_private_key.pem) is written next to your output and is yours alone
— never ship it.
2. You send the friend just the <name>_dist/ folder (exe only).
3. Friend launches → a dialog shows their machine code (a compact,
obfuscated blob of their strong hardware UIDs). They copy and send it to you.
4. You run python assets/genkey.py sign --key sl_private_key.pem, paste the
machine code, and get back an activation code. (Or --challenge "<code>"
for a one-liner, or --out .sl_token to emit a token file.)
- 默认签出永久激活码。加 --hours N 或 --days N 可签限时激活码:
自对方机器首次成功运行起 N 小时/天有效,到期必须申请新码。
genkey_gui.exe 里也有「限时激活码(可选)」输入框,填小时或天即可。
5. Friend pastes the activation code → exe verifies the Ed25519 signature with
the embedded public key, confirms it matches their hardware, writes
.sl_token, and runs.
Why it stops propagation:
- Forwarding the exe alone does nothing — there is no token to reuse, and the
exe cannot generate one (only your private key can).
- An activation code is signed for one specific machine's hardware hash;
pasting it on any other PC fails verification.
- Without your private key, no one can forge a valid activation code
(Ed25519; the public key alone cannot sign).
限时激活码(v1.4.1 新增)— 时间怎么确立、怎么防改系统时间
普通激活码不限期;限时激活码让你按「小时 / 天」出租式授权,到期必须换新码。
核心三件事:
① 计时起点 = 对方机器「首次成功激活」的那一刻,不是你签发的时刻。
时长(dur,单位秒)编进 Ed25519 签名载荷;运行时在第一次激活时把
first_run = 当前时间戳 写进令牌信封({code, first_run, floor}),
expire_at = first_run + dur。所以你提前签好、囤几天再发,不会吃掉对方的使用期。
② 防改系统时间(单调水位 / monotonic watermark)。
每台机器在令牌里额外持久化一个 floor = max(floor, 每次运行的 now)。
- 正常推进:每次运行把水位推高,落盘。
- 回拨攻击:把时钟往回拨 → now < floor − 容差(60s) → 直接拒绝,提示「检测到系统时间回拨」。
- 容差内(≤60s)的小幅抖动(NTP / 时区 / 夏令时)放行,避免误伤。
结果:无论怎么拨时钟,对方总共只能用到恰好 dur 的真实流逝时间,回拨无法续期。
③ 篡改防御 — dur 必须参与签名。
若 dur 不进签名,持有「限时码」的人可以剥离 dur 伪装成永久码。本实现把
dur 段一并纳入 Ed25519 签名(app|fp_hash|dur=…),任何对 dur 的改动都会
使验签失败。验证端 _verify_token_with_expiry 据此区分「永久 / 限时 / 过期 / 回拨」。
已知边界(与整体「防君子」定位一致):水位写在令牌文件里。若对方连同令牌文件一起
回滚备份,可把 first_run/floor 一并退回——但这等于重置整台机器的激活状态,
且仍绕不过「必须有你的签名才能激活」。真正想再上一个量级,需服务端激活或
Nuitka+混淆,那是一笔额外投入。
旧版令牌(整文件即裸激活码字符串)向后兼容:首次读取时自动升级为信封格式;
无 dur 字段一律按永久码处理。
限时码运行时强制退出(v1.4.2 新增,方案 C)
旧版只在「启动时」校验一次:过期后程序会一直跑到用户手动关闭、下次启动才拦截。
现在改为:限时码激活成功后,启动一个同进程 daemon 看门狗线程,周期复查令牌。
- 复查间隔:
WATCHDOG_INTERVAL(默认 30 秒)。 - 触发强制退出的三种情况:
1. 过期(now >= first_run + dur)—— 提示「激活码已过期…」;
2. 时钟回拨(now < floor − 60s 容差)—— 提示「检测到系统时间回拨…」;
3. 令牌被移除(例如用户点了「清除本机令牌」)—— 限时授权失效。 - 退出方式:直接
os._exit(0)硬退整个进程(不优雅、不存盘)。用户已确认
「不在乎体验、强制关闭即可」,故不做优雅存盘。融合(fuse)模式下,看门狗会
先终止被守护的子进程再退父进程;同时fuse_wrapper.py在启动子进程时
会把它挂进 Windows Job Object 并启用KILL_ON_JOB_CLOSE,即使terminate()
失败,父进程退出时系统也会强制带走子进程,避免 GUI 子进程残留。 - 额外收益:看门狗每次醒来都把单调水位
floor往前推并落盘,运行时也持续加固
防回拨(不只是启动时)。 - 永久码不启动看门狗;
ENFORCE_EXPIRY = False可整体退回「仅启动时拦截」。
> 杀软误报提示:看门狗只是一个普通 daemon 线程,进程一关即死,不跨进程、不跨重启、
> 不后台潜伏,与正常程序的定时刷新线程无异;退出用的是 Python 原生 os._exit,
> 没碰 TerminateProcess 之类敏感 API。只要最终 exe 做 Authenticode 签名,误报极低。
启动期令牌失效自动清除并回落激活(v1.4.3 修复)
旧版在「启动时」拿着一份已存在的令牌去校验,一旦失败(过期 / 时钟回拨 / 硬件变更 /
被篡改)只会 raise GateDenied 或 return False 直接报错退出,那份失效令牌仍留在
磁盘上。后果:对方改了系统时间、或限时码悄悄过期后,下次启动仍带着旧令牌、反复卡在
同一处报错,不会自动回到激活界面。
现在改为:启动时校验已有令牌失败,先安全删除该失效令牌,再回落到激活对话框
(待激活状态),让对方粘贴新码即可。两种模式都已覆盖:
- protect 模式:
_gate_activation校验失败时调用_invalidate_token()删令牌,
随后继续走_activate_dialog重新激活(不再raise)。 - fuse 模式:
fuse_check_activation校验失败时同样调用_invalidate_token()删
令牌,随后继续走激活流程(不再return False)。 - 删除失败(权限 / 被杀软锁)也静默吞掉,不影响回落——最坏只是令牌没删成、但
仍会重新弹激活框,不会让程序卡死。
> 触发「自动清除」的语义与看门狗一致:过期、回拨、硬件变更、被篡改。永久码一旦激活
> 成功且未被改动,不会触发清除。
虚拟化/沙箱环境检测(v1.4.5 新增,v1.4.6 加固)— 防止虚拟机运行
类似三角洲行动等商业游戏的反作弊策略:在启动校验阶段探测常见虚拟化环境,命中即
拒绝运行(弹窗 + sys.exit(1)),阻止在 VM 里白嫖或逆向。两种模式(protect / fuse)
都在激活门最早期调用 _enforce_no_vm(),VM 环境连激活框都进不去。
探针清单(多维度,分级判定,避免误杀开了 WSL2/Hyper-V 的实机):
- 硬证据(任一命中即拦截):
- 厂商/型号/BIOS 字符串:
Win32_ComputerSystem(Model/Manufacturer)、
Win32_BIOS(SerialNumber/Version) 含vmware/virtualbox/kvm/qemu/
xen/parallels/innotek/hyper-v等特征串(刻意不收录microsoft corporation——Surface/OEM 整机也是它,会误杀)。 - 虚拟网卡 MAC OUI:
00:05:69/00:0C:29/00:1C:14/00:50:56(VMware)、
08:00:27(VirtualBox)、52:54:00(QEMU/KVM)、00:1C:42(Parallels) 等。 - 特征驱动文件(仅客户机驱动,已剔除 Hyper-V 宿主驱动
netvsc.sys/vmsrvsc.sys
等易误杀项):VBoxMouse.sys/vmmouse.sys/vmhgfs.sys/vmci.sys等。 - 特征进程:
vmtoolsd.exe/VBoxService.exe/qemu-ga.exe/
prl_tools_service.exe等。 - 注册表特征键:VMware Tools、VirtualBox Guest Additions、Hyper-V Guest
Parameters、VBoxService/VMTools/vmci服务等。 - Linux 探针:
/proc/cpuinfo的hypervisor标志、systemd-detect-virt、
DMIsys_vendor/board_vendor/product_name/bios_vendor含虚拟厂商串。 - 软证据(单独出现不算拦截,只写入证据列表):CPUID hypervisor 标志 /
WMIWin32_ComputerSystem.HypervisorPresent == True(即 CPUID leaf 1 ECX 第 31 位)。
开了 WSL2 / Hyper-V / 安卓模拟器 / Docker Desktop 的真实 Windows 机也会置位该标志,
所以仅作辅助证据,必须配合至少一条硬证据才判 VM——这是 v1.4.6 修复「自己实机被误杀」
的关键。
所有探针都做了异常隔离,单点失败不影响整体判定(最差只是少一条证据)。
v1.4.6 关键变化:独特弹窗 + 可选开关
- 独特弹窗:VM 命中时抛
GateDeniedVM(普通GateDenied的子类),由_deny_vm()
弹出专属警告框(标题「虚拟机环境拦截」、用showwarning而非通用showerror),
明确列出命中了哪条证据,并给出解决指引:临时用set SL_ALLOW_VM=1运行,或让作者
重融时取消「禁止虚拟机运行」复选框。再也不会和「没激活 / 被篡改」混为一谈。 - 可选项(不再强制):融合器 GUI 增加「禁止在虚拟机/沙箱环境中运行(仿商业游戏反作弊)」
复选框,默认勾选;作者若担心误杀真机,取消勾选重融即可。fuse.py命令行对应
--disable-vm-block。勾选与否只影响产物 exe 里DETECT_VM的开关,不影响激活逻辑。
开关与调试:
DETECT_VM = True:默认开启。融合时取消复选框(或--disable-vm-block)会把产物
machine_lock.py里的DETECT_VM改写为False,整体关闭检测。- 环境变量
SL_ALLOW_VM=1:临时放过(作者自己若在 VM 里联调时用),绕过检测。 - 属「防君子」级——有心者把
_detect_virtual_machinepatch 成返回False即可绕过,
但对付绝大多数在 VM 里运行/逆向的场景已足够。与激活门、看门狗一样,真正的安全来自
非对称签名 + 你把私钥攥在手里。
Hardware binding (deliberately excludes unreliable traits):
- Bound to strong unique UIDs only: system UUID, BIOS serial, motherboard
serial, system-disk serial, and Windows MachineGuid. No GPU / RAM / CPU
serials (those are empty or non-unique in the wild and would false-lock
legitimate users).
- A major hardware change (motherboard / disk / OS reinstall) invalidates the
activation; the user just re-sends a fresh machine code for a new code.
Caveats (honest):
- Still "honest people" protection — a determined reverse engineer can patch
the check out of the compiled exe, same as activation mode.
- Keep sl_private_key.pem safe. With the passphrase build option (see
below) you can recover it from a remembered passphrase, so losing the file is
no longer fatal; without a passphrase a random key is used and the file is
the only recovery — back it up.
- The machine code / activation code are short (≈300 chars), copy-paste
friendly; the XOR obfuscation is readability-only, the real security is the
Ed25519 signature.
FAQ — two things people always ask
Q: Does genkey.exe contain my private key?
No. The Ed25519 private key is never embedded in genkey.exe. At runtime
the tool auto-detects sl_private_key.pem in its own folder (same dir as
the exe). If that file is absent you can instead type the build passphrase and
it derives the key on the fly. So genkey.exe is safe to keep on any machine —
without the adjacent .pem (or the passphrase) it cannot issue any code. The
.pem is the real secret; protect.py always drops it next to genkey.exe
so they ship as a matched pair.
Q: If I send an already-activated, machine-bound exe to a third person, will
it refuse to run?
Yes — it will not open on their machine. On launch it re-reads the hardware
fingerprint, finds the bundled .sl_token does not match that third
machine, and re-opens the activation dialog (showing their machine code).
With no valid activation code (only you can sign one with your private key) they
cannot proceed; clicking cancel / closing the dialog shows a "SoftwareLock
拒绝运行 / 未激活或激活码无效" popup and exits. Note it does not show a
"this is pirated" scare screen — it simply re-enters the activation flow, so a
legitimate third person could ask you to also activate their machine. To deny
them, just don't issue a code.
Recovering a lost private key (since v1.3.0+)
The Ed25519 private key is just a 32-byte seed. Two recovery situations:
-
Built with a passphrase (
protect.py --activation-passphrase "<口令>"):
the seed is derived deterministically from the passphrase (PBKDF2-SHA256,
200k iters, fixed salt inmachine_lock.py). Same passphrase → same key →
same public key, so:
- You can rebuildgame_throttle.exelater with the same passphrase and the
already-shipped exe keeps accepting old activation codes (public key
unchanged).
- Ifsl_private_key.pemis lost, regenerate it from source — no existing
exe is affected:
bash python assets/genkey.py keygen --passphrase "<口令>" --out sl_private_key.pem
- The GUIgenkey.exealso has a "用口令恢复/生成私钥文件" button, and will
sign directly from the passphrase if no.pemis present. -
Built without a passphrase (random key): the
.pemfile is the only
copy of the key. If lost you can stillgenkey.py keygen --randomto make a
new keypair, but the public key baked into the already-shipped exe won't
match → that exe becomes un-activatable and must be rebuilt + re-shipped.
So always back up the.pem, or rebuild with a passphrase going forward.
Either way, never ship sl_private_key.pem — anyone with it can forge
activation codes for your software.
Fuse mode (since v1.4.0) — EXE fusion / 融合器
For when you already have a built exe (yours or a third-party one) and want
to wrap it in a SoftwareLock-protected shell without recompiling from source.
The fuser encrypts the target exe and bundles it inside a new "shell exe" that
also enforces machine activation before running the original.
Why this exists
protect.py(activation mode) needs the source of program B.- Fuse mode takes a finished exe and produces
fused_xxx.exein one step —
compatible with most exes (including third-party / plain exes you can't rebuild).
One command
python assets/fuse.py target.exe --key "你的口令或 sl_private_key.pem" --out fused_target
# 双击 assets/fuse_gui.py 也能用图形界面完成同样的事
What fuse.py does
- Reads
target.exebytes. - Derives an AES-256 key from your key source:
---key <sl_private_key.pem>→ uses the pem's public key as the
activation gate, andSHA256(pem)as the AES key material.
---key <口令>→ derives the Ed25519 keypair and the AES key
deterministically from the passphrase (sogenkey.py sign --passphrase "<同口令>"can issue activation codes; same passphrase = same key = same
public key, old activations stay valid). - AES-256-GCM encrypts the target exe (ciphertext + nonce + tag).
- Injects the ciphertext + embedded AES key (lightly XOR-obfuscated) + public
key + app-id intofuse_wrapper.py, then compiles it (PyInstaller onefile /
windowed /--collect-all cryptography) intofused_xxx.exe.
Runtime behavior of fused_xxx.exe
- Checks for an activation token in a stable location
(%LOCALAPPDATA%/SoftwareLock/fuse/<app-id>/.sl_token) — so it survives
re-runs even though the original exe is dropped to a fresh temp dir each time. - If not activated → shows the machine code dialog; you sign it with your
genkey.exe+ private key (same as activation mode) and the friend pastes the
code back. Token is written to appdata. - On success → AES-decrypts the original exe in memory, drops it to a random
temp file, runs it with the same command-line args and working directory
(temp dir), then self-cleans on exit.
Why it is hard to split out (honest ceiling)
- On disk,
fused_xxx.exeis pure ciphertext — the original exe's bytes are
AES-encrypted, so simple carving tools (looking for anMZ/PEheader or a
contiguous copy of the original) cannot extract it. - The AES key is embedded in the shell (necessary so the exe runs with no
prompt), so a determined reverse engineer with the fused exe in hand can
eventually recover it — this is "honest people" protection, the same level as
the other modes. But because activation is also enforced, even if they
extract the original exe, it still won't run without your activation code. - The temp-dropped original exe is briefly on disk at runtime; process monitors
could catch it. If you need zero on-disk footprint at runtime, that requires
in-memory PE loading (RunPE / process hollowing) — more AV-flagged and fragile
across exe types, so it is not the default. The chosen default (temp-drop - Authenticode signing) maximizes compatibility and stability; sign the
output (fuse.py ... --sign cert.pfx 密码) to eliminate false positives.
Compatibility notes
- Works on most exes because the inner program runs as its own child process
(native arch, no re-compile). A single-file inner exe is dropped to a temp dir
with that dir as its working directory (faithful to a double-click launch). - Edge cases not handled automatically: an inner exe that requires sibling
files next to it (only the exe is fused, not its companions), or one that
needs admin rights (run the shell as admin), or a console inner exe (a
windowed shell will still launch it, but its console may flash).
First-launch binding popup (since v1.1.0)
When the binding actually happens (the first successful launch for this
machine — i.e. a shipped unbound .sl_token is bound to this hardware),
gate() shows an showinfo popup containing:
- the structured machine-code fields that were bound (uuid / bios / board /
disk / cpu / gpu / mem) plus the derived machine-code hash, and - a multi-line anti-piracy reminder:
请勿复制/请勿出售(customize the
text inside_show_bound_info()if you want different wording).
The popup fires only once (at bind time); on later launches the token is
already bound so the popup is skipped. In headless environments
(SL_NO_GUI=1) or when tkinter is unavailable it silently does nothing and
does not block launching. To change the wording, edit _show_bound_info() in
assets/machine_lock.py.