Author Topic: PasGuard – Code Protection for Lazarus / Free Pascal Applications  (Read 570 times)

pasguard

  • Newbie
  • Posts: 4
 Hi everyone,

I'd like to introduce PasGuard, a commercial code protection tool for Pascal developers. It started out for Delphi and support for Lazarus / Free Pascal executables is recently added. This is why I'm posting here.

PasGuard works on the compiled .exe as a build/release step. Nothing is added to your source and the result stays fully native: no runtime, no wrapper, no external framework.

What it does:
- Symbol protection — classes, fields, properties and published methods are renamed, with the LFM form resources kept in step so component lookups keep working
- String encryption — string literals get a per-literal keystream and are decrypted at startup, so URLs, SQL and internal messages are not sitting in the file for strings or a hex editor to find
- PackageInfo and version info scrambling
- Optional code-signing step and licence anti-tamper support

What it does not do, so nobody wastes an evening finding out: there is no control-flow obfuscation or virtualisation, the logic of your program is not rewritten.
And string encryption raises the cost of casual inspection and automated extraction; a determined reverser with a debugger can still recover strings, because the stub has to decrypt them unaided.

Scope and limits for Lazarus/FPC:
- Windows PE only — no Linux, macOS or ARM targets
- Verified on Win32 and Win64 with FPC 3.2.2
- Forms must be stored as resources ({$R *.lfm}, the modern default). Legacy .lrs projects that keep form data inside code are detected and refused, not damaged
- Lazarus packages (.lpk) are not handled

There's a free trial that runs the full workflow on your own binary so you can check compatibility; its output is watermarked and shows a demo notice so don't ship it.

I'm specifically after feedback from Lazarus/FPC developers: does it handle your executable correctly? Compatibility reports, edge cases and suggestions are all welcome.

More at pasguard.com.

pasguard

  • Newbie
  • Posts: 4
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #1 on: August 21, 2026, 07:14:02 pm »
𝗣𝗮𝘀𝗚𝘂𝗮𝗿𝗱 1.8 𝗻𝗼𝘄 𝘀𝘂𝗽𝗽𝗼𝗿𝘁𝘀 𝗟𝗶𝗻𝘂𝘅

As of this release PasGuard protects more than just Delphi binaries for Win32 and Win64. Linux (ELF64) executables generated by Free Pascal and Lazarus are now supported as well.

The workflow stays the same: hand PasGuard your compiled binary, it runs the protection passes and out comes a protected, byte-for-byte runnable executable. No source code changes required.

Note that Linux support requires a licensed version. The demo build covers Windows targets only.

Head over to 𝗽𝗮𝘀𝗴𝘂𝗮𝗿𝗱.𝗰𝗼𝗺 for the new build.

Thaddy

  • Hero Member
  • *****
  • Posts: 19805
  • Glad to be alive.
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #2 on: August 21, 2026, 07:24:44 pm »
I don't want to offend you, but that is pointless code. Obfuscation is no protection. See my answer on the use of On-Guard (which is actually IONS better). Let me add an example why such code is pointless. Given a fully unlocked binary and a restricted binary this code generates a binary patch with full unlock:

