Difference between revisions of "GMA85"

From Software Archive
Jump to navigation Jump to search
Line 1: Line 1:
==== Pages that refer to this protection ====
+
= Pages that refer to this protection =
  
 
{{Special:Whatlinkshere/GMA85}}
 
{{Special:Whatlinkshere/GMA85}}
  
==== Description ====
+
= 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
 +
<code>RobinoftheWood_SPS_s0.g64</code>. 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 [[Cholo GMA87|C64-side bulk
 +
decrypt]] and [[Eagles GMA87|drive-side per-sector decrypt]] patterns documented elsewhere on this wiki.
 +
 
 +
== Summary ==
 +
 
 +
* The drive-side upload/execute plumbing (<code>M-W</code>/<code>M-E</code>
 +
  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
 +
  <code>69</code>/<code>A9</code> 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 ==
 +
 
 +
<code>firebird</code> (boot stub, loads at <code>$02A7</code> — the same
 +
convention documented for the rest of the family) → <code>gma1</code> (the
 +
<code>gm1</code>-equivalent second-stage loader, loads at
 +
<code>$0800</code>) → <code>gma2</code>...<code>gmaa</code>,
 +
<code>bitend</code>, <code>videoend</code> (game data/graphics).
 +
 
 +
<code>gma1</code>'s main entry follows the same shape documented for every
 +
other GMA-family loader:
 +
 
 +
<pre>
 +
$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
 +
</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>
 +
$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
 +
</pre>
 +
 
 +
Every other GMA-family C64-side receiver (Cholo's <code>$C1E3</code>,
 +
Eagles' <code>$05EB</code>) uses <code>LDA $DD00</code> to actually
 +
'''read''' the drive's response into the accumulator. This routine only
 +
ever uses <code>BIT $DD00</code>, 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
 +
<code>M-W</code> and <code>M-E</code> sequences:
 +
 
 +
<pre>
 +
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
 +
</pre>
 +
 
 +
== The uploaded drive program (<code>$0300</code>–<code>$038C</code>, source at C64 <code>$1097</code>) ==
 +
 
 +
Captured two ways, in agreement: statically, by disassembling the source
 +
bytes still sitting in C64 RAM before transmission (<code>$1097</code>
 +
onward); and live, by freezing execution the instant the drive's own
 +
<code>DRVTRK</code> variable becomes 37 (<code>break store $0022 if
 +
a==$25</code>) and disassembling the drive's own RAM directly. Both agree
 +
byte-for-byte.
 +
 
 +
<pre>
 +
; ---- 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
 +
</pre>
 +
 
 +
141 bytes total (<code>$0300</code>–<code>$038C</code>), matching the
 +
upload loop's own <code>CMP #$8D</code> 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 <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>
 +
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
 +
</pre>
 +
 
 +
The three marker copies carry '''different''' salt bytes
 +
(<code>a3 a3</code>, <code>a7 a7</code>, <code>a9 a9</code>) rather than
 +
identical copies of the same payload.
 +
 
 +
== Experimental proof: the marker's content is never checked ==
 +
 
 +
Directly tested, the same way the [[Eagles GMA87|Eagles sync-swap
 +
experiment]] settled its equivalent question. All three salt pairs were
 +
zeroed in the G64's text dump (<code>56 56 a3 a3 69...</code> →
 +
<code>56 56 00 00 69...</code>, and likewise for the other two) while the
 +
<code>69</code> 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,
 +
<code>319232</code> bytes, no warnings), and booted fresh.
 +
 
 +
'''Result: the modified disk boots perfectly''' - straight to Robin of
 +
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
 +
'''a''' <code>69</code>/<code>A9</code> 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 ==
 +
 
 +
{| class="wikitable"
 +
! !! 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 ==
 +
 
 +
* [[GMA86]]
 +
* [[GMA87]]
 +
* [[Eagles GMA87|Eagles]]
 +
* [[Cholo GMA87|Cholo]]

Revision as of 02:17, 28 August 2026

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