Eagles GMA87

From Software Archive
Jump to navigation Jump to search

Eagles (C64) — GMA87 copy protection, full technical write-up

This article documents, in full technical detail, the copy protection scheme used by the Commodore 64 release of Eagles, as distributed on the Beau Jolly Big Box 2 compilation disk (BeauJolly_BigBox2_Disk6_s0.g64). Eagles carries a GMA87 disk label — the same protection family used by Cholo, Triaxos, and roughly twenty other titles linking to the GMA87 wiki page — but, uniquely among all of them, moves the actual decryption step off the C64 and onto the 1541 disk drive's own 6502 CPU. This makes Eagles a title where the C64 never sees ciphertext for the protected game data at all.

Summary

  • The disk directory is deliberately hidden from naive tools by a non-standard
 ($00 instead of $A0) filename padding.
  • The only loadable file, BOOT, is deliberately loaded at
 $02A7 so that it overlaps and hijacks the C64's own KERNAL
 page-2/3 RAM vector table — a standard auto-start trick.
  • From there, a self-decrypting bootstrap streams in a much larger program
 live from the drive, byte by byte, XORing each byte against its own
 destination address (a descramble, not a secret key).
  • That program contains a hardware-handshake primitive
 ($05EB) that talks to the drive over CIA2's $DD00
 port with cycle-accurate raster-line synchronization. It is used for
 every byte of a further custom byte-transfer protocol.
  • The drive, on command, seeks to track 39 containing a
 deliberately malformed sync region, and measures a **physically
 unclonable, disk-specific signature byte** from sync length variation —
 structurally the same measurement Cholo/Triaxos perform on track 38.
 Measured value for this disk capture: $95 (149 decimal).
  • Unlike Cholo/Triaxos (which send this byte to the C64 and decrypt a large
 static block there in one pass), Eagles uploads a fresh drive-side
 program to drive RAM ($0300$03DF) via
 standard 1541 M-W (memory-write) commands, and bakes the
 measured signature byte directly into that program's own machine code —
 as the literal operand of an EOR #$95 instruction at drive
 address $035D.
  • From that point on, the drive's own 6502 decrypts every byte of
 every subsequent game-data sector, one block at a time, and streams
 already-plaintext bytes to the C64 over the bit-banged serial link. The
 C64 never receives, stores, or applies the key itself.
  • Both halves of the scheme fail silently on a bad copy — a hang, not an
 error message or a crash on load. A disk that reproduces the sync-mark
 layout but not the exact sync length pattern sails through every check on
 both sides and decrypts to garbage far downstream

Disk structure and directory obfuscation

Non-standard filename padding

The BOOT directory entry (logical track 18, sector 1, slot 0) is the four bytes 42 4F 4F 54 ("BOOT") followed by twelve $00 bytes, not the standard CBM DOS $A0 (shifted-space) padding. Two confirmed consequences:

  • LISTing the directory truncates right after BOOT
 with no closing quote or PRG shown — BASIC's line printer
 reads the $00 mid-string as a premature line terminator.
  • LOAD"BOOT",8,1 (exact name) returns FILE NOT FOUND
 — the real KERNAL's name-matching does not tolerate the non-standard
 padding either. Only LOAD"*",8,1 (wildcard match on the
 first directory entry) works.

1306 bytes, load address $02A7.

Boot mechanism: overlapping the KERNAL's own RAM vector table

$02A7 deliberately overlaps $0300$0333, the C64's page-2/3 KERNAL RAM vector table (IERROR, IMAIN, ..., CINV, ..., IGETIN, ...). Simply loading the file overwrites these vectors with whatever bytes land at those file offsets — no explicit install instruction is needed.

  • IMAIN ($0302, the address BASIC jumps through
 every time it returns to its input loop) → becomes $0334.
  • ISTOP ($0328, checked periodically during
 KERNAL I/O, including during LOAD itself) → becomes
 $02ED.

It means the "program" starts running the moment LOAD returns control to BASIC, with no RUN needed. The execution entry is at $02ED (ISTOP), not byte 0 of the file.

Bootstrap: a self-decrypting live stream, not a static file

Tracing forward from $02ED live (the static file content at these addresses is misleading — see why below):

  • $02ED$0301: JSR $FFC3
 (CLOSE), set pointer $AE/$AF =
 $0334, disable the display ($D011), then
 JSR $FFC6 (CHKIN — itself hijacked, see next),
 then JSR $0334.
  • The hijacked ICHKIN vector ($031E
 $02CB) actually points at a small relocator that copies 9
 bytes from $0304 to $8000, then
 JMP $02A7.
  • $02A7: a tight loop —