The unit:
Code: Pascal  [Select][+][-]
  1. unit BinDiff;
  2.  
  3. { ---------------------------------------------------------------------------
  4.   BinDiff - rsync-style block/rolling-checksum binary diff & patch engine.
  5.  
  6.   Modernised re-implementation (FreePascal / Delphi, mode "delphi") of a
  7.   block-based binary differ:
  8.  
  9.     - The OLD file is split into fixed-size blocks. For every block a
  10.       "weak" rolling checksum (Adler/rsync style, 16+16 bit) and a
  11.       "strong" checksum (CRC32) are computed -> this is the "signature".
  12.     - The NEW file is scanned byte-by-byte with the rolling checksum.
  13.       Whenever the weak checksum of the current window matches an entry
  14.       in the signature's hash table, the CRC32 (strong checksum) of that
  15.       window is verified against the candidate block(s) before accepting
  16.       a match - this is the "CRC32 diff list" collision handling: a weak
  17.       hit can point at more than one candidate block, and each candidate
  18.       is confirmed (or rejected) via CRC32 before it is trusted.
  19.     - Matched windows become COPY instructions (reference into the old
  20.       file); everything else becomes LITERAL bytes. The resulting
  21.       instruction stream is the patch.
  22.     - Applying the patch replays COPY/LITERAL instructions against the
  23.       old file to reconstruct the new file, with CRC32 sanity checks
  24.       on the base file and the final size.
  25.  
  26.   Both the signature stream and the patch stream are plain, versioned
  27.   binary formats (4-byte magic + fixed header), so they round-trip
  28.   identically between FPC and Delphi.
  29.  
  30.   Known, deliberate simplifications versus "real" rsync:
  31.     - The new file is read fully into memory while building the delta
  32.       (the old file is only ever accessed via random Seek/Read, so it
  33.       does NOT need to fit in memory).
  34.     - Only full-block windows are matched; a trailing fragment of the
  35.       new file shorter than one block is always emitted as a literal.
  36.   --------------------------------------------------------------------------- }
  37.  
  38. {$ifdef fpc}
  39. {$mode delphi}{$WARN 5026 OFF}{$WARN 5027 OFF}{$WARN 5057 OFF}{$WARN 5058 OFF}
  40. {$endif}
  41. {$H+}
  42. {$POINTERMATH ON}
  43.  
  44. interface
  45.  
  46. uses
  47.   SysUtils, Classes, Generics.Collections;
  48.  
  49. const
  50.   DefaultBlockSize = 4096;
  51.  
  52. type
  53.   EBinDiffFormat = class(Exception);
  54.   ECRCMismatch   = class(Exception);
  55.  
  56.   { One block entry in a file signature. Packed so the on-disk layout is
  57.     identical regardless of compiler/platform alignment rules. }
  58.   TSignatureEntry = packed record
  59.     Weak: LongWord;    // rolling (weak) checksum of the block
  60.     Strong: LongWord;  // CRC32 (strong) checksum of the block
  61.     Offset: Int64;     // offset of the block in the old file
  62.     Size: LongWord;    // block length (last block of a file may be short)
  63.   end;
  64.  
  65.   TSignatureEntryArray = array of TSignatureEntry;
  66.  
  67.   { Full signature of a file: header info + the per-block entries. }
  68.   TFileSignature = record
  69.     BlockSize: LongWord;
  70.     FileSize: Int64;
  71.     FileCRC: LongWord;      // CRC32 of the whole file
  72.     Entries: TSignatureEntryArray;
  73.   end;
  74.  
  75.   TBinDiffEngine = class
  76.   private
  77.     FBlockSize: LongWord;
  78.  
  79.     class function CRC32Update(CRC: LongWord; const Buf; Len: LongWord): LongWord; static;
  80.     class function CRC32OfBlock(const Buf; Len: LongWord): LongWord; static;
  81.     class function WeakInit(const Buf; Len: LongWord): LongWord; static;
  82.     class function WeakRoll(W: LongWord; OldByte, NewByte: Byte; BlockLen: LongWord): LongWord; static;
  83.  
  84.     procedure WritePatchHeader(PatchStream: TStream; const Sig: TFileSignature; NewSize: Int64);
  85.     procedure ReadPatchHeader(PatchStream: TStream; out BlockSize: LongWord;
  86.       out OldSize, NewSize: Int64; out OldCRC: LongWord);
  87.   public
  88.     constructor Create(ABlockSize: LongWord = DefaultBlockSize);
  89.  
  90.     { Step 1: build the signature (per-block weak+strong checksums) of
  91.       the OLD/base file. }
  92.     function BuildSignature(OldStream: TStream): TFileSignature;
  93.     procedure SaveSignature(const Sig: TFileSignature; Dest: TStream);
  94.     function LoadSignature(Source: TStream): TFileSignature;
  95.  
  96.     { Step 2: compare NewStream against a previously built signature and
  97.       write the resulting binary delta (patch) to PatchStream. }
  98.     procedure CreateDelta(const Sig: TFileSignature; NewStream, PatchStream: TStream);
  99.  
  100.     { Step 3: replay a patch against the old file to reconstruct the new
  101.       file. Raises ECRCMismatch if the old file does not match the one
  102.       the patch was built from (unless VerifyOldCRC is False), and
  103.       EBinDiffFormat on any structural / size inconsistency. }
  104.     procedure ApplyPatch(OldStream, PatchStream, OutStream: TStream; VerifyOldCRC: Boolean = True);
  105.  
  106.     { Convenience wrappers operating directly on file names. }
  107.     procedure DiffFiles(const OldFileName, NewFileName, PatchFileName: string);
  108.     procedure PatchFiles(const OldFileName, PatchFileName, OutFileName: string);
  109.  
  110.     class function CRC32OfStream(Stream: TStream): LongWord; static;
  111.  
  112.     property BlockSize: LongWord read FBlockSize write FBlockSize;
  113.   end;
  114.  
  115. implementation
  116.  
  117. const
  118.   SigMagic:   array[0..3] of AnsiChar = 'BDS1';
  119.   PatchMagic: array[0..3] of AnsiChar = 'BDP1';
  120.  
  121.   opEnd     = 0;
  122.   opCopy    = 1;
  123.   opLiteral = 2;
  124.  
  125.   { A multiple of 65536 that is comfortably larger than any single
  126.     subtraction we perform below, so the (mod 65536) arithmetic in the
  127.     rolling checksum never has to deal with a negative intermediate. }
  128.   RollBias = LongWord($01000000);
  129.  
  130. var
  131.   CRCTable: array[Byte] of LongWord;
  132.  
  133. procedure BuildCRCTable;
  134. const
  135.   Poly = LongWord($EDB88320);
  136. var
  137.   i, j: Integer;
  138.   c: LongWord;
  139. begin
  140.   for i := 0 to 255 do
  141.   begin
  142.     c := LongWord(i);
  143.     for j := 0 to 7 do
  144.     begin
  145.       if (c and 1) <> 0 then
  146.         c := Poly xor (c shr 1)
  147.       else
  148.         c := c shr 1;
  149.     end;
  150.     CRCTable[i] := c;
  151.   end;
  152. end;
  153.  
  154. { ---------------------------------------------------------------------------
  155.   TBinDiffEngine
  156.   --------------------------------------------------------------------------- }
  157.  
  158. constructor TBinDiffEngine.Create(ABlockSize: LongWord);
  159. begin
  160.   inherited Create;
  161.   if ABlockSize = 0 then
  162.     ABlockSize := DefaultBlockSize;
  163.   FBlockSize := ABlockSize;
  164. end;
  165.  
  166. class function TBinDiffEngine.CRC32Update(CRC: LongWord; const Buf; Len: LongWord): LongWord;
  167. var
  168.   p: PByte;
  169.   i: LongWord;
  170. begin
  171.   p := PByte(@Buf);
  172.   for i := 0 to Len - 1 do
  173.     CRC := CRCTable[Byte(CRC xor p[i])] xor (CRC shr 8);
  174.   Result := CRC;
  175. end;
  176.  
  177. class function TBinDiffEngine.CRC32OfBlock(const Buf; Len: LongWord): LongWord;
  178. begin
  179.   Result := CRC32Update($FFFFFFFF, Buf, Len) xor $FFFFFFFF;
  180. end;
  181.  
  182. class function TBinDiffEngine.CRC32OfStream(Stream: TStream): LongWord;
  183. var
  184.   Buf: array[0..65535] of Byte;
  185.   n: Integer;
  186.   CRC: LongWord;
  187. begin
  188.   Stream.Position := 0;
  189.   CRC := $FFFFFFFF;
  190.   repeat
  191.     n := Stream.Read(Buf, SizeOf(Buf));
  192.     if n > 0 then
  193.       CRC := CRC32Update(CRC, Buf, n);
  194.   until n < SizeOf(Buf);
  195.   Result := CRC xor $FFFFFFFF;
  196. end;
  197.  
  198. { Weak (rolling) checksum: classic two-part Adler/rsync style checksum,
  199.   16 bits per half, combined into one 32-bit value. }
  200.  
  201. class function TBinDiffEngine.WeakInit(const Buf; Len: LongWord): LongWord;
  202. var
  203.   p: PByte;
  204.   i: LongWord;
  205.   s1, s2: LongWord;
  206. begin
  207.   p := PByte(@Buf);
  208.   s1 := 0;
  209.   s2 := 0;
  210.   for i := 0 to Len - 1 do
  211.   begin
  212.     s1 := s1 + p[i];
  213.     s2 := s2 + s1;
  214.   end;
  215.   Result := ((s2 and $FFFF) shl 16) or (s1 and $FFFF);
  216. end;
  217.  
  218. class function TBinDiffEngine.WeakRoll(W: LongWord; OldByte, NewByte: Byte; BlockLen: LongWord): LongWord;
  219. var
  220.   s1, s2, NewS1: LongWord;
  221. begin
  222.   s1 := W and $FFFF;
  223.   s2 := (W shr 16) and $FFFF;
  224.   NewS1 := (s1 + RollBias - OldByte + NewByte) and $FFFF;
  225.   s2 := (s2 + RollBias - ((BlockLen * OldByte) and $FFFF) + NewS1) and $FFFF;
  226.   Result := (s2 shl 16) or NewS1;
  227. end;
  228.  
  229. { ---------------------------------------------------------------------------
  230.   Signature building / (de)serialisation
  231.   --------------------------------------------------------------------------- }
  232.  
  233. function TBinDiffEngine.BuildSignature(OldStream: TStream): TFileSignature;
  234. var
  235.   Buf: array of Byte = [];
  236.   n: Integer;
  237.   Offset: Int64;
  238.   Cnt: Integer;
  239.   RunningCRC: LongWord;
  240. begin
  241.   Result := Default(TFileSignature);
  242.   SetLength(Buf, FBlockSize);
  243.   SetLength(Result.Entries, 0);
  244.   Result.BlockSize := FBlockSize;
  245.  
  246.   OldStream.Position := 0;
  247.   Offset := 0;
  248.   Cnt := 0;
  249.   RunningCRC := $FFFFFFFF;
  250.  
  251.   repeat
  252.     n := OldStream.Read(Buf[0], FBlockSize);
  253.     if n > 0 then
  254.     begin
  255.       RunningCRC := CRC32Update(RunningCRC, Buf[0], n);
  256.  
  257.       if Cnt >= Length(Result.Entries) then
  258.         SetLength(Result.Entries, Length(Result.Entries) + 1024);
  259.  
  260.       Result.Entries[Cnt].Weak := WeakInit(Buf[0], n);
  261.       Result.Entries[Cnt].Strong := CRC32OfBlock(Buf[0], n);
  262.       Result.Entries[Cnt].Offset := Offset;
  263.       Result.Entries[Cnt].Size := LongWord(n);
  264.  
  265.       Inc(Cnt);
  266.       Inc(Offset, n);
  267.     end;
  268.   until n < Integer(FBlockSize);
  269.  
  270.   SetLength(Result.Entries, Cnt);
  271.   Result.FileSize := Offset;
  272.   Result.FileCRC := RunningCRC xor $FFFFFFFF;
  273. end;
  274.  
  275. procedure TBinDiffEngine.SaveSignature(const Sig: TFileSignature; Dest: TStream);
  276. var
  277.   Cnt: LongWord;
  278. begin
  279.   Dest.WriteBuffer(SigMagic, SizeOf(SigMagic));
  280.   Dest.WriteBuffer(Sig.BlockSize, SizeOf(Sig.BlockSize));
  281.   Dest.WriteBuffer(Sig.FileSize, SizeOf(Sig.FileSize));
  282.   Dest.WriteBuffer(Sig.FileCRC, SizeOf(Sig.FileCRC));
  283.   Cnt := Length(Sig.Entries);
  284.   Dest.WriteBuffer(Cnt, SizeOf(Cnt));
  285.   if Cnt > 0 then
  286.     Dest.WriteBuffer(Sig.Entries[0], Cnt * SizeOf(TSignatureEntry));
  287. end;
  288.  
  289. function TBinDiffEngine.LoadSignature(Source: TStream): TFileSignature;
  290. var
  291.   Magic: array[0..3] of AnsiChar = (#0,#0,#0,#0);
  292.   Cnt: LongWord = 0;
  293. begin
  294.   Result := Default(TFileSignature);
  295.   Source.ReadBuffer(Magic, SizeOf(Magic));
  296.   if Magic <> SigMagic then
  297.     raise EBinDiffFormat.Create('BinDiff: not a valid signature stream (bad magic)');
  298.  
  299.   Source.ReadBuffer(Result.BlockSize, SizeOf(Result.BlockSize));
  300.   Source.ReadBuffer(Result.FileSize, SizeOf(Result.FileSize));
  301.   Source.ReadBuffer(Result.FileCRC, SizeOf(Result.FileCRC));
  302.   Source.ReadBuffer(Cnt, SizeOf(Cnt));
  303.  
  304.   SetLength(Result.Entries, Cnt);
  305.   if Cnt > 0 then
  306.     Source.ReadBuffer(Result.Entries[0], Cnt * SizeOf(TSignatureEntry));
  307.  
  308.   FBlockSize := Result.BlockSize;
  309. end;
  310.  
  311. { ---------------------------------------------------------------------------
  312.   Patch header
  313.   --------------------------------------------------------------------------- }
  314.  
  315. procedure TBinDiffEngine.WritePatchHeader(PatchStream: TStream; const Sig: TFileSignature; NewSize: Int64);
  316. begin
  317.   PatchStream.WriteBuffer(PatchMagic, SizeOf(PatchMagic));
  318.   PatchStream.WriteBuffer(Sig.BlockSize, SizeOf(Sig.BlockSize));
  319.   PatchStream.WriteBuffer(Sig.FileSize, SizeOf(Sig.FileSize));
  320.   PatchStream.WriteBuffer(Sig.FileCRC, SizeOf(Sig.FileCRC));
  321.   PatchStream.WriteBuffer(NewSize, SizeOf(NewSize));
  322. end;
  323.  
  324. procedure TBinDiffEngine.ReadPatchHeader(PatchStream: TStream; out BlockSize: LongWord;
  325.   out OldSize, NewSize: Int64; out OldCRC: LongWord);
  326. var
  327.   Magic: array[0..3] of AnsiChar = (#0,#0,#0,#0);
  328. begin
  329.   PatchStream.ReadBuffer(Magic, SizeOf(Magic));
  330.   if Magic <> PatchMagic then
  331.     raise EBinDiffFormat.Create('BinDiff: not a valid patch stream (bad magic)');
  332.  
  333.   PatchStream.ReadBuffer(BlockSize, SizeOf(BlockSize));
  334.   PatchStream.ReadBuffer(OldSize, SizeOf(OldSize));
  335.   PatchStream.ReadBuffer(OldCRC, SizeOf(OldCRC));
  336.   PatchStream.ReadBuffer(NewSize, SizeOf(NewSize));
  337. end;
  338.  
  339. { ---------------------------------------------------------------------------
  340.   Delta creation (the actual rsync-style rolling scan)
  341.   --------------------------------------------------------------------------- }
  342.  
  343. procedure TBinDiffEngine.CreateDelta(const Sig: TFileSignature; NewStream, PatchStream: TStream);
  344. var
  345.   HashTable: TDictionary<LongWord, TList<Integer>>;
  346.   Candidates: TList<Integer>;
  347.   List: TList<Integer>;
  348.   i, idx, MatchIndex: Integer;
  349.   NewBuf: array of Byte = nil;
  350.   NewSize: Int64;
  351.   BlockSize: LongWord;
  352.   Pos, LiteralStart: Int64;
  353.   W: LongWord;
  354.   HaveWeak: Boolean;
  355.   StrongCandidate: LongWord;
  356.   b: Byte;
  357.   Off8: Int64;
  358.   Len8: Int64;
  359.  
  360.   procedure FlushLiteral(EndPos: Int64);
  361.   var
  362.     RunLen: Int64;
  363.   begin
  364.     RunLen := EndPos - LiteralStart;
  365.     if RunLen <= 0 then
  366.       Exit;
  367.     b := opLiteral;
  368.     PatchStream.WriteBuffer(b, SizeOf(b));
  369.     Len8 := RunLen;
  370.     PatchStream.WriteBuffer(Len8, SizeOf(Len8));
  371.     PatchStream.WriteBuffer(NewBuf[LiteralStart], RunLen);
  372.   end;
  373.  
  374. begin
  375.   BlockSize := Sig.BlockSize;
  376.   if BlockSize = 0 then
  377.     raise EBinDiffFormat.Create('BinDiff: signature has zero block size');
  378.  
  379.   NewSize := NewStream.Size;
  380.   SetLength(NewBuf, NewSize);
  381.   NewStream.Position := 0;
  382.   if NewSize > 0 then
  383.     NewStream.ReadBuffer(NewBuf[0], NewSize);
  384.  
  385.   WritePatchHeader(PatchStream, Sig, NewSize);
  386.  
  387.   { Build weak-checksum -> candidate-block-index hash table. Several old
  388.     blocks can share the same weak checksum (that is precisely the
  389.     "diff list" for a given hash bucket); every candidate in the list
  390.     is confirmed against the strong CRC32 before being trusted. }
  391.   HashTable := TDictionary<LongWord, TList<Integer>>.Create;
  392.   try
  393.     for i := 0 to High(Sig.Entries) do
  394.     begin
  395.       if not HashTable.TryGetValue(Sig.Entries[i].Weak, List) then
  396.       begin
  397.         List := TList<Integer>.Create;
  398.         HashTable.Add(Sig.Entries[i].Weak, List);
  399.       end;
  400.       List.Add(i);
  401.     end;
  402.  
  403.     Pos := 0;
  404.     LiteralStart := 0;
  405.     HaveWeak := False;
  406.     W := 0;
  407.  
  408.     while Pos + Int64(BlockSize) <= NewSize do
  409.     begin
  410.       if not HaveWeak then
  411.       begin
  412.         W := WeakInit(NewBuf[Pos], BlockSize);
  413.         HaveWeak := True;
  414.       end;
  415.  
  416.       MatchIndex := -1;
  417.       if HashTable.TryGetValue(W, Candidates) then
  418.       begin
  419.         for idx := 0 to Candidates.Count - 1 do
  420.         begin
  421.           i := Candidates[idx];
  422.           if Sig.Entries[i].Size <> BlockSize then
  423.             Continue; // only match full-size blocks during the rolling scan
  424.           StrongCandidate := CRC32OfBlock(NewBuf[Pos], BlockSize);
  425.           if StrongCandidate = Sig.Entries[i].Strong then
  426.           begin
  427.             MatchIndex := i;
  428.             Break;
  429.           end;
  430.         end;
  431.       end;
  432.  
  433.       if MatchIndex >= 0 then
  434.       begin
  435.         FlushLiteral(Pos);
  436.  
  437.         b := opCopy;
  438.         PatchStream.WriteBuffer(b, SizeOf(b));
  439.         Off8 := Sig.Entries[MatchIndex].Offset;
  440.         PatchStream.WriteBuffer(Off8, SizeOf(Off8));
  441.         Len8 := BlockSize;
  442.         PatchStream.WriteBuffer(Len8, SizeOf(Len8));
  443.  
  444.         Inc(Pos, BlockSize);
  445.         LiteralStart := Pos;
  446.         HaveWeak := False;
  447.       end
  448.       else
  449.       begin
  450.         if Pos + Int64(BlockSize) < NewSize then
  451.           W := WeakRoll(W, NewBuf[Pos], NewBuf[Pos + Int64(BlockSize)], BlockSize)
  452.         else
  453.           HaveWeak := False;
  454.         Inc(Pos);
  455.       end;
  456.     end;
  457.  
  458.     { Trailing fragment shorter than one block: always literal. }
  459.     FlushLiteral(NewSize);
  460.  
  461.     b := opEnd;
  462.     PatchStream.WriteBuffer(b, SizeOf(b));
  463.   finally
  464.     for List in HashTable.Values do
  465.       List.Free;
  466.     HashTable.Free;
  467.   end;
  468. end;
  469.  
  470. { ---------------------------------------------------------------------------
  471.   Patch application
  472.   --------------------------------------------------------------------------- }
  473.  
  474. procedure TBinDiffEngine.ApplyPatch(OldStream, PatchStream, OutStream: TStream; VerifyOldCRC: Boolean);
  475. var
  476.   BlockSize: LongWord;
  477.   OldSizeHdr, NewSizeHdr: Int64;
  478.   OldCRCHdr: LongWord;
  479.   Op: Byte;
  480.   Offset, RunLen, BytesLeft, ChunkLen: Int64;
  481.   Buf: array of Byte;
  482. begin
  483.   PatchStream.Position := 0;
  484.   ReadPatchHeader(PatchStream, BlockSize, OldSizeHdr, NewSizeHdr, OldCRCHdr);
  485.  
  486.   if OldStream.Size <> OldSizeHdr then
  487.     raise EBinDiffFormat.CreateFmt(
  488.       'BinDiff: base file size mismatch (patch expects %d bytes, got %d)',
  489.       [OldSizeHdr, OldStream.Size]);
  490.  
  491.   if VerifyOldCRC then
  492.     if CRC32OfStream(OldStream) <> OldCRCHdr then
  493.       raise ECRCMismatch.Create(
  494.         'BinDiff: base file CRC32 does not match the file the patch was created from');
  495.  
  496.   SetLength(Buf, 65536);
  497.   repeat
  498.     PatchStream.ReadBuffer(Op, SizeOf(Op));
  499.     case Op of
  500.       opEnd:
  501.         Break;
  502.  
  503.       opCopy:
  504.         begin
  505.           PatchStream.ReadBuffer(Offset, SizeOf(Offset));
  506.           PatchStream.ReadBuffer(RunLen, SizeOf(RunLen));
  507.           OldStream.Position := Offset;
  508.           BytesLeft := RunLen;
  509.           while BytesLeft > 0 do
  510.           begin
  511.             ChunkLen := BytesLeft;
  512.             if ChunkLen > Length(Buf) then
  513.               ChunkLen := Length(Buf);
  514.             OldStream.ReadBuffer(Buf[0], ChunkLen);
  515.             OutStream.WriteBuffer(Buf[0], ChunkLen);
  516.             Dec(BytesLeft, ChunkLen);
  517.           end;
  518.         end;
  519.  
  520.       opLiteral:
  521.         begin
  522.           PatchStream.ReadBuffer(RunLen, SizeOf(RunLen));
  523.           BytesLeft := RunLen;
  524.           while BytesLeft > 0 do
  525.           begin
  526.             ChunkLen := BytesLeft;
  527.             if ChunkLen > Length(Buf) then
  528.               ChunkLen := Length(Buf);
  529.             PatchStream.ReadBuffer(Buf[0], ChunkLen);
  530.             OutStream.WriteBuffer(Buf[0], ChunkLen);
  531.             Dec(BytesLeft, ChunkLen);
  532.           end;
  533.         end;
  534.     else
  535.       raise EBinDiffFormat.CreateFmt('BinDiff: unknown patch opcode %d', [Op]);
  536.     end;
  537.   until False;
  538.  
  539.   if OutStream.Size <> NewSizeHdr then
  540.     raise EBinDiffFormat.CreateFmt(
  541.       'BinDiff: reconstructed size mismatch (expected %d bytes, got %d)',
  542.       [NewSizeHdr, OutStream.Size]);
  543. end;
  544.  
  545. { ---------------------------------------------------------------------------
  546.   File-based convenience wrappers
  547.   --------------------------------------------------------------------------- }
  548.  
  549. procedure TBinDiffEngine.DiffFiles(const OldFileName, NewFileName, PatchFileName: string);
  550. var
  551.   OldFS, NewFS, PatchFS: TFileStream;
  552.   Sig: TFileSignature;
  553. begin
  554.   OldFS := TFileStream.Create(OldFileName, fmOpenRead or fmShareDenyWrite);
  555.   try
  556.     Sig := BuildSignature(OldFS);
  557.   finally
  558.     OldFS.Free;
  559.   end;
  560.  
  561.   NewFS := TFileStream.Create(NewFileName, fmOpenRead or fmShareDenyWrite);
  562.   try
  563.     PatchFS := TFileStream.Create(PatchFileName, fmCreate);
  564.     try
  565.       CreateDelta(Sig, NewFS, PatchFS);
  566.     finally
  567.       PatchFS.Free;
  568.     end;
  569.   finally
  570.     NewFS.Free;
  571.   end;
  572. end;
  573.  
  574. procedure TBinDiffEngine.PatchFiles(const OldFileName, PatchFileName, OutFileName: string);
  575. var
  576.   OldFS, PatchFS, OutFS: TFileStream;
  577. begin
  578.   OldFS := TFileStream.Create(OldFileName, fmOpenRead or fmShareDenyWrite);
  579.   try
  580.     PatchFS := TFileStream.Create(PatchFileName, fmOpenRead or fmShareDenyWrite);
  581.     try
  582.       OutFS := TFileStream.Create(OutFileName, fmCreate);
  583.       try
  584.         ApplyPatch(OldFS, PatchFS, OutFS);
  585.       finally
  586.         OutFS.Free;
  587.       end;
  588.     finally
  589.       PatchFS.Free;
  590.     end;
  591.   finally
  592.     OldFS.Free;
  593.   end;
  594. end;
  595.  
  596. initialization
  597.   BuildCRCTable;
  598.  
  599. end.
« Last Edit: August 21, 2026, 07:53:14 pm by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

Thaddy

  • Hero Member
  • *****
  • Posts: 19805
  • Glad to be alive.
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #3 on: August 21, 2026, 07:33:43 pm »
The proof:
Code: Pascal  [Select][+][-]
  1. program testbindiff;
  2. {$ifdef fpc}
  3. {$mode delphi}
  4. {$endif}
  5. {$H+}
  6.  
  7. uses
  8.   SysUtils, Classes, BinDiff;
  9.  
  10. procedure MakeRandomFile(const FileName: string; Size: Integer; Seed: Integer);
  11. var
  12.   FS: TFileStream;
  13.   Buf: array of Byte =[];
  14.   i: Integer;
  15. begin
  16.   RandSeed := Seed;
  17.   SetLength(Buf, Size);
  18.   for i := 0 to Size - 1 do
  19.     Buf[i] := Random(256);
  20.   FS := TFileStream.Create(FileName, fmCreate);
  21.   try
  22.     if Size > 0 then
  23.       FS.WriteBuffer(Buf[0], Size);
  24.   finally
  25.     FS.Free;
  26.   end;
  27. end;
  28.  
  29. function ReadAllBytes(const FileName: string): TBytes;
  30. var
  31.   FS: TFileStream;
  32. begin
  33.   Result := nil;
  34.   FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  35.   try
  36.     SetLength(Result, FS.Size);
  37.     if FS.Size > 0 then
  38.       FS.ReadBuffer(Result[0], FS.Size);
  39.   finally
  40.     FS.Free;
  41.   end;
  42. end;
  43.  
  44. procedure RunCase(const CaseName, OldFile, NewFile: string; BlockSize: LongWord);
  45. var
  46.   Engine: TBinDiffEngine;
  47.   PatchFile, OutFile: string;
  48.   OldBytes, NewBytes, ResultBytes: TBytes;
  49.   PatchSize, OldSize, NewSize: Int64;
  50.   Ratio: string;
  51.   FS: TFileStream;
  52. begin
  53.   PatchFile := OldFile + '.patch';
  54.   OutFile := OldFile + '.out';
  55.  
  56.   Engine := TBinDiffEngine.Create(BlockSize);
  57.   try
  58.     Engine.DiffFiles(OldFile, NewFile, PatchFile);
  59.     Engine.PatchFiles(OldFile, PatchFile, OutFile);
  60.   finally
  61.     Engine.Free;
  62.   end;
  63.  
  64.   OldBytes := ReadAllBytes(OldFile);
  65.   NewBytes := ReadAllBytes(NewFile);
  66.   ResultBytes := ReadAllBytes(OutFile);
  67.  
  68.   OldSize := Length(OldBytes);
  69.   NewSize := Length(NewBytes);
  70.   FS := TFileStream.Create(PatchFile, fmOpenRead or fmShareDenyWrite);
  71.   try
  72.     PatchSize := FS.Size;
  73.   finally
  74.     FS.Free;
  75.   end;
  76.  
  77.   if (Length(ResultBytes) = Length(NewBytes)) and
  78.      CompareMem(@ResultBytes[0], @NewBytes[0], Length(NewBytes) * SizeOf(Byte)) then
  79.     Ratio := Format('OK  (old=%d new=%d patch=%d bytes)', [OldSize, NewSize, PatchSize])
  80.   else
  81.     Ratio := 'FAILED - reconstructed file does not match!';
  82.  
  83.   Writeln(Format('%-40s : %s', [CaseName, Ratio]));
  84.  
  85.   if (Length(ResultBytes) <> Length(NewBytes)) then
  86.     Halt(1);
  87.   if (Length(NewBytes) > 0) and not CompareMem(@ResultBytes[0], @NewBytes[0], Length(NewBytes)) then
  88.     Halt(1);
  89. end;
  90.  
  91. var
  92.   Dir: string;
  93.   A, B: TBytes;
  94.   FS: TFileStream;
  95.   i: Integer;
  96. begin
  97.   B := nil;
  98.   Dir := GetCurrentDir + PathDelim;
  99.   { Case 1: identical files -> patch should be (almost) all COPY. }
  100.   MakeRandomFile(Dir + 'c1_old.bin', 100000, 1);
  101.   A := ReadAllBytes(Dir + 'c1_old.bin');
  102.   FS := TFileStream.Create(Dir + 'c1_new.bin', fmCreate);
  103.   try
  104.     if Length(A) > 0 then
  105.       FS.WriteBuffer(A[0], Length(A));
  106.   finally
  107.     FS.Free;
  108.   end;
  109.   RunCase('Identical files', Dir + 'c1_old.bin', Dir + 'c1_new.bin', 4096);
  110.  
  111.   { Case 2: byte inserted near the start, NOT block-aligned -> only the
  112.     rolling checksum can find the shifted matches. }
  113.   A := ReadAllBytes(Dir + 'c1_old.bin');
  114.   SetLength(B, Length(A) + 1);
  115.   B[0] := 42; // insert one byte at the very front
  116.   Move(A[0], B[1], Length(A));
  117.   FS := TFileStream.Create(Dir + 'c2_new.bin', fmCreate);
  118.   try
  119.     FS.WriteBuffer(B[0], Length(B));
  120.   finally
  121.     FS.Free;
  122.   end;
  123.   RunCase('Single-byte insert at offset 0', Dir + 'c1_old.bin', Dir + 'c2_new.bin', 4096);
  124.  
  125.   { Case 3: a chunk deleted from the middle, not block-aligned. }
  126.   A := ReadAllBytes(Dir + 'c1_old.bin');
  127.   SetLength(B, Length(A) - 37);
  128.   Move(A[0], B[0], 12345);
  129.   Move(A[12345 + 37], B[12345], Length(A) - 12345 - 37);
  130.   FS := TFileStream.Create(Dir + 'c3_new.bin', fmCreate);
  131.   try
  132.     FS.WriteBuffer(B[0], Length(B));
  133.   finally
  134.     FS.Free;
  135.   end;
  136.   RunCase('37-byte mid-file deletion (unaligned)', Dir + 'c1_old.bin', Dir + 'c3_new.bin', 4096);
  137.  
  138.   { Case 4: completely different file (no matches expected). }
  139.   MakeRandomFile(Dir + 'c4_new.bin', 50000, 999);
  140.   RunCase('Completely different file', Dir + 'c1_old.bin', Dir + 'c4_new.bin', 4096);
  141.  
  142.   { Case 5: empty old file -> new file entirely literal. }
  143.   FS := TFileStream.Create(Dir + 'c5_old.bin', fmCreate);
  144.   FS.Free;
  145.   RunCase('Empty old file', Dir + 'c5_old.bin', Dir + 'c1_new.bin', 4096);
  146.  
  147.   { Case 6: empty new file. }
  148.   RunCase('Empty new file', Dir + 'c1_old.bin', Dir + 'c5_old.bin', 4096);
  149.  
  150.   { Case 7: file smaller than one block. }
  151.   MakeRandomFile(Dir + 'c7_old.bin', 200, 5);
  152.   MakeRandomFile(Dir + 'c7_new.bin', 250, 6);
  153.   RunCase('Files smaller than block size', Dir + 'c7_old.bin', Dir + 'c7_new.bin', 4096);
  154.  
  155.   { Case 8: append data at the end. }
  156.   A := ReadAllBytes(Dir + 'c1_old.bin');
  157.   SetLength(B, Length(A) + 500);
  158.   Move(A[0], B[0], Length(A));
  159.   for i := Length(A) to High(B) do
  160.     B[i] := Random(256);
  161.   FS := TFileStream.Create(Dir + 'c8_new.bin', fmCreate);
  162.   try
  163.     FS.WriteBuffer(B[0], Length(B));
  164.   finally
  165.     FS.Free;
  166.   end;
  167.   RunCase('Data appended at end', Dir + 'c1_old.bin', Dir + 'c8_new.bin', 4096);
  168.  
  169.   Writeln;
  170.   Writeln('All test cases passed.');
  171. end.

Such kind of "protection" code is simply, well, mind you, no insult intended, stupid.
This hacks anything if a copy of the unlocked code is provided to create the patch. Even on a zipped file..... >:D >:(
It is called modernized because I wrote the original in 1996....
(But this one is better because of insights developed over the years)
It proves it is pointless.

« Last Edit: August 21, 2026, 07:49:44 pm by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

pasguard

  • Newbie
  • Posts: 4
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #4 on: August 21, 2026, 08:38:21 pm »
I don't think your example demonstrates what you claim it demonstrates.

I agree that obfuscation by itself is not protection. PasGuard is not based on the assumption that a determined reverse engineer cannot inspect or modify a binary.

Your BinDiff example demonstrates something different: if an attacker has both a fully unlocked binary and the corresponding restricted binary they can use binary diff to identify the changes and construct a patch that transforms one into the other. That's certainly a valid attack against a particular build and I'm not claiming otherwise.

But that's not the same as demonstrating that the PasGuard protection is "pointless" nor does it constitute a generic unlock mechanism. Your patch is effectively a delta between two binaries for which you already have the desired end state.

The interesting question is therefore not whether binary patching is possible, of course it is. The question is what an attacker can achieve without having a valid unlocked reference binary, how much effort that requires, whether the protection survives recompilation/version changes and what parts of the licensing decision are actually enforced at runtime.

So I'd frame the claim more precisely: binary protection cannot make a determined attacker unable to modify a binary. It can however increase the cost and complexity of doing so. Whether that cost is worthwhile is the actual engineering question behind PasGuard.

Thaddy

  • Hero Member
  • *****
  • Posts: 19805
  • Glad to be alive.
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #5 on: August 21, 2026, 08:50:13 pm »
So I'd frame the claim more precisely: binary protection cannot make a determined attacker unable to modify a binary. It can however increase the cost and complexity of doing so. Whether that cost is worthwhile is the actual engineering question behind PasGuard.
That is naive: only real hackers would harm any finance. And they will do so anyway. I can also give you a demonstration for other obfuscators.
You don't need protection against script kiddies.

And my unit even patches a zipped file. And originated 30 years ago.
« Last Edit: August 21, 2026, 08:54:04 pm by Thaddy »
Any "programmer" that knows only one programming language is not a programmer

pasguard

  • Newbie
  • Posts: 4
Re: PasGuard – Code Protection for Lazarus / Free Pascal Applications
« Reply #6 on: August 21, 2026, 09:05:02 pm »
I think we're mixing up two different things here. 🙂

I completely agree that a determined hacker can eventually defeat client-side protection. If the code runs on their machine there is ultimately no magic force field around it.

But that's not the same as saying the protection is pointless.

Your BinDiff example is actually a good demonstration of this: if you have both the restricted binary and the corresponding fully unlocked binary you can calculate the differences and turn that into a patch. Sure. That's hardly surprising, you've basically been given the answer sheet. 😉

The more interesting question is what happens when you don't have the unlocked binary. Then you have to find the relevant checks, understand what they do, determine what to change and make the modification reliable. And if that has to be repeated for every new build that's additional work.

So I'm certainly not claiming that PasGuard makes a binary unbreakable. It doesn't. I'm claiming that it can make breaking it more work.

And if your argument is "a real hacker will spend the time anyway" then we're probably back to the threat model. A Ferrari can also be stolen by a professional car thief. That doesn't make the lock pointless. 😄

I'm actually happy to see demonstrations of how it can be attacked. That's much more useful to me than simply saying "obfuscation is pointless". If you can show a practical attack that works without a known unlocked reference binary that's genuinely interesting.

 

TinyPortal © 2005-2018