Cached at:
10/11/26, 12:35 AM
# Getting old Macromedia Director Games to run on modern Hardware | WerWolv
Source: [https://werwolv.net/posts/macromedia_copy_protection/](https://werwolv.net/posts/macromedia_copy_protection/)
## [Introduction](https://werwolv.net/posts/macromedia_copy_protection/#introduction)
When I was a kid in the early 2000s, I spent a lot of time playing games that I borrowed from our local library\. Many of them were German point\-and\-click adventure games, since those were the games that ran on my dad’s Windows 95 computer\. Even back then, I was interested in tech and always tried to figure out how I could keep playing these games after giving them back to the library, but I, of course, never managed to do it\. A few days ago, I stumbled across one of those failed attempts again: a set of CDs onto which I tried to burn a copy of “Findus bei den Mucklas\.” I figured that maybe now was the time where I finally have the skills to do it\!
## [Getting a Disk Image](https://werwolv.net/posts/macromedia_copy_protection/#getting-a-disk-image)
Obviously, the CD I burned back then was unusable, both because CD burners at the time often weren’t able to properly read or write copy\-protected CDs and because I haven’t had a computer with a disc drive in the past 10 years\.
disappointment\.pngThankfully, some helpful soul uploaded a set of full disk images for that and all the other games in the series to[archive\.org](https://archive.org/details/cd-01_202304)\. It’s not piracy if it’s on there, right?
## [Unpacking the Image](https://werwolv.net/posts/macromedia_copy_protection/#unpacking-the-image)
The game came as a set of three files: a`\.ccd`, a`\.cue`and a`\.img`file\. Those are apparently produced by the old[CloneCD program](https://en.wikipedia.org/wiki/CloneCD)and contain the raw data from the disc, plus additional out\-of\-band information such as track metadata\. Games from that time often came with audio tracks as well, so you could just pop these CDs into a CD player and listen to the soundtrack or other bundled media\.
Looking at the`\.cue`file shows that there’s aMODE1 trackat the start containing the game files, followed by four`AUDIO`tracks:
```
FILE "CD01.img" BINARY TRACK 1 MODE1/2352 INDEX 1 00:00:00 TRACK 2 AUDIO INDEX 1 45:08:37 TRACK 3 AUDIO INDEX 1 46:38:44 TRACK 4 AUDIO INDEX 1 47:08:50 TRACK 5 AUDIO INDEX 1 47:11:58
```
To access the data, I just used the old`ccd2iso`tool, which failed when it hit the first`AUDIO`track but was still able to convert the start of the file into a regular ISO that could be mounted\.
## [Copy Protection 1](https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-1)
Mounting the file and trying to copy the data over, I ran into what I believe to be the first copy protection measure:
Duplicate files popup while extracting the archiveSome of the files seem to be in the disk image multiple times: once as the real copy and once as a second copy that was either 0 bytes or 256 bytes long and contained random garbage\. My guess is that they did that on purpose to trick some tools into displaying only the fake decoy copy of the file instead of the real one by creating deliberately malformed directory entries on the disk\. Windows seems to be able to always read the correct file here \(otherwise it wouldn’t work there either of course\) but with the different tools I tried, each one of them showed a different combination of files in the end\.
Using a combination of`PeaZip`,`Ark`,`Dolphin`and`7z`, I ultimately managed to extract the right files from the ISO, but it was mostly manual labour: choosing the files that were the right size and skipping the decoy ones\.
Final files that were extracted from the ISO imageAt this point, just running the installer seemed easiest, and it surprisingly worked\. I ran it using`WINEPREFIX=$\(pwd\)/prefix wine "Installiere Findus4\.exe"`to make it install into a separate Wine prefix, from which I could copy the installed data afterwards\.
## [Copy Protection 2 \- Electric Boogaloo](https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-2---electric-boogaloo)
Executing`Findus4\.exe`now, of course, didn’t just work, but instead showed a nice error popup saying that the CD was missing\.
Missing CD Error popupSimply searching for that message using`rg \-a "Lege die CD"`showed that they just read it from the`Pettson3\.ini`file\.*They apparently didn’t even bother renaming that file from the previous game\.*
```
[CDROM]CDROM=?[GAME1]Vind=0[ALERT1]MESSG=Lege die CD Findus4 ein und versuche es noch einmal.[SCREEN]FULLSCREEN=1
```
Searching for a few strings ultimately led to`messg`being found in`Findus4\.exe`\. My first instinct was, of course, to just throw the binary into Ghidra and start looking for it\. Surprisingly, the string didn’t show up anywhere using the`Search \-\> For Strings\.\.\.`option, which usually means that the string isn’t actually part of the PE data but some sort of extra payload appended to the end of the executable file\.
After some more digging, I found that these games are all`Macromedia Director Projector`executables\. These were generated by Macromedia Director and basically contain the whole runtime for interpreting Director files, with a`\.dxr`file concatenated onto the end\. That file is what gets executed when the program runs\. It made sense that the CD check would be part of the embedded Director file, because why would the runtime read from a config file called`Pettson3\.ini`?
During my research, I stumbled across[ProjectorRays](https://github.com/ProjectorRays/ProjectorRays), which is a disassembler and decompiler for exactly these files\. They have the file signature`RIFX`or`XFIR`, depending on the endianness of the target system, which makes them pretty easy to identify\. Using ImHex, I found a few occurrences of those strings in the binary\. Based on the ProjectorRays source code, the format of these`\.dxr`files starts with something like this:
```
enum RifxType : u32 { MV93 = le u32("MV93"), MV95 = le u32("MV95"), FGDM = le u32("FGDM"), FGDC = le u32("FGDC"), APPL = le u32("APPL")};struct Content { type::Size<u32> length; RifxType type; // ...};struct DirectorFile { char magic[4]; if (magic == "RIFX") be Content content [[inline]]; else if (magic == "XFIR") le Content content [[inline]]; else std::error("Invalid Director File!");};
```
Only two of the`XFIR`occurrences had a valid type after them: one was`APPL`at address`0x00222151`, which, according to ScummVM, identifies a Director application wrapper \(the executable file we are working with\), and the other was`MV93`at address`0x002222F0`, which seems to be the regular, uncompressed Director movie script\.
I initially tried to copy that chunk out of the executable and process it separately using ProjectorRays, but it turned out that all the offsets used inside the appended file are absolute and account for the`\.dxr`file not starting at the beginning of the executable\. Instead, I ended up adding an`\-\-offset`option to the CLI that lets you seek forward to the actual offset of the embedded binary\. The PR can be found here in case it’s useful to somebody else:[ProjectorRays\#63](https://github.com/ProjectorRays/ProjectorRays/pull/63)\.
## [Disassembling the Director File](https://werwolv.net/posts/macromedia_copy_protection/#disassembling-the-director-file)
Finally, after all this trouble, we can dump the embedded Director file’s contents using the following command\.
```
./projectorrays decompile Findus4.exe -o output --dump-scripts --offset 0x002222F0output├── Findus4│ └── casts│ └── Internal│ ├── MovieScript 1.lasm│ └── MovieScript 1.ls└── Findus4.dir
```
To my surprise, this generated both a disassembly file \(the`\.lasm`file\)**and**a decompiled version of the script \(the`\.ls`file\), which still contains all the original function and variable names\!
Decompiled Director Script## [Analyzing the Script](https://werwolv.net/posts/macromedia_copy_protection/#analyzing-the-script)
The generated file is only around 150 lines long and contains the game’s initialization logic\. It’s written in a programming language called[Lingo](https://en.wikipedia.org/wiki/Lingo_(programming_language)), which is what Macromedia Director uses\. Even without knowing the language, it’s pretty straightforward to read\. It didn’t take long to find the function from which that`"messg"`string originally came\. As seen here, they implemented a primitive`\.ini`file parser to parse the`Pettson3\.ini`file and get a few config values out of it, one being the error message displayed when no CD can be found\.
```
-- ...on analyseIniText iniText repeat with mening = 1 to iniText.line.count ReddString = StripString(iniText.line[mening]) if ReddString contains "[" then ReddString = StripString(iniText.line[mening + 1]) antalBokstaver = ReddString.char.count if ReddString contains "cdrom" then cdromenhet = ReddString.char[7..antalBokstaver] end if if ReddString contains "Vind" then antalBokstaver = ReddString.char.count vindspelAktivt = integer(ReddString.char[6]) end if if ReddString contains "messg" then antalBokstaver = ReddString.char.count alertmessage = ReddString.char[7..antalBokstaver] end if if ReddString contains "fullscreen" then antalBokstaver = ReddString.char.count fullscreen = integer(ReddString.char[12]) end if end if end repeat resultat = [#cdromenhet: cdromenhet, #vindspelAktivt: vindspelAktivt, #alertmessage: alertmessage, #screen: fullscreen] return resultatend-- ...
```
I always find it super fun to read these old scripts and look at the weird mix of broken English, identifiers written in foreign languages \(Swedish in this case, I think?\) and probably extremely buggy code\.
A bit further down, we can also find the function where the error text is actually being used:
```
on checkpccdrompath seperator = the last char in the applicationPath drivelista = ["d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] Retvar = VOID repeat with Drive in drivelista iscdrom = baDiskInfo(Drive, "type") if iscdrom = "CD-ROM" then Path = Drive & ":" & seperator & "p3media" if checkoncd(Path) then Retvar = Drive exit repeat end if end if end repeat return Retvarend-- ... Part of setSearchPaths functiondriveletter = checkpccdrompath()if not voidp(driveletter) then ini[#cdromenhet] = driveletter & ":" hdMedia = mp & "p3media" hdljud = mp & "p3media\p3ljud" hdcast = mp & "p3media\p3cast" cdMedia = string(ini.cdromenhet & "\p3media") cdLjud = string(ini.cdromenhet & "\p3media\p3ljud") cdCast = string(ini.cdromenhet & "\p3media\p3cast") the searchPaths = [hdMedia, hdljud, hdcast, cdMedia, cdLjud, cdCast]else alert(string(ini.alertmessage)) quit()end if-- ...
```
Again, pretty straightforward code\. They just loop over all possible Windows drives, trying to find one that contains a CD\-ROM with the`p3media`folder on it\. If one is found, the search paths are set up\. Otherwise, the error message we saw is displayed and the game calls`quit\(\)`\.
## [Patching the Script](https://werwolv.net/posts/macromedia_copy_protection/#patching-the-script)
Analyzing the`setSearchPaths`function a bit, I figured the easiest way to avoid executing the`quit\(\)`branch while still allowing it to set up the search paths correctly would be to patch the`if`to jump to the next instruction instead of the`else`branch if the check failed\. That would result in the CD paths being a bit weird, but I figured it would probably still work, since the game lets you do a “Full Install” where it copies all the required data to the hard disk and presumably loads the files from there\.
In the assembly file, the instructions we want to patch are the following:
```
< 95 00 60 > [246] jmpifz [342] ......... if not voidp(driveletter) then / else< 4C 08 > [249] getlocal 8 ........... <ini>
```
ProjectorRays sadly doesn’t yet support assembling the code back, but thankfully, the Lingo bytecode is incredibly simple, so we can do it manually with ImHex\. Every instruction starts with a single opcode byte, followed by a big\-endian integer that’s either 1, 2 or 4 bytes long\. To make the manual patching a bit easier, I also made ProjectorRays output the bytes that make up each instruction \(also submitted upstream to ProjectorRays\)\.
Now for the patching:`jmpifz`has the opcode byte`0x95`, followed by the big\-endian 16\-bit relative offset`0x0060`, or 96 bytes, indicating where to jump\. To simply jump to the next instruction, we need to jump 3 bytes forward \(from offset 246 to offset 249\), so we want to patch the instruction bytes to be`< 95 00 03 \>`\. Searching for the original byte sequence yielded only a single result at address`0x002252E8`, which I overwrote with the new instruction\.
And with that, the game’s intro video was playing\! 🎉
The game's intro video playing## [Copy Protection 3 \- Rise of the Machines](https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-3---rise-of-the-machines)
Sadly, there was more to it\.
disappointment2\.pngAgain, simply searching for that string yielded two results, and I decided to start with`intro\.dxr`for now\.
```
➜ rg -a 'Disc Error !'P3media/intro.dxr...P3media/huset.dxr...
```
Throwing it into ProjectorRays again resulted in a few more scripts, one of which was straightforwardly called`BehaviorScript 36 \- Call Copy\-X CD protection DLL \(OPTGRAPH\.DLL\)\.ls`\.*I didn’t need that much handholding, devs, but thanks either way\.*
Inside was, as the filename suggested, some code that loads the`optgraph\.dll`located next to the game executable and calls its`initdisplay\(\)`function\. It then checks the return value of that function, and if it doesn’t match some magic value, the game is once again aborted\.
```
on exitFrame me if the platform contains "Mac" then nothing() else if Ok = 0 then alert("Disc Error !") avslutaSpelet() else if getReturnVal(me) <> 26079537 then alert("Disc Error !") avslutaSpelet() end if end if end ifend
```
Interestingly, they didn’t even bother implementing the copy protection for Mac…
To get past this check, we have a few options\. We could make the game believe that it’s running on a Mac, patch the comparison against the magic number or patch the DLL to make it always return that constant\. Given that the identical check is implemented again in`huset\.dxr`and that I didn’t really want to reverse engineer the runtime and patch the platform detection, I decided to patch`optgraph\.dll`instead\.
## [Patching the Support DLL](https://werwolv.net/posts/macromedia_copy_protection/#patching-the-support-dll)
Opening the DLL in Ghidra showed that it essentially contained only that one function and nothing else\.
`initdisplay\(\)`itself is a bit of a convoluted mess where they send a bunch of low\-level commands to the CD drive to somehow construct that magic value from it\.
```
// ...FUN_10001e50(&local_44,0,0x10);local_3c = 3;mciSendCommandA(local_c,0x814,0x100);local_3c = 1;local_2c = local_40;mciSendCommandA(local_c,0x814,0x102,&local_44);local_28 = (uint *)(local_40 & 0xff);local_3c = 1;local_48 = (char *)(local_40 >> 8 & 0xff);local_38 = 4;local_40._0_2_ = local_40._1_2_;mciSendCommandA(local_c,0x814,0x112,&local_44);// ...
```
I didn’t spend too much time reversing any of this but instead just used Ghidra’s wonderful`Patch Instruction`option to replace the first few instructions with a plain and boring`MOV`and`RET`\.
```
*************************************************************** FUNCTION *************************************************************** int __stdcall initdisplay(void) assume FS_OFFSET = 0xffdff000 int EAX:4 <RETURN> 0x1060 1 initdisplay initdisplay10001060 b8 31 f1 MOV EAX, 26079537 8d 0110001065 c3 RET10001066 05 ?? 05h10001067 00 ?? 00h10001068 00 ?? 00h
```
After struggling for an embarrassingly long time to get Ghidra to properly export the DLL again, I finally managed to do it through`File \-\> Export \-\> Original File`\.
This seemed to have been the last thing needed, since the game now gets all the way to the menu screen\!
Start menu screen of the game## [Writing an auto patcher](https://werwolv.net/posts/macromedia_copy_protection/#writing-an-auto-patcher)
After I was done, I wrote a quick Python script that can apply the patches discussed in this post\. This exact script will only work for the German version of the Game that I linked above\. However, it shouldn’t be hard to adapt to other versions as well \(probably only requires fixing the hash and the offset\)
```
#!/usr/bin/env python3import hashlibimport sysfrom pathlib import Pathgame_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()patches = [ ( game_dir / "Findus4.exe", "b33312ff9086c1cbc09d9cde6baa0eff4220a1ed354c9a3211ff6787fff46e39", 0x2252E8, bytes.fromhex("95 00 60"), bytes.fromhex("95 00 03"), ), ( game_dir / "optgraph.dll", "9897563fd6447976e2780ba0711f6e4e0a5a98a5c5cbecc4cb9bd608d856bd50", 0x1060, bytes.fromhex("55 8B EC 81 EC F4"), bytes.fromhex("B8 31 F1 8D 01 C3"), ),]verified = []for path, expected_hash, offset, original, replacement in patches: data = bytearray(path.read_bytes()) actual_hash = hashlib.sha256(data).hexdigest() if actual_hash != expected_hash: raise SystemExit(f"Unexpected SHA-256 for {path}: {actual_hash}") if data[offset : offset + len(original)] != original: raise SystemExit(f"Unexpected bytes at 0x{offset:X} in {path}") data[offset : offset + len(replacement)] = replacement verified.append((path, data))for path, data in verified: path.chmod(path.stat().st_mode | 0o200) path.write_bytes(data) print(f"Patched {path}")
```
## [Wrapping Up](https://werwolv.net/posts/macromedia_copy_protection/#wrapping-up)
After spending the evening playing through that game again, I also looked through a few other, similar games that I used to play\. It turns out that a surprisingly large number of them used Macromedia and had essentially the same basic copy\-protection mechanism\. Sadly, there’s no easy way to make a common patch that works for all of these games, but with a little bit of effort, the same process can be applied to them as well\.
Being able to play these games again after over 20 years, and maybe even having my future kid play them, makes me really happy :\)
I really hope this post can bring the same joy to others as well\!
---