Documentation

PasGuard — User Manual

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.

1. What PasGuard does

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.

Object Pascal only. PasGuard targets Windows executables produced by Embarcadero Delphi (7–13) or Free Pascal / Lazarus (3.2+). It does not support .NET, C/C++ or any other compiler: the obfuscation relies on Object Pascal's runtime metadata (VMT, RTTI, form resources, string layout), which other toolchains do not share. PasGuard identifies the compiler itself and refuses anything it cannot place, rather than damaging it. Lazarus specifics and limits are in §2.1.

2. System requirements

  • Windows 10 or 11. PasGuard itself is a Windows tool; what it protects does not have to be.
  • Your target must be produced by Embarcadero Delphi 7–13 or by Free Pascal / Lazarus 3.2+, as one of:
    • a Windows executable (PE), Win32 or Win64;
    • a Linux x86-64 executable (ELF64) built by Free Pascal or Lazarus — Professional license and up, see §2.2.
    PasGuard identifies which compiler built the file and adapts; binaries from other languages are refused rather than damaged. See §2.1 for the Lazarus specifics.

2.1 Free Pascal / Lazarus targets

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

  • Windows executables (PE) for i386-win32 and x86_64-win64
  • Linux executables (ELF64) for x86_64-linux, both the default static build and PIE — see §2.2
  • projects that store their forms as resources — the modern {$R *.lfm} default in Lazarus
  • console, GUI (LCL) and non-visual programs

Not supported

  • macOS, ARM, 32-bit Linux. Anything that is not a Windows PE or a Linux x86-64 ELF is refused rather than guessed at.
  • Legacy .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.
  • Lazarus packages (.lpk).

Worth knowing

  • Anything your code looks up by name at runtime stops matching after obfuscation — 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.
  • LCL and RTL classes are left alone deliberately. Renaming them buys nothing and breaks framework code that reflects on ClassName.
  • A handful of published method names are shared between classes in FPC's metadata. PasGuard skips those rather than rename one class's method out from under another.

2.2 Linux targets

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+
  • both link modes: the FPC default (ET_EXEC, statically linked) and PIE (-Cg -k-pie)
  • stripped binaries — FPC keeps its RTTI as data, not as symbols

Where a protected binary will run

Protection does not change this; your build already decided it.

your buildneedsruns 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.


3. Installation

  1. Unzip the package anywhere (e.g. C:\Tools\PasGuard\). It contains both PasGuard_win32.exe and PasGuard_win64.exe.
  2. If you were issued a license.dat, keep it next to the .exe you run (see §4).
  3. Run PasGuard_win32.exe (or PasGuard_win64.exe) as a GUI, or call either from a command prompt.

No installer, no registry changes.


4. Licensing & activation

PasGuard ships as one binary per architecture. With a valid license it runs every pass; without one it runs in DEMO mode.

Where the license file goes

On startup the tool looks for license.dat in this order (first match wins):

  1. Next to the executable…\PasGuard_win32.exe…\license.dat (simplest). The same applies to PasGuard_win64.exe.
  2. Per user%APPDATA%\PasGuard\license.dat

The same license.dat works for both the 32-bit and 64-bit builds.

Checking your status

Both the GUI log and the CLI print a status line on every run:

  • Licensed: License: Licensed to <name> <email> - expires YYYY-MM-DD
  • Demo: License: DEMO mode - no license installed

What DEMO mode means

DEMO mode is fully functional for evaluation, but:

  • Output is watermarked. The produced binary carries an identifying marker, and shows an "evaluation build" notice every time it starts.
  • Paid passes are disabled — string encryption, public-key anti-tamper and code signing are skipped with a clear log message.
  • Symbol obfuscation, package/version-info scrambling and analysis do run, so you can confirm the tool works on your real binary.

Install a valid license.dat to remove the watermark and enable the paid passes.


5. Quick start — GUI

  1. Launch PasGuard_win32.exe (or PasGuard_win64.exe).
  2. Browse to your .exe (or drag-and-drop it onto the window). Either build accepts both Win32 and Win64 targets.
  3. On the Obfuscation tab, tick the passes you want. Boxes marked * need a license:
    • Show renames in log — verbose output.
    • Obfuscate PackageInfo — scramble the unit names in that resource.
    • Scramble VersionInfo — scramble the version-info strings.
    • Create .bak backup — recommended (on by default).
    • Save rename map — emit an old→new name map file.
    • Encrypt string literals — paid; Win32 + Win64.
    • Encrypt RSA public key — paid; cloaks the uLicense key blob, Win32 + Win64.
    • Guard license-check code — paid; on by default, needs Encrypt string literals. Ties your license-verification code to the string encryption, so tampering with the check leaves the application unable to run. See §7.1.
    • Run stubs from a TLS callback — paid; leaves the entry point as the compiler wrote it. Off by default; read §7.2 before turning it on.
    • Code-sign after — paid; set the signing tool on the Code signing tab.
  4. Click Analyze first to see what will be renamed. (Obfuscate stays disabled until the current target has been analyzed.) The Analysis panel on the right then fills in four cards: Forms (DFM) — which counts LFM forms on a Lazarus target too — string literals, VMT entries, and Compiler — which reads 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.
  5. Click Obfuscate. The log streams progress; a .bak is written first.

