PC Floppy Copy Protection: Formaster Copy-Lock

Preface

This is Part 1 of a series of articles investigating various PC floppy protections, as I get them working in MartyPC. It assumes you have a familiarity with the structure of a PC floppy disk and the basic operation of a PC floppy drive.

Thanks

Thanks to NewRisingSun on VOGONS for his vast knowledge of copy-protection schemes and history, and to OBattler for his extensive knowledge of PC floppy controller operation.

Formaster Corporation

 The Formaster corporation, headquartered in San Jose, CA, sold disk duplication equipment such as the FORMASTER Series One, pictured below.

Courtesy of VintageComputing.com

InfoWorld Sept. 19, 1983

Such machines may have been attractive investments for medium-sized software houses. Not only would it allow a software company to reproduce their own diskettes in bulk (up to 326 disks per hour, depending on options), the Formaster Series One could also add basic copy protection to each disk.

The Series One started at $25,400US, or nearly $78,950 in 2024 dollars.  Part of the cost was apparently that the system was built around the DEC PDP-11

Formaster called their copy protection technology "Copy-Lock," a name which, unfortunately, several other producers of copy-protection technology used for entirely unrelated methods.

The Formaster flavor of Copy-Lock was not used exclusively on PC - the Series One supported many other different computers such as the Apple II and Commodore 64. Copy protection schemes on those platforms could be much more advanced.  This article will specifically look at Copy-Lock as it appeared on the IBM PC platform.

Enter King's Quest

One of the customers of Formaster was none other than Sierra On-Line, and Copy-Lock was present on their seminal title King's Quest in its release as a booter for the IBM PC (A 'booter' is a game disk that did not require DOS, but was inserted directly at system startup).

Let's take a look at the disk surface of Side 0.

King's Quest I (PC Booter)



There's nothing too wild going here as far as disk structure is concerned. The presence of 9 sectors implies a standard 360Kb floppy layout. But there is something off: Track 6, Sector 1, is highlighted in orange - that means it has a bad data CRC.  It's also unusually sized - if we look at the values in the sector header, we see that the sector is declared to be 256 bytes - half the size of a standard 512 byte sector.

Now, non-standard sector sizes are quite common in all sorts of copy-protection techniques, but they usually cause the next sector to be shifted by some amount, causing an offset that accumulates around the entire ring of the affected track.  Not so here: sector 2 of track 6 is right where it is supposed to be, lining right up with all the other tracks perfectly.

Sector data CRCs are calculated across the entire sector data entity - from the Data Address Mark that signifies the start of sector data, to the end of the data bytes.  Immediately after the end of the data bytes, a 16-bit CRC value follows, which should match our calculated CRC value.  If there's no match, we have a Bad Sector.

A sector with a bad data CRC can still be read. The CRC value to check is only encountered after all the data has already been delivered to the host PC via DMA - so it's not like the data can be clawed back. The read operation will complete, but status flags are set in the FDC result code bytes to indicate the bad CRC condition. It would be up to the program to either fail and alert the user, attempt to re-read the sector, or just hope for the best and continue on.

Since you couldn't directly instruct a PC floppy controller to write a sector with a bad CRC, intentionally making bad sectors on disks was a basic and fairly effective copy-protection method.  Even if you copied each sector perfectly, byte-for-byte, your disk controller would create a good CRC for the 'bad' sector on the destination diskette, which a protected program could then easily detect as an unauthorized copy.

If that's all that was going on with Copy-Lock, there wouldn't be much to write about. Simply using a disk image format that encoded disk surface data, or one that stored 'bad data CRC' metadata, would be all that was needed. Return the proper error condition flags upon encountering the bad sector, and King's Quest would just run. But Copy-Lock is slightly more advanced than a simple CRC check.

A bitstream image of King's Quest did not run in MartyPC at first. It would sit and spin reading the same sectors over and over - actually jumping to the BIOS disk bootstrap routine after each failure.  Tellingly, the last sector read before this occurs is Track 6, Head 0, Sector 1.

Sector Headers and Dirty Lies

