Eagles GMA87
Contents
- 1 Eagles (C64) — GMA87 copy protection, full technical write-up
- 1.1 Summary
- 1.2 Disk structure and directory obfuscation
- 1.3 Boot mechanism: overlapping the KERNAL's own RAM vector table
- 1.4 Bootstrap: a self-decrypting live stream, not a static file
- 1.5 The $05EB primitive: the real protection gate
- 1.6 The signature track: track 39
- 1.7 Stage 1: the track-39 signature-measurement program
- 1.8 Stage 1 → C64: $05EB receives $95
- 1.9 The M-W upload: staging a fresh drive-side program
- 1.10 Stage 2: the uploaded drive-side program, with $95 marked
- 1.11 See also
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
($00instead 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$DD00port 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 1541M-W(memory-write) commands, and bakes the measured signature byte directly into that program's own machine code — as the literal operand of anEOR #$95instruction 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 afterBOOT
with no closing quote orPRGshown — BASIC's line printer reads the$00mid-string as a premature line terminator.
LOAD"BOOT",8,1(exact name) returnsFILE 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 duringLOADitself) → 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), thenJSR $FFC6(CHKIN— itself hijacked, see next), thenJSR $0334.
- The hijacked
ICHKINvector ($031E→
$02CB) actually points at a small relocator that copies 9 bytes from$0304to$8000, thenJMP $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–$075Dlooks 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
DRVTRKhappens 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.