Difference between revisions of "GMA87"
| Line 35: | Line 35: | ||
GMA87 variants is only what happens to that measured byte afterward: | GMA87 variants is only what happens to that measured byte afterward: | ||
| − | # '''C64-side bulk decrypt''' | + | # '''C64-side bulk decrypt''' (20 of 21 titles). The measured byte is sent to the C64 over a raster-line-synced <code>$DD00</code> handshake and used as a single repeating-byte XOR key over almost the entire game, decrypted in place in one pass before jumping straight into the result. This is the scheme's "standard" form, and the one this article's disassembly is drawn from directly (Cholo, the disk the algorithm was originally worked out against). |
| − | + | # '''Drive-side per-sector decrypt''' ('''Eagles''' only, within this survey). The measured byte is instead baked as a literal <code>EOR #imm</code> operand into a second, freshly-uploaded drive-resident program, and the 1541's own 6502 decrypts every subsequent sector before it ever reaches the C64, streaming the result out over the same bit-banged serial link one byte at a time. Structurally identical, address-for-address, to the drive-side variant already documented for [[GMA86]]'s Firelord — this is an established technique in the same author's toolkit, not a one-off invention for Eagles. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | # '''Drive-side per-sector decrypt''' ('''Eagles''' only, within this survey). | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
On any genuine failure — marker not found within the retry budget, or a | On any genuine failure — marker not found within the retry budget, or a | ||
Latest revision as of 00:16, 29 August 2026
Pages that refer to this protection
- Descriptions of old disk copy protections (← links)
- Starfox (← links)
- Jagd auf Roter Oktober (← links)
- Enlightenment: Druid II (← links)
- GMA86 (← links)
- GMA88 (← links)
- Cholo (← links)
- Delta (← links)
- Beau Jolly Big Box 2 (← links)
- Kat Trap: Planet of the Cat-Men (← links)
- Triaxos (← links)
- Hit Pak: Trio (← links)
- GMA85 (← links)
- Bride of Frankenstein (← links)
- Bubble Bobble (← links)
- Flying Shark (← links)
- James Bond 007 in The Living Daylights: The Computer Game (← links)
- Sidewize (← links)
- James Bond 007 in Der Hauch des Todes: Das Computerspiel (← links)
- Twin Tornado (← links)
- Evening Star / Southern Belle (← links)
- Revs+ (← links)
- Zynaps (← links)
- Mystery of the Nile (← links)
- Scary Monsters (← links)
GMA87
GMA87 is a Commodore 64 disk copy protection scheme used across at
least 21 titles from Firebird, Thalamus, Hewson, and other publishers in
1986/87, and the third confirmed generation of the "GMA" protection family —
successor to GMA86, predecessor to GMA88. The scheme's own name comes
from a credit string embedded directly in the loader: "GMA WAS HERE
1986", found inside gm1.prg, the second-stage loader used
by nearly every title. Each disk's own BAM label follows the family's
running build-date convention (e.g. gma260687 for Bride of
Frankenstein).
Where GMA86 split three ways across its 11 surveyed titles (C64-side decrypt, drive-side decrypt, and one title with no key at all), GMA87 is almost entirely uniform: 20 of 21 surveyed titles use the C64-side bulk XOR decrypt (Cholo's architecture), with exactly one confirmed exception — Eagles, which moves decryption onto the 1541 drive's own 6502, matching the drive-side variant already established a year earlier in GMA86's Firelord/Uridium/Iridis Alpha.
Mechanism
Every GMA87 title uploads a short machine-code program to the 1541 drive's
own RAM over the serial bus (the standard DOS M-W/M-E
commands), and has the drive itself seek to track 38 — one
track short of Eagles' own out-of-range target and two short of GMA86's
drive-side titles (39) — search for the family's standard byte-aligned
marker, and measure ten raw sync-to-sync pulse-width samples there, folding
them into a single byte via the same CMP/ROL
technique used throughout the whole GMA family. What differs between the two
GMA87 variants is only what happens to that measured byte afterward:
- C64-side bulk decrypt (20 of 21 titles). The measured byte is sent to the C64 over a raster-line-synced
$DD00handshake and used as a single repeating-byte XOR key over almost the entire game, decrypted in place in one pass before jumping straight into the result. This is the scheme's "standard" form, and the one this article's disassembly is drawn from directly (Cholo, the disk the algorithm was originally worked out against). - Drive-side per-sector decrypt (Eagles only, within this survey). The measured byte is instead baked as a literal
EOR #immoperand into a second, freshly-uploaded drive-resident program, and the 1541's own 6502 decrypts every subsequent sector before it ever reaches the C64, streaming the result out over the same bit-banged serial link one byte at a time. Structurally identical, address-for-address, to the drive-side variant already documented for GMA86's Firelord — this is an established technique in the same author's toolkit, not a one-off invention for Eagles.
On any genuine failure — marker not found within the retry budget, or a failed job status — the drive hangs permanently in an infinite self-loop rather than reporting an error the C64 could act on, the same "hang, never produce wrong output" convention used throughout the whole GMA family. A disk copy that reproduces the sync-mark alignment (so the marker check passes) but not the exact physical sync-length timing is treated as fully legitimate at every checked step on both sides — the wrong byte just decrypts everything downstream to garbage, confirmed experimentally (see "The critical disk pattern" below).
The critical disk pattern
The entire scheme hinges on a small, deliberately malformed region on track 38 that a standard bit-nibbler copy cannot reproduce. Tracks 1–35 are a completely ordinary CBM DOS layout; tracks 36–40 sit outside the range any stock-formatted disk ever uses, reachable only by a loader that steps the drive head there directly:
Track 1 ───────────────────────────────────────── 35
ordinary CBM DOS layout ($08 header /
$07 data blocks, correct checksums)
Track 36 ┐
Track 37 │ 3× repeating group, every one of tracks 36-40:
Track 38 ├─ sync ~40/41
Track 39 │ bits 0110100101
Track 40 ┘ bytes <2-byte per-track "salt"> 8E 8D 55 55 ... 55 57
bits 111111
sync ~40
gcr 09 <- non-standard block-ID, not $07/$08
bytes 59 59 59 59 59 55 ... (mostly $55 filler)
bits 111111
───────────────────────────────────────────────────────────────────
Track 38 ONLY - a fourth, extra tail after its third repeating group:
sync 87 1-bits <- anomalous: ~2x a normal sync
gcr 0f
sync 136 1-bits <- anomalous: ~3x a normal sync
gcr 0f
...long run of $AA filler...
^^^^^^^^^^^^^^^^^^^^
THIS is the actual target of the drive-side read. Not
the gcr 09 marker, not the salt bytes above - those are
what a purely static/nibble-decoded read surfaces, and
the protection code ignores all of it. The drive reads
raw, byte-aligned bits straight off its own GCR shift
register (not nibble-decoded), and measures 10 raw
sync-to-sync pulse-width samples across exactly this
irregular tail.
Confirmed experimentally, not just inferred from the sync-length numbers
alone: shortening just the two outlier sync runs (87→40,
136→47, the latter chosen to keep the track's total bit count
byte-aligned) while leaving every other sync region on the disk untouched,
then reconverting to G64 and booting — the loader does not complete.
Separately, swapping (not shortening) two sync values in this same
region keeps the marker check passing and the drive reporting success, but
changes the folded result byte outright ($95 → $8D
on the disk tested) — proving the specific sync lengths in this region,
not merely their presence, are what the measurement reads. A plain D64
conversion of the same disk (no raw GCR layer at all) reaches the identical
point in the boot and then hangs permanently waiting for the drive's
handshake response — confirmed by polling registers over more than a minute
with the PC frozen at the same address — the signature of a hardware-timing
gate, not a cipher accepting a wrong key silently.
Captured live via a frozen snapshot (break on the drive executing
$0300 for the first time, don't resume, disassemble the
drive's own RAM directly) and confirmed byte-for-byte identical in structure
across every title surveyed — only the target-track constant and, for the
drive-side variant, what happens to the result afterward differ:
; ---- 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 (sync-length) 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 sync-length result
; ---- 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")
The remainder — the job-dispatch tail that submits the EXECUTE job for the
target track and either RTSes (C64-side variant) or falls into
the drive's own permanent hang loop on failure — is identical between the
two variants except for the literal track constant:
; ---- job setup: target track, submit EXECUTE job, check result (M-E entry point) ----
$0393 A9 26 LDA #$26 ; $26 = 38 (C64-side titles);
; Eagles' own build uses $27 = 39 here
$0395 85 06 STA $06
$0397 A9 01 LDA #$01
$0399 85 07 STA $07
$039B 20 18 C1 JSR $C118 ; (ROM call)
$039E A9 E0 LDA #$E0
$03A0 85 00 STA $00 ; submit EXECUTE job (buffer 0)
$03A2 A5 00 LDA $00
$03A4 30 FC BMI $03A2 ; wait for job completion
$03A6 C9 02 CMP #$02
$03A8 90 06 BCC $03B0 ; status < 2 (success) -> RTS normally
$03AA 4C AA 03 JMP $03AA ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP
$03AD 20 2C C1 JSR $C12C ; (unreached in the traced path)
$03B0 60 RTS
The C64-side loader (gm1.prg) reaches this drive routine
through a fixed, hardcoded JSR $C800 (Cholo/Triaxos's own
build; the target varies by loader memory layout but is always a fixed
address, never computed), immediately stashing the returned byte with
PHA and recovering it later with PLA/TAX.
Variant A: C64-side bulk decrypt
The measured byte is received via a raster-line-synced read of CIA2's
$DD00, decoded through four fixed 256-entry lookup tables, and
lands in A at the return of a hijacked GETIN
vector — installed unconditionally by the loader's main entry, regardless of
the separate "use fast loader (Y/N)?" prompt shown during boot:
$C308 08 PHP
$C309 78 SEI
$C30A A5 01 LDA $01 ; processor port (memory config)
$C30C 48 PHA ; save it
$C30D 29 FD AND #$FD
$C30F 09 05 ORA #$05 ; bank out BASIC ROM, keep I/O visible
$C311 85 01 STA $01
$C313 AD 00 DD LDA $DD00 ; read CIA2 port A once...
$C316 29 07 AND #$07
$C318 85 44 STA $44 ; ...build the "release" bit-pattern
$C31A 09 08 ORA #$08
$C31C 85 43 STA $43 ; ...and the "request" bit-pattern
$C31E 20 E3 C1 JSR $C1E3 ; do the actual raster-synced handshake+read
$C321 A8 TAY
$C322 68 PLA
$C323 85 01 STA $01 ; restore memory config
$C325 98 TYA
$C326 28 PLP
$C327 60 RTS
; ---- the cycle-accurate handshake itself ----
$C1E3 A5 43 LDA $43
$C1E5 8D 00 DD STA $DD00 ; write to CIA2 port A (drive-facing)
$C1E8 AD 00 DD LDA $DD00
$C1EB 10 FB BPL $C1E8 ; wait for bit 7 - a response from the drive
$C1ED AD 12 D0 LDA $D012 ; raster line
$C1F0 C9 31 CMP #$31
$C1F2 90 06 BCC $C1FA
$C1F4 29 06 AND #$06
$C1F6 C9 02 CMP #$02
$C1F8 F0 F3 BEQ $C1ED ; cycle-accurate raster-synced wait
$C1FA A5 44 LDA $44
$C1FC 8D 00 DD STA $DD00
$C1FF EA (x10) NOP ; fixed-length timing pad
$C209 AE 00 DD LDX $DD00 ; read the port value...
$C20C BD 00 01 LDA $0100,X ; ...as an INDEX into a lookup table
$C20F AE 00 DD LDX $DD00
; (three more reads, three more tables,
; OR'd together, same idea)
The received byte, stashed and later recovered into X, feeds a
self-modifying EOR/STA loop that walks upward
through memory, decrypting the game in place:
$C0DC A9 FE ... (memory-config setup, banks out BASIC ROM) $C0E1 85 01 STA $01 $C0E3 8A TXA $C0E4 4D 61 44 EOR $4461 ; self-modifying operand, walks $4461 -> ~$A7FF $C0E7 8D 61 44 STA $4461 $C0EA EE E5 C0 INC $C0E5 ; bump low byte of the EOR operand $C0ED EE E8 C0 INC $C0E8 ; bump low byte of the STA operand $C0F0 D0 F1 BNE $C0E3 $C0F2 EE E6 C0 INC $C0E6 ; bump high byte $C0F5 EE E9 C0 INC $C0E9 $C0F8 AD E9 C0 LDA $C0E9 $C0FB C9 A8 CMP #$A8 $C0FD D0 E4 BNE $C0E3 $C0FF 68 PLA $C100 85 01 STA $01 ; restore memory config $C102 60 RTS
Decrypting ~25.5 KB ($4461–$A7FF on Cholo's own
build) in place, then jumping straight into the result with no comparison
against any stored "correct" value — a wrong key just produces garbage code
and the machine crashes on the jump into it. There is no compare-and-branch
anywhere in this scheme to defeat by patching.
Variant B: drive-side per-sector decrypt (Eagles)
Eagles measures the identical kind of signature (from physical track 41,
addressed by the DOS as logical track 39 — one track past the C64-side
titles' target), but instead of sending the byte to the C64, uploads a
second program to drive RAM via a further sequence of M-W
commands, overwriting the first. It contains a GCR sector-read loop, the
standard DOS ROM's own GCRBIN/CHKBLK header
verification, and — at drive address $035D — the measured byte
baked directly in as an EOR #imm immediate operand:
; ---- header fields "decrypted" with FIXED, disk-independent ROM constants ----
$0339 AD 01 06 LDA $0601
$033C 4D 77 F5 EOR $F577 ; byte-count field - not disk-specific
$033F 8D 01 06 STA $0601
; ---- ****** THE PER-BYTE DATA DECRYPTION LOOP ****** ----
$0359 B9 00 06 LDA $0600,Y
$035C 49 95 EOR #$95 ; <====== the measured sync-length signature
; byte, injected here by the M-W upload -
; 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
The header fields (byte-count, status/type byte) are obfuscated with
fixed, disk-independent constants pulled straight from the drive's own
KERNAL ROM ($F576/$F577) — plain obfuscation, not
keyed to the disk at all. Only the actual game data bytes are
decrypted with the disk-specific sync-length key, entirely on the drive,
before the byte ever reaches the serial bus — the C64 never receives, holds,
or applies the key itself, which is why this variant can't be cracked by
watching the C64 decrypt a static block the way every other GMA87 title can.
Relationship to GMA85, GMA86, and GMA88
GMA87 inherits the whole family's shared drive-upload plumbing, marker convention, and hang-on-failure behavior from GMA85 and GMA86, and carries forward both of GMA86's real-key architectures — but where GMA86 split roughly evenly across three mechanisms over 11 titles, GMA87's surveyed titles converge overwhelmingly on the C64-side pattern, with Eagles as the sole confirmed drive-side example (itself not a new invention: the identical drive-side technique already existed in GMA86's Firelord at least a year earlier). That consolidation completes in GMA88, where every one of 15 surveyed titles — no exceptions found — uses the plain C64-side, track-38 pattern documented above.
Example from Exolon
On Track 38 we see in G64Conv text format of the track:
First the signature bytes:
sync 42 ; Following raw bytes: 69 57 57 a9 a9 55 55 55 55 55 55 55 55 55 55 aa ff
Then the following syncs appear (raw bytes after syncs omitted):
sync 39 sync 87 <- This is the reference sync length sync 40 <- This is shorter -> 0 sync 136 <- This is longer -> 1 sync 40 <- This is shorter -> 0 sync 40 <- This is shorter -> 0 sync 136 <- This is longer -> 1 sync 136 <- This is longer -> 1 sync 136 <- This is longer -> 1 sync 136 <- This is longer -> 1 result of 8 bits after shift: 01001111 -> 4F