Each sector on a disk is preceded by a sector header:


It begins with a specially-encoded sequence of 4 bytes, 0xA1A1A1FE.  This is how the drive controller detects the start of a sector.  Immediately following are four values which are commonly called C, H, S and N.

C or cylinder, is simply the track number.  H is the head number.   S is the sector ID, and N is the sector size.

You'd normally expect the values for C and H to match the physical reality of the disk - all sectors in Track 39 of a floppy should have a C of 39, and all sectors written to Side 0 of a floppy should have an H of 0.   Normally a mismatch in what the drive controller thinks and what it finds in a sector header would be detected and set off another set of error status flags, allowing the controller or program to recover - recalibrating the drive, asking the user to flip the disk over, etc. 

One value that must explicitly be correct is S - the sector ID.   The sector ID is independent of the physical layout of the sectors on the disk.  There's nothing saying sector ID 1 must be the first sector on the track.  There's also nothing saying sector IDs must be consecutive. The disk controller would typically look for Sector ID's in order, i.e. 1, 2, 3, 4, 5, 6, 7, 8 and 9 (Sector IDs are 1-indexed).  But a copy-protection scheme could just as easily write a sector ID of 222.  This sector would not normally be found in an ordinary 'read data' operation starting at sector 1, but specifically requesting sector 222 would work.

Lying about any of these values is pretty common in all sorts of copy-protection schemes, but Copy-Lock isn't lying about C, H, or S.  It is, however, telling a little fib about N.

To encode the size of a sector (which could be up to 8129 bytes) in a single byte, the value of N isn't the literal byte count, but a number representing how many times to left-shift the value '128' to produce the byte count. Thus values of N 0, 1, 2, and 3, produce sector sizes of 128, 256, 512, and 1024 bytes respectively.

For a standard, 512-byte header, we would expect to see an N of 2.   (128<<2 == 512).  King's Quest's Track 6, Sector 1 has an N of 1, indicating a 256 byte sector. 

Let's zoom in on that suspicious sector:


Despite looking mostly empty, there are some bits of data right past the halfway point, as well as a few bytes at the end.  My initial thought was that if the sector was read as a 512 byte sector, the CRC found at the very end of the sector data might actually validate. Manually calculating the CRC however proved this theory wrong. The bad CRC is actually a bit of a red herring, not important to the protection at all.

Instead, the copy-protection check routine relies on the Read Data command failing to read the sector at all.

The routine reads Track 6, Sector 1 via BIOS interrupt 13h (disk services) sub-function 2 (Read Sectors Into Memory).  There's no actual way to give the value of N to this interrupt call - the BIOS always assumes a PC will be using a standard, 512 byte sector (N==2)*. 

When scanning the disk to execute the Read Data command, the drive controller looks for a matching sector ID.  An important detail here is that it also matches the value of N.  If both the ID and N value don't match, the sector will not be found. As far as the disk controller is concerned, if you're looking for a standard sector, this little half-sector might as well not exist at all.

Let's take a look at the copy-protection check code. The check routine begins at 07BC:00C1:

                 mov     dh, 0           ; Head 0
                 mov     ch, 6           ; Track 6
                 mov     cl, 1           ; Sector 1
                 mov     al, 1           ; Sector count 1
                 mov     ah, 2           ; Read Sectors
                 int     13h             ; DISK - READ SECTORS INTO MEMORY
                                         ; AL = number of sectors to read, CH = track, CL = sector
                                         ; DH = head, DL = drive, ES:BX -> buffer to fill
                                         ; Return: CF set on error, AH = status, AL = number of sectors read
                 jb      short on_error
                 cmp     dl, 0           ; no drives were busy?
                 jz      short check_failed
                 mov     dl, 0           ; retry drive A because a drive was busy
                 jmp     short retry_read_special_sector
 check_failed:                           
                 stc                     ; CF == failure?
                 jmp     short copy_check_exit
 on_error:                               
                 test    ah, 4           ; Status 04: Sector not found
                 jnz     short continue_check0
                 jmp     short check_failed

