Submitted by: musashi9
Date: 2008-01-07 13:52
Amiga cracking: 1-disking
"All Terrain Racing" (c) 1995 Team 17
Things you will need:
- 1. The original of "All Terrain Racing (c) 1995 Team 17". Unfortunately this game hasn't been imaged for SPS yet, so you won't find a CAPS image to play with. All my work was based on an original imaged as an extended ADF by Dlfrsilver (thanks!).
- 2. An Amiga or a copy of UAE.
- 3. An Action Replay 3 cart (or ROM image for use w/ UAE above).
- 4. Asm-One or whatever your favourite assembler is.
Ok, the easiest way to see what we're dealing with is to try and copy the disk. Let's see if that tells us anything useful:-
So it's a normal format disk, with 1.5 tracks in a custom format (78 and part of 79) near the end of the disk. If you try and copy disk 2, you will see that disk is completely unprotected so only the boot/game disk needs to be cracked.
Time to reboot and jump into the AR, and start looking at disk 1:-
>>> rt 0 1 10000 >>> d 1000c
This reads in the track containing the bootblock @ $10000, and then we disassemble it from $1000c (skipping the DOS header, checksum, and location of the root block).
We can see it loads $2C00 bytes from disk offset $400 to address $78000 using the exec library DoIO() function (-$1C8, grab a copy of "Mapping the Amiga" via Google if you don't have a list of such things), enters supervisor mode (calls SuperState(), $-96), turns off DMA and jumps into the loaded code via a JMP $78000.
Now, you could modify the bootblock on your testcopy disk to BRA FOREVER instead of the JMP 78000 instruction, and fix the bootblock checksum and write it back and reboot, but we can just read it in using the AR directly so we'll do that instead:-
>>> rt 0 3 78000-400 >>> d 78000
Since we know it reads from offset $400 on the disk, this is immediately after the bootblock, from sector 2 of track 0. We need to read $2C00 bytes, which is 2 tracks, but since we start at $400 we need to ask the AR to read 3 tracks to get the last $400 bytes of the data we require. Now let's check it out...
The first things we see is 2 calls to AllocMem() ($-C6). Another thing we notice is that this game obviously doesn't work on <= 512k amigas, since it allocates $50000 bytes of chipram and $40000 bytes of any type of ram. The math wizards amongst you will be aware that $50000+$40000 = $90000, which is greater than 512k ($80000). This is fair enough, since by the time the game was published (1995), anyone who was still using an Amiga had 1mb or more of RAM!
Continue to disassemble a few lines, and you will see at $7807C some code that looks promising:-
07807C MOVE.L #$67616D65,D0 078082 LEA.L $00001000,A0 078086 LEA.L $0007CC00,A1 07808C BSR.W $000780BE 078090 TST.W $000780BC 078096 BNE.W $0007809E 07809A JMP.L $00001000
There are lots of clues here that this calls a loader. First of all, D0 is set to "game". A0 is set to $1000, and just a few lines later we see a jump to this address...currently $1000 has nothing there, so we can be certain A0 points to the load address for the loader. A1 is set to $7CC00, which at this stage we don't know what it means but I'd be willing to bet it's the MFM buffer address. We then see a familiar sequence: BSR something, TST result, BNE error. You can verify this by checking what the routine @ $7809E would do (the routine called if an error occured), and we see that it simply flashes the background colour and loops repeatedly if the loader fails here.
We would expect the loader to be @ $780BE based on the code above, but actually there's an intermediate step which we should take note of... disasm the code @ $780BE and you will see all the usual signs of a loader - it loops through a filetable @ $781A2 and compares to find the filename it's looking for, in this case "game" (remember, that was loaded in D0 earlier). Except if you look at $781A2 onwards using the "m" command, you will notice there is no filetable there! It must be loaded sometime, so we can guess that this is what the BSR $7817A call @ $780C4 does - let's quickly disasm that in the AR with "d 7817a", and this is what we see:-
07817A MOVE.L A0,-(A7) 07817C MOVE.L #$00000000,D0 07817E MOVE.W #$06DE,D1 078182 MOVE.W #$0002,D2 078186 MOVE.W #$0000,D3 07818A MOVE.L #$00000000,D4 078190 LEA.L $000781A2(PC),A0 078194 BSR.W $000787A2
Remember earlier we said that A0 was probably the load address for the loader? Recognise the address being loaded into A0 before that BSR $787A2 call? That's right, it's the address where the game is looking for the filetable. What a coincidence! Although at this stage we don't know what the other parameters mean, the ones we are interested in are D1 ($06DE) and D2 ($0002).
Because we know all about how amiga disks are structured (don't we?!), we can make an educated guess at this point. A brief recap for anyone not familiar with the details:-
A standard amiga disk has 80 tracks (0-79) on each side of the disk (0-1). A track consists of 11 sectors, each $200 (512) bytes in size. Therefore, a track is $1600 (5632) bytes long (11 * 512, of course). The size of a normal amiga disk is therefore 160 * $1600 (80 tracks lower side, 80 tracks upper side), which is a total of $DC000 (901120) bytes.
Now - if we have 160 tracks of 11 sectors each, that means we have 1760 sectors available on a disk. 1760 is $6E0 in hex - and now you see why I started rambling about disk structure. At this point D1=$6DE (2 sectors from the end of the disk), and D2=$2 (number of sectors to read). We also know from our attempt to copy the disk, that there is some custom data at track 78, so this also makes it likely that these are the parameters, and the filetable for the disk is stored immediately after the protected stuff, in the last 2 sectors of the floppy (giving it a max size of 1024 bytes).
For the final proof that we're correct in our assumptions, let's disassemble the loader a little bit (at $787A2):-
The number of sectors to load is added to the start sector, and then compared with the maximum possible number of sectors on a disk, so now we're 100% sure of the parameters. D0, D3 and D4 are still currently unknown, but we know that D1 is the start sector, D2 is the number of sectors to load, A0 is the load address. We're also quite confident that A1 is the MFM buffer address, but again we can easily check this by breaking in with the AR during/immediately after the first load and checking @ $7CC00 for MFM data. (Of course preferably we would disasm the loader and follow the code, however Rob Northen's loader (for it is his!) accesses everything as an offset from a variable base which makes it a pain to follow unless you're tracing it).
Let's get the AR to execute the loader and we'll check out the filetable once it's in memory:-
>>> m 10 00 07 80 00
>>> a c0 illegal >>> bs 78198 >>> g c0
What did we just do? We set the illegal exception vector to point to $78000 (where the start of the code is), we put an ILLEGAL instruction in memory at $C0 (this means when the illegal instruction is executed, the address @ $10 is jumped to in supervisor mode). Then we put a breakpoint immediately after the loader has read the filetable from disk, and started it all off by jumping to $C0.
Now we return to the AR when our breakpoint triggers, and we have the disk 1 filetable sitting in memory @ $781A2 to examine. Only, it's not much of a filetable at all, since it consists of only 10 bytes!
0781a2 67 61 6d 65 00 16 00 06 5d d4 game....].
We already know the first 4 bytes are the filename, how do we work out the other parameters? By checking what the loader does with them of course! Clear our breakpoint ("bda") and let's go back to the part of the code where it looks through the filetable for the filename stored in D0... this is @ $780D8 onwards.
OK. It loops through the filetable looking for "game" (which is good, since that's the only entry in the entire table!). Once that's found, it branches to $780FA and loads the other 2 parameters from the table - the 1st one ($0016) is treated as a word, the 2nd one ($00065DD4) is treated as a longword. The next few instructions tell us what these parameters mean, since D2 (the 2nd parameter) is then divided by 512 (the LSR #8,D2 and LSR #1,D2 instructions are shifting it right by 9 bits, the fastest way to divide it by 512). Coincidentally, 512 is the size of a sector, so this parameter is the filesize, and this code is working out how many sectors it will need to read to load the file (actually -1 sector, the game author then fixes this later!). The other parameter is the start sector ($0016, which is the start of track 2).
Note: If you're paying attention, you will have spotted that the game author made a mistake in his bootblock - the main game file starts @ sector $16 on disk, which is offset $2C00. However the bootblock loaded $2C00 bytes from offset $400 to address $78000, which is from $400-$3000 - $400 bytes more than he needed to (and actually loads the first 2 sectors of the main game binary for no reason)...
We know that the game simply loads this file and then executes it (with the JMP $1000 @ $7809A), so the easiest way to rip this file off is to place a breakpoint @ $7809A and let the game load it for us, then save it off to our first workdisk:-
>>> bs 7809A >>> x ...file is loaded... >>> bda >>> sm 1:game,001000 001000+65dd4
Now we have the main game binary in memory, let's take a look at it. Since we know the game will probably use the same loader as before, we can go back and look at the first few instructions of the loader @ $787A2 and see if there's a byte pattern to search for... sure enough, there is. The loader has a "LINK A6,-$24" instruction after the register save, so we check the opcodes for this and use them to find other loaders in the new game binary:-
That looks good, there are 3 possible loaders in the main game binary. The first of these was found @ $111A, so we search for references to $1116 (since we know the LINK opcode is the second instruction in the loader, and the first instruction is 4 bytes in size, $111A-$4 = $1116).
>>> fa 1116 1000
Good, only one call to $1116 is found, at $8d96. We start disassembling a few instructions before the call to see what is going on:-
Well, that's interesting. The parameters look very similar to the ones we saw earlier, however... D1=$750, and we were sure that D1 was the start sector. $750 isn't a valid sector number on a normal amiga disk. However, we know there is copy-protection on the disk (otherwise this would be the most useless tutorial in history), so this could be our protection check and therefore D1 might be a valid sector number in terms of the diskformat the custom loader is using internally. Looking further down, we see 2 compares checking something against the data loaded by this call. At $8D9E it checks for "jami" @ $6BC00, and afterwards checks for "e123" @ $6BC04. So - the protection check loads a protected track and looks for the string "jamie123" at the start of the data. Probably not a candidate for protection of the year! Incidentally if you let the loader do the protection check and look at the data, you'll see random garbage from the author's memory when he mastered the disk, including his drive labels etc. :)
To bypass the protection check, I just did the following:-
>>> a 1116 move.l #6a616d69,(a0)
move.l #65313233,4(a0) moveq #0,d0 rts
We just store the "jamie123" string where the protected track would be loaded, and return after setting D0=0 (which is the loader returncode for success). And that's the game cracked! At this point you could load up a diskeditor, hexedit your changes in, and copy the game freely. However, since we're (hopefully) not that lame, let's see if there is more we can do with the game.
If we let the game continue, we find that the first thing it does is ask for disk 2 in df0:. So we swap disks, but before we continue loading lets jump back into the AR and breakpoint on the other loaders that we found earlier.
>>> bs 58f0 >>> bs 3097c
Now we can exit the AR and continue loading, and one of our breakpoints should trigger. There it goes! It's the one @ $58F0. We can look on the stack to find out where the loader was called from, using the "m \a7" command. This shows us that the loader will return to $5366, so we start disassembling a few instructions before this. Take a look at what we find:-
It's almost identical to the code we saw earlier, so it seems the filetable for disk 2 is also stored in the last 2 sectors of the disk, only this time it will be loaded to $54F0. Let's take a look at the disk 2 filetable then, by clearing our existing breakpoints, and setting a new one when the loader returns:-
>>> bda >>> bs 5366
Let the game load the new filetable, and take a look at it with "m 54f0". At least this time there is more than one file on the disk...14 entries in fact. Also, if we keep scrolling down (remember the game loads 2 sectors for the filetable, that's 1k) we'll notice an extra filetable entry for "data", which has a sector start but no filesize! If we go back and check the disk 1 filetable (still in memory @ $781A2) we can see that it also has a special entry at the end of the filetable, this time for "qwak" and again with no filesize. These can't be files, therefore they must be the disk-labels to tell the game which disk is currently inserted...take note of that for later! (Incidentally "qwak" was another game by the same coder, Jamie Woodhouse).
We know the filetable format (4 bytes=filename, 2 bytes=start sector, 4 bytes=filesize), and we also know that disk 2 is unprotected so we can read all the data from the disk using the AR. Of course we could fire up our favourite assembler at this point, and write some code to step through the filetable and rip the files for us, but honestly there's no need since we can dump the files out from the AR in the time it would take to boot Asm-One :)
First, we need to read the entire disk into memory. In this example I have 2mb chipmem so I can safely load the disk starting @ $80000 - just be aware that you may need to change this depending on where your extra memory is located (you might need to read the disk in 2 chunks if you don't have enough memory).
>>> rt 0 !160 80000
Wait while all the tracks are read in - once that's finished we need to look at our filetable again, and save out the files to our 2nd workdisk.
We need to do the same as in the picture above for each of the files. Take care to allow for the filesizes of the last 2 files, "junk" and "save". All the other files are sector-aligned, that is the filesizes are a multiple of $200, but not these 2 so we need to be aware of that. We can see that "junk" starts on sector $69C and the filesize is $459, so we can do the following:-
>>> ?69c*200 (= $D3800) >>> ?80000+d3800 (= $153800) >>> sm 1:junk,153800 153800+459
Do a similar calc for the "save" file, and you should have all the files from disk 2 on your 2nd workdisk now. Now would be a good time to take stock of what we have...at this point, we know that disk 1 contains the following:-
- a 1k bootblock (otherwise, it wouldn't boot!)
- a $2800 byte chunk of code, loaded to $78000
- a 1k filetable (stored on the last 2 sectors of the disk)
- a $65DD4 (417236) byte file called "game"
and disk 2 contains:-
- 14 files, a total of $D4232 (868914) bytes
- a 1k filetable (stored on the last 2 sectors of the disk)
This gives us a total of $13C806 (1296390) bytes, ignoring the filetable and the 2 tiny files (for reasons I will explain later!). Anyone who was paying attention when I was talking about amiga disks knows we only have $DC000 (901120) bytes available on a disk, so that's not good news. However, we know that the "game" file from disk 1 wasn't packed (since we disassembled it as soon as it was loaded), and as it turns out none of the files are packed on the original - this IS good news :)
Time to try and pack the files, and see if we can get them to compress enough to squeeze everything onto 1 disk. For this tutorial I've gone with Imploder (IMP!), mainly because the decruncher is small and fast, and also because it's an in-place decruncher (ie: the src/dest are the same, so no temp memory is required for decrunch beyond the small amount of stackspace it uses).
Boot your favourite tooldisk, and let's put FImp to work (it's also included with the source code for this tutorial).
As you can see from the picture, the files are packing quite well and we're saving a significant amount of space. Once all the "b" and "m" files and the main "game" file are packed, we can compare:-
BEFORE:- 417236 (game), 270336 (m** files), 595968 (b** files) = 1283540 bytes
AFTER:- 237460 (game), 153628 (m** files), 277948 (b** files) = 669036 bytes
Now we should deal with something that has probably been bugging you since you saw the disk 2 filetable - why is there a file called "save"? If we look at the file ("lm 1:save,80000") we can see it's the hiscore table, so presumably if we get a new hiscore it would be saved over this file. Also on the main game menu screen, you will notice an option for "LEAGUE". Let's investigate this further by booting the game, and when it asks for disk 2 insert a copy of the unprotected disk 2. Then we choose league, setup a league and play a race, and the game tells you to press S to save. Do so, and then enter the AR and look at the filetable @ $54F0. Notice anything different? I can see a new entry in the filetable for a "leag" file, only $78 bytes long and starting on sector $6A2.
This might present a problem for our 1-disk version, since we need to include at least one extra entry in the filetable ("game"), so if the program doesn't look for the first free available space in the filetable to insert the "leag" entry, and just assumes that after the entry for "save" is a good place to put it, it could corrupt our table. An easy way to check for this, is to load the track with the filetable on it, modify it to insert an extra entry, and save it back to disk:-
>>> rt !159 1 80000 >>> m 8008c 61 62 63 64 06 A2 00 00 02 00
>>> wt !159 1 80000
Now the "leag" entry has been modified to lie to the game, and tell it there's a file called "abcd" at sector $6A2 with a filesize of $200 bytes instead. Then we exit the AR and start a new league and play another race...save our new league, and jump back into the AR and check the filetable. Phew! The game does indeed take note of the entries in the filetable, so our modified filetable wasn't overwritten.
Now it's almost time to put everything together to create our 1-disk version. First we need to take a quick look for some free space to put our decruncher - I typically put it in low memory 'under' the game, and if we look (using the "m c0" command in AR) we can see there is a lot of free space that we can safely use. Time to load the assembler!
The source included with the tutorial is fully commented, but for those of you just skimming through this text I'll recap the key points:-
- We make a filetable in the same format as the game expects (obviously, since we're not replacing the loader!). We just include the sector offsets and filesizes for our packed files instead.
- We modify the bootblock to copy a little crack patch to $100. This patch removes the protection check. We then patch the code loaded to $78000 by the bootblock (which in turn loads the main "game" file) to call our patch before jumping into the game. This is because we had already packed the "game" file in it's unpatched state.
- The bootblock also copies our decruncher to $200, and patches the main game loader to call our decrunch on exit instead of returning. The decruncher is therefore called whenever anything is loaded, but since it checks for the "IMP!" header in the first few instructions and returns if it doesn't find it, this isn't a problem.
- I chose to put the "junk" and "save" files in the same positions as the original disk 2 filetable, just in case anything in the game was hardcoded to expect them there. I suspect it wouldn't matter if we moved them, but why take the risk?
- Finally, we patch the filetable to include the "data" label from disk 2.
And that is the end! After some playtesting (saving some hiscores, create and save a new league, etc.) we see that our 1-disk version seems to work just fine!
There's also plenty of free space on the disk for your trainer, so you'd better get started coding...
- Wayne Kerr, January 7th 2008.
PS: Please leave some feedback if you read this tutorial. It's hard to know if anyone is finding the tutorials interesting or useful - I'm also open to criticism, if you think there is something I missed or could explain more clearly, let me know!
Filesize: 0KB, downloaded 79 times