{"id":1449,"date":"2020-09-06T11:48:15","date_gmt":"2020-09-06T09:48:15","guid":{"rendered":"https:\/\/www.pagetable.com\/?p=1449"},"modified":"2020-09-06T11:48:15","modified_gmt":"2020-09-06T09:48:15","slug":"inside-geowrite-5-copy-protection","status":"publish","type":"post","link":"https:\/\/www.pagetable.com\/?p=1449","title":{"rendered":"Inside geoWrite \u2013 5: Copy Protection"},"content":{"rendered":"<p>In the series about the internals of the geoWrite WYSIWYG text editor for the C64, this article discusses the geoWrite copy protection.<\/p>\n<p><img decoding=\"async\" src=\"docs\/geowrite\/geowrite_5_wrong_serial.png\" alt=\"\" \/><\/p>\n<h2 id=\"article-series\">Article Series<\/h2>\n<ol>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1425\">The Overlay System<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1428\">Screen Recovery<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1436\">Font Management<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1442\">Zero Page<\/a><\/li>\n<li><strong>Copy Protection<\/strong> \u2190 this article<\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1460\">Localization<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1471\">File Format and Pagination<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1481\">Copy &amp; Paste<\/a><\/li>\n<li><a href=\"https:\/\/www.pagetable.com\/?p=1490\">Keyboard Handling<\/a><\/li>\n<\/ol>\n<h2 id=\"geos-copy-protection-strategy\">GEOS Copy Protection Strategy<\/h2>\n<p>GEOS has one of the most notorious copy protection systems. The system disk contains bit patterns that are very hard to reproduce on a stock disk drive. These are checked on every boot in obfuscated code that is used to decrypt the core operating system code. This way, copies of the GEOS boot disk will not boot. Therefore, GEOS always has to be booted from the original disk, so these would <a href=\"https:\/\/www.pagetable.com\/?p=1118\">break frequently<\/a>, which is why GEOS came with a second boot disk, and there was a program to get broken boot disks replaced once both failed.<\/p>\n<p>GEOS maker Berkeley Softworks also created several high-profile GEOS applications like geoPublish and geoCalc, which came with a similar copy protection. Even the bundled apps geoWrite, geoSpell and geoMerge have the same protection.<\/p>\n<p>But with apps, it&rsquo;s more complicated: On a C64 system with a single disk drive, the app needs to be on the same disk as the document that is being worked on, and since it&rsquo;s a non-starter to make the user edit all their documents on the original application disk, it needs to be possible to have a copy of the app working on the user&rsquo;s work disk \u2013 and still prevent pirated copies of the app from running.<\/p>\n<p>The idea is to link the boot disk and the application through a serial number. On the <a href=\"https:\/\/www.pagetable.com\/?p=1140#Conclusion\">very first boot<\/a>, the GEOS system picks a random 16 bit serial number (excluding zero) and stores it on the boot disk as well as on the backup disk \u2013 this is called &ldquo;installing&rdquo; GEOS. On the first start of a copy-protected application, it verifies that it&rsquo;s running from the original disk, and if yes, it takes the system&rsquo;s serial number and stores it \u2013 this is called &ldquo;installing&rdquo; an application. On subsequent boots, it does not check for the original disk any more, but it only runs if its stored serial number matches the system&rsquo;s.<\/p>\n<p>As long as the GEOS boot disk cannot be copied, two users (who both bought GEOS) will have different serial numbers, and installed apps from one user won&rsquo;t work on a different user&rsquo;s GEOS. And a copy of a not-yet-installed app will refuse to install itself, because it doesn&rsquo;t run from an original disk.<\/p>\n<h2 id=\"code\">Code<\/h2>\n<p>Apart from this basic protection concept, geoWrite obfuscates what&rsquo;s going on by encrypting parts of the code, to make it hard to crack the protection.<\/p>\n<p>Let&rsquo;s walk through the components of the copy protection in the order of what happens on application startup.<\/p>\n<h3 id=\"encrypted-record-1\">Encrypted Record 1<\/h3>\n<p>As discussed in <a href=\"https:\/\/www.pagetable.com\/?p=1425\">part 1<\/a> of this series, the GEOS &ldquo;VLIR&rdquo; binary consists of 9 so-called records, which are basically individual code files. Record 0 is the main program, and records 1 through 8 get swapped in and out of memory based on what functionality is needed.<\/p>\n<p>When the application gets started, the first thing the record 0 code of geoWrite does is load record 1, which contains initialization code as well as the copy protection.<\/p>\n<pre><code>        lda     #BANK_1\n        jsr     loadCode\n<\/code><\/pre>\n<p>All of record 1 is encrypted, so after loading, it decrypts it by XORing every byte with $DE.<\/p>\n<pre><code>        lda     #$EB\n        eor     #$35\n        sta     @2\n        LoadW   r0, MEM_OVERLAY\n        LoadW   r1, -4000\n        ldy     #0\n@1:     lda     (r0),y\n@2 = * + 1\n        eor     #$00\n        sta     (r0),y\n        IncW    r0\n        IncW    r1\n        bne     @1\n<\/code><\/pre>\n<p>The decryption constant of $DE gets constructed using $ED XOR $35, for which there is no good reason other than maybe making it harder to search for the value.<\/p>\n<p>After decryption, execution is handed to the record 1 code:<\/p>\n<pre><code>        jmp     MEM_OVERLAY\n<\/code><\/pre>\n<h3 id=\"installation\">Installation<\/h3>\n<p>The first few instructions of record 1 do things in a way more complicated way than necessary, probably to deter any hackers from looking further:<\/p>\n<pre><code>.,3244  A9 32     LDA #$32\n.,3246  48        PHA\n.,3247  A9 54     LDA #$54\n.,3249  48        PHA\n.,324A  A9 3B     LDA #$3B\n.,324C  85 21     STA $21\n.,324E  A9 6F     LDA #$6F\n.,3250  85 20     STA $20\n.,3252  6C 20 00  JMP ($0020)\n<\/code><\/pre>\n<p>The source makes it clear what&rsquo;s going on:<\/p>\n<pre><code>        lda     #&gt;(@continue-1)\n        pha\n        lda     #&lt;(@continue-1)\n        pha\n        LoadW   r15, initApp\n        jmp     (r15)\n@continue:\n<\/code><\/pre>\n<p>It pushes the address of the code following it as a return address (i.e. minus one) on the stack and jumps to <code>initApp<\/code> using a vector. This is just a convoluted way of calling <code>initApp<\/code> and continuing with the code below.<\/p>\n<p><code>initApp<\/code> does some initialization and calls <code>checkSerialOrInstall<\/code>. This is the first part of it:<\/p>\n<pre><code>checkSerialOrInstall:\n        lda     serial\n        ora     serial+1                ; does app have a serial?\n        beq     @install                ; no, then install\n\n        lda     #&lt;GetSerialNumber\n        ldx     #&gt;GetSerialNumber\n        jsr     CallRoutine\n        CmpW    serial, r0              ; does the app serial match the system's?\n        beq     @rts                    ; yes, return\n\n        lda     #&lt;txt_serial_mismatch\n        ldy     #&gt;txt_serial_mismatch\n        jsr     showError               ; no, tell the user\n        jsr     swap_userzp\n        jmp     EnterDeskTop            ; and exit\n\n@rts:   rts\n<\/code><\/pre>\n<p>(For the meaning of <code>swap_userzp<\/code>, check out <a href=\"https:\/\/www.pagetable.com\/?p=1442\">part 4<\/a> of this series.)<\/p>\n<p><img decoding=\"async\" src=\"docs\/geowrite\/geowrite_5_wrong_serial.png\" alt=\"\" \/><\/p>\n<p><code>serial<\/code> is a 16 bit variable that is part of the record 1 code:<\/p>\n<pre><code>serial:\n        .word   0                       ; not installed\n<\/code><\/pre>\n<p>If it is zero, <code>checkSerialOrInstall<\/code> jumps to the install logic. Otherwise it gets the system&rsquo;s serial. (It could just call the <code>GetSerialNumber<\/code> KERNAL API directly, but instead, it calls it by loading its address into registers and calling <code>CallRoutine<\/code>, so that a reverse engineer has a harder time finding the call.)<\/p>\n<p>If the system&rsquo;s serial number is the same, the function returns, and the application can start. If no, it shows an error and exits the app.<\/p>\n<p>Let&rsquo;s look at the installer code. First, it calls <code>executeDiskBlock<\/code>, which loads a block from disk and runs it:<\/p>\n<pre><code>@install:\nprotExecTrack = * + 1\n        lda     #0                      ; protection track (stamped in by build system)\n        sta     r1L\nprotExecSector = * + 1\n        lda     #0                      ; protection sector (stamped in by build system)\n        sta     r1H\n        jsr     executeDiskBlock\n        beqx    @ok                     ; no error\n<\/code><\/pre>\n<p>This block contains the code to verify that this is an original disk and not a copy. (We will discuss all of this in detail in the next section.) If <code>executeDiskBlock<\/code> returns with X != 0, this signals a failure, and and geoWrite exits:<\/p>\n<pre><code>        lda     #&lt;txt_copy_protection   ; installing a non-original disk?\n        ldy     #&gt;txt_copy_protection   ; then show a non-informative message\n        bra     showErrorAndExit        ; and exit to deskTop\n<\/code><\/pre>\n<p><img decoding=\"async\" src=\"docs\/geowrite\/geowrite_5_wrong_disk.png\" alt=\"\" \/><\/p>\n<p>Otherwise, the installer now knows that it&rsquo;s an original disk, so it can stamp in the system&rsquo;s serial into its own code. So after fetching the system&rsquo;s serial again, it reads the block from disk that is supposed to contain the app&rsquo;s serial. This block is part of the record 1 file.<\/p>\n<pre><code>@ok:    lda     #&lt;GetSerialNumber\n        ldx     #&gt;GetSerialNumber\n        jsr     CallRoutine             ; get OS serial number to put into app\n\n        MoveW   r0, serial\n        LoadW   r4, diskBlkBuf\nprotSerialTrack = * + 1\n        lda     #0                      ; serial track (stamped in by build system)\n        sta     r1L\nprotSerialSector = * + 1\n        lda     #0                      ; serial sector (stamped in by build system)\n        sta     r1H\n        jsr     _GetBlock               ; read sector that contains code with serial\n        bnex    @ierror\n<\/code><\/pre>\n<p>Note that the track and sector numbers point to the location of this very code on disk. The geoWrite build system creates a disk image with the app on it and stamps track and sector numbers in.<\/p>\n<p>The serial has to be put at the correct location within the block and encrypted with the same XOR $DE that is used for decrypting all of record 1:<\/p>\n<pre><code>@offset = (serial-CODE1) .mod 254 + 2\n        lda     serial                  ; get serial low\n        eor     #$DE                    ; \"encrypt\"\n        sta     diskBlkBuf+@offset\n        lda     serial+1                ; get serial high\n        eor     #$DE                    ; \"encrypt\"\n        sta     diskBlkBuf+@offset+1\n<\/code><\/pre>\n<p>The offset can be calculated by the assembler: It&rsquo;s the offset of the serial in the current (record 1) code, modulus 254 (because blocks on disk are 254 bytes), plus 2 (number of header bytes at the start of each block).<\/p>\n<pre><code>        LoadW   r4, diskBlkBuf\n        jsr     _PutBlock               ; write back block\n        beqx    installOk\n\n        cpx     #WR_PR_ON\n        beq     @wperr\n@ierror:\n        lda     #&lt;txt_error_installing\n        ldy     #&gt;txt_error_installing\n        bra     showErrorAndExit\n\n@wperr: lda     #&lt;txt_install_write_protected\n        ldy     #&gt;txt_install_write_protected\n\nshowErrorAndExit:\n        jsr     showError\n        jsr     swap_userzp\n        jmp     EnterDeskTop\n<\/code><\/pre>\n<p>Finally, the block is written back. If there was an error, a dialog is shown, and the app exits.<\/p>\n<p>If installation was successful, the following code runs:<\/p>\n<pre><code>installOk:\n        asl     serial                  ; cycle serial left to obfuscate\n        rol     serial+1                ; serial = serial[14..0,15]\n        lda     serial\n        adc     #0\n        sta     serial\n\n        jsr     swap_userzp\n        jsr     GetDirHead              ; read BAM block\n        jsr     swap_userzp\n\n        MoveW   serial, curDirHead+$BE  ; store serial after \"GEOS format V1.x\"\n\n        jsr     swap_userzp\n        jsr     PutDirHead              ; write BAM block\n        jsr     swap_userzp\n<\/code><\/pre>\n<p>This writes a copy of the serial with the bits rotated into two unused bytes of the disk&rsquo;s header block, track 18, sector 0.<\/p>\n<p>The reason for this most probably had to do with ordering broken replacement disks: Once both the system and the backup disks didn&rsquo;t boot any more, the user was supposed to send in both disks, and would get new disks in return, already installed with the same serial, so that existing apps would continue to function.<\/p>\n<p>Side B of the system disk contains geoWrite, and side B of the backup disk contains geoMerge, both of which had to be installed \u2013 the manual explicitly instructs the user to open each app once.\u00a0So after this, both boot disks contain the obfuscated but plaintext serial on track 18, sector 0, offset $BE of side B. This could then be used to create proper replacement disks. After all, sides A of both disks were broken.<\/p>\n<p>Finally, it shows a success dialog and exits.<\/p>\n<pre><code>        lda     #&lt;txt_installed\n        ldy     #&gt;txt_installed         ; show success\n        jsr     showError\n        jsr     swap_userzp\n        jmp     EnterDeskTop            ; and exit\n<\/code><\/pre>\n<p><img decoding=\"async\" src=\"docs\/geowrite\/geowrite_5_installed.png\" alt=\"\" \/><\/p>\n<h3 id=\"disk-signature-check\">Disk Signature Check<\/h3>\n<p>We skipped over <code>executeDiskBlock<\/code>, which was called by the installer. First, it initializes the disk:<\/p>\n<pre><code>executeDiskBlock:\n        PushW   r1\n        jsr     swap_userzp\n        jsr     NewDisk\n        jsr     swap_userzp\n        PopW    r1\n        bnex    @rts                    ; I\/O error -&gt; fail\n<\/code><\/pre>\n<p>Then it reads the block whose track and sector was passed in by the caller. It&rsquo;s the location of the protection check code that was stamped in by the build system.<\/p>\n<pre><code>        LoadW   r4, diskBlkBuf          ; read block\n        jsr     _GetBlock\n        bnex    @rts                    ; I\/O error -&gt; fail\n<\/code><\/pre>\n<p>The last byte of the block is a checksum which is the lower 8 bits of the sum of all payload bytes of the sector:<\/p>\n<pre><code>        lda     #0\n        ldy     #2\n@loop:  clc\n        adc     diskBlkBuf,y            ; checksum bytes $02-$FE\n        iny\n        cpy     #$FF\n        bne     @loop\n        cmp     diskBlkBuf+$FF          ; checksum at offset $FF\n        beq     @ok\n\n        ldx     #$FF                    ; fail\n@rts:   rts\n<\/code><\/pre>\n<p>If the checksum matches, the block is run in place in the disk block buffer (<code>diskBlkBuf<\/code> at $8000):<\/p>\n<pre><code>@ok:    jsr     swap_userzp\n        jsr     diskBlkBuf+2            ; execute block\n        jsr     swap_userzp\n        rts\n<\/code><\/pre>\n<p>The protection check block is not part of the VLIR file and not formally referenced anywhere. The build system just writes it to a random free block when it generates the final disk image. In fact, it writes another 6 unused decoy copies of the block.<\/p>\n<p>This is the layout of this block:<\/p>\n<pre><code>00:  00 ff                                             block link pointer\n\n           4c 7a 80                                    jump at entry point\n\n                    ad 0f 18  48 29 df 8d 0f 18 20 16\n10:  07 68 8d 0f 18 60 ba 86  49 a9 ee 8d 0c 1c a9 07\n20:  85 33 a9 f5 85 32 a5 22  8d f5 07 20 10 f5 a0 02\n30:  84 00 20 55 07 a2 10 d0  12 a0 00 20 55 07 a0 45  drive code\n40:  20 55 07 20 64 07 a0 0a  20 55 07 20 64 07 ca d0\n50:  e8 e8 86 00 60 50 fe b8  ad 01 1c 88 d0 f7 60 ad\n60:  01 1c b8 60 ac 00 1c 10  f6 50 f9 b8 ad 01 1c c9\n70:  55 f0 f1 c9 67 f0 ed 68  68 60\n\n                                    ad e3 c1 85 03 ad\n80:  e2 c1 85 02 ad e1 c1 c9  4c f0 0c a0 00 b1 02 aa\n90:  c8 b1 02 85 03 86 02 a2  0a ac 89 84 b9 86 84 29\na0:  bf c9 02 90 24 d0 57 a8  88 88 b1 02 c8 c9 20 d0  computer code\nb0:  f9 b1 02 c8 c9 5c d0 f1  b1 02 c8 c9 c2 d0 e9 b1\nc0:  02 c8 c9 20 d0 f9 88 d0  11 a0 ff c8 b1 02 c9 85\nd0:  d0 f9 c8 b1 02 c9 8b d0  f3 c8 a2 00 b1 02 9d f5\ne0:  80 c8 e8 e0 06 d0 f5 20  14 c2 20 5c c2 a9 05 85\nf0:  8b a2 07 86 8c 00 00 00  00 00 00 20 5f c2 60\n\n                                                   d7  checksum\n<\/code><\/pre>\n<p>The second half of the block is GEOS code that executes on the computer side, and the first part is code that runs on the disk drive. The drive code does the actual disk authenticity check. The job of the computer part is to get the disk drive to run the drive code, and receive the result.<\/p>\n<h4 id=\"running-the-drive-code\">Running the Drive Code<\/h4>\n<p>GEOS comes with driver code for the 1541 and 1571 disk drives, which contains logic to upload code to the drive, execute it, and send commands, status messages and block data back and forth. The protection code reuses the driver to execute custom code and retrieve the result.<\/p>\n<p>But the disk drivers don&rsquo;t export this functionality as an API. Instead of adding this to the disk drivers as a private API, the authors of the protection chose to do some hacky stuff to get to these functions instead. This has the side effect of making it much harder to understand what is going on \u2013\u00a0which is a plus for protection code.<\/p>\n<p>The computer part needs to call the private functions <code>sendExecuteWithTrkSec<\/code> and <code>getDOSError<\/code> in the driver. This is some code in the 1541 driver that calls both functions in sequence:<\/p>\n<pre><code>__NewDisk:\n        jsr     EnterTurbo\n        bnex    NewDsk2\n        jsr     ClearCache\n        jsr     InitForIO\n        LoadB   errCount, 0\nNewDsk0:\n        lda     #&gt;Drv_NewDisk\n        sta     $8C\n        lda     #&lt;Drv_NewDisk\n        sta     $8B\n        jsr     SendExecuteWithTrkSec   ; &lt;------\n        jsr     GetDOSError             ; &lt;------\n        beq     NewDsk1\n<\/code><\/pre>\n<p>The protection code scans the <code>__NewDisk<\/code> function for the <code>STA $8B<\/code> and steals the following two instructions.<\/p>\n<p>It gets the pointer to the the function by looking at the GEOS KERNAL&rsquo;s API jump table entry for the symbol <code>NewDisk<\/code>:<\/p>\n<pre><code>start:  lda     NewDisk+2               ; find the code that is pointed\n        sta     r0H                     ; to by the NewDisk API\n        lda     NewDisk+1               ; by reading the operand of the\n        sta     r0L                     ; direct\/indirect JMP at the\n        lda     NewDisk                 ; entry point\n        cmp     #$4C                    ; direct or indirect JMP?\n        beq     @direct                 ; direct, then we found the code\n<\/code><\/pre>\n<p>If the KERNAL jumps directly to the driver code (opcode $4C), the two bytes after the API&rsquo;s address point to the implementation. If it&rsquo;s an indirect jump, it resolves the indirection:<\/p>\n<pre><code>        ldy     #0                      ; indirect jump, so\n        lda     (r0),y                  ; we need to read the vector\n        tax\n        iny\n        lda     (r0),y\n        sta     r0H                     ; and we have a pointer to the code\n        stx     r0L\n<\/code><\/pre>\n<p>The 1541 and 1571 drivers differ slightly, so it checks which type of driver is running:<\/p>\n<pre><code>@direct:\n        ldx     #STRUCT_MISMAT          ; default error code:\n        ldy     curDrive\n        lda     driveType-8,y           ; what kind of drive is this?\n        and     #$BF                    ; ignore the shadow bit (drive cache)\n        cmp     #$02                    ; 2: 1571\n        bcc     @is1541                 ; less, then 1541!\n        bne     @rts                    ; not 1571, then return with error (X != 0)\n<\/code><\/pre>\n<p>Let&rsquo;s look at the 1541 code. (The 1571 is be similar.)<\/p>\n<pre><code>@is1541:\n        ldy     #$FF\n@loop5: iny\n@loop6: lda     (r0),y                  ; search for $85 $8B\n        cmp     #$85                    ; (STA $8B)\n        bne     @loop5                  ; in NewDisk code\n        iny\n        lda     (r0),y\n        cmp     #$8B\n        bne     @loop6\n<\/code><\/pre>\n<p>This looks for the <code>STA $8B<\/code> just before the two calls. It then appends the next two calls to the end of its own code:<\/p>\n<pre><code>        iny\n\n@cont:  ldx     #$00\n@loop7: lda     (r0),y                  ; extract 6 bytes\n        sta     @code,x                 ; copy into this code\n        iny\n        inx\n        cpx     #6\n        bne     @loop7\n<\/code><\/pre>\n<p>Finally, it enables the disk driver, and calls the two functions it extracted the pointers of to have the driver execute code at <code>checkProtection<\/code> in its own RAM and retrieve the status.<\/p>\n<pre><code>        jsr     EnterTurbo\n        jsr     InitForIO\n\n        lda     #&lt;checkProtection       ; $0705 ptr in 1541 RAM\n        sta     $8B                     ; to execute\n        ldx     #&gt;checkProtection       ; (this sector is at $0700!)\n        stx     $8C\n@code:  .byte   0,0,0                   ; jsr SendExecuteWithTrkSec\n        .byte   0,0,0                   ; jsr GetDOSError\n        jsr     DoneWithIO\n@rts:   rts\n<\/code><\/pre>\n<p><code>checkProtection<\/code> is actually $0705, and points into the drive&rsquo;s buffer at $0700-$07FF, which is where the block was read. So the drive code does not have to be uploaded from the computer \u2013\u00a0it was the last block read by the driver, and it guaranteed to be located at $0700.<\/p>\n<h4 id=\"checking-for-the-gap-sequence\">Checking for the Gap Sequence<\/h4>\n<p>Tracks on a 1541-formatted disk contain the following sequence of structures for 17 to 21 sectors, depending on the track.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"docs\/geosdiskerrors\/1.png\" height=\"64\" width=\"600\" alt=\"\" \/><\/p>\n<p>A SYNC mark is followed by a sector header, and after a gap, there is another SYNC mark, followed by the sector&rsquo;s data and another gap. This is repeated for the next sector.<\/p>\n<p>The GEOS copy protection relies on the fact that the data in the gap, which is irrelevant for normal operation, cannot be reliably written to by stock drives. GEOS boot and application disks contain the following sequence of bytes there:<\/p>\n<pre><code>55 55 55 67 55 55 55 67\n<\/code><\/pre>\n<p>The purpose of the drive code is to test that the gap after both the header and the sector data contain only the values 0x55 and 0x67 for 16 consecutive sectors.<\/p>\n<p>Before we look at the main program, let&rsquo;s look at its two helpers. This is <code>skipBytes<\/code>. It just reads a certain number of bytes from disk and ignores them.<\/p>\n<pre><code>skipBytes:\n        bvc     skipBytes               ; wait for byte\n        clv\n        lda     $1C01                   ; read it\n        dey\n        bne     skipBytes               ; y times\n        rts\n<\/code><\/pre>\n<p>And this is <code>checkSignature<\/code>, which reads all bytes up to the next sync mark and makes sure that they are all either 0x55 or 0x67:<\/p>\n<pre><code>checkSignature:\n        ldy     $1C00                   ; if we found the SYNC mark,\n        bpl     @foundSync              ; the check is ok and we're done\n\n        bvc     checkSignature          ; wait until byte ready\n        clv\n\n        lda     $1C01                   ; get byte\n        cmp     #$55\n        beq     checkSignature          ; has to be either signature byte $55\n        cmp     #$67\n        beq     checkSignature          ; or signature byte $67\n\n        pla                             ; magic not found\n        pla                             ; -&gt; return to main code\n        rts                             ; (error code remains at \"2\")\n\n@foundSync:\n        lda     $1C01                   ; read value\n        clv\n        rts\n<\/code><\/pre>\n<p>If this enounters a gap byte other than 0x55 of 0x67, it pops the return address from the stack, which returns to the drive&rsquo;s main code with an error.<\/p>\n<p>The main code starts out with waiting until the read head passes the header of sector 0 of the current track and reading the header. It calls a function in the DOS ROM ($F510) for this:<\/p>\n<pre><code>        lda     #&gt;(buffer-$8000+$0700)\n        sta     $33\n        lda     #&lt;(buffer-$8000+$0700)\n        sta     $32                     ; set pointer to track\/sector for ROM call\n        lda     $22                     ; current track number\n        sta     $07F5                   ; (sector is 0)\n        jsr     $F510                   ; ROM call: find and read block header\n<\/code><\/pre>\n<p>There are two more bytes that are part of the header that haven&rsquo;t been read yet (the &ldquo;OFF&rdquo; bytes), which have to be skipped:<\/p>\n<pre><code>        ldy     #2\n        sty     $00                     ; set default error code 2: \"READ ERROR\"\n        jsr     skipBytes               ; skip 2 bytes\n<\/code><\/pre>\n<p>The next bytes to be read are now the sector header&rsquo;s gap bytes.<\/p>\n<p>The remainder of the code now iterates over 16 sectors, always skipping all header bytes and data bytes, and checking for 0x55 and 0x67 values in the gaps:<\/p>\n<pre><code>        ldx     #16\n        bne     @1                      ; check 16 headers and sectors\n\n@loop:  ldy     #&lt;$100                  ; skip a total of\n        jsr     skipBytes               ; 325 GCR bytes\n        ldy     #$45                    ; = 260 data bytes\n        jsr     skipBytes               ; = marker + full block + checksum + filler\n        jsr     checkSignature          ; check signature after data block\n        ldy     #10\n        jsr     skipBytes               ; skip full header\n@1:     jsr     checkSignature          ; check signature after header\n        dex\n        bne     @loop                   ; repeat\n        inx\n        stx     $00                     ; set error code 1: \"OK\"\n        rts\n<\/code><\/pre>\n<p>If <code>checkSignature<\/code> never failed, a status code indicating success will be set, which the computer part of the protection code will fetch.<\/p>\n<h3 id=\"encrypted-code-in-record-0\">Encrypted Code in Record 0<\/h3>\n<p>To make cracking the protection harder, there is one more component: Somewhere in the initialization code, record 1 checksums itself, and uses this checksum as a key to decrypt one function in record 0.  So if someone was to crack the protection by changing any of the code in record 1, the checksum would be different, the one function in record 0 would be garbled, and the app would sooner or later crash.<\/p>\n<p>First, the checksum code:<\/p>\n<pre><code>decryptR00:\n        LoadW   r0, MEM_OVERLAY         ; checksum code record #1\n        LoadW   r1, CODE1_END-CODE1\n        LoadB   r2L, 0\n        ldy     #0\n@loop1: lda     (r0),y\n        add     r2L\n        sta     r2L\n        IncW    r0\n        ldx     #r1\n        jsr     Ddec\n        bne     @loop1\n<\/code><\/pre>\n<p>It adds all bytes together and keeps the lowest 8 bits.<\/p>\n<p>There are several bytes within the record 1 code that must not be part of the checksum though: The 2 bytes containing the serial number will change once the app is installed, and the value will be different on every user&rsquo;s copy. So their values will be subtracted from the checksum again:<\/p>\n<pre><code>        lda     r2L                     ; remove variable bytes from checksum\n        sub     serial\n        sub     serial+1\n<\/code><\/pre>\n<p>Furthermore, the stamped-in track\/sector pointers of the block with the signature check code and the block with the serial have to be excluded. This is because of the necessary order in the build process, which looks something like this:<\/p>\n<ol>\n<li>assemble the source of each record as well as the drive code block<\/li>\n<li>encrypt record 1 with a constant of $DE<\/li>\n<li>encrypt one function in record 0 with the checksum of the record 1 plaintext<\/li>\n<li>write the whole geoWrite VLIR file to a disk image<\/li>\n<li>write the drive code block to a free block on the disk image<\/li>\n<li>stamp the track and sector of the drive code block into record 1 on the disk image<\/li>\n<li>stamp the track and sector of the block that contains the serial into record 1 on the disk image<\/li>\n<\/ol>\n<p>Both track\/sector pointers aren&rsquo;t known until step 6 and 7, but they are part of the checksum in step 3. Therefore, they are also excluded:<\/p>\n<pre><code>        sub     protExecTrack\n        sub     protExecSector\n        sub     protSerialTrack\n        sub     protSerialSector\n<\/code><\/pre>\n<p>Now it can decrypt the one function in record 0:<\/p>\n<pre><code>        LoadW   r0, r0_encrypted_start ; decrypt some code in record 0\n        LoadW   r1, r0_encrypted_start-r0_encrypted_end\n        ldy     #0\n@loop2: lda     (r0),y\n        eor     r2L\n        sta     (r0),y\n        IncW    r0\n        IncW    r1\n        bne     @loop2\n        rts\n<\/code><\/pre>\n<h2 id=\"discussion\">Discussion<\/h2>\n<p>While the geoWrite copy protection isn&rsquo;t as complicated or quite as <a href=\"https:\/\/www.pagetable.com\/?p=865\">mean<\/a> as the one on the GEOS system disks, it is nevertheless effective, and requires quite some effort to be cracked.<\/p>\n<p>Without disassembling through the geoWrite binary, a cracker could search the whole disk for code that looks like it&rsquo;s checking for the protection. Any code running on the disk and reading bytes from the head manually is a candidate. This is easy to find by looking for <code>LDA $1C01<\/code>, which would reveal the block with the gap signature check. But it&rsquo;s checksummed, and the cracker wouldn&rsquo;t know the algorithm, the range or the location of the checksum unless they had disassembled record 1. Besides, they might change the wrong block by mistake because of the decoy copies of this block on the disk.<\/p>\n<p>So any cracking attempt would require disassembling through the geoWrite code. It is quite straightforward to find the code to load and decrypt record 1, and record 1 can either be decrypted with a small script using the key found in the code, or by dumping the memory contents after decryption.<\/p>\n<p>The first few bytes of record 1 are a simple but clever obfuscation with the chance that the cracker would miss the serial check and installation code. If they do find it, they now know the track and sector of the serial on disk, and the encryption key, so they could change the serial of an installed copy, or deinstall geoWrite on the original disk.<\/p>\n<p>The holy grail would be a cracked version of geoWrite that didn&rsquo;t care about the serial, so a cracker could just remove the call to <code>checkSerialOrInstall<\/code>. But this would alter the checksum of record 1, and break the decryption of the one function in record 0, so the app would sooner or later crash. So removing the call to <code>checkSerialOrInstall<\/code> would also require patching the decryption to take a fixed key instead.<\/p>\n<h2 id=\"references\">References<\/h2>\n<ul>\n<li>ZAK256: <a href=\"https:\/\/www.c64-wiki.de\/wiki\/GEOS-Kopierschutz\">GEOS-Kopierschutz<\/a> (C64 Wiki)<\/li>\n<li>Michael Steil: <a href=\"https:\/\/www.pagetable.com\/?p=865\">Copy Protection Traps in GEOS for C64<\/a><\/li>\n<li>Michael Steil: <a href=\"https:\/\/www.pagetable.com\/?p=1118\">Why Do C64 GEOS Boot Disks Break?<\/a><\/li>\n<li>Michael Steil: <a href=\"https:\/\/www.pagetable.com\/?p=1140\">Reconstructing the GEOS 2.0 (de) Master Images from a Pile of Broken Disks<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In the series about the internals of the geoWrite WYSIWYG text editor for the C64, this article discusses the geoWrite copy protection. Article Series The Overlay System Screen Recovery Font Management Zero Page Copy Protection \u2190 this article Localization File Format and Pagination Copy &amp; Paste Keyboard Handling GEOS Copy Protection Strategy GEOS has one &#8230; <a title=\"Inside geoWrite \u2013 5: Copy Protection\" class=\"read-more\" href=\"https:\/\/www.pagetable.com\/?p=1449\" aria-label=\"Read more about Inside geoWrite \u2013 5: Copy Protection\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,5,41,8,13,15,22],"tags":[],"class_list":["post-1449","post","type-post","status-publish","format-standard","hentry","category-2","category-archeology","category-c64","category-commodore","category-floppy-disks","category-geos","category-operating-systems"],"_links":{"self":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts\/1449","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1449"}],"version-history":[{"count":0,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts\/1449\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1449"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1449"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1449"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}