GMA88

From Software Archive
Revision as of 23:18, 28 August 2026 by Enigma (talk | contribs)
Jump to navigation Jump to search

Pages that refer to this protection

GMA88

GMA88 is a Commodore 64 disk copy protection scheme used across a run of 1988 releases from Firebird, Hewson, and other publishers, and the fourth confirmed generation of the "GMA" protection family — successor to GMA87, predecessor to GMA89. Each disk's own BAM label follows the family's running build-date convention (e.g. arac gma88, morpheus gma88), and the loader chain keeps the same as/gm1 file-naming pattern used throughout the family: a boot stub loads at $02A7 and relocates itself over the KERNAL's page-2/3 RAM vector table exactly as documented for GMA85/86/87, then loads gm1, which uploads a signature- measurement program to 1541 drive RAM via M-W/M-E and triggers it via a fixed, hardcoded JSR $C800.

GMA88 is completely uniform. Where GMA86 split three ways across its 11 titles and GMA87 was overwhelmingly one pattern with a single confirmed exception (Eagles), every one of 15 surveyed GMA88 titles — 39 individually traced disk images, no exceptions found anywhere — uses the identical mechanism: measurement on track 38, consumed as a C64-side bulk XOR decrypt. No drive-side per-sector variant, no presence-only variant. Whatever architectural experimentation the family went through in GMA86 is over by this generation.

Mechanism

  1. The drive seeks to track 38 ($26), searches for the
 family's standard byte-aligned marker (69 xx xx xx A9, not
 decoded to the usual 5-bit GCR nibble boundaries), and measures 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.
  1. The result is sent to the C64 over a raster-line-synced $DD00
 handshake (via a hijacked GETIN vector) and used as a single
 repeating-byte XOR key over almost the entire game, decrypted in place in
 one pass.
  1. 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.

The critical disk pattern

