apple-panic-series — Part 1

The Disk That Couldn't Be Copied

How the Disk II's radical hardware design made Apple II copy protection possible — and what a flux-level disk image reveals about a 1981 floppy that defeated nearly every copy tool of its era.

11 min read intermediate
apple-ii6502copy-protectionretrocomputingapple-panic-seriesdeep-dive

This is Part 1 of a four-part series on reverse engineering the copy protection of Apple Panic (Broderbund, 1981). The full investigation — including all tools, disassembly, and the extracted runtime binary — is on GitHub.


It begins with a single file: Apple Panic - Disk 1, Side A.woz. One 5.25-inch floppy disk, captured to a modern archive format by an Applesauce flux reader. The game is Apple Panic by Ben Serki, published by Broderbund in 1981 — a platformer where you dig holes to trap monsters, one of the earliest platform games on a home computer. The disk fits everything — boot loader, copy protection, title screen, complete game — in 14 tracks. It shipped with nine distinct layers of protection that defeated virtually every automated copy utility of the era.

The goal I set for myself wasn’t just to get the game running. It was to understand every byte between the magnetic surface and JMP $4000: every protection technique, every encoding trick, every self-modification. Not to crack the disk for any practical purpose — there are plenty of emulator-friendly versions online — but because the engineering is genuinely remarkable and I wanted to understand it completely.

This first part covers the hardware context that made it all possible, what WOZ files are and why they matter for copy-protected disks, and the first real discovery: that track 0 contains two completely different disk formats coexisting on the same physical track.

Why the Disk II Was Different

To understand Apple II copy protection, you first have to understand why the Disk II drive was unusual among disk controllers of its era.

Most floppy disk controllers of the late 1970s were complex hardware systems. They handled encoding, decoding, sector formatting, and error detection in dedicated circuitry. The operating system asked the controller for “sector 5 on track 12” and received 256 bytes. The software never touched the raw bit stream.

The Disk II was Steve Wozniak’s answer to that. Instead of a complex controller, the Disk II used a handful of TTL chips and a small state machine. The controller did almost nothing — it could shift bits in and out, and it could move the head. Everything else was done in software. A 256-byte boot ROM (the “P6 ROM,” a PROM soldered to the controller card) contained just enough code to read a single sector from track 0 and jump to it. After that, the software on the disk was in charge.

This design made the Disk II astonishingly cheap — Wozniak famously reduced the chip count from around 50 in competing designs to 8. But it had a profound unintended consequence: because the software controlled the bit stream directly, the software could write anything it wanted.

The controller’s write mode worked by toggling a latch at precise intervals. If the software could time its writes correctly, it could imprint practically any magnetic flux pattern on any track. The stepper motor had four phases that the software controlled individually, giving quarter-track resolution — the head could be positioned at track 0, 0.25, 0.5, 0.75, 1.0, and so on. Half-track spacing was the practical minimum (adjacent quarter-tracks would interfere), but that still meant a protection scheme could write data at positions no standard copier expected.

There was no hardware enforcing a format. Standard 16-sector formats, standard 13-sector formats, sectors with non-standard markers, sectors with no markers at all, spiral tracks, varying bit timing, nibble counts that changed per revolution — anything the magnetic medium could physically represent was fair game. The bits on the disk were whatever the last piece of software to write them decided they should be.

The One Constraint: Nibbles

There was a hardware constraint, but it was on the read side, not the write side.

The Disk II’s shift register was self-clocking — it used the 1-bits in the data stream to maintain timing synchronization. Too many consecutive 0-bits and the hardware would lose sync. The original 13-sector controller (the P5A boot ROM, used by DOS 3.2) required that no two adjacent bits could both be zero. The later 16-sector controller (the P6 boot ROM, used by DOS 3.3) relaxed this slightly to allow one pair of adjacent zero bits.

This constraint meant you couldn’t store arbitrary byte values directly on disk. A byte like $08 (00001000) has long runs of zeros that would desynchronize the read hardware. Of the 256 possible byte values, only a subset had bit patterns dense enough in 1-bits to be read back reliably — and all of them had to have the high bit set, because the shift register used bit 7 as its “data ready” signal. Under the 13-sector constraint, only 32 different byte values were valid. Under the 16-sector constraint, 64.

These valid byte values were called disk nibbles.

But a program needs to store all 256 possible byte values — arbitrary code and data. How do you write 256 values using an alphabet of only 32 or 64? The answer is GCR — Group Code Recording — an encoding algorithm that disassembles each 8-bit data byte into smaller groups of bits and maps each group to a valid nibble value.

With 32 nibbles available, each nibble represents 5 bits of data (2⁵ = 32). So GCR extracts 5 bits into one nibble and the remaining 3 bits into another — this is “5-and-3” encoding. With 64 nibbles, each represents 6 bits (2⁶ = 64) — “6-and-2” encoding. In practice, the leftover bits from several consecutive bytes are packed together into shared nibbles rather than wasting a full nibble on just 2 or 3 bits.