$02A7  JSR $FFA5        ; ACPTR - read one byte from the IEC bus
$02AA  EOR $AE          ; XOR against the destination pointer's OWN low byte
                         ;   (a self-referential descramble, not a secret
                         ;   key - fully determined by address alone)
$02AC  STA ($AE),Y
$02AE  ...               ; increment pointer, loop
 ...continuing until the pointer reaches $075D. This is why a
 static disassembly of $0334$075D looks like
 noise: those bytes are overwritten live, streamed byte-by-byte from the
 drive, before they are ever executed.

Once populated, $0334 onward contains a real, coherent program

The $05EB primitive: the real protection gate

Deep inside the newly-populated block sits $05EB, the key routine used for every byte of the custom transfer protocol that follows — including the destination address the payload gets written through.

$05EB  A9 0A       LDA #$0A            ; border/background = orange (visual cue)
$05ED  8D 20 D0    STA $D020
$05F0  8D 21 D0    STA $D021
$05F3  A5 17       LDA $17
$05F5  8D 00 DD    STA $DD00           ; write to CIA2 port A (drive-facing)
$05F8  EE 18 D4    INC $D418           ; SID volume flicker, cosmetic
$05FB  AD 00 DD    LDA $DD00
$05FE  10 F8       BPL $05FB           ; wait for bit 7 - a response from the drive
$0600  AD 12 D0    LDA $D012           ; raster line
$0603  C9 31       CMP #$31
$0605  90 06       BCC $060D
$0607  29 06       AND #$06
$0609  C9 02       CMP #$02
$060B  F0 F3       BEQ $0600           ; cycle-accurate raster-synced wait
$060D  A9 02       LDA #$02
$060F  8D 20 D0    STA $D020           ; border = red
$0612  8D 21 D0    STA $D021
$0615  A5 18       LDA $18
$0617  8D 00 DD    STA $DD00
$061A  EA (x9)     NOP                 ; fixed-length timing pad
$0624  AE 00 DD    LDX $DD00           ; read the port value...
$0627  BD 00 01    LDA $0100,X         ; ...as an INDEX into a lookup table
$062A  AE 00 DD    LDX $DD00
$062D  1D 08 01    ORA $0108,X         ; three more reads, three more tables, OR'd together
$0630  AE 00 DD    LDX $DD00
$0633  1D 10 01    ORA $0110,X
$0636  AE 00 DD    LDX $DD00
$0639  1D 18 01    ORA $0118,X
$063C  60          RTS

This reads CIA2's $DD00 port with cycle-accurate raster-line synchronization — structurally the same "cycle-accurate $DD00" handshake mechanism the Cholo/Triaxos track-38 check uses — and decodes the drive's response into a byte using four 256-entry lookup tables as a nibble/bit decoder.

The four lookup tables at $0100/$0108/ $0110/$0118 are fixed, not disk-derived: a store-watchpoint on that range caught the first write coming from a plain, unconditional copy loop ($0725$073F) that tiles a fixed 28-byte source at $0740, eight repeats each, into $0100$01FF. They only gate whether $05EB can complete at all; they don't encode a disk-specific value.

The transfer this primitive serves uses a $01-byte escape/terminator framing: a lone $01 signals end-of-data, $01 $01 is an escaped literal $01, anything else is stored directly via STA ($AE),Y with no further transformation.

Verification: a plain D64 hangs, it does not decrypt to garbage

Attaching a plain sector-image conversion of this disk (no raw GCR track-39 signature present) and running the identical LOAD"*",8,1 sequence, execution reaches $02A7, runs identically up to this exact point, and then hangs permanently at $05FB/$05FE (LDA $DD00 / BPL $05FB).

The signature track: track 39

The signature region is not at logical track 38 the way it is for Cholo/Triaxos. Converting the G64 to text and inspecting it shows physical tracks 36–38 are entirely empty (no track N section at all — a real gap), while track 39 has a section with two unmistakable markers: its first decoded bytes start with 69 59 59 A7 A7... (matching the 69 XX XX XX A9-style signature prefix used by every other GMA87 game), and its sync N entries are wildly non-uniform (41, 40, 40, 40, 87, 136, 40, 40) versus a normal track's uniform sync-mark length throughout.

Track-seek mechanism: entirely standard DOS ROM code