Tracks 1–35 are a completely ordinary CBM DOS layout; track 38 sits outside the range any stock-formatted disk ever uses, reachable only by a loader that steps the drive head there directly. Confirmed directly against a genuine GMA88 disk's own G64 — not inferred from GMA85/86/87 alone — the raw sync-length values on track 38 (read straight from a g64conv text dump, no live tracing required) are:

 Track  1 ──────────────────────────────────────────  35
           ordinary CBM DOS layout ($08 header /
           $07 data blocks, correct checksums)

 Track 38, raw sync sequence (Arac, this disk's measured key = $C5):

     3, 41, 40, 40, 40, 40,   39, 119, 184, 184, 56, 56, 56, 184, 56, 184,   7, 17, 1, 2, ...
     └─── ordinary syncs ───┘  └──────────── the anomalous region ────────┘  └─ gap/filler noise ─
                                (10 raw samples the drive's pulse-width
                                 measurement loop actually reads)

Discarding the first sample (39) and taking the second (119) as the reference, the remaining eight (184,184,56,56,56,184,56,184) compared against it via CMP/ROL — longer-or-equal → 1, shorter → 0 — give 1,1,0,0,0,1,0,1, folding to exactly 0b11000101 = $C5, matching this disk's already-live-confirmed key byte for byte. This is the same technique validated against a known-good GMA89 disk (Ghostbusters II) and confirmed to generalize across generations: the raw sync N values g64conv reports on the signature track are the literal numbers the drive's measurement loop consumes, with no unit conversion needed — a GMA-family key can be read directly off the disk image, without booting anything, once the signature track is located.

Signature measurement: the shared drive-side routine

Byte-for-byte identical to the shared GMA85/86/87 signature-measurement program. Captured live for Arac (target track confirmed directly at $0397: LDA #$26) and structurally confirmed for every other title in the batch via the same checkpoint/track behavior:

; ---- 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: $C5 on Arac

; ---- 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                 ; Arac has 4 further NOPs here (build variant)
$038B  8D 00 18    STA $1800
$038E  A9 01       LDA #$01
$0390  4C 69 F9    JMP $F969

; ---- job dispatch (M-E entry point): target track 38, submit EXECUTE job ----
$0397  A9 26       LDA #$26            ; $26 = 38 decimal - THE TARGET TRACK, every title
$0399  85 06       STA $06
$039B  A9 01       LDA #$01
$039D  85 07       STA $07
$039F  20 18 C1    JSR $C118
$03A2  A9 E0       LDA #$E0
$03A4  85 00       STA $00
$03A6  A5 00       LDA $00
$03A8  30 FC       BMI $03A6
$03AA  C9 02       CMP #$02
$03AC  90 06       BCC $03B4
$03AE  4C AE 03    JMP $03AE           ; failure -> infinite self-loop, family convention
$03B1  20 2C C1    JSR $C12C
$03B4  60          RTS                 ; plain RTS - no second-stage upload, C64-side only

The C64-side decrypt

Reached through the same JSR $C800PHA → ... → PLA/TAX handoff idiom used throughout the family. Arac's own main loop:

        LDA #$27
        STA $01
        LDA #$00
        JSR sub_7C33          ; load file 0
        JSR $C800              ; the fixed drive-upload-and-execute trigger
        PHA                    ; stash the measured key
        LDA #$01
        JSR sub_7C33           ; load file 1
        JSR $6FC0
        LDA #$02
        JSR sub_7C33           ; load file 2
        JSR $70F8
        LDA #$03
        JSR sub_7C33           ; load file 3
        LDA #$04
        JSR sub_7C33           ; load file 4
        LDY #$1F
L7C15:  LDA $FD30,Y
        STA $FFFA,Y
        LDA #$00
        STA $D400,Y
        DEY
        BPL L7C15
        JSR $FFE7               ; CLALL
        JSR $FFCC               ; CLRCHN
        PLA                    ; recover the measured key
        TAX                    ; X = $C5 (this disk's measured key)
        JSR sub_7EFE            ; run the decrypt loop
        CLI
        JMP $0400               ; jump into the freshly-decrypted screen/game code

The decrypt loop itself is a self-modifying EOR/STA walk, decrypting $7FA0 upward through roughly $D0xx (~20.5 KB) — a range that runs into the $D000-$DFFF I/O page, which is why the loop deliberately toggles the CPU memory-config port ($01) between writes:

sub_7EFE:
        SEI
L7EFF:  LDA #$24               ; bank OUT I/O ($D000-$DFFF reads/writes as RAM)
        STA $01
        TXA
        EOR $7FA0               ; self-modifying operand, walks $7FA0 upward
        STA $7FA0               ;   (this STA lands inside $D000-$DFFF on later
                                 ;   passes, which is exactly why I/O had to be
                                 ;   banked out first - otherwise this would hit
                                 ;   VIC-II/SID/CIA registers instead of RAM)
        LDA #$27               ; bank I/O back IN, just to touch the real registers
        STA $01
        INC $D020               ; border color flicker (visual progress cue)
        INC $D418               ; SID volume flicker (audible progress cue)
        INC $7F08               ; bump EOR operand low byte
        INC $7F05               ; bump STA operand low byte
        BNE L7EFF
        INC $7F06               ; bump high bytes
        INC $7F09
        LDA $7F09
        CMP #$D0                ; end page $D0
        BNE L7EFF
        LDA #$27
        STA $01                 ; restore normal memory configuration
        RTS

No comparison against any stored "correct" value ever happens — a wrong key just produces garbage code, the same "decrypt to garbage, never an explicit error" failure convention used throughout the whole GMA family. The deliberate per-byte I/O bank-toggle is itself a small departure from Cholo/GMA87's simpler decrypt loop (which only banked out BASIC ROM once, outside the loop) — needed here specifically because this build's decrypted range extends into the I/O page, something the earlier generations' surveyed titles never required.

Relationship to GMA87 and GMA89

GMA88 completes the consolidation GMA87 was already most of the way through: where GMA87's surveyed titles were overwhelmingly the C64-side pattern with Eagles as a lone drive-side exception (itself inherited from GMA86's Firelord, not a new invention), GMA88 shows no exceptions at all across 15 titles — every one of them targets track 38 and decrypts on the C64. GMA89 continues this exact architecture unbroken for every title that has a genuine, load-bearing check at all.

See also