Reverse Engineering KV300+'s Floppy-Disk Copy Protection, 25 Years Later

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


Reverse Engineering KV300+’s Floppy-Disk Copy Protection, 25 Years Later

This is a long story — an article more than twenty years overdue. The technology in it is already a relic, but for a whole generation it’s a piece of shared memory. I’m writing it down partly as a record, and partly to settle a score I never closed out in my younger days.

Back then, mainstream software shipped on encrypted floppy disks. It was a golden age of copy protection — new tools appeared constantly — but one product stood out as famously hard to copy: KV300+, an antivirus whose key disk was notoriously resistant to duplication. Several crackers had written copy tools and circulated disk images, but as far as I could test, none of them ran every version flawlessly.

By luck, a fellow collector recently handed me every version they had gathered. I decided to see whether I could still swing the old SOFT-ICE and TR debuggers. I bought several vintage laptops, debugged on and off for months, and finally produced a perfect copy of the encrypted disk — one that runs every surviving version I could get my hands on.

KV200: the simple version

To understand the KV300+ disk, rewind to 1994. That year a wave of viruses broke out, and one antivirus program graduated to “KV100” — meaning it could kill 100 viruses. KV200 then went commercial, sold on an encrypted disk. KV200’s encryption was comparatively simple: the floppy carried five pairs of N=6 sectors, each pair being one CRC sector plus one encrypted data sector. At startup the two sectors checked against each other. KV300 and KV300+ later kept the same core idea.

A CRC sector cannot be written to — once you write to it, the CRC marker is gone. The early copy tools could partially reproduce a CRC sector, but at a cost: after writing the sector data they would manually reset the FDC to fabricate a CRC, which in turn destroyed the following sector. A perfect copy therefore required finding the actual encryption algorithm and writing the encrypted sector directly.

Here’s how to copy the KV200 disk. I’ll use KV200 (K) as the example; the other versions are identical.

KV200 (K) — getting started

TR loads the target:

TR loaded

Set a breakpoint on INT 13:

Breakpoint on INT 13

Fast-trace to the first encrypted sector — track 73, head 0, sector 4988:

Trace to sector 4988

AH returns 10 — this is a CRC sector:

AH=10, a CRC sector

Dump the data buffer at ES:BX:

Data buffer at ES:BX

Read the second encrypted sector — track 74, head 0, sector 4A88:

Read sector 4A88

It returns 00 — a “normal” N=6 sector:

AH=00, a normal N=6 sector

Look at the data buffer:

Data buffer

Take 100h bytes at offset 0DB0h from sector 4988 and compare them against 4A88 — if they match, the program proceeds. The encryption is dead simple; the later KV300 and KV300+ kept this shape but grew progressively more complex.

Now let’s look at the KV200 disk data itself:

KV200 disk data

A normal 1.44 floppy track — 18 sectors of 512 bytes:

Normal 1.44 track, 18×512

The track-73 ID uses sector 4988: 73(C) 0(H) 136(R) 6(N):

Track 73 ID

Read sector 4988:

Read sector 4988

The sector is filled with F6 — it was formatted with N=4, but the sector ID says N=6, so past 800h you read into the sector seam and the following sector’s data, which can be misaligned:

F6 fill, N=4 vs N=6

At 800h is the sector CRC:

CRC at 800h

The data at offset 0DB0:

Data at offset 0DB0

Now the track-74, sector-4A88 data:

Track 74, sector 4A88 data

Identical to 4988’s offset 0DB0, isn’t it? This era’s encryption was honest to a fault. So copying the disk just means formatting by ID, then writing 4988’s offset-0DB0 data into the 4A88 sector. The other versions are the same — five pairs of encrypted tracks in total.

KV300: the landmines

In September 1996 the company was formally founded and KV300 shipped. KV300 swapped the disk but kept KV200’s approach, adding just a bit — a few “landmines” buried in the encrypted data. They went off not long after.

KV300 (G) gained self-programmed sector-read capability. Using the H version as the example, break on BPIO 3F2 in SoftICE; once in program space you can see it read the track ID via self-programming and verify the first sector’s ID:

KV300 H: BPIO 3F2, self-programmed read

In 1997 a cracker forum published a key-disk tool, and pirated copies flooded the market. The vendor fought back hard: they buried an “active logic lock” into an online-upgraded build. Machines running pirated copies locked up — neither floppy nor hard disk would boot. History remembers this as the “logic lock” incident, which ended with a fine and no prosecution. In hindsight, the root cause was that the disk analysis had been incomplete — the hidden data simply hadn’t been found.

From then on, when the KV series detected a pirated disk it stopped being aggressive, and instead nudged the user with virus demos and the like. One group’s last KV300+ build (Z6KEY), for instance, had incomplete data; the program would appear to run normally, but pressing ESC on a copied disk triggered a virus demo.

Reading the paired tracks of track 74 and comparing them reveals the difference:

