---------------Wiziprint--------------- A 4am crack 2020-01-02 --------------------------------------- Name: Wiziprint Version: 2.01 (20-AUG-83 according to file metadata) Genre: game utility Year: 1982 Credits: Andrew Greenberg, Robert Woodhead Publisher: Sir-Tech Software Platform: Apple ][+ or later Media: 5.25-inch disk Sides: 1 OS: Apple Pascal Previous cracks: none ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA fails on last pass Locksmith Fast Disk Backup copies almost the entire disk except a few curious sectors on track $22 --v-- LOCKSMITH 7.0 FAST DISK BACKUP R..................................* W HEX 00000000000000001111111111111111222 TRK 0123456789ABCDEF0123456789ABCDEF012 0..................................A 1..................................D 2................................... 3................................... 4................................... 5................................... 6................................... 7................................... 8..................................A 9................................... A................................... B................................... C................................... D................................... 12 E................................... F..................................D --^-- This is especially curious because two of them failed differently than the other two. "A" means that Locksmith can not find or read the address field. "D" means that it can not find or read the data field. So there's definitely something funky going on on track $22. Anyway, the copy boots to the title screen then freezes. EDD 4 bit copy (no sync, no count) no errors, but same behavior as the Locksmith FDB copy: it boots to the title screen then freezes. Copy ][+ nibble editor looking through track $22, obviously, and I find a curious sector-but-not- a-sector which might explain the status result I saw on Locksmith FDB. I've replaced the inverse characters, which represent timing bits, with a "+" character for the purposes of plain text. --v-- TRACK: 22 START: 1E1A LENGTH: 180F 3220: D5 AA 96 AA AA BB AA AE VIEW 3228: AF BF AF DE AA EA+FF+FF+ 3230: FE FF+FF+FF+FF+D5 AA AD 3238: 96 FF+FF+FF+FF+FF+FF+FF+ 3240: FF+FF+FF+FF+FF+FF+FF+FF+ <-3240 3248: FF+FF+FF+FF+FF+FF+FF+FF+ 3250: FF+FF+FF+FF+FF+FF+FF+FF+ 3258: FF+FF+FF+FF+FF+FF+FF+FF+ FIND: 3260: FF+FF+FF+FF+FF+FF+FF+FF+ D5 AA 96 --^-- It goes on like that for a while. Disk Fixer bootloader is Pascal, which is also obvious from watching the disk boot (Apple Pascal has a distinctive look and sound), but no help on track $22 Why didn't COPYA work? non-sectors on track $22 Why didn't Locksmith FDB work? likewise, and there must be a run- time check that is looking at that bitstream inside the sector-that- isn't-a-sector on track $22 Why didn't my EDD copy work? likewise Next steps: 1. Find the protection check, which I really hope is implemented in assembly language and not Pascal's weird interpreted p-code 2. Disable protection check 3. Declare victory (*) (*) go to the gym ~ Chapter 1 Unlucky In Cards Since my copy goes down a different code path than the original, 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 ------------- $02/$05-$0F $10/$0E-$0F $15/$0B-$39 $18/$0B-$39 --^-- The first two matches are legitimate parts of the Pascal operating system. The other two sectors are identical, which is somewhat concerning, but we'll jump off that bridge when we come to it. T15,S0B ----------- DISASSEMBLY MODE ---------- 002A:68 PLA 002B:85 00 STA $00 002D:68 PLA 002E:85 01 STA $01 0030:68 PLA 0031:68 PLA 0032:68 PLA 0033:68 PLA 0034:A9 00 LDA #$00 0036:48 PHA ; hard-coded to slot 6 0037:A2 60 LDX #$60 ; turn on the drive motor 0039:BD 89 C0 LDA $C089,X ; and now this 003C:20 2D 00 JSR $002D 003F:CE FB 00 DEC $00FB 0042:D0 F8 BNE $003C The main program is written in Pascal, and this assembly language module is linked into the program by the linker. In linked modules, JSR targets are stored as two-byte addresses relative to the start of the module. At runtime, the Pascal interpreter decides where the linked module is going to be loaded and modifies the JSR operands to real addresses. (Other address, like the one in the "DEC $00FB", are also adjusted.) Which is kind of neat for an 8-bit virtual machine. Since this module started at byte $2A in this sector, and $2A + $2D = $57, we will find this subroutine at byte $57 in the same sector. However, it occurs to me that it will be much easier to follow this write-up if I shift everything in the sector editor by $2A bytes, so that the JSR addresses line up with the byte offsets and we can pretend that the relative JSR "addresses" are actual addresses. Thus... T15,S0B, shifted $2A bytes to the left ----------- DISASSEMBLY MODE ---------- 0000:68 PLA 0001:85 00 STA $00 0003:68 PLA 0004:85 01 STA $01 0006:68 PLA 0007:68 PLA 0008:68 PLA 0009:68 PLA 000A:A9 00 LDA #$00 000C:48 PHA 000D:A2 60 LDX #$60 000F:BD 89 C0 LDA $C089,X 0012:20 2D 00 JSR $002D 0015:CE FB 00 DEC $00FB 0018:D0 F8 BNE $0012 Now then, what's at $002D? ~ Chapter 2 Proving Ground ; reset the data latch and match the ; standard $D5 $AA $96 address field ; prologue 002D:BD 8E C0 LDA $C08E,X 0030:BD 8C C0 LDA $C08C,X 0033:10 FB BPL $0030 0035:C9 D5 CMP #$D5 0037:D0 F7 BNE $0030 0039:BD 8C C0 LDA $C08C,X 003C:10 FB BPL $0039 003E:C9 AA CMP #$AA 0040:D0 F3 BNE $0035 0042:BD 8C C0 LDA $C08C,X 0045:10 FB BPL $0042 0047:C9 96 CMP #$96 0049:D0 EA BNE $0035 004B:20 88 00 JSR $0088 ... ; fetches two nibbles and combines them ; into one 4-and-4-encoded value, like ; a standard address field 0088:BD 8C C0 LDA $C08C,X 008B:10 FB BPL $0088 008D:38 SEC 008E:2A ROL 008F:8D 9B 00 STA $009B 0092:BD 8C C0 LDA $C08C,X 0095:10 FB BPL $0092 0097:2D 9B 00 AND $009B 009A:60 RTS Continuing from $004E... ; this will be the disk volume number 004E:85 02 STA $02 0050:20 88 00 JSR $0088 ; oops, no, it looks like we're using ; zp$02 for a rolling checksum of the ; address field 0053:45 02 EOR $02 0055:85 02 STA $02 0057:20 88 00 JSR $0088 ; this will be the sector number 005A:8D FD 00 STA $00FD ; update checksum 005D:45 02 EOR $02 005F:85 02 STA $02 0061:20 88 00 JSR $0088 ; update and verify checksum 0064:45 02 EOR $02 0066:D0 C8 BNE $0030 ; standard address field prologue ; ($DE $AA) 0068:BD 8C C0 LDA $C08C,X 006B:10 FB BPL $0068 006D:C9 DE CMP #$DE 006F:D0 C4 BNE $0035 0071:BD 8C C0 LDA $C08C,X 0074:10 FB BPL $0071 0076:C9 AA CMP #$AA 0078:D0 BB BNE $0035 007A:60 RTS Nothing super interesting yet. The subroutine finds the next available sector, parses out the sector number from the address field, and saves it. The caller has some kind of DEC/BNE loop based on the value of $00FD, another relative address. Perhaps the caller reads a specific "block" (in Pascal terminology) that puts the drive head on track $22, then in assembly language we skip over a certain number of sectors until we get to the copy protection pseudo-sectors? Not sure. Continuing from the caller at $001A... 001A:20 DE 00 JSR $00DE ; load value #1, call a thing, check ; the result 00DE:AD FE 00 LDA $00FE 00E1:20 9C 00 JSR $009C 00E4:C9 10 CMP #$10 00E6:90 11 BCC $00F9 00E8:C9 1C CMP #$1C 00EA:B0 0D BCS $00F9 ; load value #2, call a thing, check ; the result 00EC:AD FF 00 LDA $00FF 00EF:20 9C 00 JSR $009C 00F2:C9 26 CMP #$26 00F4:90 03 BCC $00F9 00F6:C9 36 CMP #$36 ; return with carry clear (success) 00F8:60 RTS ; return with carry set (failure) 00F9:38 SEC 00FA:60 RTS I think this is the heart of the copy protection. We're taking two different values and calling the subroutine at $009C, checking that the return value (in the accumulator) is within a certain range, and returning success (carry clear) or failure (carry set). Here are the values in $00FE and $00FF: 00FE:0D 00FF:0F Possibly sector numbers? Let's see what's at $009C. ~ Chapter 3 The Heart of the Maelstrom 009C:20 7B 00 JSR $007B ... ; save the value we passed in 007B:85 03 STA $03 ; find the next address field and ; save the sector number 007D:20 2D 00 JSR $002D ; check if the sector number matches ; the one we passed in 0080:AD FD 00 LDA $00FD 0083:C5 03 CMP $03 ; otherwise loop until we find it 0085:D0 F6 BNE $007D 0087:60 RTS OK, this is becoming clear. The earlier calls to $002D were just to check that all the sectors exist. Now we're looking for specific sectors ($0D and $0F, the values stored in relative addresses $00FE and $00FF). Once we find a given sector, what do we do with it? Continuing from $009F... 009F:20 BF 00 JSR $00BF ... ; find the data field prologue 00BF:BD 8E C0 LDA $C08E,X 00C2:BD 8C C0 LDA $C08C,X 00C5:10 FB BPL $00C2 00C7:C9 D5 CMP #$D5 00C9:D0 F7 BNE $00C2 00CB:BD 8C C0 LDA $C08C,X 00CE:10 FB BPL $00CB 00D0:C9 AA CMP #$AA 00D2:D0 F3 BNE $00C7 00D4:BD 8C C0 LDA $C08C,X 00D7:10 FB BPL $00D4 00D9:C9 AD CMP #$AD 00DB:D0 EA BNE $00C7 00DD:60 RTS Continuing from $00A2... ; $00FA contains an "RTS", so this is ; just burning CPU cycles (6 for JSR, ; 6 for RTS) 00A2:A0 00 LDY #$00 00A4:20 FA 00 JSR $00FA 00A7:20 FA 00 JSR $00FA 00AA:20 FA 00 JSR $00FA 00AD:88 DEY 00AE:D0 F4 BNE $00A4 ; reset data latch 00B0:BD 8E C0 LDA $C08E,X ; count nibbles 00B3:BD 8C C0 LDA $C08C,X 00B6:10 FB BPL $00B3 00B8:C8 INY ; until data field epilogue 00B9:C9 DE CMP #$DE 00BB:D0 F6 BNE $00B3 ; nibble count is in Y, move it to A ; and return to caller 00BD:98 TYA 00BE:60 RTS That's it, that's the protection. A nibble count of two different regions on track $22. Looking again at the caller ($00DE), the relative addresses $00FE and $00FF contain the sector numbers of the fake sectors that really contain the protection nibbles. We call the count routine ($009C), which finds the given sector ($007D) then counts nibbles, then we return to here and check the nibble count result: 00DE:AD FE 00 LDA $00FE 00E1:20 9C 00 JSR $009C 00E4:C9 10 CMP #$10 \ 00E6:90 11 BCC $00F9 | 00E8:C9 1C CMP #$1C | 00EA:B0 0D BCS $00F9 / 00EC:AD FF 00 LDA $00FF 00EF:20 9C 00 JSR $009C 00F2:C9 26 CMP #$26 \ 00F4:90 03 BCC $00F9 | 00F6:C9 36 CMP #$36 | 00F8:60 RTS / 00F9:38 SEC 00FA:60 RTS This routine returns with a simple yes or no answer, which takes us all the way back to [...checks notes...] $001A. ; protection routine returns carry ; clear on success, set on failure 001A:20 DE 00 JSR $00DE ; turn off drive motor (does not affect ; carry bit) 001D:BD 88 C0 LDA $C088,X ; shift carry bit into bit 0 ; success -> bit 0 = 0 ; failure -> bit 0 = 1 0020:2A ROL ; invert ; success -> bit 0 = 1 ; failure -> bit 0 = 0 0021:49 FF EOR #$FF ; ignore everything but bit 0 ; success -> A = #$01 ; failure -> A = #$00 0023:29 01 AND #$01 ; push this value to the stack and ; return to the main Pascal program 0025:48 PHA 0026:A5 01 LDA $01 0028:48 PHA 0029:A5 00 LDA $00 002B:48 PHA 002C:60 RTS If we want this routine to always claim success, we need to push #$01 to the stack at $0025. The easiest way to ensure that is to change the "AND #$01" at $0023 to "LDA #$01". T15,S0B,$4D: 29 -> A9 (Aficianados of minimalism will appreciate that $29 and $A9 differ only in bit 7, making this a 1-bit crack.) Some disk analysis shows that this sector is part of the SYSTEM.STARTUP program, which makes sense. (It's like the HELLO program of Pascal.) Further analysis shows that T18,S0B is unused. Given the way Pascal works, it's likely a copy of a previous build. Perhaps a candidate for future study, but the crack is complete. Quod erat liberandum. --------------------------------------- A 4am crack No. 2137 ------------------EOF------------------