Rayman no-CD patches with DOSBox debugger

I’ve decided to tackle the CD protection of the various Rayman DOS versionsRayman, Rayman Designer/Gold, Rayman By His Fans / Par Ses Fans and Rayman 60 Levels / 60 Niveaux Inédits. All these games utilize a basic CD check that requires the correct CD to be in the drive, which is partially for copy protection, and partially because CD audio is used for the music.

My goal in CD cracks such as this is to remove CD protection, so that I can play the game if I’m too lazy to go get the CD, but at the same time I want to be able to enjoy CD audio if the CD is in the drive (or an image is mounted).

Background

I started with the technique described in my DOSBox debugger writeup – stepping through relevant portions of the code in parallel in two environments – one with a CD mounted and one without, noticing branching differences and patching the program to ignore them, so that the “no CD” case behaves like the “CD present” case. All the Rayman games use the PMODE/W DOS extender, which presented a few additional challenges. First, the DOSBox debugger fails to break on a MSCDEX real-mode interrupt (int 2f, AH=15) – an issue I did not experience with games that used DOS/4GW. This is not such a big deal, since it is always possible to set a breakpoint on the protected-mode interrupt wrapper – int 31h, AH=03, AL=00. (this requires an SVN debug build of DOSBox, since the official releases only support AH as a parameter to bpint). To know that you’ve hit an MSCDEX interrupt, and not a more common 21h, check that BL=2Fh when stopped at the breakpoint. After capturing such an event once, it is best to set a breakpoint on the specific instruction, to avoid going through all int 31h instances in the future (e.g., offset 0860:80237 in the image below).

The second issue is that the executables are compressed. This does not affect real-time debug, as the stub decompresses them in memory. However, once you find which instructions to change, you will not find them in the compressed program. PMWUNLIT or PMWUNLIC should be used to decompress it first (the two programs produce slightly different output binaries, which still appear functionally identical).

Initial analysis

All Rayman games use multiple CD checks. This is evident by different messages in case there is no CD drive at all, compared to wrong CD case. Starting with Rayman by His Fans (no particular reason), within a couple of sessions I found some conditional jumps that could be replaced with unconditional ones or with NOPs (depending on the desired logic) to get the game to bypass both checks and load the main screen. However, I could not easily find the relevant code in Rayman Designer and 60 Levels, so I kept looking and found what appeared to be a more elegant solution: a specific function call that can be skipped entirely to completely bypasses both checks. but without preventing CD music from playing, if a CD is present. An extra bonus: it bypassed a corner case where if a CD drive is present but not disc/image is loaded – instead of simply erroring out, the game would freeze in an infinite loop (this seems to be a DOSBox issue; on a real DOS PC the game would eject the CD tray at this point, so I surmise DOSBox simply does not implement this call, which confuses the game).

The approach above was easily translatable to every Rayman/Designer/Gold/By His Fans/60 Levels – because the specific code is unique and easily discovered in the hex dump – it is a sequence of 3 function calls one after the other, followed by a a single condition test and a jump (test al,al ; je +0xa). The function call offsets differ, but the test and jump length are fixed. Skipping the last of the three calls, and the subsequent test/jump works to bypass the CD check. The image below shows two live debug sessions side-by-side (patched executable is on the right). call +4E980 is replaced by jmp +7 and 3 nop instructions (to keep the program size fixed).

A more robust solution

Alas, my happiness was short lived. It turned out that this approach allows the game to work and play music off any CD, and it also allows the main menu to load even if there is no CD / no CD drive, but in such cases it still detects that there is no disc and puts the games into “limited mode” (saved games don’t work, watermark is present over screen and the game exits back to DOS after a single level).

Back to the drawing board… I’ve decided to try a different idea – to investigate and old cracked copy of Rayman 60 Levels that I’ve had for more than two decades. I don’t remember where I got it, but it worked fine with or without the CD and could play CD audio. The cracked executable was recompressed, and when I decompressed it, the size was different compared to any official version of the game I’ve had, so the offsets did not match directly. To overcome this, I’ve used, Beyond Compare‘s binary comparison feature to align the identical portions of the files, and the relevant changes were visible, fortunately limited to just a handful of instruction changes in a few offsets.

Analysis of the patched instructions showed – a few jz/jnz replaced with jmp, one jz replaced with nops, a couple of places where a mov byte [offset] instruction had the value change from 1 to 0, a conditional register set (sete al) replaced with unconditional (mov al, 0x1), and one place where an and eax, 0xff was replaced with a jmp to the end of a function, skipping a large portion of the logic. Nothing unusual from a patch of this sort, where the goal is to either bypass certain checks or change register/variables to make the program think the checks succeed even if they don’t.