The drive's own DRVTRK variable ($0022) was traced live across a full load. The DRVTRK=39 transition is reached via completely standard, unmodified DOS-ROM job-dispatch code.

  • The custom drive code's only involvement is submitting an ordinary job
 (code $E0, EXECUTE) into the standard job queue with a
 header-table track value of 39 — a value the ROM never validates
 against the normal 1–35 range.
  • The store into DRVTRK happens at $F31B
 (LDA ($32),Y / STA $22), inside the stock job
 dispatcher's buffer-scan/mismatch-handling loop — 100% ROM code, reading
 the out-of-range track value straight out of the job's own header-table
 entry.

Stage 1: the track-39 signature-measurement program

It turns out to be structurally identical to the Cholo/Triaxos track-38 algorithm, just retargeted to Eagles' own signature track (39):

; ---- sync-marker search, 90-attempt retry budget ----
$0300  AD 00 1C    LDA $1C00
$0303  29 9F       AND #$9F
$0305  8D 00 1C    STA $1C00
$0308  A0 5A       LDY #$5A            ; Y = 90 - sync-retry budget
$030A  88          DEY                 ; <-- retry entry point
$030B  D0 05       BNE $0312
$030D  A9 02       LDA #$02            ; error $02 = HEADER NOT FOUND
$030F  4C 69 F9    JMP $F969           ; ERRR - retries exhausted, report failure
$0312  2C 00 1C    BIT $1C00
$0315  30 FB       BMI $0312           ; sync-wait poll
$0317  AD 01 1C    LDA $1C01           ; discard first raw byte after sync
$031A  B8          CLV
$031B  A2 04       LDX #$04
$031D  50 FE       BVC $031D           ; CLV/BVC byte-ready wait (classic 1541 trick)
$031F  B8          CLV
$0320  AD 01 1C    LDA $1C01           ; read raw GCR byte
$0323  9D 00 05    STA $0500,X         ; store into $0500-$0504 (5 bytes)
$0326  CA          DEX
$0327  10 F4       BPL $031D
$0329  C9 A9       CMP #$A9            ; last byte read must be $A9
$032B  D0 DD       BNE $030A           ; mismatch -> retry
$032D  AD 04 05    LDA $0504           ; first byte read (2nd overall)
$0330  C9 69       CMP #$69            ; must be $69
$0332  D0 D6       BNE $030A           ; mismatch -> retry
                                        ; signature "69 xx xx xx A9" confirmed

; ---- 10-sample raw pulse-width (flux-jitter) measurement ----
$0334  A0 00       LDY #$00
$0336  2C 00 1C    BIT $1C00
$0339  30 FB       BMI $0336           ; wait for next sync
$033B  A2 00       LDX #$00
$033D  E8          INX                 ; <-- pulse-width measurement loop
$033E  2C 00 1C    BIT $1C00
$0341  10 FA       BPL $033D           ; count iterations (X) while bit7=0
$0343  8A          TXA
$0344  99 00 05    STA $0500,Y         ; store sample[Y]
$0347  C8          INY
$0348  C0 0A       CPY #$0A            ; 10 samples total
$034A  D0 EA       BNE $0336

; ---- fold 10 samples into an 8-bit result via CMP/ROL ----
$034C  A2 02       LDX #$02            ; sample[0] discarded, sample[1] = reference
$034E  BD 00 05    LDA $0500,X
$0351  CD 01 05    CMP $0501           ; compare sample[X] to reference
$0354  2E 0A 05    ROL $050A           ; roll carry (>=ref->1, <ref->0) into result byte
$0357  E8          INX
$0358  E0 0A       CPX #$0A            ; samples[2..9], 8 comparisons -> 8-bit result
$035A  D0 F2       BNE $034E
$035C  AE 0A 05    LDX $050A           ; X = computed 8-bit result: $95 for this disk

