Difference between revisions of "GMA88"
| Line 29: | Line 29: | ||
== Mechanism == | == Mechanism == | ||
| − | # The drive seeks to '''track 38''' (<code>$26</code>), searches for the | + | # The drive seeks to '''track 38''' (<code>$26</code>), searches for the family's standard byte-aligned marker (<code>69 xx xx xx A9</code>, 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 <code>CMP</code>/<code>ROL</code> technique used throughout the whole GMA family. |
| − | + | # The result is sent to the C64 over a raster-line-synced <code>$DD00</code> handshake (via a hijacked <code>GETIN</code> vector) and used as a single repeating-byte XOR key over almost the entire game, decrypted in place in one pass. | |
| − | + | # 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 result is sent to the C64 over a raster-line-synced <code>$DD00</code> | ||
| − | |||
| − | |||
| − | |||
| − | # On any genuine failure — marker not found within the retry budget, or a | ||
| − | |||
| − | |||
| − | |||
| − | |||
== The critical disk pattern == | == The critical disk pattern == | ||
Latest revision as of 00:17, 29 August 2026
Contents
Pages that refer to this protection
- Descriptions of old disk copy protections (← links)
- GMA87 (← links)
- GMA89 (← links)
- Morpheus (← links)
- Arac (← links)
- Addicta Ball (← links)
- Ikari Warriors (← links)
- The Hunt for Red October (← links)
- Netherworld (← links)
- Intensity (← links)
- Magnetron (← links)
- Pandora (← links)
- Samurai Warrior: The Battles of.... Usagi Yojimbo (← links)
- Savage (← links)
- Soldier of Fortune (← links)
- IO (← links)
- Exploding Fist + (← links)
- Black Lamp (← links)
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
- 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 sameCMP/ROLtechnique used throughout the whole GMA family. - The result is sent to the C64 over a raster-line-synced
$DD00handshake (via a hijackedGETINvector) and used as a single repeating-byte XOR key over almost the entire game, decrypted in place in one pass. - 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.
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 $C800 → PHA →
... → 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.