Porting the patch to other spinoffs

Replicating these changes in the official version of 60 Levels was a trivial task. Other spinoffs needed some more fiddling, because as you expect – both absolute and relative offsets change, so the jump opcodes or memory references are slightly different. The disassembly command of the DOSBox debugger (again, SVN builds only!) helped here. The syntax is DA [segment]:[offset] [length] [file]. By examining the resulting assembly, I was able to find the relevant code patterns in all versions of the games (those that I own, at least). For Rayman Designer/Gold, the code was almost entirely the same as 60 Levels, down to the relative jump offsets. The original Rayman, and Rayman By His Fans required me to recalculate of the offset for the jump to the end of the current function, and search a bit longer for the place where the bit had to be flipped from 1 to 0. This does get easier as you practice reading assembly code.

Another advantage of looking at the disassembly, is that I could improve on the original patch I, by reducing the number of changes as some of them turned out to be unnecessary. The final versions of my patches leave only the changes that I saw were mandatory to achieve correct operation in all of the following cases:

  1. No CD driver/MSCDEX loaded at all
  2. MSCDEX is loaded but no CD drive letter exists / no image is mounted
  3. Empty CD drive
  4. Non-Rayman disc is inserted without audio tracks
  5. Non-Rayman disc is inserted with audio tracks
  6. Rayman disc is inserted

The expectation is that in cases (1)-(4) the game works with no music, whereas in cases (5)-(6) it plays the appropriate CD tracks.

Cracking the original Rayman

This one turned out to be a bit harder, as in addition to the same batch of changes required for the spinoffs, another preliminary check is required to handle cases (1)-(2). Locating the code was not hard, again with the help of the debugger, but an extra trick was needed: the return value of one function had to be flipped from 1 to 0, otherwise case (2) would cause a hard CPU lock whenever a level was started (this was in DOSBox, I am not sure what the behavior would be on real hardware). The process is demonstrated by the screenshot below (again, patched executable is on the right).

The routine at 0860:69C1C sets al to 0 for case (1) and to 1 for any other case. Intuition suggests to change jne +0xf to nops, as this would allow code to continue regardless. However, copying 1 from al to the address at [1B0A29] causes the hard lock issue later on, whereas no such problem occurs when 0 is copied. The simple solution is to replace the jne +0xf instruction with mov al, 00 so that case (2) proceeds exactly as case (1). Two things work in our favor here: the old and new instructions are of the same 2-byte length, and this code does not affect other cases, when the CD drive is present. In the end, it seems that the crack is fully working.

I was doing my initial work on Rayman US v1.21, and once completed, wanted to port it to other versions that I had copies of – Rayman EU v1.12, US v1.12 and FR v1.21.

My copy of US v1.12 is completely unprotected (it even says so on the startup banner) and runs great without the CD and without any cracks. The one exception is that it will freeze in an infinite loop if you run it in DOSBox, with an empty CD drive mounted. This is because the game actually tries to open the CD tray at this point – a function DOSBox apparently does not implement.

For FR v1.21 and EU v1.12, the patch offsets moved slightly, but other than that they were the same. FR v1.21 worked right off the bat, but EU v1.12 proved a tough pickle.

The special case of Rayman EU v1.12

The first bizarre problem occurred when decompressing the executable. The resultant program exited immediately after displaying the startup banner, with no error. Through parallel debug of the compressed and uncompressed programs, I located a single byte-change (jz→jmp) that works around it. For a long time I believed it was a bizarre decompression artifact, until cps2x discovered on the RaymanPC forums that it is simply another protection, which aborts if the name of the program is not RAYMAN.EXE (I always kept the original executable intact and used different names for patched versions).

The bigger problem was that the cracked seemed to work at first, but when certain levels were loaded, it exited to DOS with the “Thank you for playing Rayman” message. Moreover, it would do that even with the correct disc in the drive. I would think my crack was broken, if it hadn’t been working just fine in the other versions. Comparing the debugger printouts between the cracked and original EU v1.12, I suspected “foul play” when I saw that the original program attempted to open the intro/conclusion video files on the CD right before loading the level, even though they were never supposed to play during this part. My cracked version also attempted to open files, but the filenames were empty. That explains why it crashes even with a legitimate disc. Somehow one of the skipped routines must have been responsible for setting up the correct filenames. But why are they needed here? It stumped me for a bit, but then PluMGMK on the RaymanPC forums confirmed that this is an intentional extra protection layer. Well, in that case, it should be relatively straightforward to patch out without breaking the game.

