Difference between revisions of "GMA87"

From Software Archive
Jump to navigation Jump to search
 
(One intermediate revision by the same user not shown)
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''' (20 of 21 titles). The measured byte is sent
+
# '''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).
  to the C64 over a raster-line-synced <code>$DD00</code> handshake and used
+
# '''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.
  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.
 
  
 
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
Line 364: Line 350:
 
or applies the key itself, which is why this variant can't be cracked by
 
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.
 
watching the C64 decrypt a static block the way every other GMA87 title can.
 
== Known GMA87 titles ==
 
 
{| class="wikitable"
 
! Title !! Mechanism !! Measured key
 
|-
 
| Cholo || C64-side || <code>$B1</code> (cross-validated across 3 independent runs)
 
|-
 
| Triaxos || C64-side || <code>$5F</code>
 
|-
 
| Starfox || C64-side || <code>$4F</code> (2/2 sources agree)
 
|-
 
| Jagd auf Roter Oktober || C64-side || <code>$87</code> (2/2 sources agree)
 
|-
 
| Enlightenment: Druid II || C64-side || <code>$DE</code>
 
|-
 
| Delta || C64-side || <code>$C7</code> (6/6 independently-dumped sources agree)
 
|-
 
| Kat Trap: Planet of the Cat-Men || C64-side || <code>$58</code>
 
|-
 
| Eagles (via ''Beau Jolly Big Box 2'') || drive-side || <code>$95</code>
 
|-
 
| Cataball (via ''Hit Pak: Trio'') || C64-side || <code>$2F</code>
 
|-
 
| Bride of Frankenstein || C64-side || <code>$4C</code>
 
|-
 
| Bubble Bobble || C64-side || <code>$3B</code> (Rakki) / <code>$77</code> (Firebird, 2 sources — genuinely different pressing)
 
|-
 
| Flying Shark || C64-side || <code>$61</code>
 
|-
 
| James Bond 007: The Living Daylights / ''Der Hauch des Todes'' || C64-side || <code>$5E</code> (3 sources agree; same physical release under two titles)
 
|-
 
| Sidewize || C64-side || <code>$BD</code> (2/2 sources agree)
 
|-
 
| Twin Tornado || C64-side || <code>$5A</code>
 
|-
 
| Evening Star / Southern Belle || C64-side || <code>$6D</code> (matches a value already published independently)
 
|-
 
| Revs+ || C64-side || <code>$5D</code>
 
|-
 
| Zynaps || C64-side || <code>$4B</code> (2/2 sources agree)
 
|-
 
| Mystery of the Nile || C64-side || <code>$39</code>
 
|-
 
| Scary Monsters || C64-side || <code>$97</code>
 
|-
 
| Airwolf 2 (via ''Hit Pak: Trio'') || C64-side || <code>$2F</code> (same physical disk side as Cataball/Great Gurianos)
 
|-
 
| Great Gurianos (via ''Hit Pak: Trio'') || C64-side || <code>$2F</code> (same physical disk side as above)
 
|}
 
  
 
== Relationship to GMA85, GMA86, and GMA88 ==
 
== Relationship to GMA85, GMA86, and GMA88 ==
Line 455: Line 391:
 
* [[GMA86]]
 
* [[GMA86]]
 
* [[GMA88]]
 
* [[GMA88]]
* [[Eagles GMA87|Eagles]]
 
* [[Cholo]]
 
* [[Triaxos]]
 

Latest revision as of 00:16, 29 August 2026

Pages that refer to this protection

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:

  1. C64-side bulk decrypt (20 of 21 titles). The measured byte is sent to the C64 over a raster-line-synced $DD00 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).
  2. Drive-side per-sector decrypt (Eagles only, within this survey). The measured byte is instead baked as a literal EOR #imm 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.

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.

Signature measurement: the shared drive-side routine

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
GMA87.png

See also