You can see the Int13h call set up for Track 6, Sector 1, and immediately checked for failure - if we do not fail, we set the carry flag and take an early exit out of the routine.  Otherwise, the check continues on to check the status code returned by the Read Sectors interrupt routine.  Status code 04h means "Sector Not Found" - since we didn't find a sector header that matched both the sector ID (1) and the size value N (defaulted to 2 by the BIOS).

That gets us past the first part of the check, but it wouldn't be very sophisticated protection if that was all there was to it. There's data in the second half of that funky sector, and the protection is now going to try to access it.

But if we can only read 256 bytes of sector 1 with N==1, and reading with N==2 will fail with "Sector Not Found", how exactly do we read the whole 512 bytes?

The Read Track Command

What we've established so far is that we can't fully read a sector lying about N with the Read Data command, but that's not the only way to read data from the disk.  

A standard PC floppy controller also defines a Read Track command.   It is possible to read an entire track worth of sectors with Read Data, so at first blush, this command seems redundant.  But Read Track operates in a slightly different manner - it starts at the beginning of the track and reads each sector as it encounters them, ignoring sector IDs entirely, and crucially, ignores the value of N in the sector header as well - instead using the value of N provided to the Read Track command for each sector.

There's no Int13h call for Read Track, so the protection talks to the disk controller directly:

read_track      proc near               ; 07BC:0138
                push    dx
                mov     ah, 42h ; 'B'
                call    write_fdc_byte  ; Read Track
                pop     dx
                mov     ah, dl
                or      ah, 0
                call    write_fdc_byte  ; Head/Drive select
                mov     ah, 6
                call    write_fdc_byte  ; Cylinder == 6
                mov     ah, 0
                call    write_fdc_byte  ; Head == 0
                mov     ah, 1
                call    write_fdc_byte  ; Sector == 1 (Ignored)
                mov     ah, es:[si+3]
                call    write_fdc_byte  ; Sector Size
                mov     ah, es:[si+4]
                call    write_fdc_byte  ; Track Length
                mov     ah, es:[si+5]
                call    write_fdc_byte  ; GAP3 Length
                mov     ah, es:[si+6]
                call    write_fdc_byte  ; Data Length
                call    wait_on_interrupt_flag
                call    read_fdc_result_phase
                jb      short return
                mov     al, [di+4]      ; Read first FDC result byte @ 40:42
                and     al, 0C0h        ; ST0 = Abnormal Termination?
                jz      short return    ; Normal termination, ok!
                stc                     ; Abnormal termination, set CF==1 (Error)

return:
                                        ; read_track+47↑j
                mov     ah, es:[si+2]   ; Read 50:52 (DPT)
                mov     [di+2], ah      ; Copy motor off time to BDA
                retn
read_track      endp

Once we've read the track into the buffer, the only thing left is to do something with the data. But we already established the track is mostly empty. 

Here's the actual check, looking at offset 0x1FF in the resulting track buffer, which represents the last byte of the full 512-byte sector 1:

check_data_marker proc near
                push    ds
                mov     ds, word ptr [bp+0]
                mov     si, 1FFh
                cmp     byte ptr [si], 0F7h ; Does byte #511 == F7?
                jz      short success   ; Yes, don't set carry
                stc                     ; No, set carry to indicate failure
success:
                pop     ds
                retn
check_data_marker endp

Is there actually an 0xF7 at the end of sector 1?  We can dump the sector (with a size override) from the disk image:

000180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
000190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0001F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F7 | ................

Indeed, there is.

All we really have to do is check the value of N in our Read Data command, and fail with the appropriate status bit (specifically, the NO_DATA flag in ST1) if the value of N for the specified sector does not match. Then, once we implement the Read Track command, the game springs to life:



