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 |
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.
Convention Swag from Softcon 1984 - Courtesy of Scott Cronce |
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).
You can see a behind the scenes video of Sierra-Online here, timestamped at a very brief shot of their Formaster Series One in use.
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.
Read the next posts in this series:
Part 2: SoftGuard Superlok
Part 3: Electronic Arts Interlock
*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.
Hi,
ReplyDeleteWhich tool did you use to have the circular representation of the disc?
It's my own tool, 'imgviz,' which is an example in the floppy disk image library I am working on for MartyPC.
Deletehttps://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.
Great tools, thanks
DeleteHowdy! 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.
ReplyDeleteHi 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