; ---- send the result byte to the C64 via VIA1 $1800, nibble-out ----
$035F  2C 00 18    BIT $1800
$0362  10 FB       BPL $035F           ; wait for IEC bus ready
$0364  A9 10       LDA #$10
$0366  8D 00 18    STA $1800
$0369  2C 00 18    BIT $1800
$036C  30 FB       BMI $0369
$036E  8A          TXA                 ; A = result byte
$036F  4A          LSR A               ; \  send high nibble
$0370  4A          LSR A               ;  |
$0371  4A          LSR A               ;  |
$0372  4A          LSR A               ; /
$0373  8D 00 18    STA $1800
$0376  0A          ASL A
$0377  29 0F       AND #$0F
$0379  8D 00 18    STA $1800
$037C  8A          TXA
$037D  29 0F       AND #$0F            ; \  send low nibble
$037F  8D 00 18    STA $1800           ;  |
$0382  0A          ASL A               ;  |
$0383  29 0F       AND #$0F            ;  |
$0385  8D 00 18    STA $1800           ; /
$0388  A9 0F       LDA #$0F
$038A  EA          NOP
$038B  8D 00 18    STA $1800           ; final bus state
$038E  A9 01       LDA #$01            ; status $01 = SUCCESS
$0390  4C 69 F9    JMP $F969           ; ERRR (used generically as "set status, return")

; ---- self-relocation/patch helper (install-time only, not per-visit) ----
$0393  38          SEC
$0394  AD 39 03    LDA $0339
$0397  E9 E1       SBC #$E1
$0399  85 FB       STA $FB
$039B  AD 3A 03    LDA $033A
$039E  E9 10       SBC #$10
$03A0  85 FC       STA $FC
$03A2  A9 8D       LDA #$8D            ; $8D = STA opcode - patches code in place
$03A4  A0 00       LDY #$00
$03A6  91 FB       STA ($FB),Y
$03A8  C8          INY
$03A9  38          SEC
$03AA  AD 35 03    LDA $0335
$03AD  E9 D2       SBC #$D2
$03AF  91 FB       STA ($FB),Y
$03B1  C8          INY
$03B2  AD 36 03    LDA $0336
$03B5  E9 04       SBC #$04
$03B7  91 FB       STA ($FB),Y
$03B9  60          RTS

; ---- self-decrypt the $0300-$0392 block against 3 fixed ROM bytes (anti-static-disasm) ----
$03BA  A2 00       LDX #$00
$03BC  BD 00 03    LDA $0300,X
$03BF  4D 10 F5    EOR $F510
$03C2  4D 56 F5    EOR $F556
$03C5  4D 18 C1    EOR $C118
$03C8  9D 00 03    STA $0300,X
$03CB  E8          INX
$03CC  E0 93       CPX #$93            ; $93 = 147 bytes
$03CE  D0 EC       BNE $03BC

; ---- job setup: target track 39, submit EXECUTE job, check result ----
$03D0  A9 27       LDA #$27            ; $27 = 39 decimal - THE TARGET TRACK
$03D2  85 06       STA $06
$03D4  A9 01       LDA #$01
$03D6  85 07       STA $07
$03D8  20 18 C1    JSR $C118           ; (ROM call)
$03DB  A9 E0       LDA #$E0
$03DD  85 00       STA $00             ; submit EXECUTE job (buffer 0)
$03DF  A5 00       LDA $00
$03E1  30 FC       BMI $03DF           ; wait for job completion
$03E3  C9 02       CMP #$02
$03E5  90 06       BCC $03ED           ; status < 2 (success) -> RTS normally
$03E7  4C E7 03    JMP $03E7           ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP
$03EA  20 2C C1    JSR $C12C           ; (unreached in the traced path)
$03ED  60          RTS

Both failure modes are deliberate, permanent hangs, never wrong output: if the 5-byte marker doesn't match, the routine retries the sync search up to 90 times before giving up with error $02 ("header not found"). If the job's returned status is failure, execution at $03E7 falls into an infinite self-loop (JMP $03E7) — the drive-side counterpart to the C64-side hangs at $05FB/$05FE.

Measured result byte for this disk capture: $95 (149 decimal). Raw sample buffer at $0500$0509: 12 27 3C 11 12 3D 12 3D 12 3D (sample[0]=$12 discarded, sample[1]=$27 the reference). Hand-verifying the CMP/ROL algorithm against these samples independently reproduces $95 exactly.

Stage 1 → C64: $05EB receives $95

The C64 receives the drive's track-39 result via the same $05EB routine documented above (raster-synced $DD00 poll, then the 4-lookup-table decode, RTS at $063C).

The consumer is a separate, small wrapper elsewhere in the same code block:

$071F  78          SEI
$0720  20 EB 05    JSR $05EB
$0723  58          CLI
$0724  60          RTS ; A=$95, SP=$FB

This returns to

$03B9  8D 6E 05    STA $056E      ; A=$95 (unchanged), SP=$FF

The M-W upload: staging a fresh drive-side program