Comparing files 74-0-168 and 74-1-137:
0000070B: 58 F6
0000070C: 58 F6
0000080B: 58 D9
0000080C: 58 AE
00000DB9: 58 4E
00000DBA: 58 4E

Those extra bytes written at 700h/800h/DBAh are what get used for verification later. The track-80 data caused the famous logic-lock incident. The forum’s key-disk tool went quiet after its v5, and that tool line never achieved a perfect KV300 copy, for two reasons: (1) missing track-70 data; (2) compatibility — only some machines could produce a working copy. After testing many drives I found the cause: the sector-gap length. A genuine disk has a sector gap of 54h — 84 × 4E — and at 88E/88F/890 sits the index mark. KV verifies those three bytes. On different drives, motor-speed differences mean you must tune the sector gap and write repeatedly (writing shifts the data; repeated writes eventually land the misalignment exactly where the verification needs it).

Copying KV300 just needs the correct sector gap: format by ID, write data offset 0DB0 into the verification sector, and write the other verification bytes. The other tracks are the same.

KV300+ Z.8: encrypted sectors and in-memory traps

In November 1997 the vendor swapped the disk again and launched KV300+ — the first domestic antivirus to fully neutralize macro viruses, and the moment KV entered its strongest-encryption era. Sector data was no longer stored in the clear but encrypted. Let me tear down the method using Z.8.

KV300+ Z.8 under analysis

Normal entry to the interface:

Normal entry

TR loads KV300 and I set BP INT 13:

TR load + BP INT 13

AX=201, CX=0001 reads the boot sector; keep going:

AX=201, CX=0001 boot sector

Break on the read of sector 5037 on head 1. Write 1234 at BX+402, then after the read check whether that data got overwritten:

Head 1, sector 5037, write 1234 at BX+402

Dump BX to show the data buffer as ES:BX:

Data buffer ES:BX

The read succeeds; AH returns 10, meaning this sector has a CRC error:

AH=10, CRC error

The sector is formatted with N=04, length 800h; BX+802 is the CRC position, clearly not 4E4E:

N=04, 800h, CRC at BX+802

Compare BX+884 to FFFF — this verifies whether the data is misaligned:

Compare BX+884 to FFFF

Copy 50h bytes at BX+898 into [733A] for later checking — this is exactly the disk seam, a classic “seamless lock” protection:

50h bytes at BX+898 → [733A]

rep movsb complete:

rep movsb done

Starting at BX+900, 7D0h of data is moved to CS:DI+998F:

BX+900, 7D0h → CS:DI+998F

The current CS is saved to [DC84]. Note this address down — it will be used over and over:

CS → [DC84]

Now it reads the encrypted sector 5061 on head 0:

Read encrypted sector 5061

AH returns 0 — read success. The sector data is now visible:

AH=0, read success

BX+814 is passed to DI as 6936 — it’ll be checked shortly:

BX+814 → DI = 6936

Dump BX+814 — indeed 6936:

BX+814 = 6936

8B0 and 8B2 differ:

8B0 != 8B2

As said, it compares BX+814 against 6939; if not equal it jumps to 5174:

Compare BX+814 to 6939

5174 re-reads sector 5061:

5174 re-reads 5061

Starting at BX+900, 7D0h is moved to CS:DI+A961:

BX+900, 7D0h → CS:DI+A961

The data area now starts decoding — first pass decodes 63c words:

First decode, 63c words

Decode complete:

Decode complete

Remember the 50h bytes moved to [733A] earlier? It compares 71e6+898 against them, 30h bytes at a time:

Compare 71e6+898, 30h

Comparison done — normally all 30h bytes match, and BX gains 30h:

Comparison done, BX+30h

[d8df] also gains 30h:

[d8df] + 30h

[D8DF] goes from 5050h to 5080h, [71dc] is still 0. Track 80 verification is done. Honestly, even here I felt uneasy — there was no sign of the second and third decode passes (see the track-72 decode below). Soon my hunch was confirmed: entering the interface, it promptly froze. I was stuck for weeks before I patiently tracked down the verification algorithm:

[D8DF] 5050→5080, [71dc]=0

Now the track-72 verification — this sector pair is the representative one:

Track 72 verification

Track-72 sector 4829 is a CRC-error sector, AH=10h:

Sector 4829, AH=10h

See where the sector data gets saved:

Sector data destination

Starting at BX+FA0, 7D0h of words is saved to CS:DI+B933:

BX+FA0, 7D0h → CS:DI+B933

Compare BX+820 — this is the sector gap, so of course it’s 4E4E:

Compare BX+820 to 4E4E

50h bytes at BX+1F50 are kept for later:

50h bytes at BX+1F50

The kept data is moved to [733A]:

Data → [733A]

Now it reads sector 487c:

Read sector 487c

Sector read succeeds:

Sector read success

Starting at BX+FA0, 7D0h of words is saved to CS:DI+C905:

BX+FA0, 7D0h → CS:DI+C905

First decode pass, 830 words:

First decode, 830 words