6. Quick start — CLI

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:

OptionEffect
-clog each rename
--no-packageinfoleave the PACKAGEINFO resource untouched
--no-versioninfoleave VS_VERSION_INFO untouched
--no-backupdo 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-stringsEncrypt string literals with a per-literal keystream, Win32+Win64 — paid
--encrypt-pubkeyEncrypt an embedded RSA public-key blob, Win32+Win64 — paid
--guard-licenseBind 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-licenseTurn that binding off — for bisecting a target that misbehaves after protection
--tls-stubRun 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|fpcoverride compiler detection (see §8) — auto is the default
--helpusage

-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
Every pass the GUI offers is on the command line too, so a CI build produces the same output as a desktop run. All paid passes require a valid license.dat; without one they are skipped with a log message and the run continues.

Re-running

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.


7. The protection passes

PassWhat it doesTier
Symbol obfuscationRenames published classes, properties, fields, methods, method params and DFM/LFM component names to random identifiers.All
PackageInfo / VersionInfoScrambles unit names in the package-info resource and version-info strings.All
String encryptionEncrypts 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-tamperEncrypts 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 guardTies 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 entryHas 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 signingRuns your signing tool as the final step.Paid

7.1 License-code guard (--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.

  • It rides the string-encryption stub, so --encrypt-strings must be on.
  • Your application must call the Windows crypto API the ordinary way, through its import table — which is what the 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.
  • It hardens the verification routine itself. Like every static protection it raises cost rather than making tampering impossible; treat it as one layer, not as a guarantee.

7.2 TLS-callback entry (--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.

It is off by default, and that is a deliberate trade. The resulting file layout is less common than a patched entry point, and some AV heuristics treat anything unusual as suspicious — which matters more for software you distribute than the extra hardening does. Turn it on if your threat model says otherwise, and test your own AV situation before shipping.

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.

Pre-flight warning. Before renaming, PasGuard warns when an identifier also appears as a string literal — those may be used at runtime via 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.

8. Project files (.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.


9. Licensing toolkit — protect your own apps (Professional / Enterprise)

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.

9.1 One-time: generate your keypair

KeyGenTool.exe

Produces:

FileKeep secret?Use
private.keyYESSign licenses (Delphi LicenseGen)
private.pemYESSign licenses (PHP backend)
public.key / public.pemnoDiagnostics
PublicKey.incnoEmbed 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.

9.2 Embed the verifier in your app

  1. Copy uLicense.pas, uWinCrypt.pas, PublicKey.inc and RevokedLicenses.inc into your project.
  2. Add uLicense to a uses clause.
  3. At startup:
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.

9.3 Issue a license per customer

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.

Defining your own feature bits

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

  1. Name the bits in 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, …).
  2. Gate on them in code with HasFeature(Lic, FEATURE_X) (see §9.2).
  3. Pick a value when you issue. A tier is just the sum of the bits it includes. The bits are disjoint powers of two, so OR-ing == adding:
    --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.
The bits live in the signed payload, so a customer cannot turn a feature on by editing license.dat — any change breaks the signature and the file loads as invalid. Decide a fixed bit combination per tier and issue that number.

Renewal reminder (informational)

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.

9.4 Harden the embedded key

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

9.5 Revoke a license

  1. Find the license 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.
  2. Add it to RevokedLicenses.inc:
const REVOKED_LICENSE_IDS: array of string = [
  'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'  // chargeback
];
  1. Rebuild and ship your app. From that build on, the license loads as invalid.

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.

9.6 Rotate keys

See KEY_ROTATION.md for the full procedure.


10. PHP issuance backend (Enterprise)

The php/ folder lets you issue licenses server-side — e.g. automatically after a payment, or from an admin tool.

Deploy the whole 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).


11. Troubleshooting

SymptomCause / fix
License: DEMO mode though you have a filelicense.dat not next to the exe (or in %APPDATA%\PasGuard\); or it's expired/edited. Re-check the file.
… is already obfuscatedThe 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 obfuscationA name used via FindComponent/RTTI was renamed. Heed the pre-flight warning; keep that name intact.
Recognized VMTs: 0 / nothing gets renamedThe 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 refusedA Lazarus project whose forms live in code ({$I unit.lrs}) instead of resources. Rebuild with {$R *.lfm} — see §2.1.
Output won't runRestore the .bak and re-run; report the binary to support. Always keep backups enabled while evaluating.
Questions? Contact your vendor's sales/support address: contact@pasguard.com.