$056E is not a destination in its own right — it is offset $5D (93 decimal) inside a larger, 224-byte C64-RAM buffer based at $0511. That buffer gets uploaded to the drive, 32 bytes at a time, via a sequence of standard 1541 DOS M-W (memory-write) command-channel commands:

; ---- open the drive's command channel (LFN=15, dev=8, SA=15) ----
$06D4  20 BD FF    JSR $FFBD          ; SETNAM
$06D7  A9 0F       LDA #$0F           ; A = 15
$06D9  A8          TAY                ; Y = 15 (secondary address = command channel)
$06DA  A2 08       LDX #$08           ; X = 8 (device 8)
$06DC  20 BA FF    JSR $FFBA          ; SETLFS(LFN=15, dev=8, SA=15)
$06DF  20 C0 FF    JSR $FFC0          ; OPEN
$06E2  A2 0F       LDX #$0F
$06E4  20 C9 FF    JSR $FFC9          ; CHKOUT(15)
$06E7  A9 4D       LDA #$4D / JSR $FFD2   ; CHROUT 'M'
$06EC  A9 2D       LDA #$2D / JSR $FFD2   ; CHROUT '-'
$06F1  60          RTS                ; "M-" sent so far

; ---- complete the M-W command header and send one 32-byte chunk ----
$0644  20 D2 06    JSR $06D2          ; (the "M-" open routine above)
$0647  A9 57       LDA #$57 / JSR $EDDD   ; 'W'  -> command so far: "M-W"
$064C  A5 17       LDA $17 / JSR $EDDD    ; address LOW byte  (running offset:
                                           ;   $00,$20,$40,$60,$80,$A0,$C0)
$0651  A9 03       LDA #$03 / JSR $EDDD   ; address HIGH byte (fixed: page $03)
$0656  A9 20       LDA #$20 / JSR $EDDD   ; count = $20 (32 bytes), fixed
$065B  A4 17       LDY $17
$065D  18          CLC
$065E  A5 17       LDA $17
$0660  69 20       ADC #$20           ; running offset += $20 for next pass
$0662  85 17       STA $17
$0664  B9 11 05    LDA $0511,Y        ; <-- payload byte Y of the upload buffer
$0667  20 DD ED    JSR $EDDD          ; send it
$066A  C8          INY
$066B  C4 17       CPY $17
$066D  D0 F5       BNE $0664          ; loop for all 32 bytes of this chunk
$066F  20 CC FF    JSR $FFCC          ; CLRCHN
$0672  A5 17       LDA $17
$0674  C9 DA       CMP #$DA           ; $DA = 218 decimal
$0676  90 CC       BCC $0644          ; more chunks needed -> loop
$0678  ...                            ; upload complete (7 chunks, 224 bytes)

Each pass sends one complete M-W $03xx $20 <32 bytes> command. Seven passes, at running offsets $00,$20,$40,$60,$80,$A0,$C0, upload $E0 (224) bytes total, covering drive RAM $0300$03DF. The $0511 buffer is a full replacement copy of the next drive-side job program, staged in C64 RAM and pushed to the drive one M-W chunk at a time.

Since $056E sits at buffer offset $5D, inside the third 32-byte chunk (offsets $40$5F, destined for drive addresses $0340$035F), the measured signature byte lands at drive address $0300 + $5D = $035D.

Stage 2: the uploaded drive-side program, with $95 marked

Uploaded specifically to decrypt and stream the actual game data:

; ---- read a full GCR-encoded sector from the VIA into a 256-byte buffer ----
$0300  A9 06       LDA #$06
$0302  85 31       STA $31
$0304  20 0A F5    JSR $F50A          ; seek/prep (1541 KERNAL DSTRT)
$0307  50 FE       BVC $0307          ; wait for VIA SR byte-ready
$0309  B8          CLV
$030A  AD 01 1C    LDA $1C01          ; read GCR byte from VIA2
$030D  99 00 06    STA $0600,Y
$0310  C8          INY
$0311  D0 F4       BNE $0307          ; fill $0600-$06FF (256 bytes)

$0313  A0 BA       LDY #$BA
$0315  50 FE       BVC $0315
$0317  B8          CLV
$0318  AD 01 1C    LDA $1C01
$031B  99 00 01    STA $0100,Y
$031E  C8          INY
$031F  D0 F4       BNE $0315          ; fill $01BA-$01FF (70 bytes, stack page)