This post from the Nerdly Pleasures blog has some interesting comments about Formaster Copy-Lock copy protection. Specifically,
However Formaster Copylock had its issues, it did not work at first with a DMA-less PC like the PCjr. and many early Tandy 1000 models (which is why those King's Quest booter versions do not use it) and it was speed sensitive, breaking on faster 286 machines.
While we are digging around in the code, we might as well establish why those things may be. The DMA question is easy enough to answer - it appears that they just didn't bother to implement the alternate polled IO (PIO) mode. 

check_bios_DMA  proc near               ; 07BC:011D
                push    ax
                push    es
                push    si
                mov     ax, 0F000h
                mov     es, ax          ; ES := F000
                assume es:nothing
                mov     si, 0EFC8h      ; BIOS Disk paramter block + 1 = DMA mode
                mov     al, es:[si]
                cmp     al, 3           ; Check DMA flag (and bit 0 of HLT?)
                jz      short flag_set
                clc                     ; Indicate 'no' to has DMA
                jmp     short exit
                nop
flag_set:
                stc                     ; Indicate 'yes' to has DMA
exit:
                pop     si
                pop     es
                assume es:nothing
                pop     ax
                retn
check_bios_DMA  endp

The code contains this code which examines a table in the BIOS to see if the floppy disk DMA is enabled, and returns the status in the carry flag.  Upon failing to detect DMA, the routine just seems to abort. I didn't find any relevant code that could or would have handled the disk controller in PIO mode.

As for the CPU speed sensitivity, this code might be a likely culprit:

wait_on_interrupt_flag proc near        ; 07BC:01B&
                sti
                mov     bl, 2           ; retry count == 2
                xor     cx, cx          ; Loop counter := 65535

loop:
                test    byte ptr [di], 80h ; check msb of 0040:003E (`working interrupt` flag)
                jnz     short return       ; flag is set!
                loop    loop               ; flag not set, try again
                dec     bl                 ; try another loop
                jnz     short loop         ; any more tries?
                stc                        ; out of tries, set CF==failure

return:
                pushf                      ; preserve flags to not count next instruction
                and     byte ptr [di], 7Fh ; clear the bit we were testing
                popf                       ; restore flags
                retn
wait_on_interrupt_flag endp

The good old LOOP instruction is often to blame for much speed-sensitive code.  As CPUs got faster, the time spent to execute a full 65536 iterations of LOOP decreased, with the effect that timeout windows implemented such as this started to shrink.  Eventually you execute the entire loop faster than the hardware will respond.

If you enjoyed this article, you may find this interview with former Formaster employee Scott Cronce to be interesting.  It gives you a great behind the scenes look at working for this company and in the industry of disk duplication in general.  

Copy-Lock ended up not being a terribly sophisticated protection, but it is a good start to this series, laying down some of the important concepts and techniques that more advanced protections will build on.  We'll tackle more exciting protections in future posts!






*It is however possible to override disk parameters by changing out the disk parameter table pointer at interrupt vector 1Eh. It might be possible to get the BIOS to default to different sector sizes this way, but I haven't verified this. Comments welcome.

Comments

  1. Hi,
    Which tool did you use to have the circular representation of the disc?

    ReplyDelete
    Replies
    1. It's my own tool, 'imgviz,' which is an example in the floppy disk image library I am working on for MartyPC.

      https://github.com/dbalsom/fluxfox/

      The README there has an example of how to use it. It supports imaging HFE, 86F, PRI and MFM bitstream-level images. If you have a flux level image such as Kryoflux or SCP you can convert to HFE with the HxCFloppyEmulator software or some other tool.

      Delete
  2. Howdy! I just wanted to point out something that is not correct in your info about "unsolved" flux images. With the SuperCard Pro image file format (.scp) you can in fact write back to any flux image file. There are several emulators that use that feature to write back data to the .scp image file.

    ReplyDelete
    Replies
    1. Hi Jim, can you tell me which emulator(s) write back to an SCP containing multiple revolutions? I reviewed several emulators with SCP support and they either write changes to an SCP file to a separate image (WinUAE) or produce an SCP with a single revolution (Sugarbox).

      Delete

Post a Comment

Popular posts from this blog

PC Floppy Copy Protection: Softguard Superlok

Bus Sniffing the IBM 5150: Part 1

The Complete Bus Logic of the Intel 8088