-------------Five in a Row------------- --------Addition and Subtraction------- A 4am crack 2016-04-07 --------------------------------------- Name: Five in a Row: Addition and Subtraction Version: 3.0 Genre: educational Year: 1992 Credits: by Dr. Jerzy T. Cwirko-Godyki program by Eric Thornton Publisher: Critical Thinking Press Media: double-sided 5.25-inch floppy OS: ProDOS 1.0.1 Previous cracks: none ~ Chapter 0 In Which Automated Tools Fail In Interesting Ways COPYA no read errors on either side, but copy loads ProDOS then reboots with the drive motor still on Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor nothing suspicious Disk Fixer T00 -> looks like standard ProDOS (bootloader and disk catalog) Why didn't any of my copies work? Probably a runtime protection check in the startup program. (Disks do not spontaneously reboot unless someone tells them to.) Next steps: 1. Trace the startup program 2. Find the protection check and disable it 3. Declare victory(*) (*) take a nap ~ Chapter 1 In Which We Run Into An Old Friend And Begin To Question Our Taste In Friends [S7,D1=my ProDOS hard drive] [S6,D1=original disk (side A)] ]PR#7 ... ]CAT,S6,D1 /FIVE NAME TYPE BLOCKS MODIFIED BASIC.SYSTEM SYS 21 18-JUN-84 SCREEN.IMG BIN 17 1-JUN-89 RND BIN 1 11-JUL-89 ROLL BIN 21 27-JAN-92 WINNER BIN 19 30-AUG-89 *PRODOS SYS 31 1-JAN-84 MOD2 BIN 42 28-NOV-89 STARTUP BIN 6 30-JUN-92 O9733.IMG TXT 8 24-JUN-92 INITGAME BIN 33 MOD1 BIN 44 30-JUN-92 SIDE BIN 1 30-JUN-92 *,,,,,, $AC 44032 BLOCKS FREE: 29 BLOCKS USED: 251 Weird. It claims to load BASIC.SYSTEM, but the directory does not list any STARTUP.BAS file. It lists STARTUP.BIN, though, and the extended catalog says that file starts at $0801, so... I guess I'll start there? ]PREFIX /FIVE ]BLOAD STARTUP ]CALL -151 *801L ; save some zero page locations 0801- A5 F0 LDA $F0 0803- 8D F4 08 STA $08F4 0806- A5 F1 LDA $F1 0808- 8D F5 08 STA $08F5 ; get current slot (x16) 080B- AD 30 BF LDA $BF30 080E- 8D 2B 08 STA $082B ; raw block read (MLI command $80) 0811- 20 00 BF JSR $BF00 0814- [80 2A 08] ; It doesn't check the error code, so I ; guess we only cared about moving the ; drive head to a specific track. Now ; turn on drive motor manually (always ; suspicious). 0817- AE 30 BF LDX $BF30 081A- BD 89 C0 LDA $C089,X ; looks like a Death Counter 081D- A9 0B LDA #$0B 081F- 8D F3 08 STA $08F3 0822- D0 1F BNE $0843 ; Once Death Counter hits 0, we fall ; through to here, which resets the ; computer. (It never turns off the ; drive motor.) 0824- EE F4 03 INC $03F4 0827- 6C FC FF JMP ($FFFC) This explains the behavior I saw on my non-working copy. ; MLI parameter table for block read ; (read block $0001 from ; into $601C) 082A- [03 60 1C 60 01 00] *843L 0843- CE F3 08 DEC $08F3 0846- F0 DC BEQ $0824 0848- A9 21 LDA #$21 084A- 85 F0 STA $F0 084C- C6 F0 DEC $F0 084E- F0 F3 BEQ $0843 ; find next address field (not shown) 0850- 20 AB 08 JSR $08AB 0853- B0 EE BCS $0843 ; check physical sector (from address ; field) 0855- A5 2D LDA $2D ; if not $07, try again 0857- C9 07 CMP #$07 0859- D0 F1 BNE $084C 085B- A0 28 LDY #$28 085D- A9 D5 LDA #$D5 085F- 20 33 08 JSR $0833 *833L ; find nibble A (=$D5) within the next ; Y nibbles (=$28) 0833- 85 F1 STA $F1 0835- BD 8C C0 LDA $C08C,X 0838- 10 FB BPL $0835 083A- C5 F1 CMP $F1 083C- F0 F2 BEQ $0830 ; RTS 083E- 88 DEY 083F- D0 F4 BNE $0835 0841- 68 PLA 0842- 68 PLA Then it falls through to $0843, the same entry point we called in the first place. Continuing from $0862... 0862- A0 FF LDY #$FF 0864- 20 31 08 JSR $0831 *831L 0831- A9 E7 LDA #$E7 (Then it falls through to $0833, which looks for a nibble in the time allotted by the Y register, returning gracefully if it finds the nibble or decrementing decrements the counter and starting over from $0843. So this call looks for an $E7 nibble within the next $FF nibbles.) Continuing from $0867... ; find another $E7 nibble within the ; next 2 nibbles 0867- A0 02 LDY #$02 0869- 20 31 08 JSR $0831 ; find another $E7 nibble immediately 086C- BD 8C C0 LDA $C08C,X 086F- 10 FB BPL $086C 0871- C9 E7 CMP #$E7 0873- D0 CE BNE $0843 ; reset data latch 0875- BD 8D C0 LDA $C08D,X ; waste some time 0878- A0 10 LDY #$10 087A- 24 80 BIT $80 ; find an $EE nibble within the next ; $10 nibbles (Y reset to $10 at $0878) 087C- A9 EE LDA #$EE 087E- 20 33 08 JSR $0833 ; store the next 8 nibbles in memory ; (backwards) 0881- A0 07 LDY #$07 0883- BD 8C C0 LDA $C08C,X 0886- 10 FB BPL $0883 0888- 99 F9 09 STA $09F9,Y 088B- 88 DEY 088C- 10 F5 BPL $0883 ; turn off drive motor 088E- BD 88 C0 LDA $C088,X ; copy some code to higher memory 0891- A0 1B LDY #$1B 0893- B9 F6 08 LDA $08F6,Y 0896- 99 00 60 STA $6000,Y 0899- 88 DEY 089A- 10 F7 BPL $0893 089C- AD F4 08 LDA $08F4 089F- 85 F0 STA $F0 08A1- AD F5 08 LDA $08F5 08A4- 85 F1 STA $F1 08A6- A0 00 LDY #$00 ; and continue there 08A8- 4C 00 60 JMP $6000 *8A8:60 *891G *6000L ; use the 8 nibbles we stored earlier ; as a 64-bit decryption key for the ; rest of this file 6000- 98 TYA 6001- 29 07 AND #$07 6003- AA TAX 6004- 98 TYA 6005- 5D F9 09 EOR $09F9,X 6008- 48 PHA 6009- 59 1C 60 EOR $601C,Y 600C- 99 01 08 STA $0801,Y 600F- 68 PLA 6010- 59 1C 61 EOR $611C,Y 6013- 99 01 09 STA $0901,Y 6016- C8 INY 6017- D0 E7 BNE $6000 ; and continue with decrypted code 6019- 4C 01 08 JMP $0801 Someone spent a bit of time refactoring this, but the logic remains the same: this checks for a series of $E7 nibbles before intentionally desynchronizing and looking for nibbles "out of phase." The loop at $0881 stores the decryption key at $09F9, which is part of this program space. That means I could hard- code the correct values in the program itself and skip to the decryption loop at $0891. But that's not what we're going to do. We're going to do one better. ~ Chapter 2 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.) The standard "wait for data latch to have its high bit set" loop, which you see over and over in any RWTS code, "swallows" these extra "0" bits between the nibbles: :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 most significant bit to go high again. So they get swallowed, ignored, like they were never there. But what if we miss some 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 miss bits as the disk spins. (This is why the timing of low-level RWTS code is so critical.) Let's say we waste 12 CPU cycles before we start reading this bitstream. Each bit takes 4 CPU cycles to go by, so after 12 cycles, we would have missed the first 3 bits (marked with an X). (normal start) |--E7--| |--E7--| |--E7--||--E7--| 11100111011100111001110011111100111 XXX |--EE--| |--E7--| |--FC--| (delayed start) Ah! It's interpreted as a different nibble sequence if you delay even a handful of CPU cycles before you start reading. Also, some of those "extra" bits are no longer ignored; now they're interpreted as data, as part of the nibbles returned to the higher level code. Meanwhile, other bits that were part of the $E7 nibbles get "swallowed" instead. Now, let's go back to the first stream, which had no extra bits between the nibbles, and see what happens when we waste those same 12 CPU cycles. (normal start) |--E7--||--E7--||--E7--||--E7--| 11100111111001111110011111100111 XXX |--FC--||--FC--||--FC--| (delayed start) After skipping the first three bits, the RWTS interprets the bitstream as $FC $FC $FC, repeating endlessly -- not $EE $E7 $FC like the other stream. 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" (wasting just the right number of CPU cycles at just the right time), then interpreting the bits on the disk in mid-stream, developers could determine at runtime whether you had an original disk. Here is the complete "E7 bitstream," annotated to show both the synchronized and desynchronized nibble sequences. |--E7--| |--E7--| |--E7--||--E7--| 111001110111001110011100111111001110 XXX |--EE--| |--E7--| |--FC--||--E |--E7--| |--E7--||--E7--| |--E7--| 111001110011100111111001110111001110 E--| |--E7--| |--FC--||--EE--| |--E |--E7--||--E7--| 1110011111100111 E--| |--FC--| ~ Chapter 3 It's Just A Phase You're Going Through 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 burns 12 CPU cycles (skipping 3 bits and getting out of sync with the original nibble boundary), it assumes that the next nibbles it reads (EE E7 FC EE E7 FC EE EE FC) are dependent on the timing 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 burned time intentionally to get out of sync? Imagine a sequence of nibbles which, when read by this code, would swallow an additional 5 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. This is the relevant code: 0878- A0 10 LDY #$10 087A- 24 80 BIT $80 087C- A9 EE LDA #$EE 087E- 20 33 08 JSR $0833 The Y register gets reset to $10 (at $0878) then decremented while we're looking for the $EE nibble. That means we can skip up to 15 nibbles between the third $E7 and $EE, and execution will still continue without branching to the failure path. Now watch this: (normal start) |--EF--||--F3--||--FC--||--EE--| 11101111111100111111110011101110 XXX |--FF--| |--FF--| |--EE--| (delayed start) If we put this bitstream immediately after the initial $E7 $E7 $E7 sequence, the call at $087E that's looking for the desynchronized $EE nibble will instead find two desynchronized $FF nibbles, skip over them (decrementing the Y register, but not all the way to zero), then finally find the $EE nibble and be happy. But look what happened in the meantime: we've resynchronized the bitstream to the original nibble boundary! By putting "0" bits before and after each desynchronized $FF nibbles, we've essentially "swallowed" the extra bits and shifted the phase from +3 to +8. Since nibbles are 8-bit values, a phase of +8 is the same as 0. In just 24 bits, we've resynchronized the bitstream and fooled the protection check into reading regular nibbles as if they're desynchronized nibbles. If we put this nibble sequence in the right place on our non-working copy, 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 decrypt the code with magic nibble stream. It'll all work. We've defeated the E7 E7 E7 protection check with no code changes. ~ Chapter 4 In Which We Remove All Traces Of Copy Protection Using An Automated Tool That I Wrote For Just Such An Occasion [S6,D1=demuffin'd copy] [S5,D1=my work disk] ]PR#5 ... ]BRUN PDP T00,S04,$A3 change 00AC00AC00AC00AC00AC00AC to 64B444802CDC18B4448044B4 Wait, what? ~ Chapter 5 Here's What The E7 bitstream is part of the data field of a real sector on disk. On this disk, it's on T00,S04. Here's what it looks like in the Copy ][+ nibble editor: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. ---------------------------------------- TRACK: 00 START: 1800 LENGTH: 3DFF 2BA8: FF FF FF D5 AA 96 FF FE ^^^^^^^^ ^^^^^ address prologue V=254 2BB0: AA AA AB AF FE FB DE AA ^^^^^ ^^^^^ ^^^^^ ^^^^^ T=$00 S=$04 chksm epilogue 2BB8: EB FF FE FF FF FF FF FF 2BC0: FF FF FF FF FF D5 AA AD ^^^^^^^^ data prologue 2BC8: 96 96 96 96 96 96 96 96 2BD0: 96 96 96 96 96 96 96 96 2BD8: 96 96 96 96 96 96 96 96 2BE0: 96 96 96 96 96 96 96 96 2BE8: 96 96 96 96 96 96 96 96 2BF0: 96 96 96 96 96 96 96 96 2BF8: 96 96 96 96 96 96 96 96 2C00: 96 96 96 96 96 96 96 96 2C08: 96 96 96 96 96 96 96 96 2C10: 96 96 96 96 96 96 96 96 2C18: 96 96 96 96 96 96 96 96 2C20: 96 96 96 96 96 96 96 96 2C28: 96 96 96 96 96 96 96 96 2C30: 96 96 96 96 96 96 96 96 2C38: 96 96 96 96 96 96 96 96 2C40: 96 96 96 96 96 96 96 96 2C48: 96 96 96 96 96 96 96 96 2C50: 96 96 96 96 96 96 96 96 2C58: 96 96 96 96 96 96 96 96 2C60: 96 96 96 96 96 96 96 96 2C68: 96 96 96 96 96 96 96 96 2C70: 96 96 96 96 96 96 96 96 2C78: 96 96 96 96 96 96 96 96 2C80: 96 96 96 96 96 96 96 96 2C88: 96 96 96 96 96 96 96 96 2C90: 96 96 96 96 96 96 96 96 2C98: 96 96 96 96 96 96 96 96 2CA0: 96 96 96 96 96 96 96 96 2CA8: 96 96 96 96 96 96 96 96 2CB0: 96 96 96 96 96 96 96 96 2CB8: 96 96 96 96 96 96 E7 E7 ; timing 2CC0: E7 E7 E7 E7 E7 E7 E7 E7 ; bits 2CC8: E7 E7 E7 E7 E7 E7 E7 E7 ; are in 2CD0: E7 E7 E7 E7 E7 E7 E7 E7 ; here 2CD8: E7 E7 E7 E7 E7 E7 E7 E7 2CE0: E7 E7 E7 E7 E7 E7 E7 E7 2CE8: E7 E7 E7 E7 E7 E7 E7 E7 2CF0: E7 E7 E7 E7 E7 E7 E7 E7 2CF8: E7 E7 E7 E7 E7 E7 E7 E7 2D00: E7 E7 E7 E7 E7 E7 E7 E7 2D08: E7 E7 E7 E7 E7 E7 E7 E7 2D10: E7 E7 E7 E7 E7 E7 E7 E7 2D18: E7 E7 E7 E7 E7 E7 96 DE ^^ 2D20: AA EB 9F F3 FC FF FF FF ^^^^^ data epilogue --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- Remember how the protection check looks for a $D5 nibble (the JSR at $085D)? The one it finds is the first nibble of the data prologue, shown here at offset $2BC5. 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 -- a bunch of $96 nibbles. This is a real sector. The protection check never reads T00,S04 as sector data, but you can see it in your favorite sector editor if you want. It looks like this: --v-- -------------- DISK EDIT -------------- TRACK $00/SECTOR $04/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 00 00 00 00 00 00 ........ $08: 00 00 00 00 00 00 00 00 ........ $10: 00 00 00 00 00 00 00 00 ........ $18: 00 00 00 00 00 00 00 00 ........ $20: 00 00 00 00 00 00 00 00 ........ $28: 00 00 00 00 00 00 00 00 ........ $30: 00 00 00 00 00 00 00 00 ........ $38: 00 00 00 00 00 00 00 00 ........ $40: 00 00 00 00 00 00 00 00 ........ $48: 00 00 00 00 00 00 00 00 ........ $50: 00 00 00 00 00 00 00 00 ........ $58: 00 00 00 00 00 00 00 00 ........ $60: 00 00 00 00 00 00 00 00 ........ $68: 00 00 00 00 00 00 00 00 ........ $70: 00 00 00 00 00 00 00 00 ........ $78: 00 00 00 00 00 00 00 00 ........ $80: 00 00 00 00 00 00 00 00 ........ $88: 00 00 00 00 00 00 00 00 ........ $90: 00 00 00 00 00 00 00 00 ........ $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 ,.,.,.,. --------------------------------------- BUFFER 0/SLOT 6/DRIVE 1/MASK OFF/NORMAL --------------------------------------- COMMAND : _ --^-- Those $00 bytes in memory get written to disk as $96 nibbles. No magic there, 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 $2CB6). Fun(*) fact: T00,S04 is part of the ProDOS disk catalog, which is why the catalog listing had a line of "garbage" commas at the end of it. (*) not guaranteed, actual fun may vary My non-working copy looks identical, except it lacks the timing bits between the $E7 nibbles, so the protection check fails. But after the 12-byte patch that my Post-Demuffin Patcher script made... T00,S04,$A3 change 00AC00AC00AC00AC00AC00AC to 64B444802CDC18B4448044B4 ...the sector looks like this: --v-- ------------- DISK EDIT --------------- TRACK $00/SECTOR $04/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 64 B4 44 80 2C ,.,d4D., $A8: DC 18 B4 44 80 44 B4 00 \.4D.D4. $B0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. . . [unchanged] . $F8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. --------------------------------------- BUFFER 0/SLOT 6/DRIVE 1/MASK OFF/NORMAL --------------------------------------- COMMAND : _ --^-- Moving back to the Copy ][+ nibble editor, the sector looks like this: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 00 START: 1800 LENGTH: 3DFF 2BA8: FF FF FF D5 AA 96 FF FE 2BB0: AA AA AB AF FE FB DE AA 2BB8: EB FF FE FF FF FF FF FF 2BC0: FF FF FF FF FF D5 AA AD 2BC8: 96 96 96 96 96 96 96 96 . . [unchanged] . 2CB8: 96 96 96 96 96 96 E7 E7 ; no 2CC0: E7 EF F3 FC EE E7 FC EE ; timing 2CC8: E7 FC EE EE FC EA E7 E7 ; bits! . . [unchanged] . --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- 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 $2CB6, 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 $FF nibbles) at offset $2CC4. 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 $2CC5) are "E7 FC EE E7 FC EE EE FC". That's the magic nibble sequence that the protection check looks for. That's the decryption key. The code 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. Not only have we defeated the E7 E7 E7 protection check with no code changes, we've automated the entire process. The only change was that 12-byte patch, and Post-Demuffin Patcher found and applied that patch with no human intervention. It took all of 2 seconds. The rest of this write-up was just to explain what had already happened. It's like I don't even need to be here. ]PR#6 ...works, and it is glorious... Quod erat liberandum. ~ Acknowledgements Thanks to qkumba for doing the heavy lifting here. This entire technique was his idea, and he spent a lot of time getting it to work in modern emulators (with their, um, "special" ways of reading from emulated disk images). I spent a small amount of time minimizing the byte patch. The final result may look simple, but there were a lot of almost-but-not-working solutions along the way. Read all about it here: https://www.alchemistowl.org/pocorgtfo/ pocorgtfo11.pdf Quod erat liberandum. --------------------------------------- A 4am crack No. 657 ------------------EOF------------------