-------------Rocky's Boots------------- A 4am crack 2016-04-01 -------------------. updated 2021-09-06 |___________________ Name: Rocky's Boots Version: 4.0 Genre: educational Year: 1985 Authors: Warren Robinett and Leslie Grimm Publisher: The Learning Company Media: single-sided 5.25-inch floppy OS: custom Previous cracks: none of this version ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA immediate disk read error Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy (no sync, no count) no errors, but copy swings to high track and reboots Copy ][+ nibble editor all tracks use standard prologues (address: D5 AA 96, data: D5 AA AD) but modified epilogues (address: FF FF EB, data: FF FF EB) Disk Fixer ["O" -> "Input/Output Control"] set Address Epilogue to "FF FF EB" set Data Epilogue to "FF FF EB" Success! All tracks readable! T00 -> custom bootloader no sign of DOS 3.3, ProDOS, or any kind of disk catalog Why didn't COPYA work? modified epilogue bytes (every track) Why didn't Locksmith FDB work? modified epilogue bytes (every track) Why didn't my EDD copy work? probably a nibble check during boot (because disks do not spontaneously reboot unless someone tells them to) Next steps: 1. Super Demuffin to convert the disk to a standard format 2. Patch the RWTS to read a standard disk (if necessary) 3. Find and disable the nibble check ~ Chapter 1 In Which We Choose The Right Tool For The Job I'm going to use Super Demuffin here (instead of my usual go-to conversion tool, Advanced Demuffin). The disk uses a custom bootloader, so the AUTOTRACE script on my work disk won't get very far in capturing the RWTS. But luckily, the RWTS modifications are minor -- custom epilogue bytes, same on every track, and no apparent changes to the nibble translation table -- so Super Demuffin will work just fine. When you first run Super Demuffin, it asks for the parameters of the original disk. In this case, the prologue bytes are the same, but the epilogues are "FF FF EB" instead of "DE AA EB". --v-- SUPER-DEMUFFIN AND FAST COPY Modified by: The Saltine/Coast to Coast Address prologue: D5 AA 96 Address epilogue: FF FF EB DISK ^^^^^ ORIGINAL *change from "DE AA" Data prologue: D5 AA AD Data epilogue: FF FF EB ^^^^^ *change from "DE AA" Ignore write errors while demuffining! D - Edit parameters - Advance to next parm - Exit edit mode R - Restore DOS 3.3 parameters O - Edit Original disk's parameters C - Edit Copy disk's parameters G - Begin demuffin process --^-- Pressing "G" switches to the Locksmith Fast Disk Copy UI. It assumes that both disks are in slot 6, and that drive 1 is the original and drive 2 is the copy. [S6,D1=original disk] [S6,D2=blank disk] --v-- LOCKSMITH 7.0 FAST DISK BACKUP R................................... W*********************************** HEX 00000000000000001111111111111111222 TRK 0123456789ABCDEF0123456789ABCDEF012 0................................... 1................................... 2................................... 3................................... 4................................... 5................................... 6................................... 7................................... 8................................... 9................................... A................................... B................................... C................................... D................................... 12 E................................... F................................... [ ] PRESS [RESET] TO EXIT --^-- There are two problems with this copy: 1. Depending on how the original disk was written, this copy may or may not be able to read itself. I may need to patch the disk's RWTS to deal with the fact that the disk is now in a standard format. 2. Even if it can read itself, it won't run. The copies I tried to make -- even the bit copies -- just rebooted endlessly, which means there is some code being executed during boot to check if the disk is original. (Hint: it's not.) Just by booting the copy, I can rule out problem #1. The disk seems to read itself just fine. It makes it exactly as far as the failed bit copy -- far enough to figure out that it's not an original disk, and reboot. Let's go find that protection check. ~ Chapter 2 In Which We Get Lucky Since my copy reboots, and programs don't just do that without a good reason, I'm guessing there is a runtime protection check somewhere. One thing that all protection checks have in common is they need to turn on the drive motor by accessing a specific address in the $C0xx range. For slot 6, it's $C0E9, but to allow disks to boot from any slot, developers usually use code like this: LDX LDA $C089,X There's nothing that says where the slot number has to be, although the disk controller ROM routine uses zero page $2B and lots of disks just reuse that. There's also nothing that says you have to use the X-register as the index, or that you must use the accumulator as the load register. But most RWTS code does, out of convention I suppose (or possibly fear of messing up such low-level code in subtle ways). Also, since developers don't actually want people finding their protection- related code, they may try to encrypt it or obfuscate it to prevent people from finding it. But eventually, the code must exist and the code must run, and it must run on my machine, and I have the final say on what my machine does or does not do. But sometimes you get lucky. Turning to my trusty Disk Fixer sector editor, I search the non-working copy for "BD 89 C0", which is the opcode sequence for "LDA $C089,X". [Disk Fixer] ["F"ind] ["H"ex] ["BD 89 C0"] --v-- ------------- DISK SEARCH ------------- $00/$01-$1B $00/$0C-$4F $00/$0F-$52 --^-- Looking at T00,S01, it's immediately obvious that I've hit the jackpot. ~ Chapter 3 In Which Bits Never Lie Here is T00,S01 from the beginning, as seen through Disk Fixer's built-in disassembler. This sector is loaded into memory at $4F00. (Oddly enough, T00,S00 is almost identical to standard the DOS 3.3 boot0, except it loads all of track $00 into $4E00+.) This is the first thing called after boot0 finishes reading track $00. --v-- T00,S01 ----------- DISASSEMBLY MODE ---------- ; save zero page 0000:A0 FF LDY #$FF 0002:B9 00 00 LDA $0000,Y 0005:99 00 5E STA $5E00,Y 0008:88 DEY 0009:D0 F7 BNE $0002 ; seek to track $22 (not shown) 000B:A9 00 LDA #$00 000D:8D 78 04 STA $0478 0010:A9 44 LDA #$44 0012:20 A0 56 JSR $56A0 ; high byte of Death Counter 0015:A9 0A LDA #$0A 0017:85 F0 STA $F0 ; turn on drive motor of the slot we ; just booted from (stored in zp$2B) 0019:A6 2B LDX $2B 001B:BD 89 C0 LDA $C089,X 001E:BD 8E C0 LDA $C08E,X ; probably an address, ($F2) -> $4FD8 0021:A9 D8 LDA #$D8 0023:85 F2 STA $F2 0025:A9 4F LDA #$4F 0027:85 F3 STA $F3 ; low byte of Death Counter 0029:A9 80 LDA #$80 002B:85 F1 STA $F1 002D:C6 F1 DEC $F1 ; when Death Counter hits 0, bad things ; happen 002F:F0 5C BEQ $008D ; this finds the next address prologue ; ("D5 AA 96") and skips over the ; address field 0031:20 44 56 JSR $5644 ; if that didn't work, fail 0034:B0 57 BCS $008D ; loop until we find sector $0F (in ; zero page $2D after routine at $5644) 0036:A5 2D LDA $2D 0038:C9 0F CMP #$0F 003A:D0 F1 BNE $002D ; here we go 003C:A0 00 LDY #$00 003E:BD 8C C0 LDA $C08C,X 0041:10 FB BPL $003E 0043:88 DEY 0044:F0 47 BEQ $008D ; fail ; find $D5 nibble 0046:C9 D5 CMP #$D5 0048:D0 F4 BNE $003E ; find $E7 $E7 $E7 sequence of nibbles ; within the next $100 nibbles ; (Y register serves as the mini-Death ; Counter here, if it wraps around to 0 ; then we give up) 004A:A0 00 LDY #$00 004C:BD 8C C0 LDA $C08C,X 004F:10 FB BPL $004C 0051:88 DEY 0052:F0 39 BEQ $008D ; fail 0054:C9 E7 CMP #$E7 0056:D0 F4 BNE $004C 0058:BD 8C C0 LDA $C08C,X 005B:10 FB BPL $0058 005D:C9 E7 CMP #$E7 005F:D0 2C BNE $008D ; fail 0061:BD 8C C0 LDA $C08C,X 0064:10 FB BPL $0061 0066:C9 E7 CMP #$E7 0068:D0 23 BNE $008D ; fail ; reset data latch and kill some time ; to get out of sync with the original ; nibble boundary 006A:BD 8D C0 LDA $C08D,X ; reset Y (serves as a mini-Death ; Counter again in the next loop) 006D:A0 10 LDY #$10 ; does nothing of consequence except ; burn a few more CPU cycles 006F:24 06 BIT $06 ; now start looking for nibbles that ; don't really exist (except they do, ; because we're out of sync and reading ; timing bits as data) 0071:BD 8C C0 LDA $C08C,X 0074:10 FB BPL $0071 0076:88 DEY 0077:F0 14 BEQ $008D ; fail 0079:C9 EE CMP #$EE 007B:D0 F4 BNE $0071 ; check for (still desynchronized) ; nibble sequence stored in reverse ; order at ($F2) a.k.a. $4FD8 007D:A0 07 LDY #$07 007F:BD 8C C0 LDA $C08C,X 0082:10 FB BPL $007F 0084:D1 F2 CMP ($F2),Y 0086:D0 05 BNE $008D ; fail 0088:88 DEY 0089:10 F4 BPL $007F 008B:30 03 BMI $0090 ; pass ; failure path ends up here, from ; multiple places noted above -- ; unconditionally branch further down 008D:18 CLC 008E:90 3E BCC $00CE ; successful execution continues here ; (from $4F8B) 0090:A9 60 LDA #$60 0092:8D 01 08 STA $0801 ; move the rest of this page to higher ; memory 0095:A0 80 LDY #$80 0097:B9 00 4F LDA $4F00,Y 009A:99 00 5F STA $5F00,Y 009D:C8 INY 009E:D0 F7 BNE $0097 ; and continue there 00A0:4C A3 5F JMP $5FA3 ; set up zero page so we can call the ; disk controller ROM to read a sector ; (from track $22, which we're still ; on after the successful protection ; check above) ; sector $02 00A3:A9 02 LDA #$02 00A5:85 3D STA $3D ; track $22 00A7:A9 22 LDA #$22 00A9:85 41 STA $41 ; address $4F00 00AB:A9 4F LDA #$4F 00AD:85 27 STA $27 00AF:A9 00 LDA #$00 00B1:85 26 STA $26 00B3:A5 3F LDA $3F 00B5:8D BA 5F STA $5FBA ; read it 00B8:20 5C C6 JSR $C65C ; seek back to track $00 00BB:A9 00 LDA #$00 00BD:20 A0 56 JSR $56A0 ; restore zero page 00C0:A0 00 LDY #$00 00C2:B9 00 5E LDA $5E00,Y 00C5:99 00 00 STA $0000,Y 00C8:88 DEY 00C9:D0 F7 BNE $00C2 ; and jump to actual boot1 code, which ; we just read from track $22 00CB:4C 00 4F JMP $4F00 ; failure path continues here (from the ; unconditional branch at $4F8E) -- ; decrement the high byte of the Death ; Counter and either jump back to try ; again or give up and reboot 00CE:C6 F0 DEC $F0 00D0:D0 03 BNE $00D5 00D2:4C 00 C6 JMP $C600 00D5:4C 29 4F JMP $4F29 ; array nibbles to look for after we've ; intentionally desynchronized (by ; burning CPU cycles at $4F6A..$4F6F) 00D8:FC EE EE FC E7 EE FC E7 --^-- We can bypass this entire protection check by forcing it down the success path (at $4F90). But that's not what we're going to do. We're going to do one better. ~ Chapter 4 In Which We Take A Short Digression Into Some Theory So We Can Better Understand The Upcoming Bombshell $E7 $E7 $E7 $E7. What would that nibble sequence look like on disk? The answer is, "It depends." $E7 in hexadecimal is 11100111 in binary, so here is the simplest possible answer: |--E7--||--E7--||--E7--||--E7--| 11100111111001111110011111100111 But wait. Every nibble read from disk must have its high bit set. In theory, you could insert one or two "0" bits after any of those nibbles. (Two is the maximum, due to hardware limitations.) These extra "0" bits would be swallowed by the standard "wait for data latch to have its high bit set" loop, which you see over and over in any RWTS code: :1 LDA $C08C,X BPL :1 Now consider the following bitstream: |--E7--| |--E7--| |--E7--||--E7--| 11100111011100111001110011111100111 ^ ^^ (extra) (extra) The first $E7 has one extra "0" bit after it, and the second $E7 has two extra "0" bits after it. Totally legal, works on any Apple II computer and any floppy drive. A "LDA $C08C,X; BPL" loop would still interpret this bitstream as a sequence of four $E7 nibbles. Each of the extra "0" bits appear after we've just read a nibble and we're waiting for the high bit to be set again. They get "swallowed." Ignored. Like they were never there. But what if we miss some bits of this bitstream, then start looking? The disk is always spinning, whether we're reading from it or not. If we waste too much time doing something other than reading, we'll literally miss some bits as the disk spins. (This is why low- level RWTS code is so timing critical.) It turns out we can do this in a (relatively) controlled fashion, by hitting the $C0ED softswitch. This is an integral part of writing to the disk, but it is not normally used while reading. If the disk controller is in read mode, hitting $C0ED acts like a hard reset. The data latch instantly forgets the bits it had accumulated, and it takes some time to recover. Meanwhile, the disk continues to spin. (This is an oversimplification, because everything is an oversimplification. Technically speaking, $C0ED resets the logic state sequencer to state 0. See Sather, "Understanding The Apple II," pp. 9-17, 9-22, and following.) The following diagram shows the effect of hitting $C0ED in conjunction with the CPU-burning instructions LDY and BIT, before finally starting to read another nibble from the disk. The $E7 nibbles are numbered for reference. (1) (2) (3) (4) (5) |--E7--| |--E7--| |--E7--||--E7--||--E7- 1110011101110011101110011111100111111001 ^read $C0ED^ read^ After explicitly reading three $E7 nibbles, labeled here as (1) (2) (3), we hit $C0ED to desynchronize, burn a few cycles with LDY and BIT, then finally start reading from the disk again -- after missing all of (4) and most of (5). All told, we've missed 13 bits and are now +5 bits "out of phase" from the "start" of the nibble. Of course, there is no "start" of a nibble; there is just a stream of bits. And what do we read from this starting position? Picking up the diagram at (5): -no desync- (5) (6) (7) (8) |--E7--| |--E7--||--E7--| |--E7--| 1110011100111001111110011101110011100111 XXXXX|--E7--| |--FC--||--EE--| |--E7--| -after desync- After the desynchronization, the stream of bits is interpreted as a completely different nibble sequence. Also note that some of those "extra" bits are no longer being ignored; now they're being interpreted as data, as part of the nibbles that are being returned to the higher level code. Meanwhile, other bits that were part of the $E7 nibbles are now being swallowed. And look, there's an $EE nibble. Now, let's go back to the first stream, which had no extra "0" bits between the nibbles, and see what happens when we desynchronize: -no desync- (5) (6) (7) (8) (9) |--E7--||--E7--||--E7--||--E7--||--E7--| 1110011111100111111001111110011111100111 XXXXX|--FC--||--FC--||--FC--||--FC--| -after desync- After the desynchronization, the stream of bits is interpreted as a completely different nibble sequence, but it's a different different sequence: endless $FC nibbles, not $E7 $FC $EE $E7. And no $EE nibble. Here's the kicker: generic bit copiers didn't preserve these extra "0" bits between nibbles. Even top-of-the-line bit copiers couldn't reliably detect the difference between 1 timing bit and 2 timing bits. By "desynchronizing" this specially constructed bitstream at just the right time, then interpreting the out-of-phase bits as nibbles, a developer could tell if your disk was original. Here is the rest of the E7 bitstream, picking up the diagram at (8): -no desync- (8) (9) (10) (11) ( |--E7--| |--E7--||--E7--| |--E7--| |-- 1110011100111001111110011101110011100111 |--E7--| |--FC--||--EE--| |--E7--| -after desync- / -no desync- 12) (13) (14) (15) (16) E7--||--E7--| |--E7--| |--E7--||--E7--| 001111110011101110011101110011111100111 |--FC--||--EE--| |--EE--| |--FC--| -after desync- ~ Chapter 5 It's Not Just A Phase, Mom, This Is Who I Am This protection scheme hinges on the assumption that only an original disk will present the proper sequence of nibbles after the code intentionally "desynchronizes" mid-stream. This seems like an entirely reasonable assumption. After all, even the best bit copiers could not preserve the exact number of timing bits after every single nibble. However, that assumption rests on a deeper assumption. Once it desyncs, it assumes that the nibbles after $EE ($E7 $FC $EE $E7 $FC $EE $EE $FC) are dependent on the extra bits that were originally between the $E7 nibbles. In other words, it assumes that once it gets out of sync, it stays out of sync. But what if that weren't true? What if we could resynchronize the bitstream to the original nibble boundary -- after the code reset the disk subsystem to get out of sync? Imagine a sequence of nibbles which, when read by this code, would swallow several "0" bits and get back in sync with the original nibble boundary. This would need to happen before the code found the $EE nibble, because immediately after that, it starts reading additional nibbles and checking them against a hard-coded array. But there is a small window there, after we desynchronize but before we find the $EE nibble. Here is the relevant code: ; desynchronize 006A:BD 8D C0 LDA $C08D,X 006D:A0 10 LDY #$10 006F:24 06 BIT $06 ; now start looking for an $EE nibble 0071:BD 8C C0 LDA $C08C,X 0074:10 FB BPL $0071 0076:88 DEY 0077:F0 14 BEQ $008D ; fail 0079:C9 EE CMP #$EE 007B:D0 F4 BNE $0071 The Y register gets reset to $10 (at $4F6D) and is decremented while we're looking for the $EE nibble. That means there can be up to 15 nibbles after the desync and before the $EE (the original disk has 2), and execution will still continue without branching to the failure path. Now watch this: -no desync- (4) |--E7--||--CF--||--F3--||--FC--||--EE--| 1110011111001111111100111111110011101110 XXXXXXXXXXXXX|--FE--| |--FF--| |--EE--| -after desync- If we put this bitstream immediately after nibbles (1) (2) (3), the desync will still skip 13 bits -- labeled here with Xs -- then the loop at $4F71 that's looking for an $EE nibble will find $FE, then $FF, then... $EE! Hooray! But look what happened in the meantime: we've resynchronized the bitstream to the original nibble boundary. By putting "0" bits in specific places within the synchronized nibbles ($F3 and $FC), we end up with those "0" bits between nibbles in the desynchronized stream. In just 24 bits, we've resynchronized the bitstream and fooled the protection check into reading regular nibbles as if they were desynchronized nibbles. Putting the nibble sequence $CF $F3 $FC after (4) on our non-working copy, then adding the magic 8-nibble sequence ($E7 $FC $EE $E7 $FC $EE $EE $FC), the protection check will pass despite not being run on an original disk. We don't need to bypass the protection code at all. The code can run as usual. Let it run. Let it search for nibbles. Let it desynchronize. Let it verify the magic nibble sequence. It'll all pass. We've defeated the E7 protection check with no code changes. ~ Chapter 5.5 In Which We Return From The Future To School Our Former Self From early 2016 (when I first wrote this) to late 2021 (as I write now), my no-code E7 patch did not look like the solution I just described. At the risk of muddying the waters of this specific crack, I want to show the old solution and explain why it seemed to work but was ultimately flawed. My previous desync diagram looked like this: -no desync- (4) |--E7--| |--E7--| |--E7--||--E7--| 11100111011100111001110011111100111 XXX |--EE--| |--E7--| |--FC--| -after desync- This is wrong in several ways. First, it shows incorrectly that the desync after the (1) (2) (3) $E7 nibbles only misses 3 bits, so the desynchronized nibbles start within (4). In reality, it misses 13 bits. Second, it shows incorrectly that the first desynchronized nibble is $EE. In reality, an original disk will find $E7, then $FC, then $EE. These misconceptions led me to believe incorrect things about my solution, which I diagrammed like this: -no desync- (3) |--E7--||--EF--||--F3--||--FC--||--EE--| 1110011111101111111100111111110011101110 XXX |--FF--| |--FF--| |--EE--| -after desync- Thinking that desync only misses 3 bits (it's actually 13) led me to start the resynchronization process too soon. I added $EF $F3 $FC immediately after the third $E7, under the incorrect belief that, after the desync, the code would find both $FF nibbles and the resync'd $EE. In reality, here's what happens: -no desync- (3) |--E7--||--EF--||--F3--||--FC--||--EE--| 1110011111101111111100111111110011101110 XXXXXXXXXXXXX |--FF--| |--EE--| -after desync- My new $EF nibble has no effect on the outcome whatsoever! Reading actually restarts after all the Xs, swallows a "0" bit right away, reads one (not two) $FF nibble, swallows two more "0" bits, then finds the resynchronized $EE nibble. I misunderstood almost everything and still managed to succeed. Except I didn't, quite. On later Apple II models, the entire Disk II subsystem was replaced with IWM, which stands for "Integrated Woz Machine." The IWM was essentially an abstraction layer that simulated the Disk II controller's behavior. It did this extremely well, except for one problem: after this one weird trick to desynchronize the read, it resets itself too quickly and starts reading bits again too soon. What does that look like? On a IIgs (or a //c, or even a //c+ if you can manage to find one), it looks like this: -no desync- (3) |--E7--||--EF--||--F3--||--FC--||--EE--| 1110011111101111111100111111110011101110 XXXXXXXXXXX|--9F--||--E7--| |--E -after desync- Oh no! After the IWM desyncs, it only misses 11 bits (not 13), so it starts reading on the fourth bit of my $F3 nibble. That's a "1" bit, so the data latch immediately starts accumulating bits for a nibble value, and it ends up filling with $9F, then $E7, then... And then it doesn't matter, because we've already missed our resynchronized $EE nibble, and we're still out of phase. We've missed the window of opportunity to present the magic nibble sequence to the protection code. Which is ironic, since we created that window for ourselves. (As it happens, we will resynchronize on a later nibble. We'll even find an $EE nibble before the Death Counter counts down, but it's the wrong $EE -- it's one of the ones in the magic 8- nibble sequence. So the nibbles after it are also the wrong nibbles, and the protection fails.) The next obvious question is, if the IWM and Disk II controller are so different, how does the real protection check work at all? Let's take another look at the real E7 bitstream: -no desync- (3) (4) (5) (6) (7 |--E7--||--E7--||--E7--| |--E7--||--E7 111001111110011111100111001110011111100 XXXXXXXXXXX |--E7--| |--FC--| -after desync- After the IWM misses 11 bits, the next two bits are both "0" bits. Those both get swallowed, and the desynchronized nibbles that the IWM presents are $E7, then $FC, then $EE -- just like the original Disk II controller. Finally, let's take another look at my new no-code E7 patch: -no desync- (4) |--E7--||--CF--||--F3--||--FC--||--EE--| 1110011111001111111100111111110011101110 XXXXXXXXXXX |--FF--| |--FF--| |--EE--| -after desync- After the IWM misses 11 bits, it hits a "0" bit, swallows it, then finds two $FF nibbles before resynchronizing on the correct $EE nibble. What follows will be the expected 8-nibble sequence, and the protection check will pass. ~ Chapter 6 From Nibbles To Bytes And Back The E7 bitstream is part of the data field of a real sector on disk. On this disk, it's on T22,S0F. (We saw the code seek to track $22 at $4F10 and seek to sector $0F at $4F31.) Here's what it looks like in a nibble editor: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 22 START: 1800 LENGTH: 3DFF 1AF0: FF FF FF FF FF D5 AA 96 VIEW ^^^^^^^^ address prologue 1AF8: FF FE BB AA AF AF EB FB ^^^^^ ^^^^^ ^^^^^ ^^^^^ V=254 T=$22 S=$0F chksm 1B00: FF FF EB 9B FC FF FF FF ^^^^^^^^ address epilogue 1B08: FF FF FF FF FF FF FF D5 ^^ 1B10: AA AD 96 96 96 96 96 96 ^^^^^ data prologue 1B18: 96 96 96 96 96 96 96 96 . . lots and lots of $96 nibbles . 1C00: 96 96 96 96 96 96 96 96 1C08: E7 E7 E7 E7 E7 E7 E7 E7 ; timing 1C10: E7 E7 E7 E7 E7 E7 E7 E7 ; bits 1C18: E7 E7 E7 E7 E7 E7 E7 E7 ; are in 1C20: E7 E7 E7 E7 E7 E7 E7 E7 ; here 1C28: E7 E7 E7 E7 E7 E7 E7 E7 1C30: E7 E7 E7 E7 E7 E7 E7 E7 1C38: E7 E7 E7 E7 E7 E7 E7 E7 1C40: E7 E7 E7 E7 E7 E7 E7 E7 1C48: E7 E7 E7 E7 E7 E7 E7 E7 1C50: E7 E7 E7 E7 E7 E7 E7 E7 1C58: E7 E7 E7 E7 E7 E7 E7 E7 1C60: E7 E7 E7 E7 E7 E7 E7 E7 1C68: 96 FF FF EB F7 F9 FE FF ^^^^^^^^ data epilogue --^-- Remember how the protection check looks for a $D5 nibble (starting at $4F3C)? The one it finds is the first nibble of the data prologue, shown here at offset $1B0F. Remember how the protection check gives itself a $100 nibble window to find the first $E7 nibble after it finds a $D5? It's skipping over most of the data field -- lots and lots of $96 nibbles. This is a real sector. I mean, the code never reads T22,S0F as sector data, but you can see it in your favorite sector editor if you want. It looks like this: --v-- -------------- DISK EDIT -------------- TRACK $22/SECTOR $0F/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 00 00 00 00 00 00 ........ . . lots and lots of $00 bytes . $98: 00 00 00 00 00 00 00 00 ........ $A0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $A8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $B0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $B8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $C0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $C8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $D0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $D8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $E0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $E8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $F0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $F8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. --^-- Those $00 bytes in memory are written to disk as $96 nibbles. No magic here, just the standard 6-and-2 encoding that DOS 3.3 uses to convert bytes in memory to nibbles on disk. Furthermore, that repeated pattern of "AC 00", starting at offset $A0, are the $E7 nibbles at the end of the data field (shown above in the Copy ][+ nibble ditor at offset $1C08). My non-working copy looks identical to this, except it lacks the timing bits between the $E7 nibbles, so the protection check fails. But let's make a 12-byte patch: T22,S0F,$A4 - "00 AC 00 AC 00 AC 00 AC 00 AC 00 AC" + "78 A8 58 9C 30 C0 04 A8 58 9C 58 A8" Now the sector looks like this: --v-- ------------- DISK EDIT --------------- TRACK $22/SECTOR $0F/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 00 00 00 00 00 00 ........ . . [unchanged] . $98: 00 00 00 00 00 00 00 00 ........ $A0: AC 00 AC 00 78 A8 58 9C ,@,@8(X. $A8: 30 C0 04 A8 58 9C 58 A8 0@D(X.X( $B0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. . . [unchanged] . $F8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. --^-- Moving back to the Copy ][+ nibble editor, the sector looks like this: --v-- TRACK: 22 START: 1800 LENGTH: 3DFF 1AF0: FF FF FF FF FF D5 AA 96 VIEW 1AF8: FF FE BB AA AF AF EB FB 1B00: FF FF EB 9B FC FF FF FF 1B08: FF FF FF FF FF FF FF D5 1B10: AA AD 96 96 96 96 96 96 . . [unchanged] . 1C00: 96 96 96 96 96 96 96 96 1C08: E7 E7 E7 E7 CF F3 FC EE ; no 1C10: E7 FC EE E7 FC EE EE FC ; timing 1C18: 97 E7 E7 E7 E7 E7 E7 E7 ; bits 1C20: E7 E7 E7 E7 E7 E7 E7 E7 ; here! . . [unchanged] . 1C68: 96 FF FF EB F7 F9 FE FF --^-- When the protection check looks for a $D5 nibble, it will find it. (It's the first nibble of the data prologue; that hasn't changed.) When the protection check looks for an $E7 nibble, it will find it. (It's at offset $1C08, after $F6 other nibbles that are all $96. That hasn't changed either.) When it desynchronizes and looks for an $EE nibble, it will find it (after it skips over two desynchronized nibbles) at offset $1C0F. At this point, we've resynchronized the bitstream to the original nibble boundary, but the protection code doesn't know that. The next 8 nibbles on disk after $EE (starting at offset $1C10) are $E7 $FC $EE $E7 $FC $EE $EE $FC. That's the magic sequence that the protection check looks for. It thinks it's reading out-of-phase nibbles from an original disk, but it's really reading in-sync nibbles from a specially crafted copy. ]PR#6 ...works, and it is glorious... Quod erat liberandum. ~ Acknowledgements Thanks to qkumba for doing the original research on this approach, which you can read here: https://www.alchemistowl.org/pocorgtfo/ pocorgtfo11.pdf Thanks to Nick B. for his bug report that ultimately led me to realize that my 2016 solution was incomplete. Thanks to John M., Josh M., and several other people in the Apple II community for testing my 2021 solution on real hardware. ~ Changelog 2021-09-06 - updated with new E7 patch that improves compatibility on Apple //c and IIgs - fixed explanation of desync behavior on both Disk II and IWM - added diagrams showing how the old patch failed and why the new patch succeeds 2016-04-12 - fixed date on the previous update, because I am not a time traveler (thanks Ange) 2016-04-05 - typos (thanks Ange) 2016-04-01 - initial release --------------------------------------- A 4am crack No. 655 ------------------EOF------------------