Cholo GMA87
Contents
- 1 Cholo (C64) — GMA87 copy protection, full technical write-up
Cholo (C64) — GMA87 copy protection, full technical write-up
This article documents, in full technical detail, the copy protection scheme
used by the Commodore 64 release of Cholo (1986/87). Cholo carries a
GMA87 disk label — named after a credit string embedded in the
loader itself, "GMA WAS HERE 1986" — and its exact algorithm is
shared, byte-for-byte in structure, by at least twenty other C64 titles
linking to the GMA87 wiki
page (full list at the end of this article). Every address below is
Cholo's own loader build (gm1.prg at $C000) unless
noted.
Summary
- The protection does not simply check-and-refuse. It reads a physically
unclonable property of the original disk — sync length variation in a deliberately malformed sync region on track 38 — folds it into a single byte, and uses that byte as an XOR decryption key for essentially the entire game (~25.5 KB,$4461–$A7FF).
- A disk copy that doesn't reproduce the exact sync pattern doesn't get
refused with an error — it decrypts the game into garbage and crashes when execution jumps into the decrypted region. There is no compare-and-branch to defeat by patching.
- Measured key for this disk capture:
$B1(177 decimal), - This is the "standard" GMA87 mechanism — key measured on the drive, sent
to the C64 as one byte, decrypted on the C64 in a single in-place pass. Compare this to Eagles, that moves the decryption step itself onto the drive's own 6502 instead.
Disk layout
- Tracks 1–35 are a normal CBM DOS layout (verified: standard
$08
header / $07 data blocks, correct track/sector/id checksums
throughout).
- Tracks 36–40 are non-standard — outside the range a stock-formatted
disk ever uses, reachable only by a loader that steps the drive head there directly. Decoded, each of these tracks (except track 38's third block) contains three near-identical repeating groups:
sync 41 (or 40) bits 0110100101 bytes <2-byte per-track "salt"> 8e 8d 55 55 ... 55 57 bits 111111 sync 40 gcr 09 <- non-standard block-ID; not $07 or $08 bytes 59 59 59 59 59 55 ... (mostly $55 filler) bits 111111
The actual protection-check works on raw, byte-aligned reads straight off the drive's GCR shift register and it only ever looks at track 38.
- Track 38 additionally has a distinctive tail after its third
sync/data group: several short blocks with unusual, non-standard sync lengths (87 and 136 "1"-bits, vs. the normal ~40), taggedgcr 0f, ending in a long run of$aafiller.
Loader chain
b (boot stub, relocates itself, loads GM1) →
gm1.prg (loaded to $C000) →
gma2.prg (loaded to $C800).
gm1.prg's main entry:
$C000 4C 27 C0 JMP $C027 ; cold entry $C003 4C 56 C0 JMP $C056 ; (alternate entry, not traced this pass) ... $C027 A9 00 LDA #$00 $C029 20 90 FF JSR $FF90 ; SETMSG (suppress KERNAL messages) $C02C A9 FF LDA #$FF $C02E 8D 29 03 STA $0329
then, continuing into the protection sequence itself:
$C030 ...
$C034 20 FD C2 JSR $C2FD ; install the GETIN vector hijack (see below)
$C037 20 28 C3 JSR $C328 ; build the $0100 lookup table (see below)
$C03A A9 02 LDA #$02
$C03C 20 89 C0 JSR $C089 ; (status/border helper, not traced)
$C03F 20 00 C8 JSR $C800 ; upload + execute the drive-side check,
; collect the result byte in A
$C042 48 PHA ; stash the measured key byte
$C043 A9 00 LDA #$00
$C045 8D 24 C0 STA $C024
$C048 A9 0D LDA #$0D
$C04A 8D 25 C0 STA $C025
$C04D 20 89 C0 JSR $C089
$C050 20 00 0D JSR $0D00 ; (fast-loader Y/N prompt lives around here)
$C053 4C 03 C1 JMP $C103 ; -> pull the key back and decrypt (see below)
The GETIN vector hijack: the C64-side receiver
$C2FD repurposes the KERNAL GETIN vector
($032A/$032B) to point at $C308,
unconditionally — regardless of the separate "use fast loader?" Y/N
prompt shown during boot. gma2.prg's upload-and-execute routine
($C800, below) later calls the standard GETIN
entry ($FFE4) purely to trigger this hijacked path and collect
the drive's answer in A.
$C308 08 PHP $C309 78 SEI $C30A A5 01 LDA $01 ; processor port (memory config) $C30C 48 PHA ; save it $C30D 29 FD AND #$FD $C30F 09 05 ORA #$05 ; bank out BASIC ROM, keep I/O visible $C311 85 01 STA $01 $C313 AD 00 DD LDA $DD00 ; read CIA2 port A once... $C316 29 07 AND #$07 $C318 85 44 STA $44 ; ...build the "release" bit-pattern $C31A 09 08 ORA #$08 $C31C 85 43 STA $43 ; ...and the "request" bit-pattern $C31E 20 E3 C1 JSR $C1E3 ; do the actual raster-synced handshake+read $C321 A8 TAY $C322 68 PLA $C323 85 01 STA $01 ; restore memory config $C325 98 TYA $C326 28 PLP $C327 60 RTS
The real cycle-accurate handshake lives in the shared subroutine
$C1E3:
$C1E3 A5 43 LDA $43
$C1E5 8D 00 DD STA $DD00 ; write to CIA2 port A (drive-facing)
$C1E8 AD 00 DD LDA $DD00
$C1EB 10 FB BPL $C1E8 ; wait for bit 7 - a response from the drive
$C1ED AD 12 D0 LDA $D012 ; raster line
$C1F0 C9 31 CMP #$31
$C1F2 90 06 BCC $C1FA
$C1F4 29 06 AND #$06
$C1F6 C9 02 CMP #$02
$C1F8 F0 F3 BEQ $C1ED ; cycle-accurate raster-synced wait
$C1FA A5 44 LDA $44
$C1FC 8D 00 DD STA $DD00
$C1FF EA (x9) NOP ; fixed-length timing pad
$C209 AE 00 DD LDX $DD00 ; read the port value...
$C20C BD 00 01 LDA $0100,X ; ...as an INDEX into a lookup table
$C20F AE 00 DD LDX $DD00
... ; (three more reads, three more tables,
; OR'd together - structurally identical
; to Eagles' $05EB continuation)
This reads CIA2's $DD00 port with cycle-accurate raster-line
synchronization and decodes the drive's response into a byte using
four 256-entry lookup tables as a nibble/bit decoder. Those tables are
fixed, not disk-derived — built once by a plain tiling loop:
$C328 A2 00 LDX #$00 $C32A A0 00 LDY #$00 $C32C A9 08 LDA #$08 $C32E 8D 66 C3 STA $C366 $C331 BD 4A C3 LDA $C34A,X ; fixed 28-byte source table at $C34A $C334 99 00 01 STA $0100,Y $C337 C8 INY $C338 CE 66 C3 DEC $C366 $C33B D0 F7 BNE $C334 ; each source byte repeated 8 times $C33D E8 INX $C33E E0 1C CPX #$1C ; 28 source bytes total $C340 90 EA BCC $C32C
They only gate whether the handshake can complete at all; they don't encode a disk-specific value into the transferred byte.
The drive-side check: uploaded and executed on the 1541's own CPU
gma2.prg, from $C86C, first opens the drive's
command channel and sends the "M-" prefix of a standard 1541
DOS M-W command, exactly the same KERNAL sequence documented
for Eagles' upload mechanism:
$C86C A9 00 LDA #$00 $C86E 20 BD FF JSR $FFBD ; SETNAM (empty name) $C871 A9 0F LDA #$0F ; A = 15 $C873 A8 TAY ; Y = 15 (secondary address = command channel) $C874 A2 08 LDX #$08 ; X = 8 (device 8) $C876 20 BA FF JSR $FFBA ; SETLFS(LFN=15, dev=8, SA=15) $C879 20 C0 FF JSR $FFC0 ; OPEN $C87C A2 0F LDX #$0F $C87E 20 C9 FF JSR $FFC9 ; CHKOUT(15) $C881 A9 4D LDA #$4D / JSR $FFD2 ; CHROUT 'M' $C886 A9 2D LDA #$2D / JSR $FFD2 ; CHROUT '-' $C88B 60 RTS
Immediately following, at $C88C–$C93C, is the
177-byte payload of that M-W command — which is, at the
same time, the literal source bytes of the drive-side program about to be
uploaded (this region disassembles identically to the live drive-RAM copy
at $0300 shown below, because it is that copy, still
sitting in C64 RAM before transmission):
$C88C AD 00 1C LDA $1C00
$C88F 29 9F AND #$9F
$C891 8D 00 1C STA $1C00
... ; (identical to drive $0300 below)
$C91A A9 01 LDA #$01
$C91C 4C 69 F9 JMP $F969
$C91F A9 26 LDA #$26 ; $26 = 38 decimal - THE TARGET TRACK
$C921 85 06 STA $06
$C923 A9 01 LDA #$01
$C925 85 07 STA $07
$C927 20 18 C1 JSR $C118 ; (ROM call)
$C92A A9 E0 LDA #$E0
$C92C 85 00 STA $00 ; submit EXECUTE job (buffer 0)
$C92E A5 00 LDA $00
$C930 30 FC BMI $C92E ; wait for job completion
$C932 C9 02 CMP #$02
$C934 90 06 BCC $C93C ; status < 2 (success) -> RTS normally
$C936 4C AA 03 JMP $03AA ; status >= 2 (FAILURE) -> jump into the
; just-uploaded drive copy's own hang loop
$C939 20 2C C1 JSR $C12C ; (unreached in the traced path)
$C93C 60 RTS
After the M-W data is sent, gma2.prg issues an
M-E $0393 command — the standard 1541 DOS "memory-execute"
command — to actually run the freshly-uploaded code on the drive, starting
at its job-setup entry point ($0393, see below).
Full annotated disassembly: the uploaded drive routine ($0300–$03B0)
; ---- sync-marker search, 90-attempt retry budget ----
$0300 AD 00 1C LDA $1C00
$0303 29 9F AND #$9F
$0305 8D 00 1C STA $1C00
$0308 A0 5A LDY #$5A ; Y = 90 - sync-retry budget
$030A 88 DEY ; <-- retry entry point
$030B D0 05 BNE $0312
$030D A9 02 LDA #$02 ; error $02 = HEADER NOT FOUND
$030F 4C 69 F9 JMP $F969 ; ERRR - retries exhausted, report failure
$0312 2C 00 1C BIT $1C00
$0315 30 FB BMI $0312 ; sync-wait poll
$0317 AD 01 1C LDA $1C01 ; discard first raw byte after sync
$031A B8 CLV
$031B A2 04 LDX #$04
$031D 50 FE BVC $031D ; CLV/BVC byte-ready wait (classic 1541 trick)
$031F B8 CLV
$0320 AD 01 1C LDA $1C01 ; read raw GCR byte
$0323 9D 00 05 STA $0500,X ; store into $0500-$0504 (5 bytes)
$0326 CA DEX
$0327 10 F4 BPL $031D
$0329 C9 A9 CMP #$A9 ; last byte read must be $A9
$032B D0 DD BNE $030A ; mismatch -> retry
$032D AD 04 05 LDA $0504 ; first byte read (2nd overall)
$0330 C9 69 CMP #$69 ; must be $69
$0332 D0 D6 BNE $030A ; mismatch -> retry
; signature "69 xx xx xx A9" confirmed
; ---- 10-sample raw pulse-width (flux-jitter) measurement ----
$0334 A0 00 LDY #$00
$0336 2C 00 1C BIT $1C00
$0339 30 FB BMI $0336 ; wait for next sync
$033B A2 00 LDX #$00
$033D E8 INX ; <-- pulse-width measurement loop
$033E 2C 00 1C BIT $1C00
$0341 10 FA BPL $033D ; count iterations (X) while bit7=0
$0343 8A TXA
$0344 99 00 05 STA $0500,Y ; store sample[Y]
$0347 C8 INY
$0348 C0 0A CPY #$0A ; 10 samples total
$034A D0 EA BNE $0336
; ---- fold 10 samples into an 8-bit result via CMP/ROL ----
$034C A2 02 LDX #$02 ; sample[0] discarded, sample[1] = reference
$034E BD 00 05 LDA $0500,X
$0351 CD 01 05 CMP $0501 ; compare sample[X] to reference
$0354 2E 0A 05 ROL $050A ; roll carry (>=ref->1, <ref->0) into result byte
$0357 E8 INX
$0358 E0 0A CPX #$0A ; samples[2..9], 8 comparisons -> 8-bit result
$035A D0 F2 BNE $034E
$035C AE 0A 05 LDX $050A ; X = computed 8-bit result: $B1 for this disk
; ---- send the result byte to the C64 via VIA1 $1800, nibble-out ----
$035F 2C 00 18 BIT $1800
$0362 10 FB BPL $035F ; wait for IEC bus ready
$0364 A9 10 LDA #$10
$0366 8D 00 18 STA $1800
$0369 2C 00 18 BIT $1800
$036C 30 FB BMI $0369
$036E 8A TXA ; A = result byte
$036F 4A LSR A ; \ send high nibble
$0370 4A LSR A ; |
$0371 4A LSR A ; |
$0372 4A LSR A ; /
$0373 8D 00 18 STA $1800
$0376 0A ASL A
$0377 29 0F AND #$0F
$0379 8D 00 18 STA $1800
$037C 8A TXA
$037D 29 0F AND #$0F ; \ send low nibble
$037F 8D 00 18 STA $1800 ; |
$0382 0A ASL A ; |
$0383 29 0F AND #$0F ; |
$0385 8D 00 18 STA $1800 ; /
$0388 A9 0F LDA #$0F
$038A EA NOP
$038B 8D 00 18 STA $1800 ; final bus state
$038E A9 01 LDA #$01 ; status $01 = SUCCESS
$0390 4C 69 F9 JMP $F969 ; ERRR (used generically as "set status, return")
; ---- job setup: target track 38, submit EXECUTE job, check result (M-E entry point) ----
$0393 A9 26 LDA #$26 ; $26 = 38 decimal - THE TARGET TRACK
$0395 85 06 STA $06
$0397 A9 01 LDA #$01
$0399 85 07 STA $07
$039B 20 18 C1 JSR $C118 ; (ROM call)
$039E A9 E0 LDA #$E0
$03A0 85 00 STA $00 ; submit EXECUTE job (buffer 0)
$03A2 A5 00 LDA $00
$03A4 30 FC BMI $03A2 ; wait for job completion
$03A6 C9 02 CMP #$02
$03A8 90 06 BCC $03B0 ; status < 2 (success) -> RTS normally
$03AA 4C AA 03 JMP $03AA ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP
$03AD 20 2C C1 JSR $C12C ; (unreached in the traced path)
$03B0 60 RTS
This is instruction-for-instruction identical in structure to the
track-39 routine independently found and documented for
Eagles, down to the exact same offsets within the block —
only the target track differs ($26/38 here vs. Eagles'
$27/39). Both failure modes are deliberate, permanent hangs,
never wrong output: a marker mismatch retries up to 90 times before giving
up with error $02; a failed job status falls into an infinite
self-loop at $03AA.
Measured result byte for this disk capture: $B1 (177
decimal).
Decryption
Back on the C64, the measured byte returned in A from
JSR $C800 is stashed ($C042: PHA), carried through
the fast-loader prompt handling, and pulled back at $C103:
$C103 68 PLA $C104 AA TAX ; X = the measured key byte ($B1 for this disk) $C105 20 DC C0 JSR $C0DC ; run the decrypt loop $C108 20 6A C0 JSR $C06A ; (post-decrypt cleanup, not traced this pass) $C10B 4C 61 44 JMP $4461 ; jump straight into the freshly-decrypted code
The decrypt loop itself is a single repeating-byte XOR key applied to essentially the entire game, via a self-modifying pair of operands that walk upward through memory:
$C0DC A9 FE ... (memory-config setup, banks out BASIC ROM) $C0E1 85 01 STA $01 $C0E3 8A TXA $C0E4 4D 61 44 EOR $4461 ; self-modifying operand, walks $4461 -> ~$A7FF $C0E7 8D 61 44 STA $4461 $C0EA EE E5 C0 INC $C0E5 ; bump low byte of the EOR operand $C0ED EE E8 C0 INC $C0E8 ; bump low byte of the STA operand $C0F0 D0 F1 BNE $C0E3 $C0F2 EE E6 C0 INC $C0E6 ; bump high byte $C0F5 EE E9 C0 INC $C0E9 $C0F8 AD E9 C0 LDA $C0E9 $C0FB C9 A8 CMP #$A8 $C0FD D0 E4 BNE $C0E3 $C0FF 68 PLA $C100 85 01 STA $01 ; restore memory config $C102 60 RTS
Decrypting ~25.5 KB ($4461–$A7FF) in place, then
jumping directly into it. No comparison against a stored "correct" value
ever happens — a wrong key just produces garbage code and the machine
crashes on JMP $4461.
Other games using this identical algorithm
The following titles, all linking to the GMA87 wiki page, were independently
verified to use the exact same scheme
documented above: measurement on track 38, drive-side upload targeting
$C800 on the C64 / $0300 on the drive, and a
single repeating-byte C64-side XOR decrypt. Only the surrounding loader's
own memory layout (and, of course, each disk's measured key) differs between
them.
- Triaxos — confirmed identical algorithm and fixed drive-upload target;
only the loader's memory layout differs (gm1.prgat$1000instead of$C000, decrypted range$6400–$CFFF)
- Starfox (two independently-dumped sources, keys agree exactly)
- Jagd auf Roter Oktober (two independently-dumped sources, keys agree exactly)
- Enlightenment: Druid II
- Delta (six independently-dumped sources, keys agree exactly)
- Kat Trap: Planet of the Cat-Men
- Cataball (via Hit Pak: Trio)
- Bride of Frankenstein
- Bubble Bobble (three sources; two distinct physical pressings identified by
differing keys)
- Flying Shark
- James Bond 007: The Living Daylights / Der Hauch des Todes (same
release, listed separately under its German title on the wiki)
- Sidewize (two sources, keys agree exactly)
- Twin Tornado
- Evening Star / Southern Belle (independently matches a previously-published
key value on this game's own wiki page)
- Revs+
- Zynaps (two sources, keys agree exactly)
- Mystery of the Nile
- Scary Monsters
- Airwolf 2 (via Hit Pak: Trio) — same physical disk as Cataball, same key
- Great Gurianos (via Hit Pak: Trio) — same physical disk side as
Airwolf 2, same key
Not on this list: Eagles, which carries the same disk label and measures its signature the same way, but moves the actual decryption step onto the drive's own CPU instead of sending the key to the C64 — see the dedicated Eagles article for the full mechanism.