"Thunderstrike" (c)1991 Millenium
Things you will need:
- 1. The original of "Thunderstrike" (c)1991 Millenium. The CAPS (SPS) version (numbered 0406) is the one I've used here.
- 2. An Amiga or a copy of UAE.
- 3. An Action Replay 3 cart (or ROM image for use w/ UAE above).
Ok, no need to show you a screenshot of XCopy this time - the disk is a normal DOS copyable trackloader. As the smarter amongst you will have noticed, since this tutorial is in the 'Novella cracking' section, we are dealing with a manual protection this time. So, let's boot the game up and check it out...
The first time I check a novella protection, I usually want to see what happens when it fails. If we try and guess the word, unless we're extremely lucky, we'll fail after 3 attempts and the message "ACCESS DENIED" will appear onscreen. Nothing else happens, no reset or anything, so we can break into the AR and see where we are in memory and what's going on...
We're at a "BRA FOREVER" loop (BRA 3F7A), so let's disassemble the instructions above this address and find out what led us here:
We can see that the code above "falls through" to the failure routine (there is no specific BSR FAIL call), so what we are looking for is a chunk of code that performs some kind of test and branches past the failure-loop code if a condition is met.
It should be quite obvious that the code we want begins @ $003F3E:
003F3E: LEA $40D2(PC),A1 003F42: MOVE.B (A0),D0 003F44: OR.B (A1),D0 003F46: BEQ 003F82
This is the only piece of code that will take us past the failure-loop, so we should be confident we are looking in the right place.
Let's reboot and get to the "PRESS ANY KEY" screen prior to the novella check. Enter the AR once again, and set a breakpoint @ $003F3E to confirm our findings. Enter the word "BADCODE" and press return, and our breakpoint will trigger.
If we look at the two referenced parts of memory ($40D2 and the address A0 points to), we can see that the first holds the word that we entered ("BADCODE") and the second points to the correct word that we should have entered ("POD" in this example, but of course this probably won't be the same word you see if you are following along at home). More importantly, we can also see that there is a table of page/paragraph/line/word numbers and the codewords themselves sitting unencrypted in memory... first, lets make sure this is correct by changing our entered word in memory to match the one pointed to by A0. In this example, it's "POD" followed by a trailing $00 to finish, substitute your correct word if necessary:
>>> m 40d2 50 4f 44 00
Exit the AR, and... success! The game loads, and you can enjoy literally a few seconds of slow/jerky filled vector 'fun' in a tiny game-window, before you reset and let it reboot once more.
Now, at this point we could patch the BEQ 003F82 instruction @ $3F46 to always branch, but I decided to do it another way. And yes, that decision was mainly to pad out this tutorial a little bit :)
So, we want to find the code that selects the correct codeword for this attempt. The game has to tell us where to look this up in the manual, so it must write the correct page/paragraph/line/word into the novella "challenge" text screen. If we enter the AR at the "PRESS ANY KEY" screen, we can find this text @ $411B, and we notice the fields we are interested in are all currently "00".
Now would be a good time to set a memory watchpoint, and track down the code which changes this:
As you can see from the picture, our watchpoint triggers and we can see a routine which is populating the fields we are interested in from a table pointed to by A0. We are interested in finding out how this table is indexed, so let's disasm a few instructions before our current position, and all is revealed!
003E5A: MOVEM.L D0-D7/A0-A6,-(A7) 003E5E: JSR 0025BA 003E64: MOVEM.L (A7)+,D0-D7/A0-A6 003E68: ANDI.W #$3F,D0 003E6C: ADD.W D0,D0 003E6E: ADD.W D0,D0 003E70: LEA 3F86(PC),A0 003E74: MOVEA.L 0(A0,D0.W),A0 003E78: MOVE.B (A0)+,004165 003E7E: MOVE.B (A0)+,004166
A few comments on the above are probably helpful.
- 1. JSR 25BA - returns a random-ish number in D0
- 2. ANDI.W #$3F,D0 - limits the random number to the range 0-63
- 3. ADD.W D0,D0 (twice) - D0 * 4, for indexing into a table of longwords
- 4. LEA 3F86(PC),A0 - the base of a table of pointers to codewords etc.
- 5. MOVEA.L 0(A0,D0.W),A0 - move the correct address from our table into A0.
If we look at the table @ $3F86, we can see it's a table of addresses of all the acceptable codewords for the protection check. The first entry in the table points to $420D, which = "5 1 2 3 RECOMMEND". This is in the format page/paragraph/line/word then the codeword itself, followed by a terminating $00.
Now, the way I've chosen to crack this is to force the game to accept a particular codeword ("FLASHTRO", who'd have guessed?). The eagle-eyed amongst you will have noticed that "FLASHTRO" isn't in the acceptable codeword list, so it would be nice if we could find another 8-letter word in the table that we can overwrite... scrolling through the table @ $420D, I found an entry for "MILITARY" which is @ $454F, and the entry itself begins @ $4547.
If you search the table of pointers @ $3F86 you will discover the one we want to alter ($00004547) is $3A in the list.
That'll do nicely! However, unless the user is psychic, they won't know to enter this at the novella protection check, so we need to alter the onscreen text ($41C3 onwards looks like a good spot). Let's reboot and check the bootblock out:
>>> rt 0 1 10000 >>> m 10000 (scroll and check for free space) (esc) >>> d 1000c
OK, we seem to have plenty space for our patch code, and the bootblock is extremely simple to understand (copies itself to $7DC00 onwards and jumps in @ $7DC02). If we keep disassembling, we can see a likely candidate for the jump into the loaded gamecode @ $10122:
At this point, I altered the instruction @ $10128 (JMP (A6)) into a BRA FOREVER loop, fixed the bootblock checksum and wrote this back to my test copy, just to test that the code we want to alter to bypass the protection check was in memory by this stage... thankfully, it was, so we can hook the bootloader at this point for our patching.
I wrote my alteration to the onscreen text @ $10384 onwards, and my patch code @ $102C0.
At $10384 we place the following:
(6 spaces)CRACKED BY FLASHTRO!(6 spaces),$0D,(1 space)ENTER 'FLASHTRO' AS CODEWORD(1 space),$0D,(6 spaces)
The $0D bytes are newline cmds to the game textroutine that outputs the novella challenge text to screen.
Patch the jump that would jump into the loaded game code to jump to our patch instead:
>>> a 10122 bsr 102c0 nop (esc)
And then write a little patch that makes our changes (assemble this to $102C0 onwards)
0102C0 MOVE.L #$303C003A,$00003E68 ;writes "MOVE.W #$3A,D0" over "ANDI.W #$3F,D0" ;this forces the entry we want to index in table 0102CA MOVEM.L D0-D1/A0-A2,-(A7) 0102CE LEA 10384(PC),A0 0102D2 LEA.L 41C3,A1 0102D8 LEA.L 46(A0),A2 0102DC MOVE.B (A0)+,(A1)+ 0102DE CMPA.L A0,A2 0102E0 BNE.B 102DC ;copy our changes to the novella textscreen into memory at the right place 0102E2 LEA.L 10395(PC),A0 0102E6 LEA.L 454f,A1 0102EC MOVEQ #7,D0 0102EE MOVE.B (A0)+,(A1)+ 0102F0 DBF .W D0,102EE ;copy "FLASHTRO" over the "MILITARY" entry in the codeword table 0102F4 MOVEM.L (A7)+,D0-D1/A0-A2 0102F8 MOVEA.L #400,A6 0102FE RTS
Now we just fix the checksum, and write our new improved bootblock to our testcopy:
>>> bootchk 10000 >>> wt 0 1 10000
And we're ready to test!
And much to our surprise, it works!
I must admit I haven't done a lot of testing, the game appears to be a cure for insomnia...
There are several other ways to crack the game, feel free to experiment with bypassing the entire novella check (although I don't like doing that, since some games use the protection screen as a method to enter cheatcodes!), or how about modifying it so that the protection screen tells you the correct word to enter (without forcing a particular choice)?
That's it for this one, hopefully a more interesting tutorial next time.