; ---- decode GCR, verify header track and checksum (standard DOS ROM calls) ----
$0321  20 E0 F8    JSR $F8E0          ; GCRBIN
$0324  A5 38       LDA $38
$0326  C5 47       CMP $47            ; header track check
$0328  F0 04       BEQ $032E
$032A  A9 04       LDA #$04
$032C  D0 4E       BNE $037C          ; -> error, status $04

$032E  20 E9 F5    JSR $F5E9          ; CHKBLK
$0331  C5 3A       CMP $3A            ; checksum check
$0333  F0 04       BEQ $0339
$0335  A9 05       LDA #$05
$0337  D0 43       BNE $037C          ; -> error, status $05

; ---- header fields decrypted with FIXED, disk-independent ROM constants ----
$0339  AD 01 06    LDA $0601
$033C  4D 77 F5    EOR $F577          ; byte-count field
$033F  8D 01 06    STA $0601
$0342  85 07       STA $07
$0344  A0 02       LDY #$02
$0346  A2 FF       LDX #$FF
$0348  AD 00 06    LDA $0600
$034B  4D 76 F5    EOR $F576          ; status/type byte
$034E  8D 00 06    STA $0600
$0351  F0 03       BEQ $0356
$0353  8E 01 06    STX $0601
$0356  EE 01 06    INC $0601

; ---- ****** THE PER-BYTE DATA DECRYPTION LOOP ****** ----
$0359  B9 00 06    LDA $0600,Y
$035C  49 95       EOR #$95           ; <====== HERE. The measured
                                       ;   flux-jitter signature byte, INJECTED
                                       ;   by the M-W upload above, sits as the
                                       ;   immediate operand of this EOR at
                                       ;   drive address $035D. Every data byte
                                       ;   of every subsequent sector is
                                       ;   decrypted with THIS key, on the
                                       ;   drive's own 6502, before ever being
                                       ;   sent to the C64.
$035E  AA          TAX
$035F  E0 01       CPX #$01
$0361  D0 03       BNE $0366
$0363  20 91 03    JSR $0391          ; send nibble (bit-bang $1800)
$0366  20 91 03    JSR $0391          ; send nibble
$0369  C8          INY
$036A  CC 01 06    CPY $0601
$036D  D0 EA       BNE $0359          ; loop over the whole decrypted data block

; ---- end-of-block housekeeping / next-sector dispatch ----
$036F  AD 00 06    LDA $0600
$0372  F0 0E       BEQ $0382
$0374  C5 06       CMP $06
$0376  85 06       STA $06
$0378  F0 05       BEQ $037F
$037A  A9 01       LDA #$01
$037C  4C 69 F9    JMP $F969          ; job dispatcher / error exit
$037F  4C 04 03    JMP $0304          ; loop back for next GCR block

$0382  A2 01       LDX #$01
$0384  20 91 03    JSR $0391
$0387  A2 02       LDX #$02
$0389  20 91 03    JSR $0391
$038C  A9 7F       LDA #$7F
$038E  4C 69 F9    JMP $F969          ; job status $7F = done/OK

; ---- $0391: the same $1800 bit-bang nibble-send routine used by Stage 1 ----
$0391  2C 00 18    BIT $1800
$0394  10 FB       BPL $0391
$0396  A9 10       LDA #$10
$0398  8D 00 18    STA $1800
$039B  2C 00 18    BIT $1800
$039E  30 FB       BMI $039B
$03A0  8A          TXA
$03A1  4A          LSR A
$03A2  4A          LSR A
$03A3  4A          LSR A
$03A4  4A          LSR A
$03A5  8D 00 18    STA $1800
$03A8  0A          ASL A
$03A9  29 0F       AND #$0F
$03AB  8D 00 18    STA $1800
$03AE  8A          TXA
$03AF  29 0F       AND #$0F
$03B1  8D 00 18    STA $1800
$03B4  0A          ASL A
$03B5  29 0F       AND #$0F
$03B7  8D 00 18    STA $1800
$03BA  A9 0F       LDA #$0F
$03BD  8D 00 18    STA $1800
$03C0  60          RTS

This is the actual decryption mechanism for the game's data payload. The block's own header fields (byte count, status/type byte) are "decrypted" with fixed, disk-independent constants pulled straight from the drive's own ROM ($F576/$F577) — plain obfuscation, not keyed to this disk at all. Every actual data byte, by contrast, is decrypted with $95 — the disk-specific, sync length variation signature — inside the loop at $0359$036D, entirely on the drive, before the byte is ever placed on the serial bus. The C64 receives only already-decrypted plaintext through the same $0391 nibble-out routine documented for Stage 1.

See also