Copy Protection and Copylock - HoraceAndTheSpider/Bloodwych-68k GitHub Wiki

Copy protection and Copylock

Bloodwych SPS 439 contains a Rob Northen Copylock check. It does more than look for an ordinary file or sector: it reads the floppy hardware directly, measures a protected raw track and hides much of its own code behind a single-step trace cipher.

The complete protection region begins at $D138 (CopyProtection) and ends immediately before $D74C (Print_com_menu_entry). It occupies $614 bytes, or 1,556 bytes, in the original program.

Where the game uses it

There are three calls to CopyProtection in the SPS 439 source:

  • LoadGame_ReadChampionDataFromDisk calls it before reading a saved game.
  • SaveGame_WriteChampionDataToDisk calls it before writing a saved game.
  • the $F0 text-stream extension calls it before entering the main player update path.

Each caller tests D0 and takes its failure or early-exit path when D0 is zero. This is important when replacing the protection: a routine-level bypass must return a non-zero value. A stub which returns zero prevents the normal load/save path from continuing.

What the protection does

The entry saves A4-A6 and jumps into a state-saving bootstrap. The bootstrap saves the CPU registers, redirects the illegal-instruction exception and uses a deliberate illegal instruction to enter the protected code.

The visible exception handlers then install handlers for trace and privilege exceptions. The trace handler works on a four-byte instruction window:

  1. it restores the previous encrypted longword;
  2. reads the next instruction address from the exception frame;
  3. saves the next encrypted longword;
  4. decrypts that window using the swapped inverse of the preceding longword;
  5. returns with tracing enabled so that the process repeats after the instruction executes.

This lets the code execute while leaving most of the routine unintelligible in a static disassembly. The extracted encrypted blocks should therefore be treated as executable payload, not as ordinary game data.

The portions which can be read directly show the disk-side purpose. The code:

  • waits for the floppy index pulse through CIA-B;
  • programs the Amiga disk controller and waits for a disk-sync match;
  • captures $32 (50) raw bytes after the match;
  • counts $03CE following disk words as a timing/length measurement;
  • checks whether the raw transfer completed;
  • passes the measured result back into the encrypted scanner through another deliberate illegal instruction.

An eleven-word sync table at $D584 contains:

$8A91 $8A44 $8A45 $8A51 $8912 $8911
$8914 $8915 $8944 $8945 $8951

The final visible handler at $D740 restores A4-A6 and subtracts $8488FFC4 from D0 before returning. The same constant appears in historical Bloodwych bypass material. Its exact relationship to the successful returned value is not yet proved: all three callers reject zero, so the subtraction must not be described simply as “zero means a valid disk”.

Layout in SPS 439

Address Size Purpose
$D138 $08 Entry, register save and branch into the protected bootstrap
$D140 $46 Zero-filled raw-track capture and scan buffer
$D186 $6A Saved registers, exception scratch and drive/head working state
$D1F0 $08 Address and original value of the active decrypted instruction window
$D1F8 $04 Saved cache-control value, initialized to $FFFFFFFF
$D1FC $24 State save and illegal-instruction entry
$D220 $B2 Trace-cipher bootstrap and encrypted branch maze
$D2D2 $66 Visible illegal-instruction and trace handlers
$D338 $1E6 Encrypted main disk check
$D51E $66 Visible raw-track timing and capture routine
$D584 $16 Eleven disk-sync words
$D59A $04 Returns the timing sample to the encrypted code through illegal
$D59E $1A0 Encrypted drive/head control, timing and exit setup
$D73E $02 rte belonging to the preceding protected path
$D740 $0C Protected return handler and $8488FFC4 subtraction

The next unrelated routine starts at $D74C.

Removing it safely

There are two different meanings of “remove” here.

Fixed-address removal and reuse

The safer design is to retain the public CopyProtection label at $D138, replace its body with a four-byte success stub, and reserve the rest of the original region for new code or data. A relative end marker lets new material grow while calculating the remaining fill automatically:

CopyProtection_RegionSize: equ $614

CopyProtection:
    moveq   #$01,d0
    rts

CopyProtection_RegionEnd: equ CopyProtection+CopyProtection_RegionSize

CopyProtection_NewCode:
    ; New code or data goes here.
CopyProtection_NewCodeEnd:
    ds.b    CopyProtection_RegionEnd-CopyProtection_NewCodeEnd

Print_com_menu_entry:

This is a layout example rather than a ready-made patch. The assembled result must be checked with the project's Devpac build.

The stub changes neither A4-A6 nor any exception vector and returns a non-zero result to all three existing callers. Four bytes remain occupied by the stub. The range $D13C-$D74B then provides $610 bytes, or 1,552 bytes, without moving Print_com_menu_entry or anything after it.

The ds.b count decreases as material is inserted before CopyProtection_NewCodeEnd. It becomes negative and fails to assemble if the replacement exceeds the original region. The total from CopyProtection to Print_com_menu_entry must remain exactly $614 bytes. The target end is defined relative to CopyProtection, rather than as the absolute address $D74C, so the guarded region can itself move when earlier source is relocated.

Using adrCd00D74C-CopyProtection_NewCodeEnd directly as the ds.b count is not a reliable guard because the forward label's address depends on the size of that same ds.b. Defining the intended end as CopyProtection+CopyProtection_RegionSize removes that circular dependency.

This fixed-span approach preserves every existing address outside the removed protection during an incremental build and is the recommended cautious first step.

The current relabelled source has only the three public calls listed above; references to the workspace, exception handlers and encrypted blocks stay inside the protection region. This supports in-place reuse, although it does not replace a compiled-image and runtime test.

Shrinking the executable

Deleting the remaining fill and moving later code backwards is consistent with the project's objective of producing a genuinely relocatable source tree. The risk is not that labels move: labels are intended to move. The outstanding risk is that some consumers may still contain an unlabelled numeric address, an embedded pointer or an external fixed offset. WHDLoad patches may also expect the original protection bytes or addresses.

A smaller executable should therefore be attempted only after:

  • every post-$D74C code and data pointer is represented by an assembler label or verified relocation;
  • opaque tables have been checked for embedded absolute addresses;
  • version-specific extraction offsets and editor data maps have been updated;
  • WHDLoad patch expectations have been audited;
  • the rebuilt game has passed boot, new game, single- and two-player load/save, disk-change and extended CPU tests.

Once those relocation checks pass, the ds.b guard can be removed. The later labels will move backwards and the executable becomes about 1.5 KiB smaller. The guarded pool is therefore a transitional, conservative mode for adding and testing new material, not the final limit of the rebuild project.

Recommended verification

First build and verify an unchanged SPS 439 source against the original binary. Then enable only the four-byte bypass and confirm that:

  • Print_com_menu_entry still assembles at $D74C;
  • all later labels retain their original addresses;
  • the game reaches normal play through the $F0 text extension;
  • load and save continue beyond the copy-protection test;
  • the illegal, privilege and trace vectors are untouched by the stub;
  • the result works on a 68000 Amiga configuration and on a later CPU;
  • WHDLoad does not apply an obsolete second patch to the reclaimed area.

Only after that test should new code or data be placed at CopyProtection_NewCode. The relative ds.b guard should remain until the project deliberately switches to the fully relocating build.

Remaining unknowns

The trace mechanism and visible raw-track reader are understood, but the two encrypted payloads have not been fully reconstructed as plain 68000 source. The exact derivation and meaning of the final D0 value therefore remain open. That uncertainty does not prevent an entry-point bypass, because the caller contract is visible: zero is rejected and a non-zero value continues.