GMA85

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

Pages that refer to this protection

Description

On the example of Robin of the Wood. While that game shows GMA86, it actually uses the same protection as GMA85 games.

This article documents, in full technical detail, the copy protection scheme used by the Commodore 64 release of Robin of the Wood (Odin Computer Graphics / Firebird, 1986), archived as RobinoftheWood_SPS_s0.g64. Its disk carries the GMA86 label ("GMA86 140486" per this disk's own BAM, 14 Apr 1986) and shares the same loader architecture, upload plumbing, and drive-side signature-measurement code documented for the rest of the GMA86/GMA87 family — but it does not decrypt anything at all, The check on track 37 for the marker bytes exists, runs, and reports success exactly like every other title. 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 C64-side bulk decrypt and drive-side per-sector decrypt patterns documented elsewhere on this wiki.

Summary

  • The drive-side upload/execute plumbing (M-W/M-E
 over the command channel) is architecturally identical to every other
 GMA-family title.
  • The uploaded drive program seeks an out-of-range track (**37** — a third
 distinct target track in this family, alongside Cholo/Triaxos's 38 and
 Eagles'/Firelord's/Uridium's/Iridis Alpha's 39), searches for the same
 69/A9 byte marker used everywhere else in the
 family, and reports success if found.
  • 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

firebird (boot stub, loads at $02A7 — the same convention documented for the rest of the family) → gma1 (the gm1-equivalent second-stage loader, loads at $0800) → gma2...gmaa, bitend, videoend (game data/graphics).

gma1's main entry follows the same shape documented for every other GMA-family loader:

$0819  LDA #$00
       JSR SETMSG
       LDA #$FF
       STA $0329
       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

The LDA $02 / EOR #$97 check looked, at first glance, like it might be validating the measured signature — it is not. $02 is unconditionally set to $97 by the shared "M-" command- channel opener (sub_1073, 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 ($1000, loaded as file gma3)

$1000  A9 00       LDA #$00
$1002  85 FB       STA $FB            ; target address low  -> $0300
$1004  A9 03       LDA #$03
$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-"
$1013  A9 57       LDA #$57 / JSR CHROUT   ; 'W'  -> "M-W"
$1018  A5 FB       LDA $FB / JSR CHROUT    ; address low  (current chunk target)
$101D  A5 FC       LDA $FC / JSR CHROUT    ; address high
$1022  A9 20       LDA #$20 / JSR CHROUT   ; count = $20 (32 bytes)
$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
$103A  20 D2 FF    JSR CHROUT         ; send it
$103D  E6 FD       INC $FD            ; advance source pointer
$103F  D0 02       BNE $1043
$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

Every other GMA-family C64-side receiver (Cholo's $C1E3, Eagles' $05EB) uses LDA $DD00 to actually read the drive's response into the accumulator. This routine only ever uses BIT $DD00, which sets processor flags from the memory value but never loads it anywhere. This is evidence that this build's handshake is a simple readiness poll, not a data-receive step.

The shared "M-" command-channel opener, called at the start of both the M-W and M-E sequences:

sub_1073:
       A9 00       LDA #$00
       20 BD FF    JSR SETNAM (empty name)
       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

The uploaded drive program ($0300$038C, source at C64 $1097)

Captured two ways, in agreement: statically, by disassembling the source bytes still sitting in C64 RAM before transmission ($1097 onward); and live, by freezing execution the instant the drive's own DRVTRK variable becomes 37 (break store $0022 if a==$25) and disassembling the drive's own RAM directly. Both agree byte-for-byte.

; ---- 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
$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 A9       CMP #$A9            ; last byte read must be $A9
$0333  D0 DD       BNE $0312           ; mismatch -> retry
$0335  AD 01 05    LDA $0501           ; first byte read (2nd overall)
$0338  C9 69       CMP #$69            ; must be $69
$033A  D0 D6       BNE $0312           ; mismatch -> retry
                                        ; signature "69 xx xx xx A9" confirmed
                                        ; - same marker convention as every
                                        ;   other GMA-family title

; ---- read a raw 256-byte block off the track (NOT a pulse-width fold) ----
$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. ----
$035F  A9 01       LDA #$01
$0361  4C 69 F9    JMP $F969           ; status = SUCCESS

; ---- job-dispatch caller (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
$0381  20 2C C1    JSR $C12C           ; (unreached in the traced path)

; ---- bus reset. NOT a nibble-send sequence. ----
$0384  A9 00       LDA #$00
$0386  8D 00 18    STA $1800
$0389  8D 00 18    STA $1800
$038C  60          RTS

141 bytes total ($0300$038C), matching the upload loop's own CMP #$8D end check exactly. Structurally this is the same "search for marker, then read data off the track, then run a job-dispatch caller with the same infinite-hang-on-failure pattern" shape used by every other GMA-family drive program — the difference is entirely in what happens (or rather, doesn't happen) between the marker match and the "report success" line: no 10-sample pulse-width measurement loop, no CMP/ROL fold into a result byte, and no nibble-out transmission over $1800 at the end (compare this to every other title's $1800 writes at the tail, which send real nibbles - here the two STA $1800 writes both send the literal, fixed value $00, simply resetting the bus).

Track 37: an ordinary "salt" track, not a signature track

Checked directly against the G64's own text dump (g64conv ... 5), 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 sync 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 gcr 09 block-ID, but only 3 actually contain the 69/A9 marker pair the code searches for - the other 3 gcr 09 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:

sync 40 / gcr 09
  raw bytes: 56 56 a3 a3 69 55 55 ... 7f     <- marker copy #1, salt = a3 a3
sync 39 / gcr 09
  raw bytes: 56 56 56 56 56 56 55 56 ... 99 ...   <- marker-LESS companion block

sync 40 / gcr 09
  raw bytes: 56 56 a7 a7 69 55 55 ... ff     <- marker copy #2, salt = a7 a7
sync 40 / gcr 09
  raw bytes: 56 56 56 56 56 56 55 56 ... 99 ...   <- marker-LESS companion block

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

The three marker copies carry different salt bytes (a3 a3, a7 a7, a9 a9) rather than identical copies of the same payload.

Experimental proof: the marker's content is never checked

Directly tested, the same way the Eagles sync-swap experiment settled its equivalent question. All three salt pairs were zeroed in the G64's text dump (56 56 a3 a3 69...56 56 00 00 69..., and likewise for the other two) while the 69 marker byte itself and the track's total bit count were left untouched - a simple substitution, no bit-alignment risk - reconverted to G64 (byte-identical size to the original, 319232 bytes, no warnings), and booted fresh.

Result: the modified disk boots perfectly - straight to Robin of the Wood's own title/options screen (1 KEYBOARD / 2 REDEFINE KEYS / 3 JOYSTICK / 4 START GAME, © 1985 O.C.G.), no hang, no visible failure of any kind.

This is decisive, not merely suggestive: the check verifies only that a 69/A9 marker exists somewhere on the out-of-range track. It never reads, compares, transmits, or derives anything from what surrounds it.

Why three copies of the marker

Most likely explanation: plain redundancy against physical media degradation, not anything security-related. The marker-search code already carries a 90-attempt retry budget for a single sync; three physically separate copies of the marker on the track mean one degraded/unreadable copy doesn't take out the whole check - the drive only needs to land on any one of the three. If the three copies were meant to carry meaningful, checked content, they would be expected to agree exactly (true redundant copies of the same payload) rather than differ - the fact that they differ, combined with content being proven not to matter at all, points at reliability engineering rather than a security mechanism.

Conclusion: a third distinct GMA-family protection pattern

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 "seek an out-of-range track, search for a marker, run a job-dispatch caller with an infinite-hang failure path" shape with every other title in this survey - but it is the only confirmed example, across all 32 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