----------------Oo-Topos--------------- A 4am crack 2018-01-26 -------------------. updated 2018-01-29 |___________________ Name: Oo-Topos Genre: adventure Year: 1986 Credits: story by Michael and Muffy Berlyn, graphics by Raimund Redlich and Brian Poff Publisher: Polarware Platform: Apple ][+ or later (double hi-res graphics require 128K) Media: 5.25-inch disk Sides: 2 OS: ProDOS This disk was automatically cracked by Passport. Here is the transcript for side 2 (the bootable side): --v-- READING FROM S6,D1 T00,S00 FOUND PRODOS BOOTLOADER T00,S0B VOLUME NAME IS RUN USING BUILT-IN RWTS WRITING TO S5,D2 T0E,S05 FOUND FBFF PROTECTION CHECK T0E,S05,$35: FB -> A2 T0E,S0D FOUND FBFF PROTECTION CHECK T0E,S0D,$95: EB -> B2 CRACK COMPLETE. PRESS ANY KEY --^-- More information and source code is available at https://archive.org/details/Passport4am Side 1 is unprotected. Quod erat liberand one more thing... ~ This game has an obvious protection check -- obvious in the sense that the game freezes after selecting standard or double hi-res graphics. This code is on T0E,S05, encrypted with a one-byte XOR #$FF. It is identical to the code in "The Electric Crayon: ABCs" (see 4am crack no. 622), and the patch is the same: change the "BNE +4" near the beginning of the encrypted code to "BNE +5D" so the routine returns before the Death Counter hits 0. Here's that code in Disk Fixer sector editor: --v-- T0E,S05 ----------- DISASSEMBLY MODE ---------- ; save some state from zero page 0000:A2 03 LDX #$03 0002:B5 00 LDA $00,X 0004:48 PHA 0005:CA DEX 0006:10 FA BPL $0002 ; put an "RTS" at $00 and call it (WTF) 0008:A9 60 LDA #$60 000A:85 00 STA $00 000C:20 00 00 JSR $0000 ; do some stack pointer manipulation ; (more on this in a minute) 000F:BA TSX 0010:CA DEX 0011:CA DEX 0012:9A TXS 0013:68 PLA 0014:85 00 STA $00 0016:68 PLA 0017:85 01 STA $01 ; decrypt the code that follows 0019:A0 89 LDY #$89 001B:A9 FF LDA #$FF 001D:51 00 EOR ($00),Y 001F:91 00 STA ($00),Y 0021:88 DEY 0022:C0 17 CPY #$17 0024:D0 F5 BNE $001B --v-- Here's why we're putting an "RTS" at $00 and called it. This entire routine is relocatable. There's no "program counter" on the Apple II. I mean, obviously there is a program counter, but it's not like a simple address that you can read to see what instruction is going to be executed next. But this routine figures out where in memory it lives, by creating a routine that does nothing, calling it, and taking advantage of the fact that the "stack" is just a page of memory (from $0100..$01FF) and a register that is an index into that page. After the call to $0000 (at offset $000C), the return address is still somewhere in the $0100..$01FF range. The stack page is not overwritten until you call a different subroutine or do something else that pushes bytes to the stack. This routine figures out where the return address is in the stack page and stores it in $00/$01. Then the loop at offset $0019 decrypts memory backwards from that address + $89, back to that address + $17, then stops. $17 bytes is the size of the code that manipulates the stack pointer, plus the decryption loop itself. Anyway, the code immediately after the decryption loop is encrypted, but we can still view it in Disk Fixer. Press "X" to set an EOR mask of $FF (leave the ADD, AND, and OR masks alone), then press to apply the mask. Now we can see the decrypted code starting at offset $0026. --v-- ----------- DISASSEMBLY MODE ---------- ; get slot number (x16) from ProDOS 0026:AE 30 BF LDX $BF30 ; turning on the disk motor manually: ; never not suspicious 0029:BD 89 C0 LDA $C089,X ; initialize Death Counters, probably 002C:A9 56 LDA #$56 002E:85 03 STA $03 0030:A9 08 LDA #$08 0032:C6 02 DEC $02 0034:D0 04 BNE $003A ; if Death Counter hits zero, branch 0036:C6 03 DEC $03 0038:F0 59 BEQ $0093 ; look for an $FB nibble 003A:BC 8C C0 LDY $C08C,X 003D:10 FB BPL $003A 003F:C0 FB CPY #$FB 0041:D0 ED BNE $0030 0043:F0 00 BEQ $0045 ; kill a few cycles (not pointless, ; because the disk spins independently ; of the CPU, so all of these low-level ; disk reads are highly time-sensitive) 0045:EA NOP 0046:EA NOP ; read data latch (note: no BPL loop ; here, we're just reading it once) 0047:BC 8C C0 LDY $C08C,X ; do a compare to set or clear the ; carry bit (among other things, but ; it's the carry bit we care about) 004A:C0 08 CPY #$08 ; rotate the carry into the accumulator 004C:2A ROL ; if we just rolled a "1" bit out of ; the accumulator, take this branch 004D:B0 0B BCS $005A ; next nibble needs to be $FF 004F:BC 8C C0 LDY $C08C,X 0052:10 FB BPL $004F ; ...otherwise we start over 0054:C0 FF CPY #$FF 0056:D0 D8 BNE $0030 ; loop back to get next nibble ; (unconditional branch since we didn't ; take the BNE just before it) 0058:F0 EB BEQ $0045 ; execution continues here (from offset ; $004D) -- if the accumulator is ; anything but %00001010, start over 005A:C9 0A CMP #$0A 005C:D0 D2 BNE $0030 ; next nibbles must be "D5 AA 96 AA AB" 005E:BD 8C C0 LDA $C08C,X 0061:10 FB BPL $005E 0063:C9 D5 CMP #$D5 0065:D0 C9 BNE $0030 0067:F0 00 BEQ $0069 0069:BD 8C C0 LDA $C08C,X 006C:10 FB BPL $0069 006E:C9 AA CMP #$AA 0070:D0 BE BNE $0030 0072:F0 00 BEQ $0074 0074:BD 8C C0 LDA $C08C,X 0077:10 FB BPL $0074 0079:C9 96 CMP #$96 007B:D0 B3 BNE $0030 007D:F0 00 BEQ $007F 007F:BD 8C C0 LDA $C08C,X 0082:10 FB BPL $007F 0084:C9 AA CMP #$AA 0086:D0 A8 BNE $0030 0088:F0 00 BEQ $008A 008A:BD 8C C0 LDA $C08C,X 008D:10 FB BPL $008A 008F:C9 AB CMP #$AB 0091:D0 9D BNE $0030 I got lost several times trying to follow this routine. I think the easiest way to explain it is to show the difference between the original disk and my non-working copy. Here is the original disk, as seen by the Copy II+ nibble editor. Nibbles with extra "0" bits (timing bits) after them are displayed in inverse on an original machine, marked here with a "+" after the nibble. --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF+FF FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA AB FF FF+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- It's easy to understand why a simple sector copy failed. The sequence that this code is looking for starts at offset $1C93, which is between the end of one sector and the beginning of the next. (The data epilogue is at $1C8C; the next address prologue is at $1CA2.) Sector copiers discard everything between those delimiters and rebuild the track with a default pattern of sync bytes. That pattern doesn't include an $FB nibble, so the nibble check fails. But the EDD bit copy also failed. Here is the original disk's pattern at offset $1C93: - $FB + timing bit - $FF - $FF + timing bit - $FF - $FF + timing bit And here is what the same part of the track looks like on my failed EDD copy: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF FF+FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA AB FF FF+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- A subtle difference! The sequence at offset $1C93 now looks like this: - $FB + timing bit - $FF - $FF - $FF + timing bit - $FF + timing bit This code is looking for $FF bytes with an alternating pattern of timing bit, no timing bit, timing bit, no timing bit. The accumulator holds the pattern of whether each sync byte had a timing bit after it. It's set one bit at a time, rotated into place from the carry bit that was set by the "CPY #$08" that happened after getting the value of the data latch (LDY $C08C,X) that happened after doing just enough NOPs that the value of the data latch will depend on the presence of a timing bit after the previous nibble. Which is brilliant. Anyway, if the value of the accumulator (i.e. the pattern of timing bits) is wrong, the program knows it's not running on an original disk. ~ Continuing from offset $0093... ; turn off drive motor ; (hey, this was also the failure path, ; which means the real success/failure ; check is later) 0093:DD 88 C0 CMP $C088,X 0096:A0 89 LDY #$89 And that's the end of the encrypted portion. Now press again to disable the mask, and read the rest of the code. --v-- ----------- DISASSEMBLY MODE ---------- ; re-encrypt the protection check 0098:A9 FF LDA #$FF 009A:51 00 EOR ($00),Y 009C:91 00 STA ($00),Y 009E:88 DEY 009F:C0 17 CPY #$17 00A1:D0 F5 BNE $0098 ; check the Death Counter 00A3:A4 03 LDY $03 ; restore zero page state 00A5:A2 00 LDX #$00 00A7:68 PLA 00A8:95 00 STA $00,X 00AA:E8 INX 00AB:E0 04 CPX #$04 00AD:D0 F8 BNE $00A7 ; ah! if the nibble check failed, zero ; page $03 will be 0, which means this ; will fall into an infinite loop 00AF:98 TYA 00B0:F0 FE BEQ $00B0 ; WARNING: side effects detected! 00B2:A9 00 LDA #$00 00B4:8D D0 9A STA $9AD0 00B7:A9 C6 LDA #$C6 00B9:8D D1 9A STA $9AD1 00BC:60 RTS --^-- If the protection check fails, we get into an infinite loop (at offset $00B0) and this routine never returns. That explains the behavior I saw on my non- working copy. But if the protection check succeeds, we set two seemingly arbitrary memory locations. Experience has taught me that side effects of protection checks are never innocuous, so I'll need to be sure that my crack sets them too (while bypassing all the protect-y bits of course). Backing up to the beginning of the protection code, I see this branch, on the low byte of the 16-bit Death Counter: 0032:C6 02 DEC $02 0034:D0 04 BNE $003A If I changed that branch to go straight to the exit path (at offset $0093), the high byte (zp$03) would still be non- zero, and the protection check would succeed immediately. Then I can piggy- back on the existing post-check side effects instead of having to recreate them. One problem: this code is encrypted on disk. But Disk Fixer comes to the rescue again, because we can re-apply the EOR mask, change the branch value we want at offset $0035 (from #$04 to #$5D, so it branches to offset $0093), then disable the EOR mask one final time and save the sector to disk. That's the first patch that Passport applied. It found this encrypted protection check and changed the value of the encrypted branch so that, once decrypted, the routine functions as an always-succeed check, including the side effects: T0E,S05 FOUND FBFF PROTECTION CHECK T0E,S05,$35: FB -> A2 Except... ~ About a dozen moves into the game, you get to shoot one of the aliens that are chasing you. Unbeknownst to regular players, this triggers a second protection check. But if it fails, it doesn't freeze the game. Instead, it changes an in-game condition so the other aliens always catch up to you and re-imprison you, thus restarting the game. This second protection check is found on T0E,S0D, encrypted with a different 1-byte XOR (#$EF this time). The patch is the same, changing the "BNE +4" to "BNE +5D". Now, after shooting the alien, the game allows the player to continue as expected. That's the second patch that Passport applied: T0E,S0D FOUND FBFF PROTECTION CHECK T0E,S0D,$95: EB -> B2 Whew. Sneaky delayed protection check! But now we're done, right? Welllllll... ~ Much later in the game, you get to enter a "gravtube" and push a button. This acts as a teleporter, moving between non-contiguous areas of the game map. Except when you push either button, nothing happens. You exit the gravtube and you're still in the same map area. It turns out the game checksums the second encrypted protection check on Every. Single. Turn. And if it ever fails, it changes another in-game flag so that the gravtube doesn't work. Deep within the game, there is this little loop which adds a region of memory to itself: --v-- T0D,S0B ----------- DISASSEMBLY MODE ---------- 0005:18 CLC 0006:B1 FC LDA ($FC),Y 0008:6D C2 47 ADC $47C2 000B:8D C2 47 STA $47C2 000E:88 DEY 000F:10 F5 BPL $0006 --^-- Searching for references to the memory location $47C2 finds two others: --v-- ------------- DISK SEARCH ------------- $0C/$01-$7B $0D/$0B-$09 $0D/$0B-$0C $0D/$0D-$11 --^-- T0C,S01 initializes $47C2 to 0. T0D,S0D checks it. --v-- T0D,S0D ----------- DISASSEMBLY MODE ---------- 0010:EC C2 47 CPX $47C2 <-- ! 0013:F0 09 BEQ $001E 0015:AE FD 4C LDX $4CFD 0018:8E 86 4F STX $4F86 001B:20 8C 4F JSR $4F8C 001E:AE DA 4A LDX $4ADA 0021:D0 03 BNE $0026 0023:4C 14 4A JMP $4A14 0026:A2 00 LDX #$00 0028:8E 16 4A STX $4A16 002B:BD DB 4A LDA $4ADB,X 002E:A8 TAY 002F:BD DC 4A LDA $4ADC,X 0032:AA TAX 0033:D0 03 BNE $0038 0035:4C 07 4A JMP $4A07 --^-- Keep in mind that I have no idea what those other subroutines or locations do or mean. But I know a checksum when I see it. On a hunch, I changed the "BEQ" at offset $0013 to a "BVC" (essentially a branch-always, since the overflow bit is almost always clear), and wouldn't you know it, the gravtube now works properly. I found this same pattern -- initial "decoy" protection check that freezes the game, second delayed protection check that sets a flag, and anti-tamper check that ensure the second check is intact -- on at least two other games by Polarware. So into Passport it goes, and here is the updated (and God help me, final) Passport log: --v-- READING FROM S6,D1 T00,S00 FOUND PRODOS BOOTLOADER T00,S0B VOLUME NAME IS RUN USING BUILT-IN RWTS WRITING TO S5,D2 T0E,S05 FOUND FBFF PROTECTION CHECK T0E,S05,$35: FB -> A2 T0E,S0D FOUND FBFF PROTECTION CHECK T0E,S0D,$95: EB -> B2 T0D,S0D FOUND ANTI-TAMPER CHECK T0D,S0D,$13: F0 -> 50 CRACK COMPLETE. PRESS ANY KEY --^-- Quod erat liberandum. ~ Changelog 2018-01-29 - interrupt bit -> overflow bit 2018-01-29 - update with anti-tamper check and explanation of side effects 2018-01-26 - initial release --------------------------------------- A 4am crack No. 1643 ------------------EOF------------------