------------Shadow Hawk One------------ A 4am crack 2023-12-11 --------------------------------------- Name: Shadow Hawk One Version: rev. 2 Genre: action Year: 1981 Publisher: Horizon Simulations Platform: Apple ][+ or later (48K) Media: 5.25-inch disk Sides: 1 OS: DOS 3.3 Previous cracks: none ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA no errors, but copy loads credits screen then clears memory and hangs Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor standard 16-sector disk structure nothing on track $23 nothing on half/quarter tracks Disk Fixer bootloader is standard track $11 even has a standard DOS 3.3 disk catalog no obvious signs of shenanigans I'm assuming there is some runtime protection check, since computers generally do not wipe memory and halt unless someone tells them to. ~ Chapter 1 In Which We Find A Little Extra The game boots with a normal-looking, normal-sounding DOS load followed by a BASIC prompt, so let's try the simplest thing that could possibly work: typing to break to BASIC. ; ...a well-timed on boot... BREAK ]LIST 10 PRINT "BRUN OBJ.HELLO" Well would you look at that. ]BLOAD OBJ.HELLO ]CALL-151 ; DOS location of last BLOAD address *AA72.AA73 AA72- 03 08 *803L ; reboot on reset 0803- A9 00 LDA #$00 0805- 8D F2 03 STA $03F2 0808- A9 C6 LDA #$C6 080A- 8D F3 03 STA $03F3 080D- A9 00 LDA #$00 080F- 8D F4 03 STA $03F4 ; show hi-res page 1 0812- 8D 50 C0 STA $C050 0815- 8D 57 C0 STA $C057 0818- 8D 52 C0 STA $C052 ; print followed by , ; which tells me we're about to print ; a DOS command 081B- A9 8D LDA #$8D 081D- 20 ED FD JSR $FDED 0820- A9 84 LDA #$84 0822- 20 ED FD JSR $FDED ; print a DOS command 0825- A0 00 LDY #$00 0827- B9 B1 08 LDA $08B1,Y 082A- 20 ED FD JSR $FDED 082D- C9 8D CMP #$8D 082F- F0 04 BEQ $0835 0831- C8 INY 0832- 4C 27 08 JMP $0827 0835- 20 ED FD JSR $FDED More on this in a moment. ; wait a fairly long time 0838- A0 2A LDY #$2A 083A- A9 FF LDA #$FF 083C- 20 A8 FC JSR $FCA8 083F- 88 DEY 0840- C0 00 CPY #$00 0842- D0 F6 BNE $083A ; call into code we just loaded 0844- 20 00 40 JSR $4000 *FC58G N 400<8B1.8FFM BLOAD HEAD.PICM@@@@@@@@@@@@@@@@@@@@@@@@ *BLOAD HEAD.PIC *AA72.AA73 AA72- 00 20 *AA60.AA61 AA60- 50 21 Cute. We're loading graphics data directly into the hi-res page ($2000 bytes at address $2000), but there's $150 bytes of executable code after the graphic data that gets loaded into $4000. And we're calling that after you stare at the credits page for a while. (This, incidentally, is where my non- working copy decided to wipe memory and halt, so we're definitely close.) *4000L ; get RWTS parameter table address and ; store it in $00/$01 4000- 20 26 41 JSR $4126 4003- 84 00 STY $00 4005- 85 01 STA $01 ; get slot/drive 4007- A0 01 LDY #$01 4009- B1 00 LDA ($00),Y 400B- AA TAX 400C- 20 22 41 JSR $4122 *4122L 4122- 6C 43 41 JMP ($4143) *4143.4144 4143- 0D 41 *410DL ; decrypt $401A..$410B 410D- A9 40 LDA #$40 410F- 85 3D STA $3D 4111- A9 19 LDA #$19 4113- 85 3C STA $3C 4115- A0 F2 LDY #$F2 4117- B1 3C LDA ($3C),Y 4119- 49 DD EOR #$DD 411B- 91 3C STA ($3C),Y 411D- 88 DEY 411E- D0 F7 BNE $4117 4120- 60 RTS Continuing from $400F... 400F- A9 03 LDA #$03 4011- 8D 21 41 STA $4121 4014- 20 1A 40 JSR $401A The decryption routine at $410D is self-contained, so let's just run it. *410DG *401AL 401A- 60 RTS What. 4017- 20 0D 41 JSR $410D What. *410DG *401AL ; turn on the drive motor and wait 401A- BD 89 C0 LDA $C089,X 401D- 8E 25 41 STX $4125 4020- A9 FF LDA #$FF 4022- 20 A8 FC JSR $FCA8 4025- CE 21 41 DEC $4121 4028- 10 F6 BPL $4020 Wait, so we actually ENcrypted this, then called it (which immediately returned, because the first encrypted byte was an "RTS" opcode), then we DEcrypted it, and now we're... falling through to execute the code as it was in the first place. I mean, sure. Why not. ~ Chapter 2 In Which We Count Cycles, Which Is A Perfectly Reasonable Thing To Do On A Monday Morning Or Any Time, Really Continuing from $402A. The drive motor is on and we have waited for it to spin up, and now we're going to do dastardly things (probably). ; look for a real nibble 402A- AE 25 41 LDX $4125 402D- A0 FF LDY #$FF 402F- BD 8C C0 LDA $C08C,X 4032- 30 05 BMI $4039 4034- 88 DEY 4035- D0 F8 BNE $402F 4037- F0 1A BEQ $4053 ; initialize counters, probably 4039- A9 00 LDA #$00 403B- 8D 25 41 STA $4125 403E- A9 10 LDA #$10 4040- 8D 29 41 STA $4129 4043- A9 04 LDA #$04 4045- 8D 21 41 STA $4121 4048- 78 SEI ; decrement death counters and ; continue at $4097, or fall through ; to The Badlands 4049- CE 25 41 DEC $4125 404C- D0 49 BNE $4097 404E- CE 29 41 DEC $4129 4051- D0 44 BNE $4097 *4097L ; look for $D5 nibble 4097- BD 8C C0 LDA $C08C,X 409A- 10 FB BPL $4097 409C- C9 D5 CMP #$D5 409E- D0 A9 BNE $4049 ; skip a bunch of nibbles 40A0- A0 0D LDY #$0D 40A2- EA NOP 40A3- BD 8C C0 LDA $C08C,X 40A6- 10 FB BPL $40A3 40A8- 88 DEY 40A9- D0 F7 BNE $40A2 ; look for $EB nibble (would be part of ; the address field epilogue after ; skipping $0D nibbles) 40AB- C9 EB CMP #$EB 40AD- D0 9A BNE $4049 40AF- F0 00 BEQ $40B1 ; note: no BPL loop here, we're just ; reading the data latch once 40B1- BD 8C C0 LDA $C08C,X 40B4- C9 08 CMP #$08 40B6- B0 91 BCS $4049 ; check for non-standard $FD nibble 40B8- BD 8C C0 LDA $C08C,X 40BB- 10 FB BPL $40B8 40BD- C9 FD CMP #$FD 40BF- D0 88 BNE $4049 ; decrement counter (initially 4, set ; at $4045) and do the entire thing ; again 40C1- CE 21 41 DEC $4121 40C4- D0 D1 BNE $4097 ; turn off the drive motor and exit to ; caller 40C6- BD 88 C0 LDA $C088,X 40C9- 58 CLI 40CA- 60 RTS So we're looking for timing bits after the third address field epilogue nibble ($EB), then a non-standard nibble after that ($FD). This is the heart of the protection. But I want to dig in a bit further. When we say "looking for timing bits," what is that exactly, and why does it work as a protection? On the original disk, there are two 0 bits after the $EB nibble in the address field epilogue. Bit copiers can not distinguish between one 0 bit and two 0 bits after a nibble, because the disk keeps spinning independently of the CPU, which isn't fast enough to do the necessary calculations in real time. Bit copiers don't actually (ever) make a faithful reproduction of the original bitstream, because they can't. They always take their best guess to reconstruct the bitstream. This is an unusual place to have two 0 bits in a row, and bit copiers reconstruct it incorrectly. Here is the bitstream on the original disk: 1110101100111111010011111101 |--EB--| |--FD--| |--FD--| The data latch will hold its value through the two 0 bits after the $EB nibble, then it resets to #$00, then new bits from the $FD nibble start shifting in. At a rate of one bit every four CPU cycles, the data latch will reset somewhere around the time the CPU is executing the instruction at address $40AD. Here's what's happening on the CPU while the disk spins. (I've marked the cycles required for each instruction.) 40A3- BD 8C C0 LDA $C08C,X ; 4 [first 0 bit seen, data latch remains #$EB] 40A6- 10 FB BPL $40A3 ; 2 40A8- 88 DEY ; 2 [second 0 bit seen, data latch remains #$EB] 40A9- D0 F7 BNE $40A2 ; 2 40AB- C9 EB CMP #$EB ; 2 [data latch resets to #$00, first 1 bit of $FD shifts in, data latch becomes #$01] 40AD- D0 9A BNE $4049 ; 2 40AF- F0 00 BEQ $40B1 ; 3 [second 1 bit of $FD shifts in, data latch becomes #$03] 40B1- BD 8C C0 LDA $C08C,X Most of the time (75%, as we will see shortly), when we read the data latch at $40B1, we'll get #$03. But there can be some variation, because a bit gets shifted into the data latch every four CPU cycles, and that shift might have happened 1-3 CPU cycles before the fetch at $40A3. In the worst possible case, by the time we check the data latch at $40B1, we may have spent up to 12 CPU cycles: 3 CPU cycle deficit 2 not taking the BNE at $40AD 3 taking the BEQ at $40AF + 4 on the LDA at $40B1 --- 12 This worst case will happen 25% of the time, but it's still fine. In 12 CPU cycles, only 3 bits will have shifted into the data latch (because 12/4 = 3), so its maximum value is 00000111 in binary, which is #$07. Which means... 40B4- C9 08 CMP #$08 40B6- B0 91 BCS $4049 ...it will never be #$08 or more, so execution always falls through to the success path at $40B8. Now, on my non-working EDD bit copy, the (reconstructed) bitstream looks like this: 11101011011111101011111101 |--EB--| |--FD--| |--FD--| Aha! EDD only added a single 0 bit after the $EB nibble, instead of two 0 bits like the original disk. That means the following bits will shift into the data latch four cycles sooner than they would on an original disk. It's still possible for the data latch to end up at #$07 when we need it to be less than #$08 (at $40B1), but that's now the *best* possible case and only happens 25% of the time. The other 75% of the time, an additional 1 bit will have shifted in (compared to the original disk), the value of the data latch at $40B1 will be #$0F, the comparison at $40B4 will fail, and the protection routine will branch back and try again. If it fails too often, the death counter will hit 0 and we'll fall through to The Badlands at $4053: *4053L ; turn off the drive motor 4053- BD 88 C0 LDA $C088,X ; set reset vector to $0002 4056- A9 02 LDA #$02 4058- 8D F0 03 STA $03F0 405B- 8D F2 03 STA $03F2 405E- 85 00 STA $00 4060- A9 00 LDA #$00 4062- 8D F1 03 STA $03F1 4065- 8D F3 03 STA $03F3 4068- 85 01 STA $01 406A- 49 A5 EOR #$A5 406C- 8D F4 03 STA $03F4 ; print message on text screen, which ; is hidden 406F- 20 CB 40 JSR $40CB ; copy the rest down to zero page and ; continue from there 4072- A0 18 LDY #$18 4074- B9 7F 40 LDA $407F,Y 4077- 91 00 STA ($00),Y 4079- 88 DEY 407A- 10 F8 BPL $4074 407C- 6C 00 00 JMP ($0000) ; wipe all of main memory, $0800+ 407F- A9 00 LDA #$00 4081- 85 00 STA $00 4083- A8 TAY 4084- A9 08 LDA #$08 4086- 85 01 STA $01 4088- A9 12 LDA #$12 408A- 91 00 STA ($00),Y 408C- E6 00 INC $00 408E- D0 FA BNE $408A 4090- E6 01 INC $01 4092- 10 F6 BPL $408A 4094- 58 CLI ; halt (only works on older machines) 4095- 02 ??? 4096- 60 RTS This is what I saw on my non-working copy. Fun(*) fact: if you press after it wipes memory, you can actually see the message it prints on the text screen: UNAUTHORIZED COPY PLEASE USE ORIGINAL (*) not guaranteed, actual fun may vary ~ Chapter 3 In Which We Just Say No And Our Story Comes To A Swift And Satisfying Conclusion This protection routine has no side effects. It either returns or it doesn't. I can put an "RTS" at the beginning and the caller will assume everything went fine. T12,S09,$04: 20 -> 60 But wait, there's more! For the low, low price of one protection check, you get another protection check absolutely free! There is another protection routine, identical to this one, tacked onto the end of the graphical title screen that is loaded immediately after the credits screen. Like, literally the same deal: graphic data at $2000, but the file is $150 bytes longer than it should be, and the extra code at $4000 is the protection check. T05,S0F,$04: 20 -> 60 Quod erat liberandum. --------------------------------------- A 4am crack No. 3205 ------------------EOF------------------