Difference between revisions of "GMA85"

From Software Archive
Jump to navigation Jump to search
 
(5 intermediate revisions by the same user not shown)
Line 3: Line 3:
 
{{Special:Whatlinkshere/GMA85}}
 
{{Special:Whatlinkshere/GMA85}}
  
Notable [[Chimera/Willow Pattern]] uses $A3 as marker terminator instead of $A9.
+
Notable [[Chimera / Willow Pattern (Firebird)]] uses $A3 as marker terminator instead of $A9.
  
= Description =
+
= GMA85 =
  
On the example of [[Robin of the Wood]]. While that game shows GMA86, it actually uses the same protection as GMA85 games.
+
'''GMA85''' is a Commodore 64 disk copy protection scheme used across a
 +
run of Firebird Software titles from 1985/86, and the earliest confirmed
 +
member of the "GMA" protection family — the same lineage that continued
 +
as [[GMA86]] and [[GMA87]]. Its name follows the family's own convention:
 +
each disk's directory label carries a build-date stamp
 +
(<code>gma&lt;DDMMYY&gt;</code>), and several GMA85 disks spell the scheme
 +
name out directly (e.g. <code>gma85</code> on ''Booty'').
  
This article documents, in full technical detail, the copy protection
+
Where GMA86 and GMA87 measure a disk-specific signature from sync length
scheme used by the Commodore 64 release of '''Robin of the Wood'''
+
variation on a deliberately malformed track and use it as an XOR decryption
(Odin Computer Graphics / Firebird, 1986), archived as
+
key, '''GMA85 does not derive or use a key at all'''. It performs the same
<code>RobinoftheWood_SPS_s0.g64</code>. Its disk carries the GMA86 label
+
out-of-range-track handshake, but the check stops at confirming the
("GMA86 140486" per this disk's own BAM, 14 Apr 1986) and shares the same
+
track's marker byte is present — nothing about its surrounding content is
loader architecture, upload plumbing, and drive-side signature-measurement
+
ever read, compared, transmitted, or used to decrypt anything. This makes
code documented for the rest of the GMA86/GMA87 family — but it does not decrypt anything at all,
+
GMA85 the scheme's ancestral, unencrypted form: a presence gate, not a
The check on track 37 for the marker bytes exists, runs, and reports success exactly like every other title.
+
cipher.
There is not further check for sync length variations in the following syncs to create a key byte.
 
This makes it a distinct, third protection pattern alongside the [[Cholo GMA87|C64-side bulk
 
decrypt]] and [[Eagles GMA87|drive-side per-sector decrypt]] patterns documented elsewhere on this wiki.
 
  
== Summary ==
+
== Mechanism ==
  
* The drive-side upload/execute plumbing (<code>M-W</code>/<code>M-E</code>
+
Every GMA85 title surveyed (10 games across 11 disk sides, from six
  over the command channel) is architecturally identical to every other
+
compilation releases) runs the identical loader chain used by the whole
  GMA-family title.
+
GMA family: a small boot stub (typically named <code>firebird</code>)
* The uploaded drive program seeks an out-of-range track (**37** — a third
+
loads and hands off to a second-stage loader (<code>gma1</code>), which
  distinct target track in this family, alongside Cholo/Triaxos's 38 and
+
uploads a short machine-code program to the 1541 disk drive's own memory
  Eagles'/Firelord's/Uridium's/Iridis Alpha's 39), searches for the same
+
over the serial bus (using the standard DOS <code>M-W</code>/<code>M-E</code>
  <code>69</code>/<code>A9</code> byte marker used everywhere else in the
+
memory-write/execute commands) and has the '''drive itself''' — not the
  family, and reports success if found.
+
C64 — perform the check:
* Unlike every other title surveyed, '''nothing downstream ever reads,
 
  compares, transmits, or derives a value from the content around that
 
  marker'''. No sync length variation measurement happens on this track;
 
  no key gets sent to the C64; no key gets baked into a second drive
 
  program. Two raw 256-byte blocks get read off the disk into drive RAM
 
  and are then simply never touched again.
 
* The marker is deliberately duplicated three times on the track, most
 
  likely as redundancy against physical media degradation (the code
 
  already carries a 90-attempt retry budget for a single sync), not for
 
  any content-verification reason — the three copies carry different,
 
  unchecked salt bytes rather than identical ones.
 
  
== Loader chain ==
+
# The drive seeks an out-of-range track — '''track 37''' in every GMA85 title found so far, one track short of GMA86/87's Cholo/Triaxos (38) and Eagles/Firelord/Uridium/Iridis Alpha (39).
 +
# It searches for a short, deliberately non-standard byte marker (<code>$69</code> ... <code>$A9</code>, byte-aligned to the raw GCR shift register rather than the usual 5-bit nibble boundaries — the same marker convention used across the whole GMA family) with a 90-attempt retry budget.
 +
# On a match, it reads two raw 256-byte blocks off the disk into its own RAM.
 +
# It reports success to the C64 unconditionally — the two blocks it just read are never folded into a value, compared against anything, or sent anywhere. They are simply discarded.
 +
# The C64 continues loading regardless of what (if anything) was in those blocks. On a genuine failure — no out-of-range track, or no marker found on it — the drive instead hangs permanently, the same "fail by hanging, never by producing wrong output" behavior used throughout the GMA family.
  
<code>firebird</code> (boot stub, loads at <code>$02A7</code> — the same
+
Because nothing downstream ever consumes the read data, a disk copy only
convention documented for the rest of the family) → <code>gma1</code> (the
+
needs to preserve the out-of-range track's existence and its marker byte
<code>gm1</code>-equivalent second-stage loader, loads at
+
to pass — confirmed experimentally by corrupting the surrounding data on
<code>$0800</code>) → <code>gma2</code>...<code>gmaa</code>,
+
a real GMA85-derived disk and observing no change in behavior.
<code>bitend</code>, <code>videoend</code> (game data/graphics).
 
  
<code>gma1</code>'s main entry follows the same shape documented for every
+
== The critical disk pattern ==
other GMA-family loader:
 
  
<pre>
+
Tracks 1–35 are a completely ordinary CBM DOS layout; track 37 sits outside
$0819  LDA #$00
+
the range any stock-formatted disk ever uses, reachable only by a loader
      JSR SETMSG
+
that steps the drive head there directly. It carries the same
      LDA #$FF
+
three-times-repeating structure the whole GMA family shares on every
      STA $0329
+
out-of-range track it uses:
      JSR sub_08C0
 
      JSR sub_0903          ; fast-loader Y/N prompt lives in here
 
      LDA #$03
 
      JSR sub_0888          ; load file "gma3" to its own embedded address ($1000)
 
      JSR $1000              ; <-- the M-W/M-E upload trigger, see below
 
      LDA $02
 
      EOR #$97
 
      BEQ L083A              ; (see "red herring" note below)
 
      JMP ($FFFC)            ; failure path: jump through the RESET vector
 
L083A: ...                    ; screen setup, further file loads, JMP $4000
 
</pre>
 
 
 
The <code>LDA $02 / EOR #$97</code> check looked, at first glance, like it
 
might be validating the measured signature — it is not. <code>$02</code>
 
is unconditionally set to <code>$97</code> by the shared "M-" command-
 
channel opener (<code>sub_1073</code>, below) every time it runs, as a
 
plain side effect of opening the channel, regardless of any measurement.
 
It is a red herring, not a validation of disk-specific content.
 
 
 
== The upload trigger (<code>$1000</code>, loaded as file <code>gma3</code>) ==
 
  
 
<pre>
 
<pre>
$1000 A9 00      LDA #$00
+
  Track 1 ────────────────────────────────────────── 35
$1002 85 FB      STA $FB            ; target address low -> $0300
+
           ordinary CBM DOS layout ($08 header /
$1004  A9 03      LDA #$03
+
           $07 data blocks, correct checksums)
$1006  85 FC      STA $FC           ; target address high
 
$1008  A9 97      LDA #$97
 
$100A  85 FD      STA $FD           ; source pointer low  -> $1097
 
$100C  A9 10      LDA #$10
 
$100E  85 FE      STA $FE            ; source pointer high
 
  
$1010 20 73 10    JSR $1073          ; open command channel, send "M-"
+
  Track 37  3x repeating group:
$1013  A9 57      LDA #$57 / JSR CHROUT  ; 'W'  -> "M-W"
+
              sync ~32-41
$1018  A5 FB      LDA $FB / JSR CHROUT    ; address low  (current chunk target)
+
              bits 0110100101
$101D  A5 FC      LDA $FC / JSR CHROUT    ; address high
+
              bytes <2-byte per-track "salt"> 56 56 ... 55 57
$1022  A9 20      LDA #$20 / JSR CHROUT  ; count = $20 (32 bytes)
+
              bits 111111
$1027  A2 20      LDX #$20
 
$1029  A0 00      LDY #$00
 
$102B  18          CLC
 
$102C  A5 FB      LDA $FB
 
$102E  69 20      ADC #$20          ; advance target address by $20 for next chunk
 
$1030  85 FB      STA $FB
 
$1032  A9 00      LDA #$00
 
$1034  65 FC      ADC $FC
 
$1036  85 FC      STA $FC
 
  
$1038  B1 FD      LDA ($FD),Y        ; <-- payload byte Y of the upload buffer
+
              sync ~32-41
$103A  20 D2 FF    JSR CHROUT        ; send it
+
              gcr 09                <- non-standard block-ID, not $07/$08
$103D  E6 FD      INC $FD            ; advance source pointer
+
              bytes 59 59 59 59 59 55 ... (mostly $55 filler)
$103F  D0 02      BNE $1043
+
              bits 111111
$1041  E6 FE      INC $FE
 
$1043  CA          DEX
 
$1044  D0 F2      BNE $1038          ; loop for all 32 bytes of this chunk
 
 
 
$1046  20 CC FF    JSR CLRCHN
 
$1049  A5 FB      LDA $FB
 
$104B  C9 8D      CMP #$8D          ; compare target address (now advanced)
 
$104D  A5 FC      LDA $FC            ;  against $038D - the end of the
 
$104F  E9 03      SBC #$03          ;  141-byte upload ($0300+$8D)
 
$1051  90 BD      BCC $1010          ; more chunks needed -> loop
 
 
 
$1053  20 73 10    JSR $1073          ; open command channel + send "M-" again
 
$1056  A9 45      LDA #$45 / JSR CHROUT  ; 'E'  -> "M-E"
 
$105A  A9 64      LDA #$64 / JSR CHROUT  ; exec address low  ($64)
 
$105D  A9 03      LDA #$03 / JSR CHROUT  ; exec address high ($03)
 
                                      ;  -> "M-E $0364", runs the uploaded code
 
$1065  20 CC FF    JSR CLRCHN
 
 
 
$1068  2C 00 DD    BIT $DD00
 
$106B  30 FB      BMI $1068          ; handshake - NOT raster-line-synced,
 
$106D  2C 00 DD    BIT $DD00          ;  NOT a lookup-table decode, and
 
$1070  10 FB      BPL $106D          ;  crucially never LOADS a value into
 
$1072  60          RTS                ;  A/X/Y at all - just a busy/ready poll
 
 
</pre>
 
</pre>
  
Every other GMA-family C64-side receiver (Cholo's <code>$C1E3</code>,
+
Directly confirmed against a real disk's own G64 text dump (Robin of the
Eagles' <code>$05EB</code>) uses <code>LDA $DD00</code> to actually
+
Wood, which — as established below — runs this exact GMA85 drive code
'''read''' the drive's response into the accumulator. This routine only
+
unchanged): 40 total sync entries on the track, 6 tagged <code>gcr 09</code>,
ever uses <code>BIT $DD00</code>, which sets processor flags from the
+
of which exactly 3 carry the <code>69</code>/<code>A9</code> marker pair —
memory value but never loads it anywhere. This is evidence that this build's
+
each preceded by a ''different'', otherwise-unchecked 2-byte salt
handshake is a simple readiness poll, not a data-receive step.
+
(<code>a3 a3</code>, <code>a7 a7</code>, <code>a9 a9</code> on that disk).
 +
Zeroing all three salt pairs while leaving the marker byte itself untouched
 +
still boots the game perfectly — plain reliability engineering against a
 +
single degraded copy, not a checked payload; the drive only needs to land
 +
on any one of the three.
  
The shared "M-" command-channel opener, called at the start of both the
+
'''There is nothing further.''' This is the whole physical basis of
<code>M-W</code> and <code>M-E</code> sequences:
+
GMA85's presence-only design: a full sync-length scan across the
 +
out-of-range track band found every sync in the ordinary 32-40 range, zero
 +
outliers anywhere. Compare this to [[GMA86]] and [[GMA87]]'s real-key
 +
titles, whose target track carries a '''fourth, extra tail''' after this
 +
same three-group structure — a deliberately malformed region of elongated
 +
sync runs (87 and 136 "1"-bits, versus the normal ~32-41) that a
 +
pulse-width measurement folds into a key byte. GMA85's target track simply
 +
doesn't have that tail — there is no elongated-sync region anywhere on it
 +
for a measurement to read, which is exactly why the drive-side code never
 +
attempts one: it reads two raw 256-byte blocks off the ordinary structure
 +
above (the marker's own trailing salt/filler bytes and whatever follows)
 +
and discards them unconditionally, rather than timing anything. The
 +
physical difference between GMA85 and its two successors is visible
 +
directly in the raw track layout, not just in the code that reads it: the
 +
"key" mechanism in GMA86/87 exists only because the disk carries a region
 +
purpose-built for it, and GMA85's disks simply don't.
  
<pre>
+
The one confirmed byte-level difference '''between GMA85 titles
sub_1073:
+
themselves''' is the marker's terminating byte: <code>$A9</code> in nine of
      A9 00      LDA #$00
+
the ten surveyed titles, <code>$A3</code> on Chimera and Willow Pattern only
      20 BD FF    JSR SETNAM (empty name)
+
— see "Known GMA85 titles" below.
      A9 0F      LDA #$0F
 
      A8          TAY
 
      A2 08      LDX #$08
 
      20 BA FF    JSR SETLFS (LFN=15, dev=8, SA=15)
 
      20 C0 FF    JSR OPEN
 
      A2 0F      LDX #$0F
 
      20 C9 FF    JSR CHKOUT(15)
 
      A9 97      LDA #$97
 
      85 02      STA $02            ; <-- the "red herring" - hardcoded,
 
                                      ;    unconditional, unrelated to any
 
                                      ;    measurement
 
      A9 4D      LDA #$4D / JSR CHROUT  ; 'M'
 
      A9 2D      LDA #$2D / JSR CHROUT  ; '-'
 
      60          RTS
 
</pre>
 
  
== The uploaded drive program (<code>$0300</code>–<code>$038C</code>, source at C64 <code>$1097</code>) ==
+
== The drive-side program: annotated disassembly ==
  
Captured two ways, in agreement: statically, by disassembling the source
+
Captured live (frozen at the exact instant the drive's own <code>DRVTRK</code>
bytes still sitting in C64 RAM before transmission (<code>$1097</code>
+
variable becomes 37, before resuming) and confirmed byte-for-byte identical
onward); and live, by freezing execution the instant the drive's own
+
across all ten titles surveyed, save for the one-byte marker terminator
<code>DRVTRK</code> variable becomes 37 (<code>break store $0022 if
+
noted below. This 141-byte program is uploaded into 1541 drive RAM at
a==$25</code>) and disassembling the drive's own RAM directly. Both agree
+
<code>$0300</code> by the C64-side loader's <code>M-W</code> command
byte-for-byte.
+
sequence, then run there via <code>M-E $0364</code>:
  
 
<pre>
 
<pre>
Line 193: Line 134:
 
$0322  B8          CLV
 
$0322  B8          CLV
 
$0323  A2 04      LDX #$04
 
$0323  A2 04      LDX #$04
$0325  50 FE      BVC $0325          ; CLV/BVC byte-ready wait
+
$0325  50 FE      BVC $0325          ; CLV/BVC byte-ready wait (classic 1541 trick)
 
$0327  B8          CLV
 
$0327  B8          CLV
 
$0328  AD 01 1C    LDA $1C01          ; read raw GCR byte
 
$0328  AD 01 1C    LDA $1C01          ; read raw GCR byte
Line 199: Line 140:
 
$032E  CA          DEX
 
$032E  CA          DEX
 
$032F  10 F4      BPL $0325
 
$032F  10 F4      BPL $0325
$0331  C9 A9       CMP #$A9           ; last byte read must be $A9
+
$0331  C9 69       CMP #$69           ; last byte read must be $69
 
$0333  D0 DD      BNE $0312          ; mismatch -> retry
 
$0333  D0 DD      BNE $0312          ; mismatch -> retry
 
$0335  AD 01 05    LDA $0501          ; first byte read (2nd overall)
 
$0335  AD 01 05    LDA $0501          ; first byte read (2nd overall)
$0338  C9 69       CMP #$69           ; must be $69
+
$0338  C9 A9       CMP #$A9           ; must be $A9 ($A3 on Chimera/
 +
                                      ;  Willow Pattern - the only
 +
                                      ;  variation found across all 10 titles)
 
$033A  D0 D6      BNE $0312          ; mismatch -> retry
 
$033A  D0 D6      BNE $0312          ; mismatch -> retry
 
                                         ; signature "69 xx xx xx A9" confirmed
 
                                         ; signature "69 xx xx xx A9" confirmed
                                         ; - same marker convention as every
+
                                         ; - same marker convention every
                                         ;  other GMA-family title
+
                                         ;  later GMA86/87 title uses too
  
; ---- read a raw 256-byte block off the track (NOT a pulse-width fold) ----
+
; ---- read a raw 256-byte block off the track - NOT a pulse-width fold ----
 +
; ---- this is where GMA86/87's real signature measurement would go;  ----
 +
; ---- GMA85 just reads bytes and does nothing further with them      ----
 
$033C  2C 00 1C    BIT $1C00
 
$033C  2C 00 1C    BIT $1C00
 
$033F  30 FB      BMI $033C          ; wait for next sync
 
$033F  30 FB      BMI $033C          ; wait for next sync
Line 229: Line 174:
 
$035D  D0 F4      BNE $0353
 
$035D  D0 F4      BNE $0353
  
; ---- report success. NO FOLD, NO COMPARE, NO SEND. ----
+
; ---- report success. NO FOLD, NO COMPARE, NO SEND - the two 256-byte ----
 +
; ---- blocks just read are never referenced again anywhere.         ----
 
$035F  A9 01      LDA #$01
 
$035F  A9 01      LDA #$01
 
$0361  4C 69 F9    JMP $F969          ; status = SUCCESS
 
$0361  4C 69 F9    JMP $F969          ; status = SUCCESS
  
; ---- job-dispatch caller (M-E $0364 entry point) ----
+
; ---- job-dispatch caller (the M-E $0364 entry point) ----
 
$0364  20 42 D0    JSR $D042          ; (uncross-referenced ROM call)
 
$0364  20 42 D0    JSR $D042          ; (uncross-referenced ROM call)
 
$0367  A9 25      LDA #$25            ; $25 = 37 decimal - THE TARGET TRACK
 
$0367  A9 25      LDA #$25            ; $25 = 37 decimal - THE TARGET TRACK
Line 246: Line 192:
 
$037A  C9 02      CMP #$02
 
$037A  C9 02      CMP #$02
 
$037C  90 06      BCC $0384          ; status < 2 (success) -> continue
 
$037C  90 06      BCC $0384          ; status < 2 (success) -> continue
$037E  4C 7E 03    JMP $037E          ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP
+
$037E  4C 7E 03    JMP $037E          ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP,
 +
                                      ;  the same "hang, never produce wrong
 +
                                      ;  output" failure convention used
 +
                                      ;  throughout the whole GMA family
 
$0381  20 2C C1    JSR $C12C          ; (unreached in the traced path)
 
$0381  20 2C C1    JSR $C12C          ; (unreached in the traced path)
  
; ---- bus reset. NOT a nibble-send sequence. ----
+
; ---- bus reset - NOT a nibble-send sequence like GMA86/87's receivers ----
 
$0384  A9 00      LDA #$00
 
$0384  A9 00      LDA #$00
 
$0386  8D 00 18    STA $1800
 
$0386  8D 00 18    STA $1800
Line 256: Line 205:
 
</pre>
 
</pre>
  
141 bytes total (<code>$0300</code>–<code>$038C</code>), matching the
+
The C64-side loader (<code>gma1</code>) that uploads and triggers this
upload loop's own <code>CMP #$8D</code> end check exactly. Structurally
+
program takes one of two cosmetically different but functionally
this is the same "search for marker, then read data off the track, then
+
equivalent forms across the surveyed titles. Seven titles (Rasputin,
run a job-dispatch caller with the same infinite-hang-on-failure pattern"
+
Chicken Chase, Nodes of Yesod, Microcosm, Gerry the Germ, The Arc of
shape used by every other GMA-family drive program — the difference is
+
Yesod) follow the trigger with a check that looks, at first glance, like
entirely in what happens (or rather, doesn't happen) between the marker
+
it might validate the result:
match and the "report success" line: no 10-sample pulse-width measurement
 
loop, no <code>CMP</code>/<code>ROL</code> fold into a result byte, and no
 
nibble-out transmission over <code>$1800</code> at the end (compare this
 
to every other title's <code>$1800</code> writes at the tail, which send
 
real nibbles - here the two <code>STA $1800</code> writes both send the
 
literal, fixed value <code>$00</code>, simply resetting the bus).
 
 
 
== Track 37: an ordinary "salt" track, not a signature track ==
 
 
 
Checked directly against the G64's own text dump
 
(<code>g64conv ... 5</code>), not inferred. Every confirmed GMA-family
 
'''signature''' track (Cholo's 38, Eagles'/Firelord's/Uridium's/Iridis
 
Alpha's 39/41) has a handful of dramatically elongated sync runs (87 and
 
136 "1"-bits, versus a normal ~32-41) — that elongation is the literal
 
physical mechanism the flux-jitter measurement reads.
 
 
 
'''Track 37 has none.''' A full scan of every <code>sync</code> entry on
 
the track found 40 total, all in the ordinary 32-40 range - and a scan of
 
the entire plausible out-of-range band (tracks 34-42) for any sync length
 
above 45 found zero anomalous tracks anywhere on the disk.
 
 
 
Of those 40 syncs, 6 are tagged with the family's non-standard
 
<code>gcr 09</code> block-ID, but only '''3''' actually contain the
 
<code>69</code>/<code>A9</code> marker pair the code searches for - the
 
other 3 <code>gcr 09</code> blocks are a longer, marker-less companion
 
block, part of the same "three repeating groups per track" structure
 
documented for the rest of the family:
 
  
 
<pre>
 
<pre>
sync 40 / gcr 09
+
$0819  LDA #$00 / JSR SETMSG / LDA #$FF / STA $0329
  raw bytes: 56 56 a3 a3 69 55 55 ... 7f    <- marker copy #1, salt = a3 a3
+
      JSR <screen setup>
sync 39 / gcr 09
+
      JSR <Y/N prompt handler>
  raw bytes: 56 56 56 56 56 56 55 56 ... 99 ...  <- marker-LESS companion block
+
      LDA #$03 / JSR <load file "gma3">
 
+
      JSR $C800                    ; the upload trigger
sync 40 / gcr 09
+
      LDA $02
  raw bytes: 56 56 a7 a7 69 55 55 ... ff    <- marker copy #2, salt = a7 a7
+
      EOR #$97
sync 40 / gcr 09
+
      BEQ <continue>
  raw bytes: 56 56 56 56 56 56 55 56 ... 99 ...  <- marker-LESS companion block
+
      JMP ($FFFC)                  ; "failure": jump through the RESET vector
 
 
sync 40 / gcr 09
 
  raw bytes: 56 56 a9 a9 69 55 55 ... ff    <- marker copy #3, salt = a9 a9
 
sync 40 / gcr 09
 
  raw bytes: 56 56 56 56 56 56 55 56 ... 99 ...  <- marker-LESS companion block
 
 
</pre>
 
</pre>
  
The three marker copies carry '''different''' salt bytes
+
This is not a validation of anything disk-specific: <code>$02</code> is
(<code>a3 a3</code>, <code>a7 a7</code>, <code>a9 a9</code>) rather than
+
unconditionally set to <code>$97</code> by the shared "M-" command-channel
identical copies of the same payload.
+
opener every time it runs, regardless of anything measured on the disk -
 
+
confirmed by reading that routine's own code, which contains a bare
== Experimental proof: the marker's content is never checked ==
+
<code>LDA #$97 / STA $02</code> with no input dependency at all. That
 
+
doesn't make the check pointless, though: sitting where it does - buried
Directly tested, the same way the [[Eagles GMA87|Eagles sync-swap
+
between the <code>CHKOUT</code> call and the <code>'M'</code> character
experiment]] settled its equivalent question. All three salt pairs were
+
send, nowhere near the actual <code>JMP ($FFFC)</code> failure branch it
zeroed in the G64's text dump (<code>56 56 a3 a3 69...</code>
+
enables - it reads much more plausibly as a deliberate '''code-integrity
<code>56 56 00 00 69...</code>, and likewise for the other two) while the
+
tripwire''' than an accident. A cracker who patches out or skips the
<code>69</code> marker byte itself and the track's total bit count were
+
<code>JSR $C800</code> upload sequence entirely (a very natural thing to
left untouched - a simple substitution, no bit-alignment risk -
+
try against what looks like a copy-protection call) would leave
reconverted to G64 (byte-identical size to the original,
+
<code>$02</code> at whatever it held before, and this check would then
<code>319232</code> bytes, no warnings), and booted fresh.
+
correctly detect that the real routine never ran - without needing an
 +
obvious, easily-spotted "compare against a fixed value" pattern anywhere
 +
near the branch it guards. It just can't detect a ''bad disk'' the way
 +
GMA86/87's real signature checks can, since nothing about its value
 +
depends on anything read from the drive. The other four titles (Chimera,
 +
Willow Pattern, Booty, Cylu) skip this check
 +
entirely and simply discard the <code>JSR $C800</code> result, reaching
 +
the identical outcome by an even more direct route.
  
'''Result: the modified disk boots perfectly''' - straight to Robin of
+
== Redundancy, not security ==
the Wood's own title/options screen (<code>1 KEYBOARD / 2 REDEFINE KEYS /
 
3 JOYSTICK / 4 START GAME</code>, <code>© 1985 O.C.G.</code>), no hang,
 
no visible failure of any kind.
 
  
This is decisive, not merely suggestive: the check verifies only that
+
The marker is written three separate times on the target track, each copy
'''a''' <code>69</code>/<code>A9</code> marker exists somewhere on the
+
carrying different, unchecked "salt" bytes rather than identical copies
out-of-range track. It never reads, compares, transmits, or derives
+
of one payload. Combined with the marker-search code's own 90-attempt
anything from what surrounds it.
+
retry budget, this points to plain reliability engineering — protection
 +
against a single degraded or misread copy on aging media — rather than
 +
any security purpose. The three copies share the byte-level layout used
 +
across the wider GMA family's non-signature tracks generally, not
 +
something specific to GMA85.
  
== Why three copies of the marker ==
+
== Relationship to GMA86 and GMA87 ==
  
Most likely explanation: '''plain redundancy against physical media
+
GMA85's presence-only check and the sync-length key-derivation scheme
degradation''', not anything security-related. The marker-search code
+
used by later GMA86/GMA87 titles share the same underlying
already carries a 90-attempt retry budget for a single sync; three
+
seek/search/job-dispatch infrastructure — the same drive-upload plumbing,
physically separate copies of the marker on the track mean one
+
the same marker convention, the same hang-on-failure behavior. The later
degraded/unreadable copy doesn't take out the whole check - the drive
+
schemes add a measurement step (folding ten raw pulse-width samples from
only needs to land on any one of the three. If the three copies were
+
a deliberately malformed sync region into a single byte) and put that
meant to carry meaningful, checked content, they would be expected to
+
byte to work, either sent to the C64 for a bulk in-place decrypt or baked
agree exactly (true redundant copies of the same payload) rather than
+
into a second drive-resident program for a per-sector decrypt on the 1541
differ - the fact that they differ, combined with content being proven
+
itself. GMA85 has neither addition; it is what the scheme looked like
not to matter at all, points at reliability engineering rather than a
+
before that layer existed.
security mechanism.
 
  
== Conclusion: a third distinct GMA-family protection pattern ==
+
At least one GMA86-era release ([[Robin of the Wood]]) shipped with this
 +
exact GMA85 drive program carried forward unchanged, rather than adopting
 +
the newer sync-length mechanism most other titles from that period use —
 +
worth noting as a specific case, not as evidence that GMA86 titles
 +
generally lack a key.
  
{| class="wikitable"
+
== Variations ==
! !! C64-side bulk decrypt (Cholo, 27 titles) !! Drive-side per-sector decrypt (Eagles, 4 titles) !! Presence-only gate (Robin of the Wood, 1 title)
 
|-
 
| Signature measurement || Flux-jitter pulse-width timing, folded to 1 byte || Same flux-jitter technique as Cholo || None - marker byte presence only
 
|-
 
| Key derived? || Yes - the folded byte || Yes - the folded byte || No key exists
 
|-
 
| Where used || C64, one large in-place XOR pass || Drive, per-sector, in a 2nd uploaded program || Nowhere - never used
 
|-
 
| Failure mode on bad copy || Decrypts to garbage, crashes on jump-in || Decrypts to garbage, C64 stalls downstream || None - a copy without the marker at all just can't complete the presence check; anything else boots normally
 
|}
 
  
Robin of the Wood shares its drive-upload plumbing and its general
+
All GMA85 titles were independently confirmed to run byte-identical
"seek an out-of-range track, search for a marker, run a job-dispatch
+
drive-side code, differing only in one cosmetic detail: the marker's
caller with an infinite-hang failure path" shape with every other title
+
terminating byte is <code>$A9</code> in every title except Chimera and
in this survey - but it is the only confirmed example, across all 32
+
Willow Pattern, which use <code>$A3</code>.
titles checked in both the GMA86 and GMA87 surveys, where the whole
 
mechanism terminates at "does this track exist" and goes no further.
 
  
 
== See also ==
 
== See also ==
  
* [[GMA86]]
+
* [[Robin of the Wood]]
* [[GMA87]]
 
* [[Eagles GMA87|Eagles]]
 
* [[Cholo GMA87|Cholo]]
 

Latest revision as of 00:12, 29 August 2026

Pages that refer to this protection

Notable Chimera / Willow Pattern (Firebird) uses $A3 as marker terminator instead of $A9.

GMA85

GMA85 is a Commodore 64 disk copy protection scheme used across a run of Firebird Software titles from 1985/86, and the earliest confirmed member of the "GMA" protection family — the same lineage that continued as GMA86 and GMA87. Its name follows the family's own convention: each disk's directory label carries a build-date stamp (gma<DDMMYY>), and several GMA85 disks spell the scheme name out directly (e.g. gma85 on Booty).

Where GMA86 and GMA87 measure a disk-specific signature from sync length variation on a deliberately malformed track and use it as an XOR decryption key, GMA85 does not derive or use a key at all. It performs the same out-of-range-track handshake, but the check stops at confirming the track's marker byte is present — nothing about its surrounding content is ever read, compared, transmitted, or used to decrypt anything. This makes GMA85 the scheme's ancestral, unencrypted form: a presence gate, not a cipher.

Mechanism

Every GMA85 title surveyed (10 games across 11 disk sides, from six compilation releases) runs the identical loader chain used by the whole GMA family: a small boot stub (typically named firebird) loads and hands off to a second-stage loader (gma1), which uploads a short machine-code program to the 1541 disk drive's own memory over the serial bus (using the standard DOS M-W/M-E memory-write/execute commands) and has the drive itself — not the C64 — perform the check:

  1. The drive seeks an out-of-range track — track 37 in every GMA85 title found so far, one track short of GMA86/87's Cholo/Triaxos (38) and Eagles/Firelord/Uridium/Iridis Alpha (39).
  2. It searches for a short, deliberately non-standard byte marker ($69 ... $A9, byte-aligned to the raw GCR shift register rather than the usual 5-bit nibble boundaries — the same marker convention used across the whole GMA family) with a 90-attempt retry budget.
  3. On a match, it reads two raw 256-byte blocks off the disk into its own RAM.
  4. It reports success to the C64 unconditionally — the two blocks it just read are never folded into a value, compared against anything, or sent anywhere. They are simply discarded.
  5. The C64 continues loading regardless of what (if anything) was in those blocks. On a genuine failure — no out-of-range track, or no marker found on it — the drive instead hangs permanently, the same "fail by hanging, never by producing wrong output" behavior used throughout the GMA family.

Because nothing downstream ever consumes the read data, a disk copy only needs to preserve the out-of-range track's existence and its marker byte to pass — confirmed experimentally by corrupting the surrounding data on a real GMA85-derived disk and observing no change in behavior.

The critical disk pattern

Tracks 1–35 are a completely ordinary CBM DOS layout; track 37 sits outside the range any stock-formatted disk ever uses, reachable only by a loader that steps the drive head there directly. It carries the same three-times-repeating structure the whole GMA family shares on every out-of-range track it uses:

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

 Track 37   3x repeating group:
              sync ~32-41
              bits 0110100101
              bytes <2-byte per-track "salt"> 56 56 ... 55 57
              bits 111111

              sync ~32-41
              gcr 09                <- non-standard block-ID, not $07/$08
              bytes 59 59 59 59 59 55 ... (mostly $55 filler)
              bits 111111

Directly confirmed against a real disk's own G64 text dump (Robin of the Wood, which — as established below — runs this exact GMA85 drive code unchanged): 40 total sync entries on the track, 6 tagged gcr 09, of which exactly 3 carry the 69/A9 marker pair — each preceded by a different, otherwise-unchecked 2-byte salt (a3 a3, a7 a7, a9 a9 on that disk). Zeroing all three salt pairs while leaving the marker byte itself untouched still boots the game perfectly — plain reliability engineering against a single degraded copy, not a checked payload; the drive only needs to land on any one of the three.

There is nothing further. This is the whole physical basis of GMA85's presence-only design: a full sync-length scan across the out-of-range track band found every sync in the ordinary 32-40 range, zero outliers anywhere. Compare this to GMA86 and GMA87's real-key titles, whose target track carries a fourth, extra tail after this same three-group structure — a deliberately malformed region of elongated sync runs (87 and 136 "1"-bits, versus the normal ~32-41) that a pulse-width measurement folds into a key byte. GMA85's target track simply doesn't have that tail — there is no elongated-sync region anywhere on it for a measurement to read, which is exactly why the drive-side code never attempts one: it reads two raw 256-byte blocks off the ordinary structure above (the marker's own trailing salt/filler bytes and whatever follows) and discards them unconditionally, rather than timing anything. The physical difference between GMA85 and its two successors is visible directly in the raw track layout, not just in the code that reads it: the "key" mechanism in GMA86/87 exists only because the disk carries a region purpose-built for it, and GMA85's disks simply don't.

The one confirmed byte-level difference between GMA85 titles themselves is the marker's terminating byte: $A9 in nine of the ten surveyed titles, $A3 on Chimera and Willow Pattern only — see "Known GMA85 titles" below.

The drive-side program: annotated disassembly

Captured live (frozen at the exact instant the drive's own DRVTRK variable becomes 37, before resuming) and confirmed byte-for-byte identical across all ten titles surveyed, save for the one-byte marker terminator noted below. This 141-byte program is uploaded into 1541 drive RAM at $0300 by the C64-side loader's M-W command sequence, then run there via M-E $0364:

; ---- IEC bus init ----
$0300  A9 0F       LDA #$0F
$0302  8D 00 18    STA $1800
$0305  8D 00 18    STA $1800
$0308  AD 00 1C    LDA $1C00
$030B  29 9F       AND #$9F
$030D  8D 00 1C    STA $1C00

; ---- sync-marker search, 90-attempt retry budget ----
$0310  A0 5A       LDY #$5A            ; Y = 90 - sync-retry budget
$0312  88          DEY                 ; <-- retry entry point
$0313  D0 05       BNE $031A
$0315  A9 02       LDA #$02            ; error $02 = HEADER NOT FOUND
$0317  4C 69 F9    JMP $F969           ; ERRR - retries exhausted, report failure
$031A  2C 00 1C    BIT $1C00
$031D  30 FB       BMI $031A           ; sync-wait poll
$031F  AD 01 1C    LDA $1C01           ; discard first raw byte after sync
$0322  B8          CLV
$0323  A2 04       LDX #$04
$0325  50 FE       BVC $0325           ; CLV/BVC byte-ready wait (classic 1541 trick)
$0327  B8          CLV
$0328  AD 01 1C    LDA $1C01           ; read raw GCR byte
$032B  9D 00 05    STA $0500,X         ; store into $0500-$0504 (5 bytes)
$032E  CA          DEX
$032F  10 F4       BPL $0325
$0331  C9 69       CMP #$69            ; last byte read must be $69
$0333  D0 DD       BNE $0312           ; mismatch -> retry
$0335  AD 01 05    LDA $0501           ; first byte read (2nd overall)
$0338  C9 A9       CMP #$A9            ; must be $A9 ($A3 on Chimera/
                                       ;   Willow Pattern - the only
                                       ;   variation found across all 10 titles)
$033A  D0 D6       BNE $0312           ; mismatch -> retry
                                        ; signature "69 xx xx xx A9" confirmed
                                        ; - same marker convention every
                                        ;   later GMA86/87 title uses too

; ---- read a raw 256-byte block off the track - NOT a pulse-width fold ----
; ---- this is where GMA86/87's real signature measurement would go;   ----
; ---- GMA85 just reads bytes and does nothing further with them      ----
$033C  2C 00 1C    BIT $1C00
$033F  30 FB       BMI $033C           ; wait for next sync
$0341  B8          CLV
$0342  AD 01 1C    LDA $1C01
$0345  A2 00       LDX #$00
$0347  50 FE       BVC $0347
$0349  B8          CLV
$034A  AD 01 1C    LDA $1C01
$034D  9D 08 05    STA $0508,X         ; store into $0508-$05FF (256 bytes,
$0350  E8          INX                 ;   X wraps 0 -> 0)
$0351  D0 F4       BNE $0347

; ---- read a SECOND raw 256-byte block ----
$0353  50 FE       BVC $0353
$0355  B8          CLV
$0356  AD 01 1C    LDA $1C01
$0359  9D 08 06    STA $0608,X         ; store into $0608-$06FF (256 bytes)
$035C  E8          INX
$035D  D0 F4       BNE $0353

; ---- report success. NO FOLD, NO COMPARE, NO SEND - the two 256-byte ----
; ---- blocks just read are never referenced again anywhere.          ----
$035F  A9 01       LDA #$01
$0361  4C 69 F9    JMP $F969           ; status = SUCCESS

; ---- job-dispatch caller (the M-E $0364 entry point) ----
$0364  20 42 D0    JSR $D042           ; (uncross-referenced ROM call)
$0367  A9 25       LDA #$25            ; $25 = 37 decimal - THE TARGET TRACK
$0369  85 06       STA $06
$036B  A9 01       LDA #$01
$036D  85 07       STA $07
$036F  20 18 C1    JSR $C118           ; (ROM call)
$0372  A9 E0       LDA #$E0
$0374  85 00       STA $00             ; submit EXECUTE job (buffer 0)
$0376  A5 00       LDA $00
$0378  30 FC       BMI $0376           ; wait for job completion
$037A  C9 02       CMP #$02
$037C  90 06       BCC $0384           ; status < 2 (success) -> continue
$037E  4C 7E 03    JMP $037E           ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP,
                                       ;   the same "hang, never produce wrong
                                       ;   output" failure convention used
                                       ;   throughout the whole GMA family
$0381  20 2C C1    JSR $C12C           ; (unreached in the traced path)

; ---- bus reset - NOT a nibble-send sequence like GMA86/87's receivers ----
$0384  A9 00       LDA #$00
$0386  8D 00 18    STA $1800
$0389  8D 00 18    STA $1800
$038C  60          RTS

The C64-side loader (gma1) that uploads and triggers this program takes one of two cosmetically different but functionally equivalent forms across the surveyed titles. Seven titles (Rasputin, Chicken Chase, Nodes of Yesod, Microcosm, Gerry the Germ, The Arc of Yesod) follow the trigger with a check that looks, at first glance, like it might validate the result:

$0819  LDA #$00 / JSR SETMSG / LDA #$FF / STA $0329
       JSR <screen setup>
       JSR <Y/N prompt handler>
       LDA #$03 / JSR <load file "gma3">
       JSR $C800                    ; the upload trigger
       LDA $02
       EOR #$97
       BEQ <continue>
       JMP ($FFFC)                  ; "failure": jump through the RESET vector

This is not a validation of anything disk-specific: $02 is unconditionally set to $97 by the shared "M-" command-channel opener every time it runs, regardless of anything measured on the disk - confirmed by reading that routine's own code, which contains a bare LDA #$97 / STA $02 with no input dependency at all. That doesn't make the check pointless, though: sitting where it does - buried between the CHKOUT call and the 'M' character send, nowhere near the actual JMP ($FFFC) failure branch it enables - it reads much more plausibly as a deliberate code-integrity tripwire than an accident. A cracker who patches out or skips the JSR $C800 upload sequence entirely (a very natural thing to try against what looks like a copy-protection call) would leave $02 at whatever it held before, and this check would then correctly detect that the real routine never ran - without needing an obvious, easily-spotted "compare against a fixed value" pattern anywhere near the branch it guards. It just can't detect a bad disk the way GMA86/87's real signature checks can, since nothing about its value depends on anything read from the drive. The other four titles (Chimera, Willow Pattern, Booty, Cylu) skip this check entirely and simply discard the JSR $C800 result, reaching the identical outcome by an even more direct route.

Redundancy, not security

The marker is written three separate times on the target track, each copy carrying different, unchecked "salt" bytes rather than identical copies of one payload. Combined with the marker-search code's own 90-attempt retry budget, this points to plain reliability engineering — protection against a single degraded or misread copy on aging media — rather than any security purpose. The three copies share the byte-level layout used across the wider GMA family's non-signature tracks generally, not something specific to GMA85.

Relationship to GMA86 and GMA87

GMA85's presence-only check and the sync-length key-derivation scheme used by later GMA86/GMA87 titles share the same underlying seek/search/job-dispatch infrastructure — the same drive-upload plumbing, the same marker convention, the same hang-on-failure behavior. The later schemes add a measurement step (folding ten raw pulse-width samples from a deliberately malformed sync region into a single byte) and put that byte to work, either sent to the C64 for a bulk in-place decrypt or baked into a second drive-resident program for a per-sector decrypt on the 1541 itself. GMA85 has neither addition; it is what the scheme looked like before that layer existed.

At least one GMA86-era release (Robin of the Wood) shipped with this exact GMA85 drive program carried forward unchanged, rather than adopting the newer sync-length mechanism most other titles from that period use — worth noting as a specific case, not as evidence that GMA86 titles generally lack a key.

Variations

All GMA85 titles were independently confirmed to run byte-identical drive-side code, differing only in one cosmetic detail: the marker's terminating byte is $A9 in every title except Chimera and Willow Pattern, which use $A3.

See also