The result: each 256-byte sector becomes 411 nibbles on disk under 5-and-3 encoding, or 342 nibbles under 6-and-2. Because 6-and-2 encodes more data in fewer flux transitions, the same physical disk could hold more data by reformatting it. No hardware changes — just a more efficient encoding. This was the transition from DOS 3.2 (13-sector, 5-and-3) to DOS 3.3 (16-sector, 6-and-2), which happened around 1980.

By 1981, 5-and-3 encoding was obsolete. Which is exactly why Apple Panic uses it.

Standard Disk Images vs. WOZ Files

A standard Apple II disk image — a .dsk or .do file — stores 140 kilobytes of decoded sector data: 35 tracks × 16 sectors × 256 bytes, laid out sequentially. It throws away everything about how that data was encoded on disk: the marker bytes, the sync gaps, the GCR encoding, the bit timing. For a standard DOS 3.3 disk, this is fine. The encoding is uniform and predictable, so the sector data is all you need.

For a copy-protected disk, it’s catastrophic. The protection is the encoding.

The .woz file format is different. Created by the Applesauce hardware — a modern device that connects to real Apple II drives — a WOZ file captures the actual magnetic flux transitions on the disk surface. It stores each track as a circular bit stream: the raw sequence of 0s and 1s that the drive head would see spinning past it. The WOZ2 format records:

  • The exact bit stream for each track — not decoded bytes, not nibbles, but the individual flux transitions
  • The bit count — how many bits are on the track (typically ~51,000 for a 5.25-inch disk at standard speed)
  • Quarter-track resolution — data can exist at positions 0, 0.25, 0.5, and 0.75 between integer tracks
  • Timing metadata — whether the capture was synchronized to the index hole

This means a WOZ file preserves everything: non-standard markers, unusual GCR tables, sectors that span the track’s physical wrap point, bit patterns that no standard format anticipated. Reading a WOZ file is like putting the original floppy in a drive — you see exactly what the drive head would see, and you have to decode it the same way the original software did, one bit at a time.

First Contact

The first tool I wrote was woz_reader.py — a straightforward WOZ2 parser that dumps the file structure: the TMAP (track map), the bit counts per track, and any half- or quarter-track data.

The first surprise was immediate. A standard Apple II DOS 3.3 disk has 35 tracks. This disk has 14 (tracks 0 through 13). Tracks 14-34 are empty. The entire game — boot loader, copy protection, title screen, all the graphics and game logic — fits in 40% of the disk’s capacity.

That’s not unusual for a game that predates the idea of a file system. DOS 3.3 needs a catalog track, a VTOC, and a filesystem. A raw boot disk doesn’t. You write code that reads exactly the tracks you need, in exactly the order you need them, with a custom loader that knows the disk layout by heart. It’s efficient in the way that early software had to be efficient.

The next step was woz_analyze.py, which converts each track’s raw bit stream into nibbles and scans for known sector marker sequences.

On a standard 16-sector disk, you expect to see D5 AA 96 as the address field prolog (the three-byte marker that introduces each sector’s header) and D5 AA AD as the data field prolog. On a 13-sector disk, the address prolog is D5 AA B5. These sequences were specifically chosen because no valid GCR-encoded data byte can accidentally produce them — the value $D5 is deliberately excluded from all GCR encoding tables, so it can only appear as a marker.

Track 0 had markers from both formats.

The Dual-Format Track

This was the first real discovery. Track 0 contains sectors in two different encoding formats on the same physical track:

  • One sector using 6-and-2 encoding (D5 AA 96 address prolog)
  • Thirteen sectors using 5-and-3 encoding (D5 AA B5 address prolog)

That’s 14 sectors total — neither the 13 nor the 16 that any standard format would produce. But those numbers were only conventions; custom code could write up to the track’s physical capacity. Note that both formats have their own sector 0: the 6-and-2 sector 0 (the boot sector) and a separate 5-and-3 sector 0. They coexist because their prologs are different — any RWTS searching for one format simply ignores sectors in the other.

This is deliberate copy protection, and it’s elegant in its simplicity. The Disk II’s P6 Boot ROM always boots using 6-and-2 encoding — that’s hardwired into its 256 bytes of code. It finds the one 6-and-2 sector on track 0, loads its 256 bytes to $0800, and jumps to $0801. Standard DOS 3.3 copiers work the same way. So a copier trying to duplicate the disk finds exactly one sector on track 0 instead of sixteen, copies that, and moves on. The thirteen 5-and-3 sectors — which carry the actual boot loader and RWTS — are completely invisible to it.

