ARM Code for beginners-- part 9

Protection for the uninitiated





    One of the most interesting features of a commercial arcade game these days, from an ARM coder's point of view, is the way in which the disc protection is implemented.  For it to be successful, we need to write and hide a section of code which evades any attempts at reverse-assembly while still being able to keep compatibility with future operating systems.  What we won't be doing is direct hardware access due to the variety of disc controllers used in Acorn's machines.

Floppy disc layout

    Since most 'protected' discs can easily be copied by a sector copier such as Investigator or the legendary Xcopy (Amiga), I'll only concern myself with a disc format that won't be copied by the usual RISC OS *Copy or *Backup commands.  So, as far as we're concerned, a floppy disc is laid out by the Filecore module to be a continuous 800k stream of data; the details of tracks, sectors and heads need not bother us.  For the user's convenience, there is an area on the disc called the map, which indexes where each file is on the disc.  When you write a file to the disc normally, RISC OS looks for some free space in the map, marks this free space as 'used' and writes the data into the blank area.  So, if we write some data onto the disc with ADFS_DiscOp (see box-out), without updating the map appropriately, we have data which looks like free space on the disc but is in fact essential to the running of the game, and which won't be copied by *Copy or *Backup.
 
 

Cracking the code

    To play with it, run the !MakeDisc obey file on the cover disc with a blank, double-density disc in the floppy drive (or one you don't mind being wiped).  Then open it up from the Filer and run the example game, !Shoddy.  Now examine the disc; all you'll find is the !Shoddy folder and a short, garbled piece of code as the !Run file.  What's actually on the disc is shown in the diagram; the job of the !Run file is to fetch and execute the more involved 'game code' off the disc.  Notice the use of OS_GetEnv to read the 'upper RAM limit'; as well as being used to find out whether we've got enough memory to play with, we can use the top 8k as a stack for our program by simply subtracting 8192 from the pointer passed in R1 (the RAM limit) and using this as our R13.

    The other preventative measure to stop hackers in the !Run file is to encrypt the code which executes the next piece of code off the disc.  We now have three pieces of code to think about:
 

    This is done by taking each instruction of the code and EORing it with a 'key' (see the 'Garbage in, garbage out' box if you're unsure of the implications of this).  This makes part of the code look like unreadable garbage if a hacker tries to disassemble the !Run file directly (with Zap, for instance); only the 'decryption code' will be directly visible.  The next thing to do is to make sure hackers can't alter our 'decryption code' so that they might be able to pause it and save out the decrypted 'loading code'.  This is done by using the 'decryption code' as our key!  To explain: each word of the 'loading code' is EORed with each word of the 'decryption code' in sequence before being written to the disc.  When it comes to decrypt the 'loading code' again, if the 'decryption code' is altered in any way (by a debugger inserting a breakpoint, for instance), the 'loading code' will decrypt with the wrong key and will be corrupted.  This strategy isn't implemented nearly fully enough here, since it is a trivial matter to rewire the code, but it is food for thought.

    Our other anti-hacker tactic is to set OS_CallAfter to call a piece of code which crashes the machine, after 20cs.  This is so that if the decryption code takes longer than expected to execute (that is to say, if some logging or hacking software is watching what is going on), the machine will simply crash.  A time of 150cs is sufficient to stop any debugger before it can present its findings to the potential hacker.

How do they do that?

    That's the protection from a hacker's point of view; writing this sort of system to a disc isn't as precarious as it sounds.

    Each time you make a copy of the game with !MakeDisc, you'll find it is encoded with a serial number, which is simply a random word generated from the current time and the BASIC RND function.  This is used as a seed for our random number generator, which is used in turn to generate successive 'key' bytes to encode the sound module and loading screen.  The key is converted to a readable representation and used as the title of the disc; this doesn't give away much about the encryption and serves as an example of how to implement a quotable 'serial number' for the customer; obviously if you were to use this as a serial number, random numbers would need to be substituted for something a little more ordered to avoid duplication.  The game code then reads and interprets the title of the disc to find the seed to decode its resources.
 
 
Garbage in, garbage out

EOR is an instruction commonly used for simple 'encryption' purposes, since it has the pleasing property that any number EORed with another produces garbage, but when EORed with the same number again gives the input number back again.  It works on a bit level like this:

  0 EOR 0 = 0
  0 EOR 1 = 1
  1 EOR 0 = 1
  1 EOR 1 = 0

    The game code also shows how to 'hide' modules and execute them from memory rather than *RMLoading them.  Once we've loaded the sound effect module into memory, OS_Module 10 is called to link it to the module chain, and we can call the SPlay_Howl command to hear the noise.

Chinks in the armour

    The system is a long way from perfect.  Most protected discs use a non-standard disc format, or leave some tracks unformatted, or feign defects where there is useful data.  However clever the disc format, they can nearly all be foiled by a suitable copying program, which is why I've ignored this problem.  As for hacking the disc protection off (a more 'satisfactory' solution from a hacker's point of view), our system makes it quite inconvenient seeing as no actual files are dealt with, only data.  I've seen a way to get around every detail of this protection scheme, just by explaining it, but remember that disc protection will always be hacked by somebody and a line has to be drawn.  A hypothetical 'perfect' hacking tool would be able to record a sequence of ADFS_DiscOps when the game loaded the first time, then intercept them and 'play back' the correct responses to the program.

    The best way of looking at this system is as a curiosity with some useful ARM coding tricks in.  Protecting copyright like this will always cause hassle for the user and will eventually cause incompatibility with future operating systems, and disc protection by its very nature cannot be made entirely 'legal' (as far as as Acorn are concerned) without being ineffectual.

Useful SWIs used this month

SWI "ADFS_DiscOp"
  used here to read and write directly to a floppy disc.

Entry: R1 = 1 to read, 2 to write
 R2 = disc address (0-819200)
 R3 = memory address of data to read / write
 R4 = number of bytes to read / write
 Exit: R2 = R2 + R4 and R3 = R3 +R4

(this is by no means a full documentation; the StrongHelp manual and PRMs are the best resort here)

SWI "OS_ReadUnsigned"
  converts a number in ASCII form to a value; like VAL in BASIC, but a little more flexible.

Entry: R0 = number base (anything from 2-36; 16, hex, used here)
 R1 > string to convert
Exit: R2 = value