Documentation
Protection for compiled Object Pascal executables: symbol obfuscation, string encryption, license anti-tamper and code-signing — Delphi 7–13 and Free Pascal / Lazarus 3.2+, Win32, Win64 & Linux x86-64, with no source-code changes.
PasGuard operates on an already-compiled .exe.
You point it at your binary; it rewrites the file in place to remove
meaningful names, encrypt string literals, cloak your embedded license
key, and (optionally) code-sign the result. There is no library to link
and no recompile.
It runs as a GUI (double-click) or a CLI
(for scripts / CI). The same build does both, and both the 32-bit
(PasGuard_win32.exe) and 64-bit
(PasGuard_win64.exe) editions are included.
PasGuard protects Lazarus applications the same way it protects Delphi ones: class, field, property and published-method names are randomised, the form resources are kept in step, and string literals can be encrypted. Verified on Win32 and Win64 with FPC 3.2.2.
Supported
i386-win32
and x86_64-win64x86_64-linux,
both the default static build and PIE — see §2.2{$R *.lfm} default in LazarusNot supported
.lrs form storage
({$I unit.lrs} /
LazarusResources.Add). Those projects
keep form data as constants inside code rather than as resources, so
renaming would leave the two out of step and the application would
fail at startup. PasGuard detects this and refuses the file; rebuild
the project with {$R *.lfm} form
resources instead..lpk).Worth knowing
FindComponent('Button1'),
GetPropValue,
FindClass,
MethodAddress. This is true for Delphi
too; the pre-flight report lists every identifier that also appears as
a string literal, so read it.ClassName.Every protection pass works on Linux exactly as it does on Windows: symbol and RTTI renaming, form resources kept in step, and string encryption with its own decrypt stub. Verified by running the protected programs, including a real Lazarus/LCL application, not by inspecting them.
Requires a Professional license or higher. The
Standard tier is built around code signing, and Authenticode has no
Linux equivalent, so Linux is not sold under it. Analysis
(-i) works on any license, including
none — you can always check whether your application is a candidate
before buying.
Supported
x86_64-linux produced by Free Pascal / Lazarus 3.2+ET_EXEC,
statically linked) and PIE (-Cg -k-pie)Where a protected binary will run
Protection does not change this; your build already decided it.
| your build | needs | runs on |
|---|---|---|
FPC default (ET_EXEC) |
nothing at all | any x86-64 Linux, musl included |
PIE (-Cg -k-pie) |
/lib64/ld-linux-x86-64.so.2 |
any glibc distribution, any version |
There is no minimum glibc version. The default build links no libc; the PIE build needs a loader to relocate it but asks that loader for nothing. Tested on Ubuntu, Debian 13, AlmaLinux 9 and 8, and Alpine (musl).
One Free Pascal quirk that looks like our fault.
When cross-linking, FPC writes /lib/ld64.so.1
as the ELF interpreter, and that path exists on no x86-64 Linux
system. Such a binary exits 127 before it starts — protected or
not. Build with:
-k--dynamic-linker=/lib64/ld-linux-x86-64.so.2 (glibc) -k--dynamic-linker=/lib/ld-musl-x86_64.so.1 (musl)
Check it with readelf -l yourapp | grep interpreter.
Note that ldd reports
statically linked for these PIE binaries, which is untrue
and confusing — it says that whenever a file lists no shared
libraries. Trust readelf, not
ldd.
Not supported: 32-bit Linux, ARM and macOS.
Delphi's own Linux compiler is not supported. Delphi can target Linux, and PasGuard does read such a binary — measured against Delphi's own Linux runtime packages, it identifies the compiler and finds the classes. What is not finished is the other half: writing. That path has never been verified against a running Delphi Linux program, so the file is refused rather than risked. Linux support means Free Pascal / Lazarus builds.
The tool's bitness is independent of your target. Both
PasGuard_win32.exe and
PasGuard_win64.exe protect both
Win32 and Win64 executables. The 32-bit build is the simplest, most
universal choice and runs everywhere; the 64-bit build is for those who
prefer running a native x64 process. Pick whichever matches your
environment — the protection passes and results are identical.
C:\Tools\PasGuard\).
It contains both PasGuard_win32.exe and
PasGuard_win64.exe.license.dat, keep it
next to the .exe you run (see §4).PasGuard_win32.exe (or
PasGuard_win64.exe) as a GUI, or call
either from a command prompt.No installer, no registry changes.
PasGuard ships as one binary per architecture. With a valid license it runs every pass; without one it runs in DEMO mode.
On startup the tool looks for license.dat in
this order (first match wins):
…\PasGuard_win32.exe →
…\license.dat (simplest).
The same applies to PasGuard_win64.exe.%APPDATA%\PasGuard\license.datThe same license.dat works for both the
32-bit and 64-bit builds.
Both the GUI log and the CLI print a status line on every run:
License: Licensed to <name> <email> - expires YYYY-MM-DDLicense: DEMO mode - no license installedDEMO mode is fully functional for evaluation, but:
Install a valid license.dat to remove the
watermark and enable the paid passes.
PasGuard_win32.exe (or
PasGuard_win64.exe)..exe
(or drag-and-drop it onto the window). Either build accepts both Win32
and Win64 targets.* need a license:
uLicense key blob, Win32 + Win64.Delphi,
Lazarus or
Free Pascal, with the architecture in
brackets, e.g. Lazarus (Win64). Check
that card before obfuscating: if it says something other than what you
built, PasGuard has misread the file and you should use the
Compiler= project setting (§8) rather
than continue..bak is written first.Use whichever build you prefer — the command line is identical for
PasGuard_win32.exe and
PasGuard_win64.exe:
PasGuard_win32.exe run the GUI PasGuard_win32.exe -i <file> analyze (dump VMT/DFM counts) PasGuard_win32.exe -o <file> [options] obfuscate in place PasGuard_win64.exe -o <file> [options] same flags, native x64 process
Options for -o:
| Option | Effect |
|---|---|
-c | log each rename |
--no-packageinfo | leave the PACKAGEINFO resource untouched |
--no-versioninfo | leave VS_VERSION_INFO untouched |
--no-backup | do not create <file>.bak |
-m <map> | write the rename map to <map> |
--project <doproj> | load options from a .doproj profile (flags override) |
--sign-tool <exe> | run <exe> after obfuscation, target as its only arg — paid |
--encrypt-strings | Encrypt string literals with a per-literal keystream, Win32+Win64 — paid |
--encrypt-pubkey | Encrypt an embedded RSA public-key blob, Win32+Win64 — paid |
--guard-license | Bind the uLicense verification code into the string keystream. On by default with --encrypt-strings; the flag only makes that explicit — paid (§7.1) |
--no-guard-license | Turn that binding off — for bisecting a target that misbehaves after protection |
--tls-stub | Run the decrypt stubs from a TLS callback instead of the entry point — paid, off by default (§7.2) |
--strenc-map <file> | Write the string-encrypt diagnostic map to <file>. It contains the seed — keep it out of what you ship. Off by default. |
--compiler auto|delphi|fpc | override compiler detection (see §8) — auto is the default |
--help | usage |
-i prints the same facts the GUI's Analysis
panel shows, including a Target line naming
the toolchain and architecture, e.g.
Target : Lazarus (Win64).
Exit codes: 0 success,
1 usage, 2 failure.
Example — full protection on a release build:
PasGuard_win64.exe -o Release\MyApp.exe --encrypt-strings --encrypt-pubkey --sign-tool signtool-wrapper.bat
license.dat; without one they are
skipped with a log message and the run continues.
After a successful run the file is stamped so it can't be obfuscated
twice (double obfuscation would corrupt it). To re-protect,
rebuild from source or restore the
.bak, then run again.
| Pass | What it does | Tier |
|---|---|---|
| Symbol obfuscation | Renames published classes, properties, fields, methods, method params and DFM/LFM component names to random identifiers. | All |
| PackageInfo / VersionInfo | Scrambles unit names in the package-info resource and version-info strings. | All |
| String encryption | Encrypts string literals in place, each with its own keystream; a small stub decrypts them at startup. The stub derives its seed from a checksum over its own code, so patching the stub scrambles the strings instead of exposing them. Win32 + Win64. It raises the cost of casual inspection and automated extraction — a reverser with a debugger can still recover strings, because the stub must decrypt them unaided. | Paid |
| Public-key anti-tamper | Encrypts the RSA public-key blob that uLicense embeds. Enable string encryption as well and the two are tied together: a substituted key changes the seed the strings decrypt with, so the application breaks instead of accepting a forged license. On its own this pass raises the effort of a key swap rather than preventing one. Win32 + Win64. | Paid |
| License-code guard | Ties your license-verification code to the string encryption, so tampering with the check leaves the application unable to run. On by default; needs string encryption. Win32 + Win64. | Paid |
| TLS-callback entry | Has the Windows loader run the decrypt stubs before the entry point, which is left as the compiler wrote it. Off by default. Win32 + Win64. | Paid |
| Code signing | Runs your signing tool as the final step. | Paid |
--guard-license)The public-key pass protects your RSA key. This one protects the code that uses it — the routine in your application that verifies a license — which is the cheaper target of the two.
PasGuard locates that routine in the compiled binary and ties it to the string-encryption keystream: the bytes of the verification code become part of what the literals are encrypted with. If those bytes change after protection, the keystream no longer matches and the application's string literals do not come back. There is no comparison to invert and no error message to look for — the program simply stops working.
It is on by default. It costs a few hash loops at
startup, it cannot collide with any other pass, and on a target that
does not embed uLicense it finds nothing
and says so in one line. Switch it off with
--no-guard-license when you are bisecting
a protected binary that misbehaves and want the variable gone.
--encrypt-strings must be on.uLicense unit shipped in the toolkit
does. If PasGuard cannot find the verification path it says so in the
log rather than pretending it protected something.--tls-stub)Normally the decrypt stubs are reached through the executable's entry point. With this option the entry point is left exactly as the compiler wrote it and the stubs are run earlier, by the Windows loader itself, so the protected binary looks less like a patched executable.
Two notes. The demo watermark always runs from the entry point regardless of this setting. And if the target cannot take this treatment safely, PasGuard says so in the log and uses the entry point instead — both encryption passes make that decision together, so a build is never half one and half the other.
FindComponent / RTTI /
FindClass. If your app relies on such
reflection, exclude those names or keep that data intact, or the lookups
will return nil after obfuscation.
.doproj)Save your chosen options to a .doproj (INI)
profile from the GUI, then reuse them:
PasGuard_win32.exe -o MyApp.exe --project myprofile.doproj
CLI flags override values loaded from the project.
One key has no GUI control, on purpose:
[General] Compiler=auto
auto (the default) lets PasGuard identify
the compiler itself. delphi or
fpc force the choice. Only reach for it if
detection is demonstrably wrong — forcing the wrong compiler makes
PasGuard read the file's metadata with the wrong layout, which corrupts
the output. The same override exists on the command line as
--compiler auto|delphi|fpc.
The toolkit lets you add signed, per-customer licensing to your own Delphi or Lazarus applications, using the same RSA-2048 + SHA-256 scheme PasGuard uses on itself.
KeyGenTool.exe
Produces:
| File | Keep secret? | Use |
|---|---|---|
private.key | YES | Sign licenses (Delphi LicenseGen) |
private.pem | YES | Sign licenses (PHP backend) |
public.key / public.pem | no | Diagnostics |
PublicKey.inc | no | Embed in your app |
Back up the private keys in a safe place. This keypair is yours and is unrelated to the key that licenses PasGuard.
uLicense.pas,
uWinCrypt.pas,
PublicKey.inc and
RevokedLicenses.inc into your project.uLicense to a
uses clause.var
Lic: TLicenseInfo;
begin
Lic := LoadLicense(ExtractFilePath(ParamStr(0)) + 'license.dat');
if Lic.Valid then
btnExport.Enabled := HasFeature(Lic, FEATURE_EXPORT)
else
btnExport.Enabled := False; // demo mode: gate paid features
end;
The four files above are everything you need;
embed\uLicense.pas documents each entry
point at the top of the unit. A worked example — including an
"Install license…" button that copies a
.license file into
%APPDATA%\<YourApp>\ — is available
from support on request.
GUI: run LicenseGenGui.exe,
fill in name / email / dates / feature checkboxes, browse for your
private.key, save as
license.dat.
CLI:
LicenseGen.exe --name "Customer" --email c@x.com ^
--expiry 2027-06-03 --features 7 ^
--private private.key --out license.dat
--features is a 32-bit bitfield you define
for your own product tiers (see FEATURE_* in
uLicense.pas). Save the printed license
id — you need it to revoke.
--features is a single 32-bit integer; each
bit you decide to use is one independent feature flag — bit 0 … bit 31, so
up to 32 features.
uLicense.pas.
Each constant is a distinct power of two. The shipped examples:
const FEATURE_EXPORT = $00000001; // bit 0 FEATURE_PRINT = $00000002; // bit 1 FEATURE_PRO = $00000004; // bit 2 FEATURE_NETWORK = $00000008; // bit 3 FEATURE_PLUGINS = $00000010; // bit 4 FEATURE_ALL = $FFFFFFFF;Rename them and add your own; each new feature is the next power of two (
$20, $40,
$80, …).HasFeature(Lic, FEATURE_X) (see §9.2).--features 1 EXPORT only --features 7 EXPORT + PRINT + PRO (1+2+4) --features 19 EXPORT + PRINT + PLUGINS (1+2+16, = 0x13) --features 0x13 same value in hex --features 4294967295 everything (0xFFFFFFFF)
--features is parsed as hex when prefixed
with $ or 0x,
decimal otherwise. In LicenseGenGui.exe the
feature checkboxes do this arithmetic for you. You can also predefine
tiers in a presets file and issue with
--product <name> --tier <name>
instead of a raw number.license.dat — any
change breaks the signature and the file loads as invalid. Decide a fixed bit
combination per tier and issue that number.
LicenseGen and
LicenseGenGui print a
non-blocking note if your own PasGuard license is
missing or expired — issuing your app's licenses keeps working
regardless. By default they look for your PasGuard
license.dat in
%APPDATA%\PasGuard\ and next to the tool;
point them elsewhere with
--pasguard-license <path> (CLI) or the
PASGUARD_LICENSE environment variable.
After building your app, run PasGuard on it with "Encrypt embedded RSA public key" enabled. This cloaks the public key blob so an attacker can't substitute their own.
Leave "Guard license-check code" on (it is, by default) and keep string encryption enabled in the same run. The key pass protects the key; the guard protects the code that uses it, which is the cheaper thing to attack. §7.1 says what it covers and what it does not.
PasGuard_win64.exe -o Release\MyApp.exe --encrypt-strings --encrypt-pubkey
id. It's printed when
you issue the license; when issued through the web backend it's also the
<id>.license filename and is recorded
in audit.log.RevokedLicenses.inc:const REVOKED_LICENSE_IDS: array of string = [ 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' // chargeback ];
Revocation is offline: your app checks a list baked into the build, with no call-home. The trade-off is that it only takes effect once a customer runs a build containing the updated list — so revocation follows your release cadence rather than being instant.
See KEY_ROTATION.md for the full procedure.
The php/ folder lets you issue licenses
server-side — e.g. automatically after a payment, or from an admin tool.
php/ folder together.
The scripts require one another, so the endpoint
fatal-errors if a dependency is missing. Minimum set for issuing over HTTP:
license_gen_web.php +
LicenseSigner.php +
products.php +
crypto.php. The CLI issuer needs one file
fewer — license_gen.php +
LicenseSigner.php +
products.php.
LicenseSigner.php — the shared signing core
(RSA-2048 + SHA-256, byte order matched to the Delphi side).
Required: both license_gen.php
and license_gen_web.php
require it. A library, not a page you call.license_gen.php — CLI issuer (cron / one-off).license_gen_web.php — HTTP endpoints:
POST signs a new license (shared-secret
auth), GET ?id=<uuid> re-downloads one.products.php — named feature sets
(e.g. --features encrypt-strings).
Loaded by LicenseSigner.php.crypto.php — encrypts the issuance
audit trail at rest. Required by
license_gen_web.php. A library, not a
page you call.selftest.php — offline check:
php selftest.php.license_admin.php — optional example admin
page: a password-protected browser form that posts to
license_gen_web.php to issue a license (with
per-feature checkboxes). Keeps the shared secret server-side — never put
that token in a plain HTML form.Named features and a demo helper:
php license_gen.php --name "Trial" --email t@x.com \
--demo 14 --product yourproduct \
--features feature-a,feature-b \
--private private.pem --out license.dat
--demo N sets expiry to today + N days. See
php/README.md for deployment (env vars,
HTTPS, rate-limiting, the OpenSSL↔CryptoAPI byte-order note).
| Symptom | Cause / fix |
|---|---|
License: DEMO mode though you have a file | license.dat not next to the exe (or in %APPDATA%\PasGuard\); or it's expired/edited. Re-check the file. |
… is already obfuscated | The file was protected before. Rebuild from source or restore the .bak. |
| String encryption "skipped: registered feature" | Running unlicensed. Install a license.dat with the right features. |
| App can't find a component after obfuscation | A name used via FindComponent/RTTI was renamed. Heed the pre-flight warning; keep that name intact. |
Recognized VMTs: 0 / nothing gets renamed | The input isn't an Object Pascal binary (PasGuard supports Delphi and Free Pascal / Lazarus only), or the compiler was detected wrongly — check the Compiler card / Target line and, if it is wrong, set --compiler (§8). |
.lrs / legacy form storage refused | A Lazarus project whose forms live in code ({$I unit.lrs}) instead of resources. Rebuild with {$R *.lfm} — see §2.1. |
| Output won't run | Restore the .bak and re-run; report the binary to support. Always keep backups enabled while evaluating. |