To locate the code that accesses these files, I trapped the DOS file open interrupt in the DOSBox debugger (bpint 21 3d). Since the game frequently opens files, the best technique to avoid hitting the interrupt too often, was to break in with the debugger (Alt+Break) right before loading the level, and enable the breakpoint only at that point. Once hit, it took several sessions of trial and error to figure out when the call stack exited from the general file access code and into the “business logic”, and eventually all the way into the “main” function of the game, which I was able to recognize due to the proximity to the early CD check I encountered during the early analysis. The basic code flow is something like this:

// various stuff in main()
...
// these three routines handle the early CD checks during startup
0860:1624E  E80D2C0500         call 00068E6A ($+52c0d)
0860:16253  E8282B0500         call 00068D85 ($+52b28)
0860:16258  E8032D0500         call 00068F60 ($+52d03)
...
// lots more code - loading the main menu, game map, etc.
...
// this call is executed during level open and opens various files
// including the video files used for copy protection
0860:163DE  E879E00100         call 000342D6 ($+1e079)
...
// some more code...
...
// this call either loads the level or quits to DOS if cd check fails
0860:164D1  E8FAE8FFFF         call 00014B57 ($-1706)
...

The protection routine tries to open the files and sets some variables depending on the outcome. Then the final level load routine checks the values of the variables and either proceeds or exits. There are a few such variables and any one of them having the “wrong” value ends up terminating the program. To make things more annoying, I could spot similar code in several places (since some of the files are actually being opened to be used). Fortunately, following the debugger output on which files were being accessed, I could precisely locate the call to the function that was checking the video files. Because the variables in question are used as ‘error flags’, bypassing the function entirely simply leaves them at a valid (non-error) state, and allows the game to proceed. This saved me from a messy ordeal of skipping multiple checks / replacing multiple memory values.

Post-mortem

At this point, my task was finished and the crack was working. Out of curiosity, I determined that the protection routine is triggered every time a level with a number 7 or higher is loaded, from any game world, but the first. This suddenly makes the code below very clear:

Starting from offset 0860:345C1, first the variable holding the world number is compared to 2, and then the level number is compared to 7. If any of the numbers is lower, the function call at 0860:345D5 is skipped; otherwise it is executed. This is the function that tries to open the video files. In this case, the world is 5 and the level is 9 (PCMAP\cave\RAY9.LEV), so the protection routine is triggered. On the left side, the uncracked executable attempts to access the correct files, whereas on the right there is no filename, so the routine would fail.

The check can be bypassed either by changing one of the jl‘s to jmp, or by changing the call to nop‘s. The patch I chose is simply to patch the first instruction of the function being called (at 0860:6925C) to ret, so that it returns immediately. Funnily, this is exactly what was Ubisoft’s programmers have done in v1.21: the routine is still there and is still being called for every level>=7 in world >=2; it is simply empty and does nothing.

Post-post-mortem

Ironically, my habit of renaming the modified files and leaving the originals, which caused me to hit the first-tier protection of EU v1.12, also caused me to miss another protection, which is triggered right before the main menu is displayed, and checks that RAYMAN.EXE in the game directory has not been modified (if this check fails, DOSBox freezes; I haven’t verified behavior on real hardware yet). Once again, it was cps2x who encountered the problem as well as the fact that it has been brought up before. With that knowledge, locating the offending routine was not difficult. Fortunately, it is called directly from the main procedure and can be skipped entirely (like the aforementioned video check) without any negative side effects.

Patch offset table for known versions

  • Both original and uncompressed file sizes are shown. The executables must be decompressed before patching. It is possible to recompress them later with PMWLITE, but this serves no purpose other than saving a few hundred kilobytes.
  • RAYKIT.EXE (Rayman Designer/Gold) has several different versions listed in the table, but the patch offsets are the same for all of them.
  • The offsets shown are only those that must be changed. In most cases they refer to parts of instructions, not complete ones. For example:
    • Various jne (75) replaced with jmp (EB) to the same offset.
    • All instances of 01→00 changes are arguments to a mov byte with a 32-bit displacement (C6 05).
    • In all cases where an and eax (25) is replaced with jmp (E9) – the top 2 bytes of the arguments are always zero, so only the bottom two are changed.
  • The table is a picture, because I am fed up with WordPress’s poor table formatting facilities.
Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s