Rubicon is one of the most protected videogames, code-level and track-side, in the history of the Commodore 64. Released in 1991, the disk version of this game seemed to be very difficult to crack even for the most advanced and prolific crackers of the scene. Flavio Pasqualin (Flavioweb, a member of the Italian Hokuto Force Cracking Group) wanted to experiment with this videogame, succeeding in analyzing the code, so to spot all the "diabolic" protections included during the programming. This article is highly technical and therefore mostly addressed to expert programmers and code breakers. However, we also recommend this reading to the less experienced ones, if anything, to have a taste, albeit mild, of some protection and cracking techniques. Besides the complexity of the subject, the article has the primary purpose of documenting the modus operandi adopted by Flavio in effectively decipher the code of what has immediately appeared an extremely intricate maze of protections.
How is Rubicon disk version protected?
In this text I will outline the inner workings of the protections applied by Siefried Jagott (aka Sigi, Snaky / GP, SJ / AMOK) to the game Rubicon, published in 1991.
I will write useful information to crack it, but without going too much into details, because only the description of the protection applied in the boot step (Timex 3.0 plus SJ code) would require a whole text alone, in addition to the fact that this text is not intended as a tutorial on how to crack Rubicon.
To get started, it can be enough to know that the boot protection consists of two parts:
1 - a code fully encrypted via a timer;
2 - the check of a particular track on the disk (track 18, sector 18) which, as it turned out, can't be rewritten using a normal CBM drive, thwarting attempts at copying the disk with Burst Nibbler or Fast Hack'em with its parameters.
I will not delve into details of what could happen here, since this first part of the protection is not a real problem for cracking the game.
One of the first real problems to deal with is the fact that almost everything the code does and the data handled when booting are coded. The coding method used can be defined "timed": it is based on the exact values that the CIA counters must have in an exact moment during the execution. These values are used to decrypt the code that will be run from that moment on.
The problem that this approach causes to a cracker is that the drivecode loaded into the drive to handle all Input/Output from the disk is coded, and therefore it is not visible nor can be intercepted. The drivecode is required, along with the part of the C64 resident loader, to access and extract files to disk.
How can the drive code be extracted?
It is not possible to review the code with the Action Replay for two reasons:
1 - Decrypting the code is based on timing, so freezing it, even only to examine the memory content, will crash the machine.
2 - The code is stuffed with illegal opcodes that make it unreadable if disassembled.
There are two possibilities:
1 - Rewriting all the routines, step by step, to decode the various sections so to get to where the code uploads the drivecode, avoiding to send the "M-E" command and leaving the drive in a state in which it is possible to analyze the RAM and extract the code. To achieve this, we have to juggle between illegal opcodes and the I/O managed with the use of "mirror" registers and not through the "official" CIA locations. At best, it will would require a few months of work!
2 - Creating a modified ROM version of the 1541 disk drive, "burned" on an EPROM and used only to load this game up to the "start" point of the drivecode. This "custom ROM" must be identical to the official one, except for its initial checksum (so to allow the modification the portion of the needed ROM only) and for the portion of code that runs the command "M-E" when sent from the C64 to the disk drive.
As you can imagine, I chose the second hypothesis.
Obviously, with this kind of ROM mounted on the drive, the game cannot work... but this is not what we are interested in. When the loader code is started, we want the drive to ignore the "M-E" command and continue with its "normal" operations, waiting for further commands from the C64.
At this point, we can use our trusted Action Replay to freeze the game and analyze the RAM of the drive, having the string "M-E" at $0200, followed by the start loader address ($04B3 in this case). Here we can save the drivecode to disk, print it, or do whatever we want with it. I chose to print it to "transform it" into compilable source code, usable in the next cracking phases.
At the time of the "crash" we also save the memory from $CEA0 to $CFFF. That portion of code loads the "large file" that occupies almost all the RAM except the $CE00-$CFFF area. This code also loads the ZP (Zero Page) and the Stack (from $00 to $1F used for return addresses and values taken with PLA during the early boot phases). After saving this file, writing an ad hoc code it is possible to load the drivecode into the drive and run this routine from $CF00 with the original floppy disk in the drive. At the end of its execution (which corresponds to an RTS command at $CF62), we will have all the necessary files for the game boot, including the initial zero page (to be saved).
Now we need the "C64-side" loader.
The easiest thing to do when looking for a loader is to check if there is any reading/writing in $DD00. Whether running the AR (Action Replay) search command, or starting to disassemble the memory perhaps from $0100, you soon get to:
.> 0120 4c fd 01 jmp $01fd
.> 0123 ad 00 dd lda $dd00
.> 0126 29 0f and #$0f
.> 0128 8d d6 01 sta $01d6
.> 012b 49 10 eor #$10
.> 012d 8d c7 01 sta $01c7
.> 0130 49 30 eor #$30
.> 0132 8d 70 01 sta $0170
.> 0135 49 b0 eor #$b0
.> 0137 8d 9d 01 sta $019d
We should immediately suspect we are close to a loader.
This portion of memory seems to end with:
.,0338 PROTECTED BY SJ/AMOK 10/1991 !!@
Ok... I would say we officially have a loader.
How will this loader work?
Let's load the game until the intro is displayed and let's check if there is any call to $0120:
.h 0000 ffff 20 20 01
050b 0515 063f
We have found it. Well done!
.> 0506 78 sei
.> 0507 a2 30 ldx #$30
.> 0509 a0 33 ldy #$33
.> 050b 20 20 01 jsr $0120
.> 050e 2c 11 49 bit $4911
.> 0511 a2 30 ldx #$30
.> 0513 a0 34 ldy #$34
.> 0515 20 20 01 jsr $0120
.> 063b a2 30 ldx #$30
.> 063d a4 d1 ldy $d1
.> 063f 20 20 01 jsr $0120
The files are read from the disk using a pair of values in X and Y.
At this point, it is possible to assemble a code that loads the drivecode previously saved in the drive and running this loader to load our files. We would soon notice that the loader does NOT report errors for any X/Y value we can use... as if the files to load on the record were infinite. So what can we do to understand which files are needed for the game to work?
Once again, we have two ways: to analyze the code in the RAM, or to use the Action Replay in order to place a Break Point (BRK command) at $0120 and start playing the game, marking the X/Y values for each call (the BP should be renewed for each file).
First in-game protection.
We freeze the game, set the breakpoints, get out of the freeze, re-freeze and everything seems to work properly... until we lose a life! Instead of restarting, the game crashes in strange ways.
In 1991, this thing would probably have let us down, making us wonder about possible incompatibilities with the cartridge or perhaps other mysterious problems. Today we know that we are probably dealing with an in-game protection. Usually the anti-cartridge protectors use a NMI timing (Non Maskable Interrupt) to check if a cartridge is running. How? By starting a Timer A and Timer B lag of few cycles more than the four classic of the opcode STore, and periodically controlling that this lag remains constant. Why does the lag change when freezing the game? The answer is simple: because the cartridge, when leaving the freeze, resets the counters to their initial value and starts them with a number of cycles at a very variable distance.
Therefore, we know we have to look for portions of code that read at $DD04, $DD05, $DD06 and $DD07 and, somehow, compare the values to check for the lag. The search for $04/05/06/07 $DD does not return good results, but before giving up, let's take a general look at the memory to see if there is anything that can look like a "check" on the difference of value of 2 pairs of bytes.
We do not have to go too far from where we are to see:
.> 08db ac d0 11 ldy $11d0
.> 08de b9 ae 11 lda $11ae,y
.> 08e1 85 30 sta $30
.> 08e3 b9 ca 11 lda $11ca,y
.> 08e6 85 31 sta $31
.> 08e8 b9 af 11 lda $11af,y
.> 08eb 85 32 sta $32
.> 08ed b9 cb 11 lda $11cb,y
.> 08f0 85 33 sta $33
.> 08f2 a2 00 ldx #$00
.> 08f4 ac 12 d0 ldy $d012
.> 08f7 d0 fb bne $08f4
.> 08f9 b1 30 lda ($30),y
.> 08fb 85 34 sta $34
.> 08fd b1 32 lda ($32),y
.> 08ff 85 35 sta $35
.> 0901 ee 06 12 inc $1206
.> 0904 e8 inx
.> 0905 e0 14 cpx #$14
.> 0907 f0 0c beq $0915
.> 0909 a5 34 lda $34
.> 090b 38 sec
.> 090c e5 35 sbc $35
.> 090e a8 tay
.> 090f 88 dey
.> 0910 d0 e2 bne $08f4
.> 0912 8c 06 12 sty $1206
.> 0915 20 c4 07 jsr $07c4
A quick look at $11d0: $20
$11AE + $20 = $11CE = $04
$11CA + $20 = $11EA = $DD
$11AF + $20 = $11CF = $06
$11CB + $20 = $11EB = $DD
We do not even need to set a Break Point. Moreover, we know that between reading the first register and reading the second one there are 8 cycles. Consequently, starting the counters with the same value, but with a distance of 7 cycles from each other, we will always have one as a result of the difference between the two. In order to know the starting value to which the counters must be set, we can always use the Action Replay:
.-d000 fe 00 fe 00 fe 00 32 79
.-d008 98 82 36 72 98 ab 98 c0
.-d010 3f 1b f1 9d 82 ff d6 00
.-d018 85 f1 f1 00 ff 00 ff ff
.-d020 f0 f0 fc fb f3 ff fb fa
.-d028 fa fa fa fa fa fa fa 00
.-dc00 7f ff ff 00 03 23 ff ff
.-dc08 00 00 00 81 d0 7f 01 08
.-dd00 82 ff 3f 00 7f 7f 7f 7f
.-dd08 00 00 00 91 00 7f 01 01
We can easily see that memory locations $DC04/$05 have to be initialized at $03/$23, while $DD04/$05/$06/$07 at $7F/$7F/$7F/$7F, keeping in mind that the two counters must be started with a difference of 7 cycles, from the first to the second.
We will need this information to get a working crack.
Just to be sure, while studying the code, it is enough for now to force the value $01 into A, instead of having the code performing the subtraction.
Let's retrieve the files.
It is time to dump the files from the original disk and save them to be reprocessed in the crack.
Again with some "ad hoc" code, we load the drivecode into the drive and the loader in the C64 RAM, calling the loader at $0120 with the file values to load stored in X and Y. Each "pointed" file from a pair of X/Y values is actually a batch of multiple files. At different times, the loader stores the start and end address in $7A/$7B locations for each file in the batch that is loading. As the files are loaded, all we need to do is to save them on another drive, giving each file a name to indicate which batch it belongs to, and the job is done.
The easiest solution to obtain a working Rubicon crack is to make the smallest possible set of code modifications! The various in-game protections are intended to control both some values set during the I/O area boot, and some value locations in the loader area in the C64 RAM. There are also some other checks performed on some portions of code to verify that the game has not been modified.
A small example: setting the $DC04/$05 timer is required to manage the behavior of the enemies. An incorrect setting of those two locations will cause a different behavior of all the enemies encountered during the game. Not that the game stops working, it will only behave differently from the original.
This text is not intended as a full tutorial on how to crack Rubicon on Real Hardware. It just acts as a starting point for those wishing to venture into its code.
End of communication!
Happy hacking =)