The boot sector itself, once loaded at $0800, is where things get interesting. It doesn’t just start the game — it builds its own GCR decode table for the 5-and-3 format, then reads those thirteen sectors using its own custom RWTS. This is the fundamental pattern of Apple II copy protection: the standard ROM does just enough to load the first sector, and that first sector rewrites the rules for everything that follows.

Why use 5-and-3 specifically? By 1981 it was obsolete — replaced by 6-and-2 two years earlier. Fewer copy tools supported it. Mixing it with 6-and-2 on the same track was virtually unheard of. Choosing an archaic, poorly-supported format for the bulk of the disk was a calculated decision: the protection wasn’t breaking any rules, it was relying on the fact that most copy utilities simply didn’t know the older format existed.

The Sector Structure

Each sector on a track follows a standard layout, regardless of encoding format. Understanding this layout is essential for understanding the protection techniques that come later.

A sector has two fields, each surrounded by synchronization gaps:

[sync bytes] [address prolog] [address data] [address epilog]
[sync bytes] [data prolog]    [sector data]  [data epilog]

The address field identifies the sector: volume number, track number, sector number, and a checksum — all encoded in 4-and-4 format (two nibbles per byte). The data field carries the 256 bytes of payload, GCR-encoded, with a running XOR checksum.

The three-byte prolog marks the start of each field. For standard disks, address prologs are D5 AA 96 (16-sector) or D5 AA B5 (13-sector), and data prologs are always D5 AA AD. The epilogDE AA EB in standard formats — marks the end of each field and lets the RWTS confirm it read the right amount of data.

All of these markers are conventional, not physical. There’s nothing special about $D5 at the hardware level except that GCR encoding tables are designed to never produce it as encoded data. If you want to write sectors that use $DE as an address prolog, or have no epilog at all, or use a non-standard third prolog byte — the hardware won’t stop you. The RWTS code is what enforces format conventions, and custom RWTS code can choose any conventions it likes.

Apple Panic’s boot loader takes full advantage of this. The nine layers of protection it uses are all implemented at the software level, exploiting the Disk II’s complete flexibility. We’ll trace all of them in the next three parts.

What the nibbler scan Output Reveals

Running nibbler scan against the WOZ file confirms the dual-format structure and reveals the full scope of what we’re dealing with:

Track  Encoding   6+2   5+3  Addr CK   Data CK  Notes
    0      dual     1    13    13/14      1/14
    1       5+3     0    13    13/13      0/13   addr=$D5 $BE $B5
    2       5+3     0    13    13/13      0/13   addr=$D5 $BE $B5
    ...
    6       5+3     0    13    13/13      0/13   addr=$DE $FB $B5
    ...
   13       5+3     0    13    13/13      0/13   addr=$DE $BB $B5

Several things stand out immediately:

Bad address checksums on all 5-and-3 sectors on track 0. The standard checksum for a 5-and-3 address field is volume XOR track XOR sector. Every single 5-and-3 sector on track 0 fails this check. This is deliberate — it defeats nibble copiers that validate address checksums before reading data.

One bad data checksum on track 0. Sector 11’s data fails checksum verification. Sector 11 is unused by the boot loader — sectors 0-9 are the ones that get loaded. The bad checksum appears to be a deliberate decoy for copiers that validate all sector checksums.

Non-standard address prologs on every track past 0. The second byte of the address prolog varies per track — $BE for tracks 1-2, $AB for track 3, $BF for track 4, and so on through $BB for track 13. And tracks 6-13 use $DE as the first byte of the prolog instead of the standard $D5.

That last point is particularly striking. The standard prolog starts with $D5 because that value is excluded from GCR encode tables, making it impossible to accidentally produce it as encoded data. But $DE is also excluded — and using it as a prolog marker means any copier that scans for $D5 as the start of a sector finds absolutely nothing on tracks 6-13.

How does the game’s own RWTS handle this, given that it starts life searching for $D5? The answer is that it doesn’t — not initially. The RWTS gets patched at runtime after tracks 1-5 are loaded, changing the search byte from $D5 to $DE. We’ll trace exactly how that patch works in Part 2.

The Shape of the Investigation

At this point, the static picture tells us a lot: 14 tracks, dual format, 5-and-3 encoding, non-standard prologs, bad checksums, per-track prolog variations. Each of these is a separate protection layer, and each targets a different class of copy tool.

But the static picture has limits. Three more protection techniques — GCR table corruption, self-modifying code, and a custom post-decode permutation — are invisible to analysis of the raw disk structure. They live in the boot code itself, and you can only discover them by reading and tracing that code.

That’s what Part 2 is about.


Next: Reading Code That Lies — disassembling the boot sector, the self-modification that changes behavior before the code runs, the mathematical trick that keeps checksums valid while corrupting the GCR table, and the per-track marker scheme that makes the disk illegible unless you already have the key.