First pass complete:

First pass complete

After the first decode, 50h bytes go to [3AAE]:

50h bytes → [3AAE]

Second decode pass. 830 + 7D0 = 1000, exactly 2000h (8192) bytes — the arithmetic is tidy:

Second decode, 830+7D0=1000

Third decode pass. Track 72 decoding is done, and now the sector data is crystal clear:

Third decode pass

This part is too long for one screenshot:

Decode continued

71E6+200h is the data buffer, so SI adds 2150h here — it’s really checking BX+1F50. At this point all pre-interface verification is complete. My method: at the end of decoding, save the decoded data to disk, replace the parts my own formatted sector data needs to verify with the decoded data, then run the inverse of the decode to re-encrypt and write back to the encrypted sector. That passed every check — until Z.8, which froze right after entering the interface. From Z.8 onward the encryption upgraded again: it added in-memory verification during the virus scan. (One person’s SCS-made disk showed the virus demo, and while tracing Z.7 I’d spotted a hint about the track-80 decode.)

Track-80 decode hint (Z.7)

Track-80 decode hint, continued

This is the virus demo that appears on ESC after entering the interface:

Virus demo on ESC

This is the F7 demo interface:

F7 demo interface

This is the first decode pass:

First decode pass

And here, unexpectedly, is the third decode pass. Still missing: the track-80 second decode algorithm:

Third decode pass found

Z.8 froze after entering the interface! That reminded me that the data I’d painstakingly saved at CS:DI+998F, CS:DI+A961, CS:DI+B933, and CS:DI+C905 hadn’t been used at all yet. Remember the CS I said to note down? Break on [DC84] and trace slowly. 998F+37c is passed to [88e6], then ADD 1357:

998F+37c → [88e6], ADD 1357

A961+37C is passed to [8AAF]:

A961+37C → [8AAF]

[8AAF] is passed to [8917]:

[8AAF] → [8917]

8917 begins decoding — there it is, the track-80 second decode, hiding in plain sight:

8917 decode: track-80 second decode

It starts comparing. If [88E6] and [8917] differ, it NOPs 739c and JMPs 71f7 — the road to a crash. I fixed that byte and re-entered the scan, and damn it, it froze again!!! No problem — this time I had a feel for it. Onward:

Patch 739c→NOP, 71f7→JMP

Same routine: break on [DC84]. Soon the track-72 in-memory verification surfaced. Another little trick: C905 shifts by 32h, then takes DI+954 into [D87e]:

C905 shifts 32h, DI+954 → [D87e]

6829:9989 retrieves the CS saved in [DC84]:

6829:9989 retrieves CS

[D87e] is passed to [D858]:

[D87e] → [D858]

The track-72 fourth decode:

Track-72 fourth decode

This decode is messy — writing its inverse made my head spin:

Complex decode

Watch the core variables; [D858] is processed:

[D858] processed

[B933+986] ADD 7531 → [D70A]:

[B933+986] ADD 7531 → [D70A]

[D70A] and [D858] don’t match — it mangles the control flow beyond recognition. Just when I’d cleared this byte’s verification and thought I was done, I re-entered the scan, let it run a bit longer, and it froze yet again!!! I could almost feel the original author setting trap after trap, half hoping a successor would find them and half hoping the secret would stay buried. I collected myself and fought on:

Continuing — BX+12F4 → [D881]

Same routine. BX+12F4 goes to [D881]:

BX+12F4 → [D881]

[D881] is passed to [D8CE]:

[D881] → [D8CE]

[D8CE] subtracts 98:

[D8CE] − 98

[D8CE] is complemented:

[D8CE] complement

[D8CE] rotates right by one:

[D8CE] rotate right

[DA6a] is 35h; continue decoding:

[DA6a] = 35h

[B933+154] is passed to [D878]:

[B933+154] → [D878]

[D8CE] is passed to [D856]:

[D8CE] → [D856]

[D856] vs [D878] — if they differ, it rewrites a few spots in ES beyond recognition: crash, garbled screen, memory overflow. I’ve now seen every way to die. After clearing this byte, the last piece of the puzzle snapped into place. The KV key disk was finally, perfectly reproduced.

Copying the KV300+ disk means finding the correct sector gap, formatting by ID, replacing the parts of the fully-decoded sector data that need verification, then running the inverse operation to re-encrypt and write back to the encrypted sector.

Wrap-up

When I typed the last line of code, compiled it, and verified every version in hand, months had slipped by. The author of this protection is gone, yet finishing this felt like a conversation across time. I got to see a top-tier programmer’s tricks up close. Technologies age and some things vanish from our lives — but clever design, clean thinking, and precise implementation keep shining. That’s a lesson worth learning for a generation that too often piles up throwaway code.

I closed the laptop. Goodbye, my 90s.

Postscript — a version-collection effort. I’m trying to gather the original files of every version ever shipped. KV300A is KV300+; here is everything I currently have:

Version collection

All versions on hand


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch