Difference between revisions of "GMA85"
| Line 1: | Line 1: | ||
| − | + | = Pages that refer to this protection = | |
{{Special:Whatlinkshere/GMA85}} | {{Special:Whatlinkshere/GMA85}} | ||
| − | ==== | + | = 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
Contents
- 1 Pages that refer to this protection
- 2 Description
- 2.1 Summary
- 2.2 Loader chain
- 2.3 The upload trigger ($1000, loaded as file gma3)
- 2.4 The uploaded drive program ($0300–$038C, source at C64 $1097)
- 2.5 Track 37: an ordinary "salt" track, not a signature track
- 2.6 Experimental proof: the marker's content is never checked
- 2.7 Why three copies of the marker
- 2.8 Conclusion: a third distinct GMA-family protection pattern
- 2.9 See also
Pages that refer to this protection
- Descriptions of old disk copy protections (← links)
- GMA86 (← links)
- GMA87 (← links)
- Rasputin / Chickin Chase (Firebird) (← links)
- Chimera / Willow Pattern (Firebird) (← links)
- Nodes of Yesod (← links)
- GMA92 (← links)
- Booty / Cylu (Firebird) (← links)
- Microcosm / Gerry the Germ (Firebird) (← links)
- The Arc of Yesod / Nodes of Yesod (Firebird) (← links)
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 same